במדריך הזה מוסבר איך להגדיר ולהטמיע שירות שניתן להבחנה ב-Android (שירות Foreground, או FGS) בתהליך פרטי מאפליקציית Unity.
1. הגדרת תמיכה בשירותים שניתן להבחין בהם
בקטע הזה מוסבר איך מגדירים את ההרשאות הנדרשות ומצהירים על השירות במניפסט של הפרויקט.
1.1 הצהרה על הרשאות ושירותים
ב-Assets/Plugins/Android/AndroidManifest.xml המותאם אישית צריך להצהיר על פעילות ההפעלה, על הרשאות שירות שניתנות לתפיסה, על הרשאת ההתראה, על הרשאת הרשת ועל השירות:
<?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 בספרייה הזו ב-libs/ input של Gradle, ואורז אותם ב-APK. תהליך השירות טוען את המחלקות האלה בזמן הריצה. מידע נוסף על הטמעת השירות ב-Java או ב-Kotlin זמין במאמר הטמעה ב-Java.
2. הגדרת תהליך נפרד
גבול התהליך מופעל על ידי מאפיין מניפסט אחד:
android:process=":downloader"
הנקודתיים המובילות יוצרות תהליך פרטי לאפליקציה בשם
your.package.name:downloader. יש לו PID שונה מהתהליך הראשי, והוא יכול להמשיך לפעול גם אחרי שהתהליך הראשי מסתיים.
2.1 תהליך השירות לא כולל Unity
כדי למנוע את ההפעלה של libunity.so, זמן הריצה של IL2CPP ורכיבים דומים בתהליך השירות, קוד Java בצד השרת לא צריך להשתמש באף מחלקה של Unity (כולל UnityPlayer.currentActivity), והוא צריך להיות תלוי רק ב-Android Context, בתוספים של Intent ובממשקי API של הפלטפורמה. זמן הריצה של מנוע Unity נטען רק דרך UnityPlayerActivity של התהליך הראשי. התהליך הפרטי :downloader לא טוען את הספריות האלה, ולכן הפניה למחלקות Unity לא פועלת בתהליך השירות.
2.2 נקודות כניסה להפעלת תהליך
אחרי שהתהליך הראשי קורא ל-startForegroundService, המערכת יוצרת פיצול של תהליך השירות :downloader:
- יוצר מופע של
Applicationוקורא ל-Application.onCreate– הפעולה הזו מתבצעת בכל תהליך, ולכן צריך לבצע כאן שוב את האתחול שנדרש לתהליך השירות. מידע נוסף זמין בקטע 2.3. - יוצר
DownloadServiceוקורא ל-onCreate– זו נקודת הכניסה לתהליך השירות. - האפליקציה מתקשרת חזרה למספר
onStartCommandעם הכוונה שנוצרה בהפעלה. השירות מקדם את עצמו לחזית ומתחיל את העובד. מידע נוסף זמין בקטע 3.1.
2.3 מצב קיים פעם אחת לכל תהליך
קוד Dex משותף לקריאה בלבד, אבל מצב זמן הריצה לא משותף:
-
Application.attachBaseContextו-Application.onCreateפועלים בכל תהליך שמארח רכיבי אפליקציה. - מאחלילים סטטיים ושדות סטטיים קיימים בנפרד בכל תהליך. הקצאה של שדה סטטי בתהליך הראשי לא מתקשרת עם השירות.
- Unity, C# ופעילויות נשארים בתהליך הראשי.
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 הוא wrapper של 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
צריך להפעיל את הפונקציה הזו כשמפעילים את האפליקציה, כדי שההתראה תופיע
מיד כשמתחיל תהליך השירות.