Menjalankan layanan yang dapat dirasakan dalam proses terpisah dengan Unreal

Panduan ini membahas konfigurasi dan implementasi yang diperlukan untuk menjalankan layanan yang dapat dirasakan (FGS) Android dalam proses pribadi dari aplikasi Unreal.

1. Mengonfigurasi dukungan layanan yang dapat dirasakan

Bagian ini menjelaskan cara menyiapkan izin yang diperlukan dan mendeklarasikan layanan dalam manifes project Anda.

1.1 Deklarasi izin dan layanan (penambahan UPL)

Unreal tidak mengganti manifes mesin—UnrealBuildTool menghasilkan GameActivity yang sudah dideklarasikan AndroidManifest.xml, sehingga UPL hanya perlu menambahkan izin dan layanan; Aktivitas peluncur tidak perlu dinyatakan kembali. Tambahkan hal berikut di bagian <androidManifestUpdates> di 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 harus cocok dengan pekerjaan sebenarnya: dataSync untuk transfer, mediaPlayback untuk pemutaran, location untuk pelacakan lokasi. Izin ini harus sesuai dengan izin FOREGROUND_SERVICE_<TYPE> yang sesuai dan panggilan startForeground—ketiganya harus konsisten, atau startup layanan akan gagal.

Layanan ini menggunakan nama yang sepenuhnya memenuhi syarat com.sample.fgs.DownloadService—titik awal akan di-resolve relatif terhadap ID aplikasi com.sample.psunreal, tetapi modul Java berada dalam paket com.sample.fgs sehingga tidak akan ditemukan.

1.2 Menambahkan Java proses layanan ke project Unreal

Kompilasi kode Java implementasi layanan ke dalam jar dan salin ke direktori libs penahapan menggunakan <prebuildCopies> UPL. UEDeployAndroid menyalin libs penahapan/ ke app/libs/ project Gradle, dan Gradle otomatis menyertakan setiap jar di sana, mengemasnya ke dalam APK; proses layanan memuat class ini saat runtime:

<prebuildCopies>
    <copyFile src="$S(PluginDir)/fgs-android.jar"
              dst="$S(BuildDir)/libs/fgs-android.jar" />
</prebuildCopies>

$S(PluginDir) adalah direktori tempat file UPL berada—tempatkan jar yang dikompilasi di sana; $S(BuildDir) adalah direktori penahapan.

Class ini hanya dapat dijangkau melalui JNI dan manifes, tanpa situs panggilan Java, sehingga class ini juga harus disimpan di <proguardAdditions>, atau alat penyusut akan menganggapnya tidak digunakan dan menghapusnya:

<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. Mengonfigurasi proses terpisah

Batas proses diaktifkan oleh satu atribut manifes:

android:process=":downloader"

Titik dua di awal membuat proses khusus aplikasi bernama com.sample.psunreal:downloader. Proses ini memiliki PID yang berbeda dari proses utama dan dapat bertahan setelah proses utama dihentikan.

2.1 Proses layanan tidak memiliki Unreal

Untuk menghindari penggunaan library native Unreal, seperti libUE4.so atau libUE5.so, ke dalam proses layanan, Java sisi layanan tidak boleh menggunakan class Unreal apa pun, dan hanya boleh bergantung pada Android Context, tambahan Intent, dan API platform. Runtime mesin Unreal hanya dimuat melalui GameActivity proses utama; proses :downloader pribadi tidak memuat library ini, sehingga mereferensikan class Unreal tidak berfungsi dalam proses layanan.

2.2 Titik entri startup proses

Setelah proses utama memanggil startForegroundService, sistem akan membuat proses layanan :downloader:

  • Membuat instance Application dan memanggil Application.onCreate—proses ini berjalan di setiap proses, sehingga inisialisasi yang diperlukan oleh proses layanan harus dilakukan lagi di sini (lihat 2.3).
  • Membuat DownloadService dan memanggil onCreate—ini adalah titik entri proses layanan.
  • Memanggil kembali onStartCommand dengan Intent yang dibuat saat startup. Layanan ini mempromosikan dirinya ke latar depan di sini dan memulai pekerja (lihat 3.1).

2.3 Status ada satu kali per proses

Kode Dex dibagikan hanya baca, tetapi status runtime tidak:

  • Application.attachBaseContext dan Application.onCreate berjalan di setiap proses yang menghosting komponen aplikasi.
  • Penginisialisasi statis dan kolom statis ada secara independen di setiap proses. Menetapkan kolom statis dalam proses utama tidak berkomunikasi dengan layanan.
  • Unreal, C++, dan Aktivitas tetap berada dalam proses utama.

3. Implementasi Java

Bagian ini membahas sisi Java dari modul FGS: 3.1 dan 3.2 adalah DownloadService dalam proses layanan; 3.3 adalah FgsBridge dalam proses utama (panggilan C++ titik entri menggunakan JNI).

3.1 Promosikan ke latar depan terlebih dahulu

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 Menambahkan notifikasi

Layanan yang dapat dirasakan memerlukan notifikasi berkelanjutan di saluran notifikasi untuk melaporkan progres. Notifikasi ini membawa tindakan Berhenti yang mengirimkan ACTION_STOP ke layanan itu sendiri menggunakan PendingIntent.getService untuk menghentikannya.

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 Titik entri host Java

FgsBridge adalah titik entri Java yang digunakan proses utama untuk mengontrol FGS (berjalan dalam proses utama, bukan proses layanan). C++ memanggil metode statis ini menggunakan JNI untuk memulai dan menghentikan layanan serta menangani izin notifikasi:

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. Memulai dan mengontrol FGS dari Unreal (C++)

PSUnrealAndroidBridge adalah wrapper C++ yang memanggil FgsBridge menggunakan JNI. Berikut adalah tiga implementasi metode:

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 adalah helper JNI internal yang memanggil metode statis Java yang sesuai, menggunakan FJavaWrapper::GameActivityThis sebagai Konteks. Untuk memastikan notifikasi segera muncul saat proses layanan dimulai, panggil RequestNotificationPermission di BeginPlay.