Exécuter un service perceptible dans un processus distinct avec Unreal

Ce guide couvre la configuration et l'implémentation nécessaires pour exécuter un service perceptible Android (FGS) dans un processus privé à partir d'une application Unreal.

1. Configurer la prise en charge des services perceptibles

Cette section explique comment configurer les autorisations requises et déclarer le service dans le fichier manifeste de votre projet.

1.1 Autorisations et déclaration de service (ajouts UPL)

Unreal ne remplace pas le fichier manifeste du moteur. UnrealBuildTool génère déjà AndroidManifest.xml déclaré GameActivity. L'UPL n'a donc qu'à ajouter des autorisations et le service. Il n'est pas nécessaire de redéfinir l'activité du lanceur. Ajoutez le code suivant sous <androidManifestUpdates> dans Source/PSUnreal/PSUnreal_UPL.xml :

<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 doit correspondre au travail réel : dataSync pour les transferts, mediaPlayback pour la lecture et location pour le suivi de la position. Il doit correspondre à l'autorisation FOREGROUND_SERVICE_<TYPE> correspondante et à l' startForeground appel. Les trois doivent être cohérents, sinon le démarrage du service échoue.

Le service utilise le nom complet com.sample.fgs.DownloadService. Un point au début serait résolu par rapport à l'ID d'application com.sample.psunreal, mais le module Java se trouve dans le package com.sample.fgs. Il ne serait donc pas trouvé.

1.2 Ajouter le processus de service Java au projet Unreal

Compilez le code Java d'implémentation du service dans un fichier JAR et copiez-le dans le répertoire des bibliothèques de préparation à l'aide de <prebuildCopies> de l'UPL. UEDeployAndroid copie les bibliothèques de préparation dans app/libs/ du projet Gradle, et Gradle inclut automatiquement tous les fichiers JAR, en les empaquetant dans l'APK. Le processus de service charge ces classes au moment de l'exécution :

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

$S(PluginDir) est le répertoire dans lequel se trouve le fichier UPL. Placez-y le fichier JAR compilé. $S(BuildDir) est le répertoire de préparation.

Ces classes ne sont accessibles que via JNI et le fichier manifeste, sans aucun site d'appel Java. Elles doivent donc également être conservées dans <proguardAdditions>, sinon le réducteur les considérera comme inutilisées et les supprimera :

<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. Configurer un processus distinct

La limite du processus est activée par un attribut de fichier manifeste :

android:process=":downloader"

Le signe deux-points au début crée un processus privé à l'application nommé com.sample.psunreal:downloader. Il possède un PID différent de celui du processus principal et peut survivre après l'arrêt du processus principal.

2.1 Le processus de service n'a pas de Unreal

Pour éviter d'intégrer les bibliothèques natives d'Unreal, telles que libUE4.so ou libUE5.so, dans le processus de service, le code Java côté service ne doit pas utiliser de classes Unreal et ne doit dépendre que des extras Context, Intent et des API de plate-forme Android. L'environnement d'exécution du moteur Unreal n'est chargé que via GameActivity du processus principal. Le processus privé :downloader ne charge pas ces bibliothèques. Par conséquent, le référencement des classes Unreal ne fonctionne pas dans le processus de service.

2.2 Points d'entrée du démarrage du processus

Une fois que le processus principal appelle startForegroundService, le système duplique le processus de service :downloader :

  • Instancie Application et appelle Application.onCreate. Cette opération s'exécute dans chaque processus. L'initialisation nécessaire au processus de service doit donc être effectuée à nouveau ici (voir 2.3).
  • Crée DownloadService et appelle onCreate. Il s'agit du point d'entrée du processus de service.
  • Rappelle onStartCommand avec l'Intent construit au démarrage. Le service se promeut au premier plan et démarre le nœud de calcul (voir 3.1).

2.3 L'état existe une fois par processus

Le code Dex est partagé en lecture seule, mais l'état d'exécution ne l'est pas :

  • Application.attachBaseContext et Application.onCreate s'exécutent dans chaque processus qui héberge des composants d'application.
  • Les initialiseurs statiques et les champs statiques existent indépendamment dans chaque processus. L'attribution d'un champ statique dans le processus principal ne communique pas avec le service.
  • Unreal, C++ et les activités restent dans le processus principal.

3. Implémentation Java

Cette section couvre le côté Java du module FGS : 3.1 et 3.2 sont DownloadService dans le processus de service ; 3.3 est FgsBridge dans le processus principal (les appels C++ du point d'entrée à l'aide de JNI).

3.1 Promouvoir d'abord au premier plan

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 Ajouter la notification

Un service perceptible a besoin d'une notification d'activité en cours sur un canal de notification pour signaler la progression. La notification comporte une action d'arrêt qui envoie ACTION_STOP au service lui-même à l'aide de PendingIntent.getService pour l'arrêter.

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 Point d'entrée de l'hôte Java

FgsBridge est le point d'entrée Java que le processus principal utilise pour contrôler le FGS (il s'exécute dans le processus principal, et non dans le processus de service). C++ appelle ces méthodes statiques à l'aide de JNI pour démarrer et arrêter le service, et gérer l'autorisation de notification :

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. Démarrer et contrôler le FGS à partir d'Unreal (C++)

PSUnrealAndroidBridge est le wrapper C++ qui appelle FgsBridge à l'aide de JNI. Voici les trois implémentations de méthode :

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 est un assistant JNI interne qui appelle la méthode statique Java correspondante, en utilisant FJavaWrapper::GameActivityThis comme contexte. Pour vous assurer que la notification s'affiche immédiatement au démarrage du processus de service, appelez RequestNotificationPermission dans BeginPlay.