Esegui un servizio percettibile in un processo separato con Unreal

Questa guida illustra la configurazione e l'implementazione necessarie per eseguire un servizio in primo piano (FGS) di Android in un processo privato da un'applicazione Unreal.

1. Configurare il supporto del servizio in primo piano

Questa sezione spiega come configurare le autorizzazioni richieste e dichiarare il servizio nel file manifest del progetto.

1.1 Autorizzazioni e dichiarazione del servizio (aggiunte UPL)

Unreal non sostituisce il file manifest del motore. UnrealBuildTool genera già AndroidManifest.xml dichiarato GameActivity, quindi l'UPL deve solo aggiungere le autorizzazioni e il servizio. L'attività di avvio non deve essere ripetuta. Aggiungi quanto segue in <androidManifestUpdates> in 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 deve corrispondere al lavoro effettivo: dataSync per i trasferimenti, mediaPlayback per la riproduzione, location per il monitoraggio della posizione. Deve corrispondere all'autorizzazione FOREGROUND_SERVICE_<TYPE> corrispondente e alla startForeground chiamata. Tutti e tre devono essere coerenti, altrimenti l'avvio del servizio non riesce.

Il servizio utilizza il nome completo com.sample.fgs.DownloadService—un punto iniziale verrebbe risolto in relazione all'ID applicazione com.sample.psunreal, ma il modulo Java si trova nel pacchetto com.sample.fgs, quindi non verrà trovato.

1.2 Aggiungere il codice Java del processo di servizio al progetto Unreal

Compila il codice Java di implementazione del servizio in un file JAR e copialo nella directory delle librerie di staging utilizzando <prebuildCopies> di UPL. UEDeployAndroid copia le librerie di staging/ nel file app/libs/ del progetto Gradle e Gradle include automaticamente ogni file JAR, inserendolo nell'APK. Il processo di servizio carica queste classi in fase di runtime:

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

$S(PluginDir) è la directory in cui si trova il file UPL. Inserisci il file JAR compilato in questa directory. $S(BuildDir) è la directory di staging.

Queste classi sono raggiungibili solo tramite JNI e il file manifest, senza siti di chiamata Java, quindi devono essere mantenute anche in <proguardAdditions>, altrimenti il riduttore le considererà inutilizzate e le rimuoverà:

<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. Configurare un processo separato

Il limite del processo è abilitato da un attributo del file manifest:

android:process=":downloader"

Il punto iniziale crea un processo privato dell'applicazione denominato com.sample.psunreal:downloader. Ha un PID diverso dal processo principale e può sopravvivere dopo la terminazione del processo principale.

2.1 Il processo di servizio non ha Unreal

Per evitare di inserire le librerie native di Unreal, come libUE4.so o libUE5.so, nel processo di servizio, il codice Java lato servizio non deve utilizzare classi Unreal e deve dipendere solo da Context, Intent extra e API della piattaforma Android. Il runtime del motore Unreal viene caricato solo tramite GameActivity del processo principale. Il processo privato :downloader non carica queste librerie, quindi il riferimento alle classi Unreal non funziona nel processo di servizio.

2.2 Punti di ingresso per l'avvio del processo

Dopo che il processo principale chiama startForegroundService, il sistema esegue il fork del processo di servizio :downloader:

  • Crea un'istanza di Application e chiama Application.onCreate. Questa operazione viene eseguita in ogni processo, quindi l'inizializzazione necessaria per il processo di servizio deve essere eseguita di nuovo qui (vedi 2.3).
  • Crea DownloadService e chiama onCreate. Questo è il punto di ingresso del processo di servizio.
  • Richiama onStartCommand con Intent creato all'avvio. Il servizio viene promosso in primo piano e avvia il worker (vedi 3.1).

2.3 Lo stato esiste una volta per processo

Il codice Dex è condiviso in sola lettura, ma lo stato di runtime no:

  • Application.attachBaseContext e Application.onCreate vengono eseguiti in ogni processo che ospita i componenti dell'applicazione.
  • Gli inizializzatori statici e i campi statici esistono indipendentemente in ogni processo. L'assegnazione di un campo statico nel processo principale non comunica con il servizio.
  • Unreal, C++ e le attività rimangono nel processo principale.

3. Implementazione Java

Questa sezione illustra il lato Java del modulo FGS: 3.1 e 3.2 sono DownloadService nel processo di servizio; 3.3 è FgsBridge nel processo principale (il punto di ingresso che C++ chiama utilizzando JNI).

3.1 Promuovere prima in primo piano

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 Aggiungere la notifica

Un servizio in primo piano ha bisogno di una notifica continua su un canale di notifica per segnalare l'avanzamento. La notifica include un'azione di arresto che invia ACTION_STOP al servizio stesso utilizzando PendingIntent.getService per arrestarlo.

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 Punto di ingresso dell'host Java

FgsBridge è il punto di ingresso Java utilizzato dal processo principale per controllare l'FGS (viene eseguito nel processo principale, non nel processo di servizio). C++ chiama questi metodi statici utilizzando JNI per avviare e arrestare il servizio e gestire l'autorizzazione alle notifiche:

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. Avviare e controllare l'FGS da Unreal (C++)

PSUnrealAndroidBridge è il wrapper C++ che chiama FgsBridge utilizzando JNI. Ecco le tre implementazioni dei metodi:

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 è un helper JNI interno che chiama il metodo statico Java corrispondente, utilizzando FJavaWrapper::GameActivityThis come contesto. Per assicurarti che la notifica venga visualizzata immediatamente all'avvio del processo di servizio, chiama RequestNotificationPermission in BeginPlay.