本指南說明如何設定及實作,以便從 Unreal 應用程式在私人程序中執行 Android 可感知服務 (FGS)。
1. 設定可感知服務支援
本節說明如何設定必要權限,以及在專案資訊清單中宣告服務。
1.1 權限和服務聲明 (UPL 新增內容)
Unreal 不會取代引擎資訊清單,UnrealBuildTool 會產生已宣告 GameActivity 的 AndroidManifest.xml,因此 UPL 只需新增權限和服務,不必重新聲明啟動器活動。在 Source/PSUnreal/PSUnreal_UPL.xml 的 <androidManifestUpdates> 下方新增以下內容:
<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,開頭的點會相對於應用程式 ID com.sample.psunreal 解析,但 Java 模組位於 com.sample.fgs 套件中,因此系統找不到該模組。
1.2 將服務程序 Java 新增至 Unreal 專案
使用 UPL 的 <prebuildCopies>,將服務實作項目的 Java 程式碼編譯為 JAR,並複製到暫存 libs 目錄。UEDeployAndroid 會將暫存 libs/ 複製到 Gradle 專案的 app/libs/,而 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 引擎執行階段只會透過主要程序的 GameActivity 載入;私有 :downloader 程序不會載入這些程式庫,因此在服務程序中參照 Unreal 類別會無效。
2.2 處理程序啟動進入點
主要程序呼叫 startForegroundService 後,系統會分叉 :downloader 服務程序:
- 將
Application例項化並呼叫Application.onCreate,這會在每個程序中執行,因此服務程序所需的初始化作業必須在此處重新完成 (請參閱 2.3)。 - 建立
DownloadService並呼叫onCreate,這是服務程序進入點。 - 在啟動時建構
Intent,並使用該物件回呼onStartCommand。服務會在此處將自己升級為前景,並啟動工作站 (請參閱 3.1)。
2.3 每個程序都有一個狀態
Dex 程式碼是共用的唯讀程式碼,但執行階段狀態不是:
Application.attachBaseContext和Application.onCreate會在代管應用程式元件的每個程序中執行。- 每個程序中都有獨立的靜態初始設定式和靜態欄位。 在主要程序中指派靜態欄位時,不會與服務通訊。
- Unreal、C++ 和 Activities 仍保留在主要程序中。
3. Java 實作
本節涵蓋 FGS 模組的 Java 端:3.1 和 3.2 位於
DownloadService服務程序中;3.3 位於FgsBridge主要程序中 (使用 JNI 的進入點 C++ 呼叫)。
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 新增通知
可察覺的服務需要在通知管道中顯示持續性通知,才能回報進度。通知會攜帶「停止」動作,使用 PendingIntent.getService 將 ACTION_STOP 傳送至服務本身,以停止服務。
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 是主要程序用來控制 FGS 的 Java 進入點 (在主要程序中執行,而非服務程序)。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. 從 Unreal (C++) 啟動及控制 FGS
PSUnrealAndroidBridge 是使用 JNI 呼叫 FgsBridge 的 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 是內部 JNI 輔助程式,會使用 FJavaWrapper::GameActivityThis 做為 Context,呼叫對應的 Java 靜態方法。為確保服務程序開始時立即顯示通知,請在 RequestNotificationPermission 中呼叫 BeginPlay。