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

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

1. 知覚可能なサービスのサポートを設定する

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

1.1 権限とサービスの宣言(UPL の追加)

Unreal はエンジン マニフェストを置き換えません。UnrealBuildTool は GameActivity をすでに宣言した AndroidManifest.xml を生成するため、UPL は権限とサービスを追加するだけで済みます。ランチャー アクティビティを再記述する必要はありません。Source/PSUnreal/PSUnreal_UPL.xml の <androidManifestUpdates> に以下を追加します。

<androidManifestUpdates>
    <addPermission android:name="android.permission.FOREGROUND_SERVICE" />
    <addPermission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <addPermission android:name="android.permission.POST_NOTIFICATIONS" />

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

foregroundServiceType は実際の動作と一致している必要があります。転送の場合は dataSync、再生の場合は mediaPlayback、位置情報の追跡の場合は location です。対応する FOREGROUND_SERVICE_<TYPE> 権限と startForeground 呼び出しと一致している必要があります。3 つすべてが一致していないと、サービスの起動に失敗します。

サービスは完全修飾名 com.sample.fgs.DownloadService を使用します。先頭のドットはアプリケーション ID com.sample.psunreal を基準に解決されますが、Java モジュールは com.sample.fgs パッケージに存在するため、見つかりません。

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

サービス実装の Java コードを jar にコンパイルし、UPL の <prebuildCopies> を使用してステージング libs ディレクトリにコピーします。UEDeployAndroid はステージング libs/ を Gradle プロジェクトの app/libs/ にコピーし、Gradle はそこにあるすべての jar を自動的に含めて APK にパッケージ化します。サービス プロセスはこれらのクラスをランタイムに読み込みます。

<prebuildCopies>
    <copyFile src="$S(PluginDir)/fgs-android.jar"
              dst="$S(BuildDir)/libs/fgs-android.jar" />
</prebuildCopies>

$S(PluginDir) は UPL ファイルが配置されているディレクトリです。コンパイル済みの jar をここに配置します。$S(BuildDir) はステージング ディレクトリです。

これらのクラスは JNI とマニフェストでのみ到達可能で、Java 呼び出しサイトがないため、<proguardAdditions> にも保持する必要があります。そうしないと、シュリンカーが未使用とみなし、削除してしまいます。

<proguardAdditions>
    <insert>
        -keep class com.sample.fgs.FgsBridge { public *; }
        -keep class com.sample.fgs.DownloadService { public *; }
        -keep class com.sample.fgs.ProgressFile { public *; }
        -keep class com.sample.fgs.FgsLogger { public *; }
    </insert>
</proguardAdditions>

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

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

android:process=":downloader"

先頭のコロンは、com.sample.psunreal:downloader というアプリ専用のプロセスを作成します。メインプロセスとは異なる PID を持ち、メインプロセスが終了した後も存続できます。

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

libUE4.solibUE5.so などの Unreal のネイティブ ライブラリをサービス プロセスに持ち込まないようにするため、サービス側の Java は Unreal クラスを使用せず、Android の ContextIntent エクストラ、プラットフォーム API のみに依存する必要があります。Unreal エンジン ランタイムは、メインプロセスの GameActivity を通してのみ読み込まれます。プライベート :downloader プロセスはこれらのライブラリを読み込まないため、サービスプロセスで Unreal クラスを参照することはできません。

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

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

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

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

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

  • Application.attachBaseContextApplication.onCreate は、アプリ コンポーネントをホストするすべてのプロセスで実行されます。
  • 静的イニシャライザと静的フィールドは、各プロセスに個別に存在します。メインプロセスで静的フィールドを割り当てても、サービスとの通信は行われません。
  • Unreal、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. Unreal(C++)から FGS を開始して制御する

PSUnrealAndroidBridge は、JNI を使用して FgsBridge を呼び出す C++ ラッパーです。3 つのメソッドの実装は次のとおりです。

void FPSUnrealAndroidBridge::StartDownloadService()
{
    CallActivityVoid(
            GBridgeInfo.StartDownloadService, TEXT("StartDownloadService"));
}

void FPSUnrealAndroidBridge::StopDownloadService()
{
    CallActivityVoid(
            GBridgeInfo.StopDownloadService, TEXT("StopDownloadService"));
}

void FPSUnrealAndroidBridge::RequestNotificationPermission()
{
    CallActivityVoid(
            GBridgeInfo.RequestNotificationPermission, TEXT("RequestNotificationPermission"));
}

CallActivityVoid は、FJavaWrapper::GameActivityThis を Context として使用して、対応する Java 静的メソッドを呼び出す内部 JNI ヘルパーです。サービス プロセスが開始されたときに通知がすぐに表示されるようにするには、BeginPlayRequestNotificationPermission を呼び出します。