it-swarm.dev

Implementazione delle indicazioni di Android 6.0 in unity3d

Ho installato Android Support Library ma nello sviluppatore. Il sito Android dice che per implementarlo sul mio progetto ho bisogno di modificare il mio file build.gradle che non ho perché è un progetto Unity.

Ho creato un file build.gradle che copia il contenuto di questo sito web: http://gradleplease.appspot.com/ e metto il file nella root del mio progetto Unity ma quando provo ad usare la libreria non funziona

if (ContextCompat.checkSelfPermission(thisActivity,
                                      Manifest.permission.READ_CONTACTS)
    != PackageManager.PERMISSION_GRANTED) {

    // Should we show an explanation?
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
                                                            Manifest.permission.READ_CONTACTS)) {

        // Show an expanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.

    } else {

        // No explanation needed, we can request the permission.

        ActivityCompat.requestPermissions(thisActivity,
                                          new String[]{Manifest.permission.READ_CONTACTS},
        MY_PERMISSIONS_REQUEST_READ_CONTACTS);

        // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
        // app-defined int constant. The callback method gets the
        // result of the request.
    }
}
6
mithrandir

È necessario il codice Java per chiedere l'autorizzazione e è necessaria un'interfaccia nel suddetto codice Java dal runtime C # di Unity. Devi creare un Unity Plugin per farlo.

Di seguito è riportato il plug-in che ho creato per concedere l'autorizzazione WRITE_EXTERNAL_STORAGE al runtime.

Hai bisogno di una struttura di progetto come questa:

Plugins/
  Android/
    NoodlePermissionGranter/
      project.properties
      AndroidManifest.xml
      NoodlePermissionGranter.cs
      libs/
        NoodlePermissionGranter.jar

NoodlePermissionGranter.cs:

      ///////////////////////////////////////////////////////////
     ///////////////// NoodlePermissionGranter /////////////////
    /// Implements runtime granting of Android permissions. ///
   /// This is necessary for Android M (6.0) and above. //////
  ///////////////////////////////////////////////////////////   
 //////////////////// Noodlecake Studios ///////////////////
///////////////////////////////////////////////////////////

using UnityEngine;
using System.Collections;
using System;

public class NoodlePermissionGranter : MonoBehaviour {

    // subscribe to this callback to see if your permission was granted.
    public static Action<bool> PermissionRequestCallback;

    // for now, it only implements the external storage permission
    public enum NoodleAndroidPermission
    {
        WRITE_EXTERNAL_STORAGE
    }
    public static void GrantPermission(NoodleAndroidPermission permission) 
    {
        if (!initialized)
            initialize ();

        noodlePermissionGranterClass.CallStatic ("grantPermission", activity, (int)permission);
    }







      //////////////////////////////
     /// Initialization Stuff /////
    //////////////////////////////

    // it's a singleton, but no one needs to know about it. hush hush. dont touch me.
    private static NoodlePermissionGranter instance;
    private static bool initialized = false;

    public void Awake()
    {
        // instance is also set in initialize.
        // having it here ensures this thing doesnt break
        // if you added this component to the scene manually
        instance = this;
        DontDestroyOnLoad (this.gameObject);
        // object name must match UnitySendMessage call in NoodlePermissionGranter.Java
        if (name != NOODLE_PERMISSION_GRANTER)
            name = NOODLE_PERMISSION_GRANTER;
    }


    private static void initialize()
    {
        // runs once when you call GrantPermission

        // add object to scene
        if (instance == null) {
            GameObject go = new GameObject();
            // instance will also be set in awake, but having it here as well seems extra safe
            instance = go.AddComponent<NoodlePermissionGranter>();
            // object name must match UnitySendMessage call in NoodlePermissionGranter.Java
            go.name = NOODLE_PERMISSION_GRANTER; 
        }

        // get the jni stuff. we need the activty class and the NoodlePermissionGranter class.
        noodlePermissionGranterClass = new AndroidJavaClass("com.noodlecake.unityplugins.NoodlePermissionGranter");
        AndroidJavaClass u3d = new AndroidJavaClass ("com.unity3d.player.UnityPlayer");
        activity = u3d.GetStatic<AndroidJavaObject> ("currentActivity");

        initialized = true;
    }







      ///////////////////
     //// JNI Stuff ////
    ///////////////////

