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

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

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 (adições da UPL)

O Unreal não substitui o manifesto do mecanismo. O UnrealBuildTool gera o AndroidManifest.xml já declarado GameActivity. Portanto, a UPL só precisa adicionar permissões e o serviço. A atividade do inicializador não precisa ser declarada novamente. Adicione o seguinte em <androidManifestUpdates> em 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 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 falhará.

O serviço usa o nome totalmente qualificado com.sample.fgs.DownloadService—um ponto inicial seria resolvido em relação ao ID do aplicativo com.sample.psunreal, mas o módulo Java reside no pacote com.sample.fgs, então ele não seria encontrado.

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

Compile o código Java de implementação do serviço em um arquivo jar e copie-o para o diretório de bibliotecas de preparação usando <prebuildCopies> da UPL. O UEDeployAndroid copia as bibliotecas de preparação/ para o app/libs/ do projeto do Gradle, e o Gradle inclui automaticamente todos os arquivos jar, empacotando-os no APK. O processo de serviço carrega essas classes no momento da execução:

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

$S(PluginDir) é o diretório em que o arquivo UPL está localizado. Coloque o arquivo jar compilado lá. $S(BuildDir) é o diretório de preparação.

Essas classes são acessadas apenas pelo JNI e pelo manifesto, sem sites de chamada Java. Portanto, elas também precisam ser mantidas em <proguardAdditions>, ou o redutor as considerará não utilizadas e as removerá:

<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. 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 com.sample.psunreal: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 o Unreal

Para evitar trazer bibliotecas nativas do Unreal, como libUE4.so ou libUE5.so, para o processo de serviço, o Java do lado do serviço não deve usar nenhuma classe do Unreal e só deve depender de extras Context, Intent e APIs da plataforma do Android. O ambiente de execução do mecanismo do Unreal é carregado apenas pela GameActivity do processo principal. O processo :downloader particular não carrega essas bibliotecas. Portanto, a referência a classes do Unreal 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 (consulte 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 (consulte 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 Unreal, 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 interrupção 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

O 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 do Unreal (C++)

PSUnrealAndroidBridge é o wrapper C++ que chama FgsBridge usando JNI. Confira as três implementações de método:

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 é um auxiliar JNI interno que chama o método estático Java correspondente, usando FJavaWrapper::GameActivityThis como o contexto. Para garantir que a notificação apareça imediatamente quando o processo de serviço for iniciado, chame RequestNotificationPermission em BeginPlay.