Ejecuta un servicio perceptible en un proceso independiente con Unity

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

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

El Assets/Plugins/Android/AndroidManifest.xml personalizado debe declarar la actividad del selector, los permisos de servicio perceptible, el permiso de notificación, el permiso de red y el servicio:

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

1.2 Agrega el Java del proceso de servicio al proyecto de Unity

El proceso de servicio de FGS ejecuta Java. Empaqueta el código Java de implementación del servicio en un archivo jar y colócalo en Assets/Plugins/Android/. Unity incluye automáticamente archivos jar en ese directorio en la entrada libs/ de Gradle y los empaqueta en el APK. El proceso de servicio carga estas clases en el tiempo de ejecución. Para obtener más información sobre cómo implementar el servicio en Java o Kotlin, consulta Implementación en Java.

2. Configura un proceso independiente

El límite del proceso se habilita con un atributo de manifiesto:

android:process=":downloader"

Los dos puntos iniciales crean un proceso privado de la aplicación llamado your.package.name: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 Unity

Para evitar incorporar libunity.so, el tiempo de ejecución de IL2CPP y elementos similares al proceso de servicio, el Java del servicio no debe usar ninguna clase de Unity (incluida UnityPlayer.currentActivity) y solo debe depender de Android Context, los extras de Intent y las APIs de la plataforma. El tiempo de ejecución del motor de Unity se carga solo a través de UnityPlayerActivity del proceso principal. El proceso privado :downloader no carga estas bibliotecas, por lo que hacer referencia a las clases de Unity 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í. Para obtener más información, consulta la sección 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 a sí mismo al primer plano y comienza el trabajador. Para obtener más información, consulta la sección 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. La asignación de un campo estático en el proceso principal no se comunica con el servicio.
  • Unity, C# y las actividades permanecen en el proceso principal.

3. Implementación en Java

En esta sección, se abarca 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 en sí 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 Unity (C#)

AndroidBridge es el wrapper de C# que llama a FgsBridge con JNI. Las tres implementaciones de métodos:

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 es un auxiliar interno de JNI que resuelve la clase FgsBridge y llama al método estático de Java correspondiente. Se debe llamar a RequestNotificationPermission al inicio de la aplicación para que la notificación aparezca de inmediato cuando se inicie el proceso de servicio.