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

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

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

Le fichier personnalisé Assets/Plugins/Android/AndroidManifest.xml doit déclarer l'activité de lancement, les autorisations de service perceptible, l'autorisation de notification, l'autorisation réseau et le service :

<?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 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.

1.2 Ajouter le processus de service Java au projet Unity

Le processus de service FGS exécute Java. Empaquetez le code Java d'implémentation du service dans un fichier JAR et placez-le sous Assets/Plugins/Android/. Unity inclut automatiquement les fichiers JAR de ce répertoire dans l'entrée libs/ de Gradle et les empaquète dans l'APK. Le processus de service charge ces classes au moment de l'exécution. Pour en savoir plus sur l'implémentation du service en Java ou Kotlin, consultez Implémentation Java.

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 en début de ligne crée un processus privé à l'application nommé your.package.name: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 Unity

Pour éviter d'intégrer libunity.so, l'environnement d'exécution IL2CPP et d'autres éléments similaires dans le processus de service, le Java côté service ne doit pas utiliser de classes Unity (y compris UnityPlayer.currentActivity) et ne doit dépendre que du contexte Android, des extras d'intent et des API de plate-forme. L'environnement d'exécution du moteur Unity n'est chargé que via UnityPlayerActivity du processus principal. Le processus privé :downloader ne charge pas ces bibliothèques. Par conséquent, le référencement des classes Unity 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 requise par le processus de service doit donc être effectuée à nouveau ici. Pour en savoir plus, consultez la section 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 créé au démarrage. Le service se promeut au premier plan et démarre le nœud de calcul. Pour en savoir plus, consultez la section 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.
  • Unity, 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'Unity (C#)

AndroidBridge est le wrapper C# qui appelle FgsBridge à l'aide de JNI. Les trois implémentations de méthode :

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 est un helper JNI interne qui résout la classe FgsBridge et appelle la méthode statique Java correspondante. RequestNotificationPermission doit être appelé au démarrage de l'application afin que la notification s'affiche immédiatement au démarrage du processus de service.