Wahrnehmbaren Dienst in einem separaten Prozess mit Unreal ausführen

In dieser Anleitung werden die Konfiguration und Implementierung beschrieben, die erforderlich sind, um einen für den Nutzer sichtbaren Android-Dienst (Foreground Service, FGS) in einem privaten Prozess aus einer Unreal-Anwendung heraus auszuführen.

1. Unterstützung für sichtbare Dienste konfigurieren

In diesem Abschnitt wird erläutert, wie Sie die erforderlichen Berechtigungen einrichten und den Dienst im Manifest Ihres Projekts deklarieren.

1.1 Berechtigungen und Dienstdeklaration (UPL-Ergänzungen)

Unreal ersetzt das Engine-Manifest nicht. UnrealBuildTool generiert bereits AndroidManifest.xml, in dem GameActivity deklariert ist. Daher müssen mit der UPL nur Berechtigungen und der Dienst hinzugefügt werden. Die Launcher-Aktivität muss nicht neu deklariert werden. Fügen Sie in Source/PSUnreal/PSUnreal_UPL.xml unter <androidManifestUpdates> Folgendes hinzu:

<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 muss mit der tatsächlichen Arbeit übereinstimmen: dataSync für Übertragungen, mediaPlayback für die Wiedergabe und location für die Standortermittlung. Es muss mit der entsprechenden FOREGROUND_SERVICE_<TYPE> Berechtigung und dem startForeground Aufruf übereinstimmen. Alle drei müssen konsistent sein, andernfalls schlägt der Dienststart fehl.

Der Dienst verwendet den voll qualifizierten Namen com.sample.fgs.DownloadService—ein vorangestellter Punkt würde relativ zur Anwendungs-ID com.sample.psunreal aufgelöst, aber das Java-Modul befindet sich im Paket com.sample.fgs, daher würde es nicht gefunden.

1.2 Java für den Dienstprozess zum Unreal-Projekt hinzufügen

Kompilieren Sie den Java-Code für die Dienstimplementierung in eine JAR-Datei und kopieren Sie sie mit <prebuildCopies> der UPL in das Staging-Verzeichnis für Bibliotheken. UEDeployAndroid kopiert die Staging-Bibliotheken in app/libs/ des Gradle-Projekts und Gradle fügt automatisch alle JAR-Dateien dort ein und packt sie in die APK. Der Dienstprozess lädt diese Klassen zur Laufzeit:

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

$S(PluginDir) ist das Verzeichnis, in dem sich die UPL-Datei befindet. Platzieren Sie dort die kompilierte JAR-Datei. $S(BuildDir) ist das Staging-Verzeichnis.

Diese Klassen sind nur über JNI und das Manifest erreichbar, ohne Java-Aufrufstellen. Sie müssen daher auch in <proguardAdditions> beibehalten werden, andernfalls betrachtet der Shrinker sie als ungenutzt und entfernt sie:

<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. Separaten Prozess konfigurieren

Die Prozessgrenze wird durch ein Manifestattribut aktiviert:

android:process=":downloader"

Der vorangestellte Doppelpunkt erstellt einen anwendungsprivaten Prozess namens com.sample.psunreal:downloader. Er hat eine andere PID als der Hauptprozess und kann auch nach Beendigung des Hauptprozesses weiterlaufen.

2.1 Der Dienstprozess enthält kein Unreal

Damit die nativen Bibliotheken von Unreal wie libUE4.so oder libUE5.so nicht in den Dienstprozess aufgenommen werden, sollte Java auf der Dienstseite keine Unreal-Klassen verwenden und nur von Android-Extras für Context und Intent sowie von Plattform-APIs abhängig sein. Die Unreal-Engine-Laufzeit wird nur über die GameActivity des Hauptprozesses geladen. Der private Prozess :downloader lädt diese Bibliotheken nicht, daher funktioniert das Verweisen auf Unreal-Klassen im Dienstprozess nicht.

2.2 Einstiegspunkte für den Prozessstart

Nachdem der Hauptprozess startForegroundService aufgerufen hat, verzweigt das System den Dienstprozess :downloader:

  • Application wird instanziiert und Application.onCreate aufgerufen. Dies wird in jedem Prozess ausgeführt, daher muss die für den Dienstprozess erforderliche Initialisierung hier noch einmal erfolgen (siehe 2.3).
  • DownloadService wird erstellt und onCreate aufgerufen. Dies ist der Einstiegspunkt für den Dienstprozess.
  • onStartCommand wird mit dem beim Start erstellten Intent zurückgerufen. Der Dienst wird hier in den Vordergrund verschoben und der Worker gestartet (siehe 3.1).

2.3 Status ist einmal pro Prozess vorhanden

Dex-Code wird schreibgeschützt freigegeben, der Laufzeitstatus jedoch nicht:

  • Application.attachBaseContext und Application.onCreate werden in jedem Prozess ausgeführt, der Anwendungskomponenten hostet.
  • Statische Initialisierer und statische Felder sind in jedem Prozess unabhängig voneinander vorhanden. Wenn Sie ein statisches Feld im Hauptprozess zuweisen, wird dies nicht an den Dienst weitergegeben.
  • Unreal, C++ und Aktivitäten bleiben im Hauptprozess.

3. Java-Implementierung

In diesem Abschnitt wird die Java-Seite des FGS-Moduls behandelt: 3.1 und 3.2 sind DownloadService im Dienstprozess, 3.3 ist FgsBridge im Hauptprozess (der Einstiegspunkt, den C++ mit JNI aufruft).

3.1 Zuerst in den Vordergrund verschieben

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 Benachrichtigung hinzufügen

Ein sichtbarer Dienst benötigt eine Benachrichtigung über laufende Aktivitäten auf einem Benachrichtigungskanal, um den Fortschritt zu melden. Die Benachrichtigung enthält eine Aktion zum Beenden, die ACTION_STOP mit PendingIntent.getService an den Dienst selbst sendet, um ihn zu beenden.

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 Einstiegspunkt für den Java-Host

FgsBridge ist der Java-Einstiegspunkt, den der Hauptprozess verwendet, um den FGS zu steuern. Er wird im Hauptprozess und nicht im Dienstprozess ausgeführt. C++ ruft diese statischen Methoden mit JNI auf, um den Dienst zu starten und zu beenden und die Berechtigung zum Senden von Benachrichtigungen zu verarbeiten:

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. FGS aus Unreal (C++) starten und steuern

PSUnrealAndroidBridge ist der C++-Wrapper, der FgsBridge mit JNI aufruft. Hier sind die drei Methodenimplementierungen:

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 ist ein interner JNI-Helfer, der die entsprechende statische Java-Methode aufruft und FJavaWrapper::GameActivityThis als Kontext verwendet. Damit die Benachrichtigung sofort angezeigt wird, wenn der Dienstprozess gestartet wird, rufen Sie RequestNotificationPermission in BeginPlay auf.