返回

Unity 游戏集成 Google Health Connect 步数权限解决方案

Android

Unity游戏中获取步数读取权限

问题

在 Unity 游戏中集成 Google Health Connect,以获取用户的步数数据是一项常见的需求。 但当使用插件架构时,会遇到权限获取问题。 例如,Android 端的原生代码通常依赖 ComponentActivity 进行权限请求。 若插件类仅为普通的 Java 类,直接调用 registerForActivityResult 会引发错误,因为此方法属于 Activity 上下文。

原因分析

registerForActivityResult 函数只能在继承自 Activity 的类(比如 AppCompatActivityComponentActivity 等)中使用。插件作为普通的 Java 类运行时,无法直接使用 Activity 上下文相关的接口,权限请求就难以执行。Unity 项目调用插件代码时,插件代码会运行在后台线程,并不会关联一个 Android 的 Activity。 因此,在插件中使用 Activity 的上下文会直接抛出异常。

解决方案一:利用 UnityPlayerActivity

这种方式的核心思想是让 UnityPlayerActivity 来承担权限请求的责任。

操作步骤:

  1. 在你的 Android 插件项目中,创建一个静态的帮助类,例如 PermissionHelper
  2. 该帮助类包含静态方法,用于存储 Activity 实例,接收权限请求,并发送回调给 Unity。
  3. 在 Unity 应用启动后(或需要进行权限请求之前),从 Unity 调用插件方法来获得当前 UnityPlayerActivity 的 Activity 实例。
  4. 当需要权限请求时,从你的 Java 代码中,调用 Activity 的 requestPermissions 启动权限请求流程。
  5. 通过Activity 的回调( onRequestPermissionsResult ),接收授权结果,再利用 Unity 的发送事件(UnitySendMessage)将结果传递回Unity。

Java 代码示例:

// PermissionHelper.java
import android.app.Activity;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import com.unity3d.player.UnityPlayer;
import android.Manifest;
import androidx.health.connect.client.permission.HealthPermission;
import androidx.health.connect.client.records.StepsRecord;

public class PermissionHelper {

    private static Activity sActivity;
    private static String sUnityCallbackObjectName;
    private static int PERMISSION_REQUEST_CODE = 1; //随便定义一个唯一码
    private static String UNITY_CALLBACK_METHOD = "OnPermissionResult";

    private static final String[] HEALTH_PERMISSIONS = new String[]{
           HealthPermission.getReadPermission(StepsRecord.class).toString(),
           HealthPermission.getWritePermission(StepsRecord.class).toString(),
        };

    public static void setActivity(Activity activity){
        sActivity = activity;
    }
    
    public static void setUnityCallbackObjectName(String unityCallbackObjectName){
        sUnityCallbackObjectName = unityCallbackObjectName;
    }
    public static boolean checkHealthPermissions() {
    if (sActivity != null) {
        for(String perm : HEALTH_PERMISSIONS)
           if (ActivityCompat.checkSelfPermission(sActivity,perm) != PackageManager.PERMISSION_GRANTED)
            return false;
        return true;
     }else{
        return false;
    }
    }

    public static void requestHealthPermissions(){
         if (sActivity != null){
                ActivityCompat.requestPermissions(sActivity, HEALTH_PERMISSIONS, PERMISSION_REQUEST_CODE);
           }

    }
   public static void onActivityRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
      if (requestCode == PERMISSION_REQUEST_CODE) {
         //此处是安卓授权完毕,回调回JAVA层的处理处,将结果通过 Unity 发送事件传给 Unity 侧处理
          boolean allGranted = true;
             if(grantResults!=null){
                    for (int result : grantResults) {
                            if (result != PackageManager.PERMISSION_GRANTED) {
                            allGranted = false;
                             break;
                         }
                     }
               }else {
                   allGranted=false;
              }

        UnityPlayer.UnitySendMessage(sUnityCallbackObjectName, UNITY_CALLBACK_METHOD,allGranted?"true":"false");
        }

    }


}

Unity 代码示例:

using UnityEngine;
using System;
using UnityEngine.Android;

public class HealthConnectPlugin : MonoBehaviour
{
    private static AndroidJavaClass _pluginClass;
    public  static string CallBackObjectName = "HealthConnectPlugin";

   void Awake() {
        // 获取 Plugin class
        using(var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")){
            using(var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity")){
             using (var jc = new AndroidJavaClass("com.xxx.plugin.PermissionHelper"))//包名换成你的实际包名
             {
                 jc.CallStatic("setActivity",currentActivity);
                 jc.CallStatic("setUnityCallbackObjectName",CallBackObjectName);
              }
             }
           }
       }
      
       public void CheckAndRequestHealthPermissions() {

           using (var jc = new AndroidJavaClass("com.xxx.plugin.PermissionHelper"))//包名换成你的实际包名
            {
             if(jc.CallStatic<bool>("checkHealthPermissions")){
                Debug.Log("Already all permission Granted.");
             } else {
                  jc.CallStatic("requestHealthPermissions");
                 }
            }

      }
   //收到 Android 的授权结果后的回调
   public  void OnPermissionResult(string allPermissionGranted)
       {

         Debug.Log( "permissionResult:" + allPermissionGranted);
          if (allPermissionGranted=="true") {
               Debug.Log( "Permission Granted !");
                 //在这里去调用下一步骤 获取健康数据的 方法。
            }else {
             Debug.Log( "Permission Denied  !");

         }
       }


}

补充说明:
在 Android 的 activity (一般就是 UnityPlayerActivity) 的 onRequestPermissionsResult 方法内添加:
PermissionHelper.onActivityRequestPermissionsResult(requestCode, permissions, grantResults);
该方法主要是给静态类传递授权回调,并返回给unity侧。

解决方案二:使用第三方 Activity

这种方式比较通用,但复杂性稍微高。其主要原理是,在你的插件中创建或者使用一个自定义的 Activity ,该 Activity 的作用专门负责权限请求和步数数据获取。这种模式下,所有的 Android 上下文处理和调用都由自定义的 Activity 完成,降低了和UnityplayerActivity的耦合度。
操作步骤:

  1. 定义一个新的 Activity,专门用于权限请求和 Health Connect 功能。
  2. 使用 ContextCompat.startActivity() 从插件启动此 Activity 。
  3. 该 Activity 接收权限请求, 处理数据并回调到Unity (同样通过UnitySendMessage)。

这种方法的步骤稍复杂,需要细致地规划 AndroidManifest.xml ,并且注意进程的管理,本文暂不做示例,如有必要再补全此方案示例代码。
需要注意的是,通过定义单独的Activity来启动权限请求流程,会打断 Unity 主线程的生命周期,造成用户界面的瞬间跳转,因此不建议在游戏主要场景频繁请求授权。可以引导用户跳转到专门的健康模块来操作授权。

额外的安全建议:

  1. 请求最小权限 : 只请求必要权限,避免过度请求。
  2. 权限检查 : 始终在使用权限相关功能前,检查权限是否已获得。
  3. 用户说明 : 在请求权限前,提供明确的理由,提高用户信任。
  4. 错误处理 : 在权限请求失败时,处理并给出用户反馈,避免崩溃或功能失效。