    static AndroidJavaClass noodlePermissionGranterClass;
    static AndroidJavaObject activity;
    private const string WRITE_EXTERNAL_STORAGE="WRITE_EXTERNAL_STORAGE";
    private const string PERMISSION_GRANTED = "PERMISSION_GRANTED"; // must match NoodlePermissionGranter.Java
    private const string PERMISSION_DENIED = "PERMISSION_DENIED"; // must match NoodlePermissionGranter.Java
    private const string NOODLE_PERMISSION_GRANTER = "NoodlePermissionGranter"; // must match UnitySendMessage call in NoodlePermissionGranter.Java

    private void permissionRequestCallbackInternal(string message)
    {
        // were calling this method from the Java side.
        // the method name and gameobject must match NoodlePermissionGranter.Java's UnitySendMessage
        bool permissionGranted = (message == PERMISSION_GRANTED);
        if (PermissionRequestCallback != null)
            PermissionRequestCallback (permissionGranted);
    }
}

NoodlePermissionGranter.Java:

package com.noodlecake.unityplugins;


      ///////////////////////////////////////////////////////////
     ///////////////// NoodlePermissionGranter /////////////////
    /// Implements runtime granting of Android permissions. ///
   /// This is necessary for Android M (6.0) and above. //////
  ///////////////////////////////////////////////////////////   
 //////////////////// Noodlecake Studios ///////////////////
///////////////////////////////////////////////////////////

import Android.Manifest;
import Android.os.Build;
import Android.app.Activity;
import Android.app.Fragment;
import Android.app.FragmentManager;
import Android.app.FragmentTransaction;
import Android.util.Log;
import Android.content.pm.PackageManager;
import Java.io.File;
import com.unity3d.player.UnityPlayerActivity;
import com.unity3d.player.UnityPlayer;

public class NoodlePermissionGranter
{
    // Only implements WRITE_EXTERNAL_STORAGE so far.
    // Implement the rest by matching the enum in NoodlePermissionGranter.cs
    // to the getPermissionStringFromEnumInt below.

    private final static String UNITY_CALLBACK_GAMEOBJECT_NAME = "NoodlePermissionGranter";
    private final static String UNITY_CALLBACK_METHOD_NAME = "permissionRequestCallbackInternal";
    private final static String PERMISSION_GRANTED = "PERMISSION_GRANTED"; // this will be an arg to the above method
    private final static String PERMISSION_DENIED = "PERMISSION_DENIED";

    public static String getPermissionStringFromEnumInt(int permissionEnum) throws Exception
    {
        switch (permissionEnum)
        {
            case 0:
                return Manifest.permission.WRITE_EXTERNAL_STORAGE;
            // "and the rest is still unwritten" - Natasha Bedingfield
        }
        Log.e("NoodlePermissionGranter", "Error. Unknown permissionEnum " + permissionEnum);
        throw new Exception(String.format("Error. Unknown permissionEnum %d",permissionEnum));
    }

    public static void grantPermission(Activity currentActivity, int permissionEnum)
    {
        // permission enum must match ordering in NoodlePermissionGranter.cs
        final Activity act = currentActivity;
        Log.i("NoodlePermissionGranter","grantPermission " + permissionEnum) ;
        if (Build.VERSION.SDK_INT < 23) {
            Log.i("NoodlePermissionGranter","Build.VERSION.SDK_INT < 23 (" + Build.VERSION.SDK_INT+")");
            UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_GRANTED);
            return;
        }

        try
        {
            final int PERMISSIONS_REQUEST_CODE = permissionEnum;
            final String permissionFromEnumInt = getPermissionStringFromEnumInt(permissionEnum);
            if (currentActivity.checkCallingOrSelfPermission(permissionFromEnumInt) == PackageManager.PERMISSION_GRANTED) {
                Log.i("NoodlePermissionGranter", "already granted");
                UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_GRANTED);
                return;
            }

