اجرای یک سرویس قابل درک در یک فرآیند جداگانه با Unreal

این راهنما پیکربندی و پیاده‌سازی مورد نیاز برای اجرای یک سرویس ادراک‌پذیر اندروید (FGS) در یک فرآیند خصوصی از یک برنامه Unreal را پوشش می‌دهد.

۱. پیکربندی پشتیبانی از خدمات محسوس

این بخش نحوه تنظیم مجوزهای مورد نیاز و اعلام سرویس در مانیفست پروژه شما را توضیح می‌دهد.

۱.۱ مجوزها و اعلامیه خدمات (افزوده‌های 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 مطابقت داشته باشد - هر سه باید سازگار باشند، در غیر این صورت راه‌اندازی سرویس با شکست مواجه می‌شود.

این سرویس از نام کامل com.sample.fgs.DownloadService استفاده می‌کند - یک نقطه در ابتدای نام، نسبت به شناسه برنامه com.sample.psunreal را مشخص می‌کند، اما ماژول جاوا در بسته com.sample.fgs قرار دارد، بنابراین پیدا نمی‌شود.

۱.۲ اضافه کردن سرویس-فرآیند جاوا به پروژه Unreal

کد جاوای پیاده‌سازی سرویس را در یک فایل jar کامپایل کنید و با استفاده از <prebuildCopies> مربوط به UPL، آن را در دایرکتوری staging libs کپی کنید. UEDeployAndroid فایل staging 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 و مانیفست قابل دسترسی هستند، بدون هیچ سایت فراخوانی جاوا، بنابراین باید در <proguardAdditions> نیز نگهداری شوند، در غیر این صورت shrinker آنها را بلااستفاده در نظر گرفته و حذف می‌کند:

<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"

علامت دو نقطه در ابتدای هر فرآیند، یک فرآیند خصوصی-برنامه‌ای با نام com.sample.psunreal:downloader ایجاد می‌کند. این فرآیند PID متفاوتی از فرآیند اصلی دارد و می‌تواند پس از خاتمه فرآیند اصلی به حیات خود ادامه دهد.

۲.۱ فرآیند سرویس فاقد Unreal است

برای جلوگیری از وارد کردن کتابخانه‌های بومی Unreal، مانند libUE4.so یا libUE5.so ، به فرآیند سرویس، جاوای سمت سرویس نباید از هیچ کلاس Unreal استفاده کند و فقط باید به Android Context ، Intent extras و APIهای پلتفرم وابسته باشد. زمان اجرای موتور Unreal فقط از طریق GameActivity فرآیند اصلی بارگذاری می‌شود؛ فرآیند private :downloader این کتابخانه‌ها را بارگذاری نمی‌کند، بنابراین ارجاع به کلاس‌های Unreal در فرآیند سرویس کار نمی‌کند.

۲.۲ نقاط ورود راه‌اندازی فرآیند

پس از اینکه فرآیند اصلی، startForegroundService را فراخوانی می‌کند، سیستم فرآیند سرویس :downloader را منشعب می‌کند:

  • Application نمونه‌سازی کرده و Application.onCreate را فراخوانی می‌کند — این در هر فرآیند اجرا می‌شود، بنابراین مقداردهی اولیه مورد نیاز فرآیند سرویس باید دوباره در اینجا انجام شود (به ۲.۳ مراجعه کنید).
  • DownloadService ایجاد می‌کند و onCreate فراخوانی می‌کند - این نقطه ورود فرآیند سرویس است.
  • با Intent ساخته شده در هنگام راه‌اندازی، onStartCommand فراخوانی می‌کند. سرویس در اینجا خود را به پیش‌زمینه ارتقا می‌دهد و worker را شروع می‌کند (به ۳.۱ مراجعه کنید).

۲.۳ حالت (state) یک بار در هر فرآیند وجود دارد

کد Dex فقط خواندنی به اشتراک گذاشته می‌شود، اما وضعیت زمان اجرا اینطور نیست:

  • Application.attachBaseContext و Application.onCreate در هر فرآیندی که میزبان اجزای برنامه است، اجرا می‌شوند.
  • مقداردهی اولیه استاتیک و فیلدهای استاتیک به طور مستقل در هر فرآیند وجود دارند. اختصاص یک فیلد استاتیک در فرآیند اصلی با سرویس ارتباطی برقرار نمی‌کند.
  • Unreal، C++ و Activityها در فرآیند اصلی باقی می‌مانند.

۳. پیاده‌سازی جاوا

این بخش به بخش جاوای ماژول FGS می‌پردازد: نسخه‌های ۳.۱ و ۳.۲ در فرآیند سرویس، DownloadService هستند؛ نسخه ۳.۳ در فرآیند اصلی (فراخوانی‌های نقطه ورودی C++ با استفاده از JNI) FgsBridge است.

۳.۱ ابتدا به پیش‌زمینه بروید

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

    ...

۳.۲ اعلان را اضافه کنید

یک سرویس قابل درک برای گزارش پیشرفت به یک اعلان مداوم در یک کانال اعلان نیاز دارد. این اعلان شامل یک اقدام توقف است که 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();
}

۳.۳ نقطه ورود میزبان جاوا

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

۴. شروع و کنترل FGS از Unreal (C++)

PSUnrealAndroidBridge یک wrapper مربوط به زبان C++ است که FgsBridge با استفاده از JNI فراخوانی می‌کند. در اینجا سه ​​پیاده‌سازی از این متد آمده است:

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 helper داخلی است که متد استاتیک جاوای مربوطه را با استفاده FJavaWrapper::GameActivityThis به عنوان Context فراخوانی می‌کند. برای اطمینان از اینکه اعلان بلافاصله پس از شروع فرآیند سرویس ظاهر می‌شود، RequestNotificationPermission در BeginPlay فراخوانی کنید.