Запустите видимый сервис в отдельном процессе с помощью Unreal Engine.

В этом руководстве описаны настройка и реализация, необходимые для запуска службы, воспринимаемой 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 .