В этом руководстве описаны настройка и реализация, необходимые для запуска службы, видимой для Android (Foreground Service, или FGS), в приватном процессе из приложения Unity.
1. Настройте поддержку воспринимаемого сервиса.
В этом разделе объясняется, как настроить необходимые права доступа и объявить сервис в манифесте вашего проекта.
1.1 Заявление о правах и предоставлении услуг
В пользовательском файле Assets/Plugins/Android/AndroidManifest.xml необходимо указать Activity запуска, разрешения для службы 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 — все три параметра должны быть согласованы, иначе запуск службы завершится неудачей.
1.2 Добавьте Java-код сервис-процесса в проект Unity.
Процесс работы сервиса FGS выполняется на Java. Упакуйте код реализации сервиса на Java в JAR-файл и поместите его в папку Assets/Plugins/Android/ . Unity автоматически включает JAR-файлы из этой директории в входной каталог Gradle libs/ и упаковывает их в APK; процесс работы сервиса загружает эти классы во время выполнения. Для получения дополнительной информации о реализации сервиса на Java или Kotlin см. раздел «Реализация на Java» .
2. Настройте отдельный процесс.
Граница процесса обеспечивается одним атрибутом манифеста:
android:process=":downloader"
Двоеточие в начале имени создает процесс, доступный только для вашего приложения, с именем your.package.name:downloader . У него другой PID, отличный от основного процесса, и он может продолжать работу после завершения основного процесса.
2.1 В процессе обслуживания отсутствует Unity.
Чтобы избежать переноса библиотеки libunity.so , среды выполнения IL2CPP и аналогичных компонентов Unity в процесс сервиса, Java-код на стороне сервиса не должен использовать никакие классы Unity (включая UnityPlayer.currentActivity ) и должен зависеть только от контекста Android, дополнительных параметров Intent и API платформы. Среда выполнения движка Unity загружается только через UnityPlayerActivity основного процесса; частный процесс :downloader не загружает эти библиотеки, поэтому ссылки на классы Unity в процессе сервиса не работают.
2.2 Точки входа в процесс запуска
После того, как основной процесс вызовет startForegroundService , система создаст дочерний процесс службы :downloader :
- Создает экземпляр
Applicationи вызываетApplication.onCreate— это выполняется в каждом процессе, поэтому инициализация, необходимая для процесса службы, должна быть выполнена здесь снова. Для получения дополнительной информации см. раздел 2.3. - Создает
DownloadServiceи вызываетonCreate— это точка входа в процесс службы. - Вызывает
onStartCommandс Intent, сформированным при запуске. В этот момент служба переходит в активный режим и запускает рабочий процесс. Для получения дополнительной информации см. раздел 3.1.
2.3 Состояние существует один раз за процесс
Код Dex является общим и доступен только для чтения, но состояние во время выполнения — нет:
-
Application.attachBaseContextиApplication.onCreateвыполняются в каждом процессе, в котором размещены компоненты приложения. - Статические инициализаторы и статические поля существуют независимо в каждом процессе. Присвоение значения статическому полю в основном процессе не взаимодействует со службой.
- Unity, C# и Activity остаются в основном процессе.
3. Реализация на Java
В этом разделе рассматривается Java-часть модуля FGS: 3.1 и 3.2 — это DownloadService в процессе службы; 3.3 — это FgsBridge в основном процессе (точка входа, вызываемая из C# с использованием JNI).
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, которую использует основной процесс для управления 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);
}
}
}
4. Запуск и управление FGS из Unity (C#)
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, который разрешает класс FgsBridge и вызывает соответствующий статический метод Java. RequestNotificationPermission следует вызывать при запуске приложения, чтобы уведомление появлялось немедленно после запуска процесса службы.