Executar um serviço perceptível em um processo separado com o Unity

Este guia aborda a configuração e a implementação necessárias para executar um serviço perceptível do Android (serviço em primeiro plano ou FGS, na sigla em inglês) em um processo particular de um aplicativo Unity.

1. Configurar o suporte a serviços perceptíveis

Esta seção explica como configurar as permissões necessárias e declarar o serviço no manifesto do projeto.

1.1 Permissões e declaração de serviço

O personalizado Assets/Plugins/Android/AndroidManifest.xml precisa declarar a atividade do inicializador, as permissões de serviço perceptível, a permissão de notificação, a permissão de rede e o serviço:

<?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 precisa corresponder ao trabalho real: dataSync para transferências, mediaPlayback para reprodução, location para rastreamento de localização. Ele precisa concordar com a permissão FOREGROUND_SERVICE_<TYPE> correspondente e a startForeground chamada. Os três precisam ser consistentes, ou a inicialização do serviço falha.

1.2 Adicionar o Java de processo de serviço ao projeto do Unity

O processo de serviço do FGS executa o Java. Empacote o código Java de implementação do serviço em um arquivo JAR e coloque-o em Assets/Plugins/Android/. O Unity inclui automaticamente arquivos JAR nesse diretório na entrada libs/ do Gradle e os empacota no APK. O processo de serviço carrega essas classes no ambiente de execução. Para mais informações sobre como implementar o serviço em Java ou Kotlin, consulte Implementação em Java.

2. Configurar um processo separado

O limite do processo é ativado por um atributo de manifesto:

android:process=":downloader"

Os dois pontos iniciais criam um processo particular do aplicativo chamado your.package.name:downloader. Ele tem um PID diferente do processo principal e pode sobreviver após o encerramento do processo principal.

2.1 O processo de serviço não tem Unity

Para evitar trazer libunity.so, o ambiente de execução do IL2CPP e semelhantes para o processo de serviço, o Java do lado do serviço não deve usar nenhuma classe do Unity (incluindo UnityPlayer.currentActivity) e só deve depender do contexto do Android, extras de intent e APIs da plataforma. O ambiente de execução do mecanismo do Unity é carregado apenas pela UnityPlayerActivity do processo principal. O processo :downloader particular não carrega essas bibliotecas. Portanto, a referência a classes do Unity não funciona no processo de serviço.

2.2 Pontos de entrada de inicialização do processo

Depois que o processo principal chama startForegroundService, o sistema ramifica o processo de serviço :downloader:

  • Instancia Application e chama Application.onCreate. Isso é executado em todos os processos. Portanto, a inicialização necessária pelo processo de serviço precisa ser feita novamente aqui. Para mais informações, consulte a seção 2.3.
  • Cria DownloadService e chama onCreate. Esse é o ponto de entrada do processo de serviço.
  • Retorna a chamada onStartCommand com a intent construída na inicialização. O serviço se promove ao primeiro plano aqui e inicia o worker. Para mais informações, consulte a seção 3.1.

2.3 O estado existe uma vez por processo

O código Dex é compartilhado somente leitura, mas o estado do ambiente de execução não é:

  • Application.attachBaseContext e Application.onCreate são executados em todos os processos que hospedam componentes do aplicativo.
  • Os inicializadores estáticos e os campos estáticos existem de forma independente em cada processo. A atribuição de um campo estático no processo principal não se comunica com o serviço.
  • O Unity, o C# e as atividades permanecem no processo principal.

3. Implementação em Java

Esta seção aborda o lado Java do módulo FGS: 3.1 e 3.2 são DownloadService no processo de serviço; 3.3 é FgsBridge no processo principal (as chamadas C# do ponto de entrada usando JNI).

3.1 Promover para o primeiro plano primeiro

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 Adicionar a notificação

Um serviço perceptível precisa de uma notificação em andamento em um canal de notificação para informar o progresso. A notificação tem uma ação de parada que envia ACTION_STOP para o próprio serviço usando PendingIntent.getService para interrompê-lo.

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

FgsBridge é o ponto de entrada Java que o processo principal usa para controlar o FGS (ele é executado no processo principal, não no processo de serviço). O C# chama esses métodos estáticos usando JNI para iniciar e interromper o serviço e processar a permissão de notificação:

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. Iniciar e controlar o FGS no Unity (C#)

AndroidBridge é o wrapper C# que chama FgsBridge usando JNI. As três implementações de método:

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 é um auxiliar JNI interno que resolve a classe FgsBridge e chama o método estático Java correspondente. RequestNotificationPermission precisa ser chamado na inicialização do aplicativo para que a notificação apareça imediatamente quando o processo de serviço for iniciado.