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

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

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

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

1.1 การประกาศสิทธิ์และบริการ

ที่กำหนดเอง Assets/Plugins/Android/AndroidManifest.xml ต้องประกาศกิจกรรมตัวเรียกใช้ สิทธิ์บริการที่รับรู้ได้ สิทธิ์การแจ้งเตือน สิทธิ์เครือข่าย และบริการ ดังนี้

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        <activity
            android:name="com.unity3d.player.UnityPlayerActivity"
            android:theme="@style/UnityThemeSelector"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>

        <service
            android:name="com.sample.fgs.DownloadService"
            android:process=":downloader"
            android:exported="false"
            android:stopWithTask="false"
            android:foregroundServiceType="dataSync" />
    </application>
</manifest>

foregroundServiceType ต้องตรงกับงานจริง เช่น dataSync สำหรับการโอน mediaPlayback สำหรับการเล่น location สำหรับการติดตามตำแหน่ง โดยต้องสอดคล้องกับสิทธิ์ ที่เกี่ยวข้อง FOREGROUND_SERVICE_<TYPE>และการเรียก startForeground ซึ่งทั้ง 3 อย่างต้องสอดคล้องกัน ไม่เช่นนั้นบริการจะเริ่มต้น ไม่สำเร็จ

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

กระบวนการบริการ FGS จะเรียกใช้ Java แพ็กเกจโค้ด Java ของการใช้งานบริการลงในไฟล์ Jar แล้ววางไว้ใน Assets/Plugins/Android/ Unity จะรวมไฟล์ Jar ในไดเรกทอรีนั้นลงในอินพุต libs/ ของ Gradle และแพ็กเกจลงใน APK โดยอัตโนมัติ ซึ่งกระบวนการบริการจะโหลดคลาสเหล่านี้ในรันไทม์ ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีใช้งานบริการใน Java หรือ Kotlin ได้ที่ ดู การใช้งาน Java

2. กำหนดค่ากระบวนการแยกต่างหาก

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

android:process=":downloader"

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

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

เพื่อหลีกเลี่ยงการนำ libunity.so ของ Unity, รันไทม์ IL2CPP และสิ่งที่คล้ายกันมาไว้ในกระบวนการบริการ Java ฝั่งบริการจึงไม่ควรใช้คลาส Unity ใดๆ (รวมถึง UnityPlayer.currentActivity) และควรขึ้นอยู่กับบริบท Android, ส่วนเพิ่มเติมของ Intent และ API ของแพลตฟอร์มเท่านั้น ระบบจะโหลดรันไทม์ของเอนจิน Unity ผ่าน UnityPlayerActivity ของกระบวนการหลักเท่านั้น โดยกระบวนการ :downloader ส่วนตัวจะไม่โหลดไลบรารีเหล่านี้ ดังนั้นการอ้างอิงคลาส Unity จึงใช้ไม่ได้ในกระบวนการบริการ

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

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

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

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

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

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

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 จาก Unity (C#)

AndroidBridge คือ Wrapper C# ที่เรียก FgsBridge โดยใช้ JNI การใช้งานเมธอด 3 รายการ

public static void StartDownloadService()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    CallStatic("startDownloadService");
#else
    Debug.Log("StartDownloadService() no-op outside Android");
#endif
}

public static void StopDownloadService()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    CallStatic("stopDownloadService");
#else
    Debug.Log("StopDownloadService() no-op outside Android");
#endif
}

public static void RequestNotificationPermission()
{
#if UNITY_ANDROID && !UNITY_EDITOR
    CallStatic("requestNotificationPermission");
#else
    Debug.Log("RequestNotificationPermission() no-op outside Android");
#endif
}

CallStatic คือตัวช่วย JNI ภายในที่แก้ปัญหาคลาส FgsBridge และเรียกใช้เมธอดแบบคงที่ของ Java ที่เกี่ยวข้อง ควรเรียก RequestNotificationPermission เมื่อเริ่มต้นแอปพลิเคชันเพื่อให้การแจ้งเตือนปรากฏขึ้นทันทีเมื่อกระบวนการบริการเริ่มต้น