            final FragmentManager fragmentManager = currentActivity.getFragmentManager();
            final Fragment request = new Fragment() {

                @Override public void onStart()
                {
                    super.onStart();
                    Log.i("NoodlePermissionGranter","fragment start");
                    String[] permissionsToRequest = new String [] {permissionFromEnumInt};
                    Log.i("NoodlePermissionGranter","fragment start " + permissionsToRequest[0]);
                    requestPermissions(permissionsToRequest, PERMISSIONS_REQUEST_CODE);
                }

                @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
                {
                    Log.i("NoodlePermissionGranter", "onRequestPermissionsResult");
                    if (requestCode != PERMISSIONS_REQUEST_CODE)
                        return;

                    if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        // permission was granted, yay! Do the
                        // contacts-related task you need to do.
                        Log.i("NoodlePermissionGranter", PERMISSION_GRANTED);
                        UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_GRANTED);
                    } else {

                        // permission denied, boo! Disable the
                        // functionality that depends on this permission.
                        Log.i("NoodlePermissionGranter",PERMISSION_DENIED);
                        UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_DENIED);
                    }


                    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
                    fragmentTransaction.remove(this);
                    fragmentTransaction.commit();

                    // shouldBeOkayToStartTheApplicationNow();
                }
            };

            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.add(0, request);
            fragmentTransaction.commit();
        }
        catch(Exception error)
        {
            Log.w("[NoodlePermissionGranter]", String.format("Unable to request permission: %s", error.getMessage()));
            UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_DENIED);
        }
    }

}

BuildNoodlePermissionGranter.sh

export Java_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
ClASSPATH=$UNITY_ROOT"/Unity.app/Contents/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar"

javac NoodlePermissionGranter.Java -bootclasspath $Android_SDK_ROOT/platforms/Android-23/Android.jar -classpath $ClASSPATH -d .
javap -s com.noodlecake.unityplugins.NoodlePermissionGranter
jar cvfM NoodlePermissionGranter.jar com/
rm -rf com

Sono necessari project.properties e un dummy AndroidManifest.xml per fare in modo che Unity esegua un jar esterno a Plugin/Android/libs

project.properties

target=Android-9
Android.library=true

AndroidManifest.xml

<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
    package="com.noodlecake.unityplugins.noodlepermissiongranter"
    Android:versionCode="1"
    Android:versionName="1.0" >
    <uses-sdk Android:targetSdkVersion="23" />
</manifest>

Sarebbe bello se PermissionRequestCallback fornisse l'enum di autorizzazione richiesto come parametro, ma UnityPlayer.UnitySendMessage supporta solo un singolo argomento di stringa e ho deciso di non implementare la serializzazione delle stringhe (usare JSON per farlo sarebbe una buona scelta).

25
Jason Knight

Un'altra aggiunta all'eccellente codice di Jason per Unity 5.3.3 e versioni successive (sto usando 5.4), l'ho aggiunto al manifest per bloccare Unity dal chiedere automaticamente al momento del lancio:

 <application>
     <meta-data Android:name="unityplayer.SkipPermissionsDialog" Android:value="true" />
 </application>
6
hawkwood

Oltre al post di Jason Knight (che ho usato per il mio plugin Unity per la gestione delle autorizzazioni di runtime):

Ho usato Android Studio per creare un plugin. Ho seguito le indicazioni sul seguente sito e ho funzionato perfettamente: http://www.thegamecontriver.com/2015/04/Android-plugin-unity-Android-studio.html

Ho anche aggiunto un altro metodo utilizzando la funzione shouldShowRequestPermissionRationale () in modo che sia in grado di nascondere determinati elementi dell'interfaccia utente se l'utente ha negato l'autorizzazione e ha selezionato la casella di controllo "Non chiedere più".

2
Jasper

Ho usato la risposta di Jason Knight per creare questo plugin che fa il lavoro, l'intero codice è disponibile sul repository github.

C'è anche un file di pacchetto dell'unità per una facile integrazione.

2
Jimmar

Le altre risposte (specialmente quelle di Jason Knight) mi sono state di grande aiuto, ma ho dovuto modificare il codice per farlo funzionare, quindi condivido queste modifiche qui.

Come notato nei commenti, quel codice ha questo errore in Android Studio: final Fragment request = new Fragment(); parte che dice "I frammenti dovrebbero essere statici in modo tale da poter essere nuovamente istanziati dal sistema, e le classi anonime non sono statiche"

Ora non sono un esperto di Java quindi forse ho fatto delle cose sbagliate, ma ho provato ad aggiustare le cose come spiegato qui: I frammenti dovrebbero essere statici in modo tale da poter essere riattivati ​​dal sistema, e le classi anonime non sono statiche

Per lo più, ho diviso il frammento in una nuova classe, in modo che non fosse una classe anonima. Quindi ora ci sono due file Java:

package com.synapse.unityplugins;

import Android.Manifest;
import Android.os.Build;
import Android.app.Activity;
import Android.app.Fragment;
import Android.app.FragmentManager;
import Android.app.FragmentTransaction;
import Android.util.Log;
import Android.content.pm.PackageManager;

import com.unity3d.player.UnityPlayer;

