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

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

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

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

۱.۱ مجوزها و اعلامیه خدمات

فایل سفارشی Assets/Plugins/Android/AndroidManifest.xml باید فعالیت اجراکننده، مجوزهای perceptible-service، مجوز اعلان، مجوز شبکه و سرویس را تعریف کند:

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

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

فرآیند سرویس FGS جاوا را اجرا می‌کند. کد جاوای پیاده‌سازی سرویس را در یک jar بسته‌بندی کرده و آن را در مسیر Assets/Plugins/Android/ قرار دهید. یونیتی به طور خودکار jarهای موجود در آن دایرکتوری را در libs/input مربوط به Gradle قرار می‌دهد و آنها را در APK بسته‌بندی می‌کند. فرآیند سرویس این کلاس‌ها را در زمان اجرا بارگذاری می‌کند. برای اطلاعات بیشتر در مورد نحوه پیاده‌سازی سرویس در جاوا یا کاتلین، به بخش پیاده‌سازی جاوا مراجعه کنید.

۲. پیکربندی یک فرآیند جداگانه

مرز فرآیند توسط یک ویژگی آشکار فعال می‌شود:

android:process=":downloader"

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

۲.۱ فرآیند خدمات فاقد وحدت است

برای جلوگیری از وارد کردن libunity.so یونیتی، فایل زمان اجرای IL2CPP و موارد مشابه به فرآیند سرویس، جاوای سمت سرویس نباید از هیچ کلاس یونیتی (از جمله UnityPlayer.currentActivity ) استفاده کند و فقط باید به Android Context، Intent extras و APIهای پلتفرم وابسته باشد. زمان اجرای موتور یونیتی فقط از طریق UnityPlayerActivity فرآیند اصلی بارگذاری می‌شود؛ فرآیند private :downloader این کتابخانه‌ها را بارگذاری نمی‌کند، بنابراین ارجاع به کلاس‌های یونیتی در فرآیند سرویس کار نمی‌کند.

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

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

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

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

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

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

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

این بخش به بخش جاوای ماژول 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 از آن استفاده می‌کند (در فرآیند اصلی اجرا می‌شود، نه در فرآیند سرویس). سی‌شارپ این متدهای استاتیک را با استفاده از 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 از Unity (سی شارپ)

AndroidBridge یک پوشش C# است که FgsBridge با استفاده از JNI فراخوانی می‌کند. سه پیاده‌سازی متد:

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