Unity で知覚可能なサービスを別のプロセスで実行する

このガイドでは、Unity アプリケーションからプライベート プロセスで Android の知覚可能サービス(フォアグラウンド サービス、FGS)を実行するために必要な構成と実装について説明します。

1. 知覚可能サービスのサポートを構成する

このセクションでは、必要な権限を設定し、プロジェクトのマニフェストでサービスを宣言する方法について説明します。

1.1 権限とサービスの宣言

カスタム Assets/Plugins/Android/AndroidManifest.xml は、ランチャー アクティビティ、知覚可能サービスの権限、 通知権限、ネットワーク権限、サービスを宣言する必要があります。

<?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呼び出しと一致する必要があります。これら 3 つは一貫している必要があります。そうでない場合、サービスの起動は 失敗します。

1.2 サービス プロセス Java を Unity プロジェクトに追加する

FGS サービス プロセスは Java を実行します。サービス実装の Java コードを jar にパッケージ化し、 Assets/Plugins/Android/ に配置します。 Unity は、そのディレクトリ内の jar を Gradle の libs/ 入力に自動的に含め、APK にパッケージ化します。サービス プロセスは、これらのクラスを実行時に読み込みます。 Java または Kotlin でサービスを実装する方法について詳しくは、 Java の実装をご覧ください。

2. 別のプロセスを構成する

プロセス境界は、1 つのマニフェスト属性で有効になります。

android:process=":downloader"

先頭のコロンは、your.package.name:downloader という名前のアプリケーション専用プロセスを作成します。メインプロセスとは異なる PID を持ち、メインプロセスが終了した後も存続できます。

2.1 サービス プロセスに Unity がない

Unity の libunity.so、IL2CPP ランタイムなどをサービス プロセスに含めないようにするには、サービス側の Java で Unity クラス(UnityPlayer.currentActivity を含む)を使用せず、Android Context、Intent エクストラ、プラットフォーム API のみに依存する必要があります。Unity エンジン ランタイムは、メインプロセスの UnityPlayerActivity を介してのみ読み込まれます。プライベート :downloader プロセスはこれらのライブラリを読み込まないため、Unity クラスを参照してもサービス プロセスでは機能しません。

2.2 プロセスの起動エントリ ポイント

メインプロセスが startForegroundService を呼び出すと、システムは :downloader サービス プロセスをフォークします。

  • Application をインスタンス化して Application.onCreate を呼び出します。これは すべてのプロセスで実行されるため、サービス プロセス に必要な初期化をここで行う必要があります。詳細については、2.3 をご覧ください。
  • DownloadService を作成して onCreate を呼び出します。これがサービス プロセスのエントリ ポイントです。
  • 起動時に作成された Intent で onStartCommand をコールバックします。サービスはここでフォアグラウンドに昇格し、ワーカーを開始します。 詳細については、3.1 をご覧ください。

2.3 プロセスごとに 1 つの状態が存在する

Dex コードは読み取り専用で共有されますが、ランタイム状態は共有されません。

  • Application.attachBaseContextApplication.onCreate は、アプリケーション コンポーネントをホストするすべてのプロセスで実行されます。
  • 静的イニシャライザと静的フィールドは、各プロセスに個別に存在します。 メインプロセスで静的フィールドを割り当てても、サービスと通信しません。
  • Unity、C#、アクティビティはメインプロセスに残ります。

3. Java の実装

このセクションでは、FGS モジュールの Java 側について説明します。3.1 と 3.2 はサービス プロセスの DownloadService です。3.3 はメインプロセスの FgsBridge です(JNI を使用して C# が呼び出すエントリ ポイント)。

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.getService を使用してサービス自体に ACTION_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# ラッパーです。3 つのメソッドの実装は次のとおりです。

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 は、FgsBridge クラスを解決して対応する Java 静的メソッドを呼び出す内部 JNI ヘルパーです。RequestNotificationPermission は、サービス プロセスの開始時に通知がすぐに表示されるように、アプリケーションの起動時に呼び出す必要があります。