使用 Unity 在個別程序中執行可察覺的服務

本指南說明如何設定及實作,以便從 Unity 應用程式在私有程序中執行 Android 可感知服務 (前景服務或 FGS)。

1. 設定可感知服務支援

本節說明如何設定必要權限,以及在專案資訊清單中宣告服務。

1.1 權限和服務聲明

自訂Assets/Plugins/Android/AndroidManifest.xml必須宣告啟動器 Activity、可感知服務權限、通知權限、網路權限和服務:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        <activity
            android:name="com.unity3d.player.UnityPlayerActivity"
            android:theme="@style/UnityThemeSelector"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>

        <service
            android:name="com.sample.fgs.DownloadService"
            android:process=":downloader"
            android:exported="false"
            android:stopWithTask="false"
            android:foregroundServiceType="dataSync" />
    </application>
</manifest>

foregroundServiceType 必須與實際工作相符:dataSync 用於轉移、mediaPlayback 用於 播放、location 用於位置追蹤。這項權限必須與對應的 FOREGROUND_SERVICE_<TYPE> 權限和 startForeground 呼叫一致,否則服務啟動會失敗。

1.2 將服務程序 Java 新增至 Unity 專案

FGS 服務程序會執行 Java。將服務實作 Java 程式碼封裝到 JAR 中,並放在 Assets/Plugins/Android/ 下方。Unity 會自動將該目錄中的 JAR 檔案納入 Gradle 的 libs/input,並將其封裝至 APK 中;服務程序會在執行階段載入這些類別。如要進一步瞭解如何以 Java 或 Kotlin 實作這項服務,請參閱「Java 實作」。

2. 設定個別程序

您可以使用一個資訊清單屬性啟用程序界線:

android:process=":downloader"

開頭的半形冒號會建立名為 your.package.name:downloader 的應用程式專屬程序。這個程序與主要程序有不同的 PID,且在主要程序終止後仍可存留。

2.1 服務程序沒有 Unity

為避免將 Unity 的 libunity.so、IL2CPP 執行階段和類似項目帶入服務程序,服務端的 Java 不應使用任何 Unity 類別 (包括 UnityPlayer.currentActivity),且只應依附於 Android Context、Intent extras 和平台 API。Unity 引擎執行階段只會透過主要程序的 UnityPlayerActivity 載入;私有 :downloader 程序不會載入這些程式庫,因此在服務程序中參照 Unity 類別會無效。

2.2 處理程序啟動進入點

主要程序呼叫 startForegroundService 後,系統會分叉 :downloader 服務程序:

  • 例項化 Application 並呼叫 Application.onCreate,這會在每個程序中執行,因此服務程序所需的初始化作業必須在此重新完成。詳情請參閱第 2.3 節。
  • 建立 DownloadService 並呼叫 onCreate,這是服務程序進入點。
  • 在啟動時建構 Intent,並使用該 Intent 回呼 onStartCommand。服務會在此將自己升級為前景服務,並啟動工作站。詳情請參閱第 3.1 節。

2.3 每個程序都有一個狀態

Dex 程式碼是共用的唯讀程式碼,但執行階段狀態不是:

  • Application.attachBaseContextApplication.onCreate 會在代管應用程式元件的每個程序中執行。
  • 每個程序中都有獨立的靜態初始設定式和靜態欄位。 在主要程序中指派靜態欄位時,不會與服務通訊。
  • Unity、C# 和活動仍保留在主要程序中。

3. Java 實作

本節涵蓋 FGS 模組的 Java 端:3.1 和 3.2 位於服務程序中;3.3 位於主要程序中 (使用 JNI 的進入點 C# 呼叫)。DownloadServiceFgsBridge

3.1 先升級為前景服務

public class DownloadService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        try {
            startForegroundCompat();

            // Start the download thread; must come after promoting to foreground
            // ...
        } catch (Exception e) {
            Log.e(TAG, "onStartCommand() failed [errorType="
                    + e.getClass().getSimpleName() + "]: " + e.getMessage(), e);
            stopSelf();
        }

        return START_NOT_STICKY;
    }

    private void startForegroundCompat() {
        Notification notification = buildNotification(0L);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(NOTIFICATION_ID, notification,
                    ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
        } else {
            startForeground(NOTIFICATION_ID, notification);
        }
    }

    ...

