Hướng dẫn này trình bày cấu hình và cách triển khai cần thiết để chạy một dịch vụ có thể nhận biết (FGS) trên Android trong một quy trình riêng tư từ ứng dụng Unreal.
1. Định cấu hình chế độ hỗ trợ perceptible-service
Phần này giải thích cách thiết lập các quyền bắt buộc và khai báo dịch vụ trong tệp kê khai của dự án.
1.1 Tuyên bố về quyền và dịch vụ (nội dung bổ sung cho UPL)
Unreal không thay thế tệp kê khai công cụ – UnrealBuildTool đã tạo AndroidManifest.xml đã khai báo GameActivity, vì vậy UPL chỉ cần thêm các quyền và dịch vụ; Launcher Activity không cần phải được khai báo lại. Thêm nội dung sau vào <androidManifestUpdates> trong 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 phải khớp với hoạt động thực tế: dataSync cho hoạt động chuyển, mediaPlayback cho hoạt động phát, location cho hoạt động theo dõi vị trí. Nó phải đồng ý với quyền FOREGROUND_SERVICE_<TYPE> tương ứng và lệnh gọi startForeground – cả ba phải nhất quán, nếu không quá trình khởi động dịch vụ sẽ thất bại.
Dịch vụ này sử dụng tên đủ điều kiện com.sample.fgs.DownloadService – dấu chấm ở đầu sẽ phân giải tương ứng với mã ứng dụng com.sample.psunreal, nhưng mô-đun Java nằm trong gói com.sample.fgs nên sẽ không tìm thấy.
1.2 Thêm Java quy trình dịch vụ vào dự án Unreal
Biên dịch mã Java triển khai dịch vụ thành một tệp jar và sao chép tệp đó vào thư mục libs dàn dựng bằng <prebuildCopies> của UPL. UEDeployAndroid sao chép libs dàn dựng vào app/libs/ của dự án Gradle và Gradle sẽ tự động thêm mọi tệp jar vào đó, đóng gói chúng vào APK; quy trình dịch vụ sẽ tải các lớp này trong thời gian chạy:
<prebuildCopies>
<copyFile src="$S(PluginDir)/fgs-android.jar"
dst="$S(BuildDir)/libs/fgs-android.jar" />
</prebuildCopies>
$S(PluginDir) là thư mục chứa tệp UPL – hãy đặt tệp jar đã biên dịch vào đó; $S(BuildDir) là thư mục dàn dựng.
Các lớp này chỉ được truy cập thông qua JNI và tệp kê khai, không có vị trí gọi Java, vì vậy, chúng cũng phải được giữ trong <proguardAdditions>, nếu không, trình rút gọn sẽ coi chúng là không dùng đến và loại bỏ chúng:
<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. Định cấu hình một quy trình riêng biệt
Ranh giới quy trình được bật bằng một thuộc tính tệp kê khai:
android:process=":downloader"
Dấu hai chấm ở đầu sẽ tạo một quy trình dành riêng cho ứng dụng có tên là com.sample.psunreal:downloader. Nó có PID khác với quy trình chính và có thể tồn tại sau khi quy trình chính kết thúc.
2.1 Quy trình dịch vụ không có Unreal
Để tránh đưa các thư viện gốc của Unreal, chẳng hạn như libUE4.so hoặc libUE5.so, vào quy trình dịch vụ, Java phía dịch vụ không được dùng bất kỳ lớp Unreal nào và chỉ được phụ thuộc vào các tiện ích bổ sung Context, Intent của Android và các API nền tảng. Thời gian chạy của công cụ Unreal chỉ được tải thông qua GameActivity của quy trình chính; quy trình :downloader riêng tư không tải các thư viện này, vì vậy, việc tham chiếu các lớp Unreal sẽ không hoạt động trong quy trình dịch vụ.
2.2 Điểm truy cập khởi động quy trình
Sau khi quy trình chính gọi startForegroundService, hệ thống sẽ phân nhánh quy trình dịch vụ :downloader:
- Tạo thực thể
Applicationvà gọiApplication.onCreate– thao tác này chạy trong mọi quy trình, vì vậy, quá trình khởi chạy mà quy trình dịch vụ cần phải thực hiện lại tại đây (xem phần 2.3). - Tạo
DownloadServicevà gọionCreate– đây là điểm truy cập vào quy trình dịch vụ. - Gọi lại
onStartCommandbằngIntentđược tạo khi khởi động. Dịch vụ này tự quảng bá lên nền trước tại đây và khởi động trình chạy (xem phần 3.1).
2.3 Trạng thái chỉ tồn tại một lần cho mỗi quy trình
Mã Dex được chia sẻ ở chế độ chỉ đọc, nhưng trạng thái thời gian chạy thì không:
Application.attachBaseContextvàApplication.onCreatechạy trong mọi quy trình lưu trữ các thành phần ứng dụng.- Trình khởi tạo tĩnh và trường tĩnh tồn tại độc lập trong mỗi quy trình. Việc chỉ định một trường tĩnh trong quy trình chính không giao tiếp với dịch vụ.
- Unreal, C++ và các Hoạt động vẫn nằm trong quy trình chính.
3. Triển khai Java
Phần này đề cập đến phía Java của mô-đun FGS: 3.1 và 3.2 là DownloadService trong quy trình dịch vụ; 3.3 là FgsBridge trong quy trình chính (các lệnh gọi C++ tại điểm truy cập bằng JNI).
3.1 Quảng bá lên nền trước trước
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 Thêm thông báo
Một dịch vụ mà người dùng có thể nhận biết cần có một thông báo hiển thị liên tục trên một kênh thông báo để báo cáo tiến trình. Thông báo này có một thao tác Dừng, thao tác này sẽ gửi ACTION_STOP đến chính dịch vụ bằng cách dùng PendingIntent.getService để dừng dịch vụ.
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 Điểm truy cập vào máy chủ Java
FgsBridge là điểm truy cập Java mà quy trình chính dùng để kiểm soát FGS (chạy trong quy trình chính, chứ không phải quy trình dịch vụ). C++ gọi các phương thức tĩnh này bằng JNI để bắt đầu và dừng dịch vụ, đồng thời xử lý quyền gửi thông báo:
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. Bắt đầu và kiểm soát FGS từ Unreal (C++)
PSUnrealAndroidBridge là trình bao bọc C++ gọi FgsBridge bằng JNI.
Sau đây là 3 cách triển khai phương thức:
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 là một trình trợ giúp JNI nội bộ gọi phương thức tĩnh Java tương ứng, sử dụng FJavaWrapper::GameActivityThis làm Ngữ cảnh.
Để đảm bảo thông báo xuất hiện ngay khi quy trình dịch vụ bắt đầu, hãy gọi RequestNotificationPermission trong BeginPlay.