Unreal ব্যবহার করে একটি বোধগম্য পরিষেবা আলাদা প্রসেসে চালান।

এই নির্দেশিকায় একটি আনরিয়েল অ্যাপ্লিকেশন থেকে একটি প্রাইভেট প্রসেসে অ্যান্ড্রয়েড পারসেপ্টিবল সার্ভিস (FGS) চালানোর জন্য প্রয়োজনীয় কনফিগারেশন এবং বাস্তবায়ন সম্পর্কে আলোচনা করা হয়েছে।

১. উপলব্ধিযোগ্য-পরিষেবা সমর্থন কনফিগার করুন

এই অংশে প্রয়োজনীয় অনুমতিগুলো কীভাবে সেট আপ করতে হয় এবং আপনার প্রোজেক্টের ম্যানিফেস্টে সার্ভিসটি কীভাবে ঘোষণা করতে হয়, তা ব্যাখ্যা করা হয়েছে।

১.১ অনুমতি ও পরিষেবা ঘোষণা (ইউপিএল সংযোজন)

Unreal ইঞ্জিন ম্যানিফেস্ট প্রতিস্থাপন করে না—UnrealBuildTool দ্বারা তৈরি AndroidManifest.xml ফাইলে GameActivity আগে থেকেই ঘোষিত থাকে, তাই UPL-এ শুধুমাত্র পারমিশন এবং সার্ভিস যোগ করতে হয়; লঞ্চার অ্যাক্টিভিটি পুনরায় উল্লেখ করার প্রয়োজন নেই। 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 অ্যাপ্লিকেশন আইডির সাপেক্ষে রেজলভ হতো, কিন্তু জাভা মডিউলটি com.sample.fgs প্যাকেজে থাকে, তাই এটি খুঁজে পাওয়া যেত না।

১.২ আনরিয়েল প্রজেক্টে সার্ভিস-প্রসেস জাভা যোগ করুন

সার্ভিস ইমপ্লিমেন্টেশন জাভা কোড কম্পাইল করে একটি jar ফাইলে পরিণত করুন এবং UPL-এর <prebuildCopies> ব্যবহার করে এটিকে স্টেজিং libs ডিরেক্টরিতে কপি করুন। UEDeployAndroid স্টেজিং libs/ ডিরেক্টরিটিকে গ্রেডল প্রজেক্টের app/libs/-এ কপি করে, এবং গ্রেডল স্বয়ংক্রিয়ভাবে সেখানকার প্রতিটি 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 এবং ম্যানিফেস্টের মাধ্যমে অ্যাক্সেস করা যায়, কোনো জাভা কল সাইট নেই, তাই এগুলোকেও অবশ্যই <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>

২. একটি পৃথক প্রক্রিয়া কনফিগার করুন

প্রসেস বাউন্ডারিটি একটি ম্যানিফেস্ট অ্যাট্রিবিউটের মাধ্যমে সক্রিয় করা হয়:

android:process=":downloader"

শুরুতে কোলন (vol) দিলে com.sample.psunreal:downloader নামের একটি অ্যাপ্লিকেশন-প্রাইভেট প্রসেস তৈরি হয়। এটির পিআইডি (PID) মূল প্রসেস থেকে আলাদা এবং মূল প্রসেস বন্ধ হয়ে যাওয়ার পরেও এটি চালু থাকতে পারে।

২.১ পরিষেবা প্রক্রিয়ার কোনো আনরিয়েল নেই

সার্ভিস প্রসেসে আনরিয়েলের নেটিভ লাইব্রেরি, যেমন libUE4.so বা libUE5.so , আনা এড়ানোর জন্য, সার্ভিস-সাইড জাভার কোনো আনরিয়েল ক্লাস ব্যবহার করা উচিত নয় এবং শুধুমাত্র অ্যান্ড্রয়েড Context , Intent এক্সট্রাস ও প্ল্যাটফর্ম এপিআই-এর উপর নির্ভর করা উচিত। আনরিয়েল ইঞ্জিন রানটাইম শুধুমাত্র মেইন প্রসেসের GameActivity এর মাধ্যমে লোড হয়; প্রাইভেট :downloader প্রসেস এই লাইব্রেরিগুলো লোড করে না, তাই সার্ভিস প্রসেসে আনরিয়েল ক্লাস রেফারেন্স করা কাজ করে না।