3.2 新增通知

可察覺的服務需要在通知管道中顯示持續性通知,才能回報進度。通知會執行「停止」動作,使用 PendingIntent.getServiceACTION_STOP 傳送至服務本身,藉此停止服務。

private Notification buildNotification(long progressBytes) {
    int progressMib = (int) (progressBytes / MIB);

    Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
    PendingIntent contentIntent = launchIntent != null
            ? PendingIntent.getActivity(this, 0, launchIntent,
                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT)
            : null;

    Intent stopIntent = new Intent(this, DownloadService.class)
            .setAction(ACTION_STOP);
    PendingIntent stopPendingIntent = PendingIntent.getService(this, 0,
            stopIntent, PendingIntent.FLAG_IMMUTABLE
            | PendingIntent.FLAG_UPDATE_CURRENT);

    Notification.Builder builder = new Notification.Builder(this,
            CHANNEL_ID)
            .setContentTitle("Download service")
            .setContentText("Downloaded " + progressMib + " MB / " + TOTAL_MIB + " MB")
            .setSmallIcon(android.R.drawable.stat_sys_download)
            .setProgress(TOTAL_MIB, progressMib, false)
            .setOngoing(true)
            .setOnlyAlertOnce(true)
            .addAction(new Notification.Action.Builder(null,
                    "Stop", stopPendingIntent).build());

    if (contentIntent != null) {
        builder.setContentIntent(contentIntent);
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setForegroundServiceBehavior(
                Notification.FOREGROUND_SERVICE_IMMEDIATE);
    }
    return builder.build();
}

3.3 Java 主機進入點

FgsBridge 是主要程序用來控管 FGS 的 Java 進入點 (在主要程序中執行,而非服務程序)。C# 會使用 JNI 呼叫這些靜態方法,啟動及停止服務,並處理通知權限:

public class FgsBridge {
    // Start the :downloader perceptible service and begin downloading
    public static void startDownloadService(Context context) {
        try {
            context.startForegroundService(new Intent(context, DownloadService.class));
        } catch (Exception e) {
            Log.e(TAG, "startDownloadService() failed [errorType="
                    + e.getClass().getSimpleName() + "]: " + e.getMessage(), e);
        }
    }

    // Stop the service: sends a stop intent; the service removes its
    // notification before exiting
    public static void stopDownloadService(Context context) {
        try {
            context.startService(new Intent(context, DownloadService.class)
                    .setAction(ACTION_STOP));
        } catch (Exception e) {
            Log.e(TAG, "stopDownloadService() failed [errorType="
                    + e.getClass().getSimpleName() + "]: " + e.getMessage(), e);
        }
    }

    // Request notification permission (only needed on API 33+; if already
    // granted, no dialog is shown and it returns immediately)
    public static void requestNotificationPermission(Activity activity) {
        if (Build.VERSION.SDK_INT < 33) {
            return;
        }
        try {
            activity.requestPermissions(
                    new String[] { POST_NOTIFICATIONS }, NOTIFICATION_PERMISSION_REQUEST);
        } catch (Exception e) {
            Log.e(TAG, "requestNotificationPermission() failed [errorType="
                    + e.getClass().getSimpleName() + "]: " + e.getMessage(), e);
        }
    }
}

4. 從 Unity (C#) 啟動及控制 FGS

AndroidBridge 是使用 JNI 呼叫 FgsBridge 的 C# 包裝函式。這三種方法實作方式如下:

public static void StartDownloadService()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    CallStatic("startDownloadService");
#else
    Debug.Log("StartDownloadService() no-op outside Android");
#endif
}

public static void StopDownloadService()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    CallStatic("stopDownloadService");
#else
    Debug.Log("StopDownloadService() no-op outside Android");
#endif
}

public static void RequestNotificationPermission()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    CallStatic("requestNotificationPermission");
#else
    Debug.Log("RequestNotificationPermission() no-op outside Android");
#endif
}

CallStatic 是內部 JNI 輔助程式,可解析 FgsBridge 類別並呼叫對應的 Java 靜態方法。RequestNotificationPermission 應在應用程式啟動時呼叫,以便在服務程序啟動時立即顯示通知。