Unreal के साथ, अलग प्रोसेस में ऐसी सेवा चलाएं जिसे समझा जा सके

इस गाइड में, Unreal ऐप्लिकेशन से निजी प्रोसेस में, Android की ऐसी सेवा (एफ़जीएस) को चलाने के लिए ज़रूरी कॉन्फ़िगरेशन और लागू करने के तरीके के बारे में बताया गया है जिसे उपयोगकर्ता देख सकते हैं.

1. उपयोगकर्ता को दिखने वाली सेवा के लिए, सहायता कॉन्फ़िगर करना

इस सेक्शन में, ज़रूरी अनुमतियां सेट अप करने और अपने प्रोजेक्ट के मेनिफ़ेस्ट में सेवा का एलान करने का तरीका बताया गया है.

1.1 अनुमतियां और सेवा का एलान (यूपीएल में जोड़े गए एलिमेंट)

Unreal, इंजन मेनिफ़ेस्ट को नहीं बदलता. UnrealBuildTool, AndroidManifest.xml को पहले से जनरेट करता है, जिसमें GameActivity का एलान किया गया होता है. इसलिए, यूपीएल को सिर्फ़ अनुमतियां और सेवा जोड़ने की ज़रूरत होती है. लॉन्चर ऐक्टिविटी को दोबारा बताने की ज़रूरत नहीं होती. Source/PSUnreal/PSUnreal_UPL.xml में, <androidManifestUpdates> के तहत यह कोड जोड़ें:

<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 Unreal प्रोजेक्ट में, सेवा-प्रोसेस Java जोड़ना

सेवा लागू करने वाले Java कोड को jar में कंपाइल करें और यूपीएल के <prebuildCopies> का इस्तेमाल करके, इसे स्टेजिंग libs डायरेक्ट्री में कॉपी करें. UEDeployAndroid, स्टेजिंग libs/ को Gradle प्रोजेक्ट के app/libs/ में कॉपी करता है. Gradle, वहां मौजूद हर jar को अपने-आप शामिल कर लेता है और उन्हें एपीके में पैकेज कर देता है. सेवा प्रोसेस, रनटाइम पर इन क्लास को लोड करती है:

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

$S(PluginDir) वह डायरेक्ट्री है जिसमें यूपीएल फ़ाइल मौजूद होती है. कंपाइल की गई jar को वहां रखें. $S(BuildDir) स्टेजिंग डायरेक्ट्री है.

इन क्लास तक सिर्फ़ जेएनआई और मेनिफ़ेस्ट के ज़रिए पहुंचा जा सकता है. इनमें 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 नाम की ऐप्लिकेशन-प्राइवेट प्रोसेस बनाता है. इसका पीआईडी, मुख्य प्रोसेस से अलग होता है. साथ ही, मुख्य प्रोसेस के बंद होने के बाद भी यह चालू रह सकती है.

2.1 सेवा प्रोसेस में Unreal का इस्तेमाल नहीं किया जाता

सेवा प्रोसेस में, Unreal की नेटिव लाइब्रेरी (जैसे, libUE4.so या libUE5.so) को शामिल न करने के लिए, सेवा-साइड Java को Unreal की किसी भी क्लास का इस्तेमाल नहीं करना चाहिए. साथ ही, इसे सिर्फ़ Android Context, Intent एक्स्ट्रा, और प्लैटफ़ॉर्म एपीआई पर निर्भर रहना चाहिए. Unreal इंजन रनटाइम को सिर्फ़ मुख्य प्रोसेस की GameActivity के ज़रिए लोड किया जाता है. प्राइवेट :downloader प्रोसेस, इन लाइब्रेरी को लोड नहीं करती. इसलिए, सेवा प्रोसेस में Unreal क्लास का रेफ़रंस देने से कोई फ़ायदा नहीं होता.

2.2 प्रोसेस स्टार्टअप एंट्री पॉइंट

मुख्य प्रोसेस के startForegroundService को कॉल करने के बाद, सिस्टम :downloader सेवा प्रोसेस को फ़ोर्क करता है:

  • Application को इंस्टैंशिएट करता है और Application.onCreate को कॉल करता है. यह हर प्रोसेस में चलता है. इसलिए, सेवा प्रोसेस के लिए ज़रूरी इनिशियलाइज़ेशन यहां दोबारा करना होगा (2.3 देखें).
  • DownloadService बनाता है और onCreate को कॉल करता है. यह सेवा-प्रोसेस का एंट्री पॉइंट है.
  • स्टार्टअप के दौरान बनाए गए Intent के साथ, onStartCommand को वापस कॉल करता है. सेवा यहां खुद को फ़ोरग्राउंड में प्रमोट करती है और वर्कर को शुरू करती है (3.1 देखें).

2.3 हर प्रोसेस के लिए एक बार स्टेट मौजूद होती है

Dex कोड को सिर्फ़ पढ़ा जा सकता है. हालांकि, रनटाइम स्टेट को नहीं:

  • Application.attachBaseContext और Application.onCreate, ऐप्लिकेशन के कॉम्पोनेंट को होस्ट करने वाली हर प्रोसेस में चलते हैं.
  • स्टैटिक इनिशियलाइज़र और स्टैटिक फ़ील्ड, हर प्रोसेस में अलग-अलग मौजूद होते हैं. मुख्य प्रोसेस में स्टैटिक फ़ील्ड असाइन करने से, सेवा के साथ कोई कम्यूनिकेशन नहीं होता.
  • Unreal, C++, और गतिविधियां मुख्य प्रोसेस में बनी रहती हैं.

3. Java को लागू करना

इस सेक्शन में, एफ़जीएस मॉड्यूल के Java साइड के बारे में बताया गया है: 3.1 और 3.2, सेवा प्रोसेस में DownloadService हैं. वहीं, 3.3, मुख्य प्रोसेस में FgsBridge है (एंट्री पॉइंट C++, जेएनआई का इस्तेमाल करके कॉल करता है).

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 का एंट्री पॉइंट है. इसका इस्तेमाल मुख्य प्रोसेस, एफ़जीएस को कंट्रोल करने के लिए करती है. यह सेवा प्रोसेस में नहीं, बल्कि मुख्य प्रोसेस में चलता है. C++, जेएनआई का इस्तेमाल करके इन स्टैटिक तरीकों को कॉल करता है, ताकि सेवा को शुरू और बंद किया जा सके. साथ ही, सूचना की अनुमति को मैनेज किया जा सके:

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. Unreal (C++) से एफ़जीएस को शुरू और कंट्रोल करना

PSUnrealAndroidBridge, C++ रैपर है. यह जेएनआई का इस्तेमाल करके FgsBridge को कॉल करता है. यहां तीन तरीकों को लागू करने के बारे में बताया गया है:

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 , इंटरनल जेएनआई हेल्पर है. यह FJavaWrapper::GameActivityThis को कॉन्टेक्स्ट के तौर पर इस्तेमाल करके, Java के स्टैटिक तरीके को कॉल करता है. यह पक्का करने के लिए कि सेवा प्रोसेस शुरू होने पर, सूचना तुरंत दिखे, BeginPlay में RequestNotificationPermission को कॉल करें.