public class PermissionGranter {
    public final static String UNITY_CALLBACK_GAMEOBJECT_NAME = "SynapsePlugin_listener";
    public final static String UNITY_CALLBACK_METHOD_NAME = "permissionRequestCallbackInternal";
    public final static String PERMISSION_GRANTED = "PERMISSION_GRANTED";
    public final static String PERMISSION_DENIED = "PERMISSION_DENIED";

    // only implemented WRITE_EXTERNAL_STORAGE so far
    public static String getPermissionStringFromEnumInt(int permissionEnum) throws Exception
    {
        switch (permissionEnum) {
            case 0:
                return Manifest.permission.WRITE_EXTERNAL_STORAGE;
            // "and the rest is still unwritten" - Natasha Bedingfield
        }
        Log.e("PermissionGranter", "Error. Unknown permissionEnum " + permissionEnum);
        throw new Exception(String.format("Error. Unknown permissionEnum %d",permissionEnum));
    }

    public static void grantPermission(int permissionEnum)
    {
        final Activity act = UnityPlayer.currentActivity;
        Log.i("PermissionGranter","grantPermission " + permissionEnum) ;
        if (Build.VERSION.SDK_INT < 23) {
            Log.i("PermissionGranter","Build.VERSION.SDK_INT < 23 (" + Build.VERSION.SDK_INT+")");
            UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_GRANTED);
            return;
        }

        try {
            final String permissionFromEnumInt = getPermissionStringFromEnumInt(permissionEnum);
            if (act.checkCallingOrSelfPermission(permissionFromEnumInt) == PackageManager.PERMISSION_GRANTED) {
                Log.i("PermissionGranter", "already granted");
                UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_GRANTED);
                return;
            }

            final Fragment request = PermissionRequestFragment.newInstance(permissionEnum);
            final FragmentManager fragmentManager = act.getFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.add(0, request);
            fragmentTransaction.commit();
        }
        catch(Exception error)
        {
            Log.w("PermissionGranter", String.format("Unable to request permission: %s", error.getMessage()));
            UnityPlayer.UnitySendMessage(UNITY_CALLBACK_GAMEOBJECT_NAME, UNITY_CALLBACK_METHOD_NAME, PERMISSION_DENIED);
        }
    }
}

Questa è la principale classe di plugin, ed ecco il frammento:

package com.synapse.unityplugins;

import Android.app.Activity;
import Android.app.Fragment;
import Android.app.FragmentManager;
import Android.app.FragmentTransaction;
import Android.content.pm.PackageManager;
import Android.os.Bundle;
import Android.util.Log;

import com.unity3d.player.UnityPlayer;

public class PermissionRequestFragment extends Fragment {
    public static PermissionRequestFragment newInstance(int permissionEnum) {
        PermissionRequestFragment frag = new PermissionRequestFragment();
        Bundle args = new Bundle();
        args.putInt("requested", permissionEnum);
        frag.setArguments(args);
        return frag;
    }

    @Override
    public void onStart() {
        super.onStart();
        int permissionEnum = getArguments().getInt("requested");
        final int PERMISSIONS_REQUEST_CODE = permissionEnum;

        try {
            final String permissionFromEnumInt = PermissionGranter.getPermissionStringFromEnumInt(permissionEnum);
            String[] permissionsToRequest = new String[]{permissionFromEnumInt};
            Log.i("PermissionGranter", "fragment start " + permissionsToRequest[0]);
            requestPermissions(permissionsToRequest, PERMISSIONS_REQUEST_CODE);
        } catch (Exception error) {
            Log.w("PermissionGranter", String.format("Unable to request permission: %s", error.getMessage()));
            UnityPlayer.UnitySendMessage(PermissionGranter.UNITY_CALLBACK_GAMEOBJECT_NAME,
                    PermissionGranter.UNITY_CALLBACK_METHOD_NAME, PermissionGranter.PERMISSION_DENIED);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        Log.i("PermissionGranter", "onRequestPermissionsResult");
        int permissionEnum = getArguments().getInt("requested");
        final int PERMISSIONS_REQUEST_CODE = permissionEnum;
        if (requestCode != PERMISSIONS_REQUEST_CODE)
            return;

        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            // permission was granted, yay! Do the task now
            Log.i("PermissionGranter", PermissionGranter.PERMISSION_GRANTED);
            UnityPlayer.UnitySendMessage(PermissionGranter.UNITY_CALLBACK_GAMEOBJECT_NAME,
                    PermissionGranter.UNITY_CALLBACK_METHOD_NAME, PermissionGranter.PERMISSION_GRANTED);
        } else {

            // permission denied, boo! Disable the functionality that needed it
            Log.i("PermissionGranter", PermissionGranter.PERMISSION_DENIED);
            UnityPlayer.UnitySendMessage(PermissionGranter.UNITY_CALLBACK_GAMEOBJECT_NAME,
                    PermissionGranter.UNITY_CALLBACK_METHOD_NAME, PermissionGranter.PERMISSION_DENIED);
        }

        final Activity act = UnityPlayer.currentActivity;
        final FragmentManager fragmentManager = act.getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.remove(this);
        fragmentTransaction.commit();
    }
}

