В этом руководстве описаны настройка и реализация, необходимые для запуска службы, воспринимаемой Android (FGS), в приватном процессе из приложения Unreal.
1. Настройте поддержку воспринимаемого сервиса.
В этом разделе объясняется, как настроить необходимые права доступа и объявить сервис в манифесте вашего проекта.
1.1 Заявление о правах доступа и предоставлении услуг (дополнения UPL)
Unreal не заменяет манифест движка — UnrealBuildTool генерирует AndroidManifest.xml, в котором уже объявлена GameActivity, поэтому UPL нужно только добавить разрешения и службу; Activity запуска не нужно переопределять. Добавьте следующее в раздел <androidManifestUpdates> в 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 должен соответствовать выполняемой задаче: dataSync для передачи данных, mediaPlayback для воспроизведения, location для отслеживания местоположения. Он должен соответствовать соответствующему разрешению FOREGROUND_SERVICE_<TYPE> и вызову startForeground — все три параметра должны быть согласованы, иначе запуск службы завершится неудачей.
Сервис использует полное имя com.sample.fgs.DownloadService — точка в начале имени позволила бы определить его относительно идентификатора приложения com.sample.psunreal , но модуль Java находится в пакете com.sample.fgs , поэтому он не будет найден.
1.2 Добавьте Java-код сервис-процесса в проект Unreal.
Скомпилируйте Java-код реализации сервиса в JAR-файл и скопируйте его в каталог staging libs, используя функцию <prebuildCopies> из UPL. UEDeployAndroid скопирует staging libs/ в app/libs/ проекта Gradle, и Gradle автоматически включит все находящиеся там JAR-файлы, упаковав их в APK; процесс сервиса загружает эти классы во время выполнения:
<prebuildCopies>
<copyFile src="$S(PluginDir)/fgs-android.jar"
dst="$S(BuildDir)/libs/fgs-android.jar" />
</prebuildCopies>
$S(PluginDir) — это каталог, в котором находится файл UPL — поместите скомпилированный jar-файл туда; $S(BuildDir) — это каталог для временного хранения.
Доступ к этим классам осуществляется только через JNI и манифест, без вызовов Java, поэтому их также необходимо хранить в <proguardAdditions>, иначе программа сжатия посчитает их неиспользуемыми и удалит:
<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. Настройте отдельный процесс.
Граница процесса обеспечивается одним атрибутом манифеста:
android:process=":downloader"
Двоеточие в начале имени создает частный процесс приложения с именем com.sample.psunreal:downloader . Он имеет другой PID, отличный от основного процесса, и может продолжать работу после завершения основного процесса.
2.1 В процессе обслуживания отсутствует Unreal Engine.
Чтобы избежать использования собственных библиотек Unreal Engine, таких как libUE4.so или libUE5.so , в процессе сервиса, Java-код на стороне сервиса не должен использовать никакие классы Unreal Engine и должен зависеть только от Context Android, дополнительных элементов Intent и API платформы. Среда выполнения Unreal Engine загружается только через GameActivity основного процесса; частный процесс :downloader не загружает эти библиотеки, поэтому ссылки на классы Unreal Engine не работают в процессе сервиса.
2.2 Точки входа в процесс запуска
После того, как основной процесс вызовет startForegroundService , система создаст дочерний процесс службы :downloader :
- Создает экземпляр
Applicationи вызываетApplication.onCreate— это выполняется в каждом процессе, поэтому инициализация, необходимая для процесса службы, должна быть выполнена здесь снова (см. 2.3). - Создает
DownloadServiceи вызываетonCreate— это точка входа в процесс службы. - Вызывает
onStartCommandсIntent, сформированным при запуске. В этот момент служба переходит в активный режим и запускает рабочий процесс (см. 3.1).
2.3 Состояние существует один раз за процесс
Код Dex является общим и доступен только для чтения, но состояние во время выполнения — нет:
-
Application.attachBaseContextиApplication.onCreateвыполняются в каждом процессе, в котором размещены компоненты приложения. - Статические инициализаторы и статические поля существуют независимо в каждом процессе. Присвоение значения статическому полю в основном процессе не взаимодействует со службой.
- Unreal Engine, C++ и Activity остаются в основном процессе.
3. Реализация на Java
В этом разделе рассматривается Java-часть модуля FGS: 3.1 и 3.2 — это DownloadService в процессе службы; 3.3 — это FgsBridge в основном процессе (точка входа, вызываемая из C++ с использованием JNI).
3.1 Сначала вывести на передний план.
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 Добавить уведомление
Для корректной работы сервиса необходимо постоянное уведомление по каналу уведомлений. Уведомление содержит действие «Стоп», которое отправляет ACTION_STOP самому сервису с помощью PendingIntent.getService для его остановки.
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 Точка входа Java-хоста
FgsBridge — это точка входа в Java, используемая основным процессом для управления FGS (она работает в основном процессе, а не в процессе службы). В C++ эти статические методы вызываются с помощью JNI для запуска и остановки службы, а также для обработки разрешений на уведомления:
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. Запуск и управление FGS из Unreal (C++)
PSUnrealAndroidBridge — это обертка на C++, которая вызывает FgsBridge с использованием JNI. Вот три реализации методов:
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 — это внутренний вспомогательный метод JNI, который вызывает соответствующий статический метод Java, используя FJavaWrapper::GameActivityThis в качестве контекста. Чтобы гарантировать немедленное появление уведомления при запуске процесса службы, вызовите RequestNotificationPermission в BeginPlay .