במדריך הזה מוסבר איך להגדיר ולהטמיע שירות גלוי למשתמש (FGS) ב-Android בתהליך פרטי מאפליקציית Unreal.
1. הגדרת תמיכה בשירותים שניתן להבחין בהם
בקטע הזה מוסבר איך מגדירים את ההרשאות הנדרשות ומצהירים על השירות במניפסט של הפרויקט.
1.1 הצהרה על הרשאות ושירותים (תוספות ל-UPL)
Unreal לא מחליף את מניפסט המנוע – UnrealBuildTool יוצר את AndroidManifest.xml שכבר מוצהר בו GameActivity, ולכן UPL צריך רק להוסיף הרשאות ושירות; לא צריך להצהיר מחדש על פעילות מרכז האפליקציות. מוסיפים את השורות הבאות בקטע <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, אבל מודול Java נמצא בחבילה com.sample.fgs, ולכן הוא לא יימצא.
1.2 מוסיפים את Java של תהליך השירות לפרויקט Unreal
קומפילציה של קוד ה-Java של הטמעת השירות לקובץ jar והעתקה שלו לספריית libs של staging באמצעות UPL's <prebuildCopies>. UEDeployAndroid מעתיק את libs/ staging אל 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 ומניפסט, ללא אתרי קריאה של Java, ולכן צריך לשמור אותן גם ב-<proguardAdditions>, אחרת הכלי לכיווץ יזהה אותן כלא בשימוש ויסיר אותן:
<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>
2. הגדרת תהליך נפרד
גבול התהליך מופעל על ידי מאפיין מניפסט אחד:
android:process=":downloader"
הנקודתיים המובילות יוצרות תהליך פרטי לאפליקציה בשם
com.sample.psunreal:downloader. יש לו PID שונה מהתהליך הראשי, והוא יכול להמשיך לפעול גם אחרי שהתהליך הראשי מסתיים.
2.1 תהליך השירות לא כולל Unreal
כדי להימנע מהוספה של ספריות מקוריות של Unreal, כמו libUE4.so או libUE5.so, לתהליך השירות, קוד Java בצד השרת לא צריך להשתמש באף מחלקה של Unreal, והוא צריך להיות תלוי רק ב-Android Context, בתוספים של Intent ובממשקי API של הפלטפורמה. זמן הריצה של Unreal Engine נטען רק דרך GameActivity של התהליך הראשי. התהליך הפרטי :downloader לא טוען את הספריות האלה, ולכן הפניה למחלקות Unreal לא פועלת בתהליך השירות.
2.2 נקודות כניסה להפעלת תהליך
אחרי שהתהליך הראשי קורא ל-startForegroundService, המערכת מבצעת פיצול (fork) של תהליך השירות :downloader:
- יוצר מופע של
Applicationוקורא ל-Application.onCreate– הפעולה הזו מתבצעת בכל תהליך, ולכן צריך לבצע כאן שוב את האתחול שנדרש לתהליך השירות (ראו סעיף 2.3). - יוצר
DownloadServiceוקורא ל-onCreate– זו נקודת הכניסה של תהליך השירות. - התקשרות חזרה למספר
onStartCommandבאמצעותIntentשנבנה במהלך ההפעלה. השירות מקדם את עצמו לחזית ומתחיל את העובד (ראו 3.1).
2.3 מצב קיים פעם אחת לכל תהליך
קוד Dex משותף לקריאה בלבד, אבל מצב זמן הריצה לא משותף:
-
Application.attachBaseContextו-Application.onCreateפועלים בכל תהליך שמארח רכיבי אפליקציה. - מאחלילים סטטיים ושדות סטטיים קיימים באופן עצמאי בכל תהליך. הקצאה של שדה סטטי בתהליך הראשי לא מתקשרת עם השירות.
- הפעילויות Unreal, C++ ו-Activities יישארו בתהליך הראשי.
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 מ-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 שקורא לשיטה הסטטית המתאימה של Java, באמצעות FJavaWrapper::GameActivityThis כהקשר.
כדי לוודא שההתראה תופיע מיד כשמתחיל תהליך השירות, צריך להתקשר אל RequestNotificationPermission ב-BeginPlay.