Chạy một dịch vụ có thể nhận biết trong một quy trình riêng biệt bằng Unreal

Hướng dẫn này trình bày cấu hình và cách triển khai cần thiết để chạy một dịch vụ có thể nhận biết (FGS) trên Android trong một quy trình riêng tư từ ứng dụng Unreal.

1. Định cấu hình chế độ hỗ trợ perceptible-service

Phần này giải thích cách thiết lập các quyền bắt buộc và khai báo dịch vụ trong tệp kê khai của dự án.

1.1 Tuyên bố về quyền và dịch vụ (nội dung bổ sung cho UPL)

Unreal không thay thế tệp kê khai công cụ – UnrealBuildTool đã tạo AndroidManifest.xml đã khai báo GameActivity, vì vậy UPL chỉ cần thêm các quyền và dịch vụ; Launcher Activity không cần phải được khai báo lại. Thêm nội dung sau vào <androidManifestUpdates> trong 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 phải khớp với hoạt động thực tế: dataSync cho hoạt động chuyển, mediaPlayback cho hoạt động phát, location cho hoạt động theo dõi vị trí. Nó phải đồng ý với quyền FOREGROUND_SERVICE_<TYPE> tương ứng và lệnh gọi startForeground – cả ba phải nhất quán, nếu không quá trình khởi động dịch vụ sẽ thất bại.

Dịch vụ này sử dụng tên đủ điều kiện com.sample.fgs.DownloadService – dấu chấm ở đầu sẽ phân giải tương ứng với mã ứng dụng com.sample.psunreal, nhưng mô-đun Java nằm trong gói com.sample.fgs nên sẽ không tìm thấy.

1.2 Thêm Java quy trình dịch vụ vào dự án Unreal

Biên dịch mã Java triển khai dịch vụ thành một tệp jar và sao chép tệp đó vào thư mục libs dàn dựng bằng <prebuildCopies> của UPL. UEDeployAndroid sao chép libs dàn dựng vào app/libs/ của dự án Gradle và Gradle sẽ tự động thêm mọi tệp jar vào đó, đóng gói chúng vào APK; quy trình dịch vụ sẽ tải các lớp này trong thời gian chạy:

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

$S(PluginDir) là thư mục chứa tệp UPL – hãy đặt tệp jar đã biên dịch vào đó; $S(BuildDir) là thư mục dàn dựng.

Các lớp này chỉ được truy cập thông qua JNI và tệp kê khai, không có vị trí gọi Java, vì vậy, chúng cũng phải được giữ trong <proguardAdditions>, nếu không, trình rút gọn sẽ coi chúng là không dùng đến và loại bỏ chúng:

<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. Định cấu hình một quy trình riêng biệt

Ranh giới quy trình được bật bằng một thuộc tính tệp kê khai:

android:process=":downloader"

Dấu hai chấm ở đầu sẽ tạo một quy trình dành riêng cho ứng dụng có tên là com.sample.psunreal:downloader. Nó có PID khác với quy trình chính và có thể tồn tại sau khi quy trình chính kết thúc.

2.1 Quy trình dịch vụ không có Unreal

Để tránh đưa các thư viện gốc của Unreal, chẳng hạn như libUE4.so hoặc libUE5.so, vào quy trình dịch vụ, Java phía dịch vụ không được dùng bất kỳ lớp Unreal nào và chỉ được phụ thuộc vào các tiện ích bổ sung Context, Intent của Android và các API nền tảng. Thời gian chạy của công cụ Unreal chỉ được tải thông qua GameActivity của quy trình chính; quy trình :downloader riêng tư không tải các thư viện này, vì vậy, việc tham chiếu các lớp Unreal sẽ không hoạt động trong quy trình dịch vụ.

2.2 Điểm truy cập khởi động quy trình

Sau khi quy trình chính gọi startForegroundService, hệ thống sẽ phân nhánh quy trình dịch vụ :downloader:

  • Tạo thực thể Application và gọi Application.onCreate – thao tác này chạy trong mọi quy trình, vì vậy, quá trình khởi chạy mà quy trình dịch vụ cần phải thực hiện lại tại đây (xem phần 2.3).
  • Tạo DownloadService và gọi onCreate – đây là điểm truy cập vào quy trình dịch vụ.
  • Gọi lại onStartCommand bằng Intent được tạo khi khởi động. Dịch vụ này tự quảng bá lên nền trước tại đây và khởi động trình chạy (xem phần 3.1).

2.3 Trạng thái chỉ tồn tại một lần cho mỗi quy trình

Mã Dex được chia sẻ ở chế độ chỉ đọc, nhưng trạng thái thời gian chạy thì không:

  • Application.attachBaseContextApplication.onCreate chạy trong mọi quy trình lưu trữ các thành phần ứng dụng.
  • Trình khởi tạo tĩnh và trường tĩnh tồn tại độc lập trong mỗi quy trình. Việc chỉ định một trường tĩnh trong quy trình chính không giao tiếp với dịch vụ.
  • Unreal, C++ và các Hoạt động vẫn nằm trong quy trình chính.

3. Triển khai Java

Phần này đề cập đến phía Java của mô-đun FGS: 3.1 và 3.2 là DownloadService trong quy trình dịch vụ; 3.3 là FgsBridge trong quy trình chính (các lệnh gọi C++ tại điểm truy cập bằng JNI).

3.1 Quảng bá lên nền trước trước

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 Thêm thông báo

Một dịch vụ mà người dùng có thể nhận biết cần có một thông báo hiển thị liên tục trên một kênh thông báo để báo cáo tiến trình. Thông báo này có một thao tác Dừng, thao tác này sẽ gửi ACTION_STOP đến chính dịch vụ bằng cách dùng PendingIntent.getService để dừng dịch vụ.

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 Điểm truy cập vào máy chủ Java

FgsBridge là điểm truy cập Java mà quy trình chính dùng để kiểm soát FGS (chạy trong quy trình chính, chứ không phải quy trình dịch vụ). C++ gọi các phương thức tĩnh này bằng JNI để bắt đầu và dừng dịch vụ, đồng thời xử lý quyền gửi thông báo:

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. Bắt đầu và kiểm soát FGS từ Unreal (C++)

PSUnrealAndroidBridge là trình bao bọc C++ gọi FgsBridge bằng JNI. Sau đây là 3 cách triển khai phương thức:

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 là một trình trợ giúp JNI nội bộ gọi phương thức tĩnh Java tương ứng, sử dụng FJavaWrapper::GameActivityThis làm Ngữ cảnh. Để đảm bảo thông báo xuất hiện ngay khi quy trình dịch vụ bắt đầu, hãy gọi RequestNotificationPermission trong BeginPlay.