คู่มือนี้ครอบคลุมการกำหนดค่าและการติดตั้งใช้งานที่จำเป็นในการเรียกใช้บริการที่รับรู้ได้ (FGS) ของ Android ในกระบวนการส่วนตัวจากแอปพลิเคชัน Unreal
1. กำหนดค่าการรองรับบริการที่รับรู้ได้
ส่วนนี้อธิบายวิธีตั้งค่าสิทธิ์ที่จำเป็นและประกาศบริการในไฟล์ Manifest ของโปรเจ็กต์
1.1 การประกาศสิทธิ์และบริการ (การเพิ่ม UPL)
Unreal ไม่ได้แทนที่ไฟล์ Manifest ของเอนจิน โดย UnrealBuildTool จะสร้าง AndroidManifest.xml ที่ประกาศ GameActivity ไว้แล้ว ดังนั้น UPL จึงต้องเพิ่ม สิทธิ์และบริการเท่านั้น และไม่จำเป็นต้องระบุ Activity ของตัวเรียกใช้ซ้ำ เพิ่มโค้ดต่อไปนี้ในส่วน <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เรียก ซึ่งทั้ง 3 รายการต้องสอดคล้องกัน ไม่เช่นนั้นการเริ่มต้นบริการ
จะล้มเหลว
บริการใช้ชื่อที่สมบูรณ์ในตัวเอง com.sample.fgs.DownloadService ซึ่งจุดนำหน้าจะแก้ปัญหาที่เกี่ยวข้องกับรหัสแอปพลิเคชัน com.sample.psunreal แต่โมดูล Java อยู่ในแพ็กเกจ com.sample.fgs จึงไม่พบ
1.2 เพิ่ม Java ของกระบวนการบริการลงในโปรเจ็กต์ Unreal
คอมไพล์โค้ด Java ของการติดตั้งใช้งานบริการเป็นไฟล์ Jar แล้วคัดลอกลงในไดเรกทอรี libs ที่จัดเตรียมไว้โดยใช้ <prebuildCopies> ของ UPL โดย UEDeployAndroid จะคัดลอก libs ที่จัดเตรียมไว้ลงใน 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 และไฟล์ Manifest เท่านั้น โดยไม่มีการเรียก 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. กำหนดค่ากระบวนการแยกต่างหาก
การเปิดใช้ขอบเขตกระบวนการทำได้โดยใช้แอตทริบิวต์ไฟล์ Manifest 1 รายการ ดังนี้
android:process=":downloader"
โคลอนนำหน้าจะสร้างกระบวนการส่วนตัวของแอปพลิเคชันชื่อ com.sample.psunreal:downloader โดยมี PID ที่แตกต่างจากกระบวนการหลักและสามารถทำงานต่อไปได้หลังจากกระบวนการหลักสิ้นสุดลง
2.1 กระบวนการบริการไม่มี Unreal
Java ฝั่งบริการไม่ควรใช้คลาส Unreal ใดๆ และควรขึ้นอยู่กับ Context ของ Android, ข้อมูลเพิ่มเติมของ Intent และ API ของแพลตฟอร์มเท่านั้น เพื่อหลีกเลี่ยงการนำไลบรารีเนทีฟของ Unreal เช่น libUE4.so หรือ libUE5.so เข้าสู่กระบวนการบริการ รันไทม์ของเอนจิน Unreal จะโหลดผ่าน GameActivity ของกระบวนการหลักเท่านั้น กระบวนการ :downloader ส่วนตัวจะไม่โหลดไลบรารีเหล่านี้ ดังนั้นการอ้างอิงคลาส Unreal จึงใช้ไม่ได้ในกระบวนการบริการ
2.2 จุดแรกเข้าของการเริ่มต้นกระบวนการ
หลังจากกระบวนการหลักเรียก startForegroundService แล้ว ระบบจะแยกกระบวนการบริการ :downloader ดังนี้
- สร้างอินสแตนซ์
ApplicationและเรียกApplication.onCreateซึ่งจะทำงาน ในทุกกระบวนการ ดังนั้นจึงต้องทำการเริ่มต้นที่กระบวนการบริการจำเป็นต้องใช้อีกครั้งที่นี่ (ดู 2.3) - สร้าง
DownloadServiceและเรียกonCreateซึ่งเป็นจุดแรกเข้าของกระบวนการบริการ - เรียกกลับ
onStartCommandด้วยIntentที่สร้างขึ้นเมื่อเริ่มต้น บริการจะเลื่อนระดับตัวเองไปทำงานอยู่เบื้องหน้าและเริ่ม Worker ที่นี่ (ดู 3.1)
2.3 สถานะมีอยู่ 1 ครั้งต่อกระบวนการ
โค้ด Dex จะแชร์แบบอ่านอย่างเดียว แต่สถานะรันไทม์จะไม่แชร์
Application.attachBaseContextและApplication.onCreateจะทำงานในทุกกระบวนการที่โฮสต์คอมโพเนนต์ของแอปพลิเคชัน- ตัวเริ่มต้นแบบคงที่และฟิลด์แบบคงที่จะมีอยู่แยกกันในแต่ละกระบวนการ การกำหนดฟิลด์แบบคงที่ในกระบวนการหลักจะไม่สื่อสารกับบริการ
- Unreal, C++ และ Activity จะยังคงอยู่ในกระบวนการหลัก
3. การติดตั้งใช้งาน Java
ส่วนนี้ครอบคลุมโมดูล FGS ฝั่ง Java โดย 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
การติดตั้งใช้งานเมธอด 3 รายการมีดังนี้
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 เพื่อให้การแจ้งเตือนปรากฏขึ้นทันทีเมื่อกระบวนการบริการเริ่มต้น