Ejecuta un servicio perceptible en un proceso separado con Unreal

En esta guía, se explica la configuración y la implementación necesarias para ejecutar un servicio perceptible de Android (FGS) en un proceso privado desde una aplicación de Unreal.

1. Configura la compatibilidad con el servicio perceptible

En esta sección, se explica cómo configurar los permisos necesarios y declarar el servicio en el manifiesto de tu proyecto.

1.1 Permisos y declaración de servicio (adiciones de UPL)

Unreal no reemplaza el manifiesto del motor. UnrealBuildTool genera AndroidManifest.xml que ya declaró GameActivity, por lo que la UPL solo necesita agregar permisos y el servicio. No es necesario volver a declarar la actividad del selector. Agrega lo siguiente en <androidManifestUpdates> en 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 debe coincidir con el trabajo real: dataSync para las transferencias, mediaPlayback para la reproducción y location para el seguimiento de la ubicación. Debe coincidir con el permiso FOREGROUND_SERVICE_<TYPE> correspondiente y la startForeground llamada. Los tres deben ser coherentes o fallará el inicio del servicio .

El servicio usa el nombre completamente calificado com.sample.fgs.DownloadService—un punto inicial se resolvería en relación con el ID de aplicación com.sample.psunreal, pero el módulo de Java reside en el paquete com.sample.fgs, por lo que no se encontraría.

1.2 Agrega el proceso de servicio Java al proyecto de Unreal

Compila el código Java de implementación del servicio en un archivo jar y cópialo en el directorio libs de etapa de pruebas con <prebuildCopies> de UPL. UEDeployAndroid copia libs de etapa de pruebas en app/libs/ del proyecto de Gradle, y Gradle incluye automáticamente cada archivo jar allí y los empaqueta en el APK. El proceso de servicio carga estas clases en el tiempo de ejecución:

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

$S(PluginDir) es el directorio en el que se encuentra el archivo UPL. Coloca allí el archivo jar compilado. $S(BuildDir) es el directorio de etapa de pruebas.

Se accede a estas clases solo a través de JNI y el manifiesto, sin sitios de llamadas de Java, por lo que también deben conservarse en <proguardAdditions>, o el reductor las considerará sin usar y las quitará:

<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. Configura un proceso independiente

El límite del proceso está habilitado por un atributo de manifiesto:

android:process=":downloader"

Los dos puntos iniciales crean un proceso privado de la aplicación llamado com.sample.psunreal:downloader. Tiene un PID diferente del proceso principal y puede sobrevivir después de que finaliza el proceso principal.

2.1 El proceso de servicio no tiene Unreal

Para evitar incorporar las bibliotecas nativas de Unreal, como libUE4.so o libUE5.so, en el proceso de servicio, el Java del lado del servicio no debe usar ninguna clase de Unreal y solo debe depender de los extras Context y Intent de Android, y de las APIs de la plataforma. El tiempo de ejecución del motor de Unreal se carga solo a través de GameActivity del proceso principal. El proceso privado :downloader no carga estas bibliotecas, por lo que hacer referencia a las clases de Unreal no funciona en el proceso de servicio.

2.2 Puntos de entrada de inicio del proceso

Después de que el proceso principal llama a startForegroundService, el sistema bifurca el proceso de servicio :downloader:

  • Crea una instancia de Application y llama a Application.onCreate. Esto se ejecuta en cada proceso, por lo que la inicialización que necesita el proceso de servicio debe volver a realizarse aquí (consulta 2.3).
  • Crea DownloadService y llama a onCreate. Este es el punto de entrada del proceso de servicio.
  • Vuelve a llamar a onStartCommand con el Intent construido al inicio. El servicio se promueve al primer plano aquí y comienza el trabajador (consulta 3.1).

2.3 El estado existe una vez por proceso

El código Dex se comparte como de solo lectura, pero el estado del tiempo de ejecución no:

  • Application.attachBaseContext y Application.onCreate se ejecutan en cada proceso que aloja componentes de la aplicación.
  • Los inicializadores estáticos y los campos estáticos existen de forma independiente en cada proceso. Asignar un campo estático en el proceso principal no se comunica con el servicio.
  • Unreal, C++ y las actividades permanecen en el proceso principal.

3. Implementación en Java

En esta sección, se aborda el lado Java del módulo FGS: 3.1 y 3.2 son DownloadService en el proceso de servicio; 3.3 es FgsBridge en el proceso principal (las llamadas de C++ del punto de entrada que usan JNI).

3.1 Primero, promueve al primer plano

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 Agrega la notificación

Un servicio perceptible necesita una notificación continua en un canal de notificaciones para informar el progreso. La notificación incluye una acción de detención que envía ACTION_STOP al servicio con PendingIntent.getService para detenerlo.

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 de entrada del host de Java

FgsBridge es el punto de entrada de Java que usa el proceso principal para controlar el FGS (se ejecuta en el proceso principal, no en el proceso de servicio). C++ llama a estos métodos estáticos con JNI para iniciar y detener el servicio, y controlar el permiso de notificación:

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. Inicia y controla el FGS desde Unreal (C++)

PSUnrealAndroidBridge es el wrapper de C++ que llama a FgsBridge con JNI. Estas son las tres implementaciones de métodos:

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 es un auxiliar interno de JNI que llama al método estático de Java correspondiente y usa FJavaWrapper::GameActivityThis como el contexto. Para asegurarte de que la notificación aparezca de inmediato cuando se inicia el proceso de servicio, llama a RequestNotificationPermission en BeginPlay.