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
Applicationy llama aApplication.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
DownloadServicey llama aonCreate. Este es el punto de entrada del proceso de servicio. - Vuelve a llamar a
onStartCommandcon 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.attachBaseContextyApplication.onCreatese 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.