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
Applicatione chamaApplication.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
DownloadServicee chamaonCreate. Esse é o ponto de entrada do processo de serviço. - Retorna a chamada
onStartCommandcom aIntentconstruí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.attachBaseContexteApplication.onCreatesã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.