Questa guida illustra la configurazione e l'implementazione necessarie per eseguire un servizio in primo piano (FGS) di Android in un processo privato da un'applicazione Unreal.
1. Configurare il supporto del servizio in primo piano
Questa sezione spiega come configurare le autorizzazioni richieste e dichiarare il servizio nel file manifest del progetto.
1.1 Autorizzazioni e dichiarazione del servizio (aggiunte UPL)
Unreal non sostituisce il file manifest del motore. UnrealBuildTool genera già AndroidManifest.xml dichiarato GameActivity, quindi l'UPL deve solo aggiungere le autorizzazioni e il servizio. L'attività di avvio non deve essere ripetuta. Aggiungi quanto segue in <androidManifestUpdates> in 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 deve corrispondere al lavoro effettivo: dataSync per i trasferimenti, mediaPlayback per la riproduzione, location per il monitoraggio della posizione. Deve corrispondere
all'autorizzazione FOREGROUND_SERVICE_<TYPE> corrispondente e alla
startForeground chiamata. Tutti e tre devono essere coerenti, altrimenti l'avvio del servizio
non riesce.
Il servizio utilizza il nome completo com.sample.fgs.DownloadService—un
punto iniziale verrebbe risolto in relazione all'ID applicazione com.sample.psunreal,
ma il modulo Java si trova nel pacchetto com.sample.fgs, quindi non verrà
trovato.
1.2 Aggiungere il codice Java del processo di servizio al progetto Unreal
Compila il codice Java di implementazione del servizio in un file JAR e copialo nella directory delle librerie di staging utilizzando <prebuildCopies> di UPL. UEDeployAndroid copia le librerie di staging/ nel file app/libs/ del progetto Gradle e Gradle include automaticamente ogni file JAR, inserendolo nell'APK. Il processo di servizio carica queste classi in fase di runtime:
<prebuildCopies>
<copyFile src="$S(PluginDir)/fgs-android.jar"
dst="$S(BuildDir)/libs/fgs-android.jar" />
</prebuildCopies>
$S(PluginDir) è la directory in cui si trova il file UPL. Inserisci il file JAR compilato in questa directory. $S(BuildDir) è la directory di staging.
Queste classi sono raggiungibili solo tramite JNI e il file manifest, senza siti di chiamata Java, quindi devono essere mantenute anche in <proguardAdditions>, altrimenti il riduttore le considererà inutilizzate e le rimuoverà:
<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. Configurare un processo separato
Il limite del processo è abilitato da un attributo del file manifest:
android:process=":downloader"
Il punto iniziale crea un processo privato dell'applicazione denominato com.sample.psunreal:downloader. Ha un PID diverso dal processo principale e può sopravvivere dopo la terminazione del processo principale.
2.1 Il processo di servizio non ha Unreal
Per evitare di inserire le librerie native di Unreal, come libUE4.so o libUE5.so, nel processo di servizio, il codice Java lato servizio non deve utilizzare classi Unreal e deve dipendere solo da Context, Intent extra e API della piattaforma Android. Il runtime del motore Unreal viene caricato solo tramite GameActivity del processo principale. Il processo privato :downloader non carica queste librerie, quindi il riferimento alle classi Unreal non funziona nel processo di servizio.
2.2 Punti di ingresso per l'avvio del processo
Dopo che il processo principale chiama startForegroundService, il sistema esegue il fork del processo di servizio :downloader:
- Crea un'istanza di
Applicatione chiamaApplication.onCreate. Questa operazione viene eseguita in ogni processo, quindi l'inizializzazione necessaria per il processo di servizio deve essere eseguita di nuovo qui (vedi 2.3). - Crea
DownloadServicee chiamaonCreate. Questo è il punto di ingresso del processo di servizio. - Richiama
onStartCommandconIntentcreato all'avvio. Il servizio viene promosso in primo piano e avvia il worker (vedi 3.1).
2.3 Lo stato esiste una volta per processo
Il codice Dex è condiviso in sola lettura, ma lo stato di runtime no:
Application.attachBaseContexteApplication.onCreatevengono eseguiti in ogni processo che ospita i componenti dell'applicazione.- Gli inizializzatori statici e i campi statici esistono indipendentemente in ogni processo. L'assegnazione di un campo statico nel processo principale non comunica con il servizio.
- Unreal, C++ e le attività rimangono nel processo principale.
3. Implementazione Java
Questa sezione illustra il lato Java del modulo FGS: 3.1 e 3.2 sono DownloadService nel processo di servizio; 3.3 è FgsBridge nel processo principale (il punto di ingresso che C++ chiama utilizzando JNI).
3.1 Promuovere prima in primo piano
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 Aggiungere la notifica
Un servizio in primo piano ha bisogno di una notifica continua su un canale di notifica per segnalare l'avanzamento. La notifica include un'azione di arresto che invia ACTION_STOP al servizio stesso utilizzando PendingIntent.getService per arrestarlo.
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 di ingresso dell'host Java
FgsBridge è il punto di ingresso Java utilizzato dal processo principale per controllare l'FGS (viene eseguito nel processo principale, non nel processo di servizio). C++ chiama questi metodi statici utilizzando JNI per avviare e arrestare il servizio e gestire l'autorizzazione alle notifiche:
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. Avviare e controllare l'FGS da Unreal (C++)
PSUnrealAndroidBridge è il wrapper C++ che chiama FgsBridge utilizzando JNI.
Ecco le tre implementazioni dei metodi:
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 è un helper JNI interno che chiama il metodo statico Java corrispondente, utilizzando FJavaWrapper::GameActivityThis come contesto.
Per assicurarti che la notifica venga visualizzata immediatamente all'avvio del processo di servizio, chiama RequestNotificationPermission in BeginPlay.