২.২ প্রক্রিয়া শুরুর প্রবেশ বিন্দু

মূল প্রসেস startForegroundService কল করার পর, সিস্টেম :downloader সার্ভিস প্রসেসটি ফর্ক করে:

  • Application ইনস্ট্যানশিয়েট করে এবং Application.onCreate কল করে — এটি প্রতিটি প্রসেসে চলে, তাই সার্ভিস প্রসেসের জন্য প্রয়োজনীয় ইনিশিয়ালাইজেশন এখানে আবার করতে হবে (২.৩ দেখুন)।
  • DownloadService তৈরি করে এবং onCreate কল করে —এটি সার্ভিস-প্রসেসের এন্ট্রি পয়েন্ট।
  • স্টার্টআপের সময় তৈরি করা Intent সহ onStartCommand কলব্যাক করে। এখানে সার্ভিসটি নিজেকে ফোরগ্রাউন্ডে নিয়ে আসে এবং ওয়ার্কারটি চালু করে (দেখুন ৩.১)।

২.৩ প্রতিটি প্রসেসের জন্য স্টেট একবার বিদ্যমান থাকে।

ডেক্স কোড শুধুমাত্র পঠনযোগ্য হিসেবে শেয়ার করা হয়, কিন্তু রানটাইম স্টেট তা নয়:

  • Application.attachBaseContext এবং Application.onCreate সেইসব প্রতিটি প্রসেসে রান করে যেগুলো অ্যাপ্লিকেশন কম্পোনেন্টগুলো হোস্ট করে।
  • স্ট্যাটিক ইনিশিয়ালাইজার এবং স্ট্যাটিক ফিল্ড প্রতিটি প্রসেসে স্বাধীনভাবে বিদ্যমান থাকে। মূল প্রসেসে একটি স্ট্যাটিক ফিল্ড অ্যাসাইন করলে তা সার্ভিসের সাথে যোগাযোগ করে না।
  • Unreal, C++, এবং Activities মূল প্রসেসেই থেকে যায়।

৩. জাভা বাস্তবায়ন

এই অংশে FGS মডিউলের জাভা অংশটি আলোচনা করা হয়েছে: ৩.১ এবং ৩.২ হলো সার্ভিস প্রসেসের DownloadService ; ৩.৩ হলো মেইন প্রসেসের FgsBridge (এন্ট্রি পয়েন্ট, যা C++ JNI ব্যবহার করে কল করে)।

৩.১ প্রথমে অগ্রভাগে নিয়ে আসুন

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);
        }
    }

    ...

৩.২ বিজ্ঞপ্তিটি যোগ করুন

একটি বোধগম্য পরিষেবার অগ্রগতি জানানোর জন্য একটি নোটিফিকেশন চ্যানেলে চলমান নোটিফিকেশনের প্রয়োজন হয়। এই নোটিফিকেশনে একটি স্টপ অ্যাকশন থাকে, যা PendingIntent.getService ব্যবহার করে পরিষেবাটিকে বন্ধ করার জন্য সরাসরি ACTION_STOP পাঠায়।

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();
}

৩.৩ জাভা হোস্ট এন্ট্রি পয়েন্ট

FgsBridge হলো জাভা এন্ট্রি পয়েন্ট যা মূল প্রসেস 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);
        }
    }
}

৪. আনরিয়েল থেকে এফজিএস চালু এবং নিয়ন্ত্রণ করুন (সি++)

PSUnrealAndroidBridge হলো একটি C++ র‍্যাপার যা JNI ব্যবহার করে 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 হলো একটি অভ্যন্তরীণ JNI হেল্পার যা FJavaWrapper::GameActivityThis Context হিসেবে ব্যবহার করে সংশ্লিষ্ট জাভা স্ট্যাটিক মেথডকে কল করে। সার্ভিস প্রসেস শুরু হওয়ার সাথে সাথে নোটিফিকেশনটি যেন অবিলম্বে প্রদর্শিত হয়, তা নিশ্চিত করতে BeginPlay তে RequestNotificationPermission কল করুন।