E per completezza ecco il C # all'interno di Unity:

using UnityEngine;
using System.Collections;
using System;

public class SynapsePlugin : MonoBehaviour {

    // subscribe to this callback to see if your permission was granted.
    public static Action<bool> PermissionRequestCallback;

    // for now, it only implements the external storage permission
    public enum AndroidPermission {
        WRITE_EXTERNAL_STORAGE
    }

    public static void GrantPermission(AndroidPermission permission) {
        if (!initialized)
            initialize ();

        PermissionGranterClass.CallStatic ("grantPermission", (int)permission);
    }







    //////////////////////////////
    /// Initialization Stuff /////
    //////////////////////////////

    private const string PLUGIN_LISTENER_NAME = "SynapsePlugin_listener"; // must match UnitySendMessage call in Java

    // it's a singleton, but no one needs to know about it. hush hush. dont touch me.
    private static SynapsePlugin instance;
    private static bool initialized = false;

    static AndroidJavaClass PermissionGranterClass;
    private const string PERMISSION_GRANTED = "PERMISSION_GRANTED"; // must match Java
    private const string PERMISSION_DENIED = "PERMISSION_DENIED"; // must match Java

    // runs automatically when making calls, or can pre-init manually
    public static void initialize() {

        // add object to scene
        if (instance == null) {
            GameObject go = new GameObject();
            go.name = PLUGIN_LISTENER_NAME;

            // instance will also be set in awake, but having it here as well seems extra safe
            instance = go.AddComponent<SynapsePlugin>();
        }

        // get the jni stuff
        new AndroidJavaClass("com.synapse.unityplugins.PermissionGranter");

        initialized = true;
    }

    public void Awake() {
        DontDestroyOnLoad (this.gameObject);

        // instance is also set in initialize.
        // having it here ensures this thing doesnt break
        // if you added this component to the scene manually
        instance = this;

        if (name != PLUGIN_LISTENER_NAME)
            name = PLUGIN_LISTENER_NAME;
    }

    // we're calling this method from the Java side.
    // the method name and gameobject must match Java's UnitySendMessage
    private void permissionRequestCallbackInternal(string message) {
        bool permissionGranted = (message == PERMISSION_GRANTED);
        if (PermissionRequestCallback != null)
            PermissionRequestCallback(permissionGranted);
    }
}
2
jhocking

Bene, se puoi usare Android Studio e scrivere codici Java, allora ...

public interface PermissionAction {
    int MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL = 1;
    //You can add other integers too if you want
}

public void RequestPermissions(){

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // Here, thisActivity is the current activity
        if (ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {

            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    PermissionAction.MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL);
        }
        //More if statements for other permissions
    }

E sotto

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case PermissionAction.MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // permission was granted, yay! Do the contacts-related task you need to do.
                Toast.makeText(this, "WRITE SUCCESS", Toast.LENGTH_SHORT).show();

            } else {
                // permission denied, boo! Disable the functionality that depends on this permission.
                Toast.makeText(this, "Permission denied to write your External storage", Toast.LENGTH_LONG).show();

            }
            return;
        }
        //More Cases for other permissions
    }
}

Quindi puoi sostanzialmente chiamare RequestPermissions (); nel metodo OnCreate

E la cosa più importante qui è che devi support-compat-25.1.0.aar file (o l'ultima versione di questo file) dal tuo

"SDK> extra> Android> m2repository> com> Android> supporto> support-compat"

Metti il ​​ support-compat-25.1.0.aar file in Risorse/Plugin/Android con gli altri tuoi file plugin

Questo è tutto. in aggiunta a ciò puoi usare l'esempio manifest di hawkwood per disabilitare i permessi extra dell'unità da quando hai creato il tuo.

1
TheCoffee