Ten przewodnik omawia konfigurację i implementację potrzebną do uruchomienia w prywatnym procesie usługi widocznej dla użytkownika na Androidzie (FGS) z aplikacji Unreal.
1. Konfigurowanie obsługi usługi widocznej dla użytkownika
W tej sekcji dowiesz się, jak skonfigurować wymagane uprawnienia i zadeklarować usługę w pliku manifestu projektu.
1.1 Deklaracja uprawnień i usługi (dodatki UPL)
Unreal nie zastępuje manifestu silnika – UnrealBuildTool generuje plik AndroidManifest.xml, w którym jest już zadeklarowana GameActivity, więc UPL musi tylko dodać uprawnienia i usługę. Nie trzeba ponownie deklarować aktywności uruchamiającej. Dodaj ten kod w sekcji <androidManifestUpdates> w pliku 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 musi odpowiadać rzeczywistej pracy: dataSync w przypadku przesyłania, mediaPlayback w przypadku odtwarzania, location w przypadku śledzenia lokalizacji. Musi być zgodny
z odpowiednim uprawnieniem FOREGROUND_SERVICE_<TYPE> i wywołaniem
startForeground – wszystkie 3 muszą być spójne, w przeciwnym razie uruchomienie usługi
się nie powiedzie.
Usługa używa pełnej i jednoznacznej nazwy com.sample.fgs.DownloadService – a kropka na początku spowodowałaby rozwiązanie względem identyfikatora aplikacji com.sample.psunreal, ale moduł Java znajduje się w pakiecie com.sample.fgs, więc nie zostałby znaleziony.
1.2 Dodawanie kodu Java procesu usługi do projektu Unreal
Skompiluj kod Java implementacji usługi do pliku JAR i skopiuj go do katalogu bibliotek przejściowych za pomocą <prebuildCopies> w UPL. UEDeployAndroid kopiuje biblioteki przejściowe do katalogu app/libs/ w projekcie Gradle, a Gradle automatycznie uwzględnia wszystkie pliki JAR w tym katalogu, pakując je do pliku APK. Proces usługi wczytuje te klasy w czasie działania:
<prebuildCopies>
<copyFile src="$S(PluginDir)/fgs-android.jar"
dst="$S(BuildDir)/libs/fgs-android.jar" />
</prebuildCopies>
$S(PluginDir) to katalog, w którym znajduje się plik UPL – umieść w nim skompilowany plik JAR. $S(BuildDir) to katalog przejściowy.
Dostęp do tych klas jest możliwy tylko przez JNI i manifest, bez wywołań Java, więc muszą one być też przechowywane w <proguardAdditions>. W przeciwnym razie narzędzie do zmniejszania kodu uzna je za nieużywane i usunie:
<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. Konfigurowanie osobnego procesu
Granica procesu jest włączana przez 1 atrybut manifestu:
android:process=":downloader"
Dwukropek na początku tworzy proces prywatny aplikacji o nazwie com.sample.psunreal:downloader. Ma on inny PID niż proces główny i może działać po zakończeniu procesu głównego.
2.1 Proces usługi nie ma dostępu do Unreal
Aby uniknąć przenoszenia do procesu usługi natywnych bibliotek Unreal, takich jak libUE4.so czy libUE5.so, kod Java po stronie usługi nie powinien używać żadnych klas Unreal i powinien zależeć tylko od Context Androida, dodatków Intent i interfejsów API platformy. Środowisko wykonawcze silnika Unreal jest wczytywane tylko przez GameActivity procesu głównego. Proces prywatny :downloader nie wczytuje tych bibliotek, więc odwoływanie się do klas Unreal nie działa w procesie usługi.
2.2 Punkty wejścia uruchamiania procesu
Gdy proces główny wywoła startForegroundService, system rozwidla proces usługi :downloader:
- Tworzy instancję
Applicationi wywołujeApplication.onCreate– dzieje się to w każdym procesie, więc inicjalizację potrzebną procesowi usługi trzeba wykonać ponownie (patrz sekcja 2.3). - Tworzy
DownloadServicei wywołujeonCreate– jest to punkt wejścia procesu usługi. - Wywołuje zwrotnie
onStartCommandz elementemIntentutworzonym podczas uruchamiania. Usługa przechodzi na pierwszy plan i uruchamia proces roboczy (patrz sekcja 3.1).
2.3 Stan istnieje raz na proces
Kod Dex jest udostępniany tylko do odczytu, ale stan środowiska wykonawczego nie jest:
Application.attachBaseContextiApplication.onCreatesą uruchamiane w każdym procesie, który hostuje komponenty aplikacji.- Inicjatory statyczne i pola statyczne istnieją niezależnie w każdym procesie. Przypisanie pola statycznego w procesie głównym nie komunikuje się z usługą.
- Unreal, C++ i aktywności pozostają w procesie głównym.
3. Implementacja Java
Ta sekcja omawia stronę Java modułu FGS: 3.1 i 3.2 to DownloadService w procesie usługi, a 3.3 to FgsBridge w procesie głównym (punkt wejścia wywołań C++ za pomocą JNI).
3.1 Najpierw przejdź na pierwszy plan
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 Dodawanie powiadomienia
Usługa widoczna dla użytkownika potrzebuje powiadomienia o trwającej aktywności w kanale powiadomień, aby informować o postępach. Powiadomienie zawiera działanie Stop, które wysyła ACTION_STOP do samej usługi za pomocą PendingIntent.getService, aby ją zatrzymać.
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 Punkt wejścia hosta Java
FgsBridge to punkt wejścia Java, którego proces główny używa do sterowania FGS (działa w procesie głównym, a nie w procesie usługi). C++ wywołuje te metody statyczne za pomocą JNI, aby uruchamiać i zatrzymywać usługę oraz obsługiwać zgodę na wyświetlanie powiadomień:
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. Uruchamianie FGS i sterowanie nim z Unreal (C++)
PSUnrealAndroidBridge to otoka C++, która wywołuje FgsBridge za pomocą JNI.
Oto 3 implementacje metod:
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 to wewnętrzna funkcja pomocnicza JNI, która wywołuje odpowiednią statyczną metodę Java, używając FJavaWrapper::GameActivityThis jako kontekstu.
Aby powiadomienie pojawiło się natychmiast po uruchomieniu procesu usługi, wywołaj RequestNotificationPermission w BeginPlay.