Uruchamianie widocznej usługi w osobnym procesie za pomocą Unity

Ten przewodnik opisuje konfigurację i implementację potrzebną do uruchomienia w aplikacji Unity usługi widocznej dla użytkownika (Foreground Service lub FGS) w procesie prywatnym.

1. Konfigurowanie obsługi usługi widocznej dla użytkownika

W tej sekcji dowiesz się, jak skonfigurować wymagane uprawnienia i zadeklarować usługę w pliku manifestu projektu.

1.1 Deklaracja uprawnień i usługi

Niestandardowy Assets/Plugins/Android/AndroidManifest.xml musi deklarować aktywność uruchamiającą, uprawnienia usługi widocznej dla użytkownika, zgodę na wyświetlanie powiadomień, uprawnienia do sieci i usługę:

<?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 musi odpowiadać rzeczywistemu działaniu: dataSync w przypadku przesyłania, mediaPlayback w przypadku odtwarzania, location w przypadku śledzenia lokalizacji. Musi być zgodny z odpowiednim uprawnieniem FOREGROUND_SERVICE_<TYPE> i wywołaniem startForeground – wszystkie 3 muszą być spójne, w przeciwnym razie uruchomienie usługi się nie powiedzie.

1.2 Dodawanie kodu Java procesu usługi do projektu Unity

Proces usługi FGS jest uruchamiany w Javie. Spakuj kod Java implementacji usługi do pliku JAR i umieść go w folderze Assets/Plugins/Android/. Unity automatycznie uwzględnia pliki JAR w tym katalogu w danych wejściowych Gradle libs/ i pakuje je do pliku APK. Proces usługi wczytuje te klasy w czasie działania. Więcej informacji o implementowaniu usługi w Javie lub Kotlinie, zobacz Implementacja w Javie.

2. Konfigurowanie osobnego procesu

Granica procesu jest włączana przez jeden atrybut manifestu:

android:process=":downloader"

Początkowy dwukropek tworzy proces prywatny aplikacji o nazwie your.package.name:downloader. Ma on inny identyfikator PID niż proces główny i może działać po zakończeniu procesu głównego.

2.1 Proces usługi nie ma Unity

Aby uniknąć przenoszenia do procesu usługi biblioteki libunity.so, środowiska wykonawczego IL2CPP i podobnych, kod Java po stronie usługi nie powinien używać żadnych klas Unity (w tym UnityPlayer.currentActivity) i powinien zależeć tylko od kontekstu Androida, dodatków Intent i interfejsów API platformy. Środowisko wykonawcze silnika Unity jest wczytywane tylko przez UnityPlayerActivity procesu głównego. Proces prywatny :downloader nie wczytuje tych bibliotek, więc odwoływanie się do klas Unity nie działa w procesie usługi.

2.2 Punkty wejścia uruchamiania procesu

Gdy proces główny wywoła startForegroundService, system rozwidla proces usługi :downloader:

  • Tworzy instancję Application i wywołuje Application.onCreate – to jest uruchamiane w każdym procesie, więc inicjowanie potrzebne procesowi usługi musi zostać wykonane ponownie. Więcej informacji znajdziesz w sekcji 2.3.
  • Tworzy DownloadService i wywołuje onCreate – jest to punkt wejścia procesu usługi.
  • Wywołuje zwrotnie onStartCommand z intencją utworzoną podczas uruchamiania. Usługa promuje się tutaj na pierwszy plan i uruchamia proces roboczy. Więcej informacji znajdziesz w sekcji 3.1.

2.3 Stan istnieje raz na proces

Kod Dex jest udostępniany tylko do odczytu, ale stan środowiska wykonawczego nie jest:

  • Application.attachBaseContext i Application.onCreate są uruchamiane w każdym procesie, który hostuje komponenty aplikacji.
  • Inicjatory statyczne i pola statyczne istnieją niezależnie w każdym procesie. Przypisanie pola statycznego w procesie głównym nie komunikuje się z usługą.
  • Unity, C# i aktywności pozostają w procesie głównym.

3. Implementacja w Javie

W tej sekcji omówimy stronę Java modułu FGS: 3.1 i 3.2 to DownloadService w procesie usługi, a 3.3 to FgsBridge w procesie głównym (punkt wejścia wywołań C# za pomocą JNI).

3.1 Najpierw promuj na pierwszy plan

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 Dodawanie powiadomienia

Usługa widoczna dla użytkownika potrzebuje powiadomienia o trwającej aktywności w kanale powiadomień, aby informować o postępach. Powiadomienie zawiera działanie Stop, które wysyła ACTION_STOP do samej usługi za pomocą PendingIntent.getService, aby ją zatrzymać.

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 Punkt wejścia hosta Java

FgsBridge to punkt wejścia Java, którego proces główny używa do sterowania FGS (jest on uruchamiany w procesie głównym, a nie w procesie usługi). C# wywołuje te metody statyczne za pomocą JNI, aby uruchamiać i zatrzymywać usługę oraz obsługiwać uprawnienia do powiadomień:

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. Uruchamianie FGS i sterowanie nim z poziomu Unity (C#)

AndroidBridge to otoka C#, która wywołuje FgsBridge za pomocą JNI. Implementacje 3 metod:

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 to wewnętrzna pomocnicza funkcja JNI, która rozwiązuje klasę FgsBridge i wywołuje odpowiednią statyczną metodę Java. RequestNotificationPermission należy wywołać podczas uruchamiania aplikacji, aby powiadomienie pojawiło się natychmiast po uruchomieniu procesu usługi.