เรียกใช้บริการที่รับรู้ได้ในกระบวนการแยกต่างหากด้วย Unreal

คู่มือนี้ครอบคลุมการกำหนดค่าและการติดตั้งใช้งานที่จำเป็นในการเรียกใช้บริการที่รับรู้ได้ (FGS) ของ Android ในกระบวนการส่วนตัวจากแอปพลิเคชัน Unreal

1. กำหนดค่าการรองรับบริการที่รับรู้ได้

ส่วนนี้อธิบายวิธีตั้งค่าสิทธิ์ที่จำเป็นและประกาศบริการในไฟล์ Manifest ของโปรเจ็กต์

1.1 การประกาศสิทธิ์และบริการ (การเพิ่ม UPL)

Unreal ไม่ได้แทนที่ไฟล์ Manifest ของเอนจิน โดย 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เรียก ซึ่งทั้ง 3 รายการต้องสอดคล้องกัน ไม่เช่นนั้นการเริ่มต้นบริการ จะล้มเหลว

บริการใช้ชื่อที่สมบูรณ์ในตัวเอง com.sample.fgs.DownloadService ซึ่งจุดนำหน้าจะแก้ปัญหาที่เกี่ยวข้องกับรหัสแอปพลิเคชัน com.sample.psunreal แต่โมดูล Java อยู่ในแพ็กเกจ com.sample.fgs จึงไม่พบ

1.2 เพิ่ม Java ของกระบวนการบริการลงในโปรเจ็กต์ Unreal

คอมไพล์โค้ด Java ของการติดตั้งใช้งานบริการเป็นไฟล์ Jar แล้วคัดลอกลงในไดเรกทอรี libs ที่จัดเตรียมไว้โดยใช้ <prebuildCopies> ของ UPL โดย UEDeployAndroid จะคัดลอก 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 และไฟล์ Manifest เท่านั้น โดยไม่มีการเรียก 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. กำหนดค่ากระบวนการแยกต่างหาก

การเปิดใช้ขอบเขตกระบวนการทำได้โดยใช้แอตทริบิวต์ไฟล์ Manifest 1 รายการ ดังนี้

android:process=":downloader"

โคลอนนำหน้าจะสร้างกระบวนการส่วนตัวของแอปพลิเคชันชื่อ com.sample.psunreal:downloader โดยมี PID ที่แตกต่างจากกระบวนการหลักและสามารถทำงานต่อไปได้หลังจากกระบวนการหลักสิ้นสุดลง

2.1 กระบวนการบริการไม่มี Unreal

Java ฝั่งบริการไม่ควรใช้คลาส Unreal ใดๆ และควรขึ้นอยู่กับ Context ของ Android, ข้อมูลเพิ่มเติมของ Intent และ API ของแพลตฟอร์มเท่านั้น เพื่อหลีกเลี่ยงการนำไลบรารีเนทีฟของ Unreal เช่น libUE4.so หรือ libUE5.so เข้าสู่กระบวนการบริการ รันไทม์ของเอนจิน Unreal จะโหลดผ่าน GameActivity ของกระบวนการหลักเท่านั้น กระบวนการ :downloader ส่วนตัวจะไม่โหลดไลบรารีเหล่านี้ ดังนั้นการอ้างอิงคลาส Unreal จึงใช้ไม่ได้ในกระบวนการบริการ

2.2 จุดแรกเข้าของการเริ่มต้นกระบวนการ

หลังจากกระบวนการหลักเรียก startForegroundService แล้ว ระบบจะแยกกระบวนการบริการ :downloader ดังนี้

  • สร้างอินสแตนซ์ Application และเรียก Application.onCreate ซึ่งจะทำงาน ในทุกกระบวนการ ดังนั้นจึงต้องทำการเริ่มต้นที่กระบวนการบริการจำเป็นต้องใช้อีกครั้งที่นี่ (ดู 2.3)
  • สร้าง DownloadService และเรียก onCreate ซึ่งเป็นจุดแรกเข้าของกระบวนการบริการ
  • เรียกกลับ onStartCommand ด้วย Intent ที่สร้างขึ้นเมื่อเริ่มต้น บริการจะเลื่อนระดับตัวเองไปทำงานอยู่เบื้องหน้าและเริ่ม Worker ที่นี่ (ดู 3.1)

2.3 สถานะมีอยู่ 1 ครั้งต่อกระบวนการ

โค้ด Dex จะแชร์แบบอ่านอย่างเดียว แต่สถานะรันไทม์จะไม่แชร์

  • Application.attachBaseContext และ Application.onCreate จะทำงานในทุกกระบวนการที่โฮสต์คอมโพเนนต์ของแอปพลิเคชัน
  • ตัวเริ่มต้นแบบคงที่และฟิลด์แบบคงที่จะมีอยู่แยกกันในแต่ละกระบวนการ การกำหนดฟิลด์แบบคงที่ในกระบวนการหลักจะไม่สื่อสารกับบริการ
  • Unreal, C++ และ Activity จะยังคงอยู่ในกระบวนการหลัก

3. การติดตั้งใช้งาน Java

ส่วนนี้ครอบคลุมโมดูล FGS ฝั่ง Java โดย 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 คือ Wrapper C++ ที่เรียก FgsBridge โดยใช้ JNI การติดตั้งใช้งานเมธอด 3 รายการมีดังนี้

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 เพื่อให้การแจ้งเตือนปรากฏขึ้นทันทีเมื่อกระบวนการบริการเริ่มต้น