이 가이드에서는 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 프로젝트에 추가
서비스 구현 Java 코드를 jar로 컴파일하고 UPL의 <prebuildCopies>를 사용하여 스테이징 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)은 스테이징 디렉터리입니다.
이러한 클래스는 Java 호출 사이트 없이 JNI와 매니페스트를 통해서만 연결되므로 <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이 없음
libUE4.so 또는 libUE5.so와 같은 Unreal의 네이티브 라이브러리가 서비스 프로세스에 포함되지 않도록 하려면 서비스 측 Java에서 Unreal 클래스를 사용하지 않아야 하며 Android Context, Intent extras, 플랫폼 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++, 활동은 기본 프로세스에 남아 있습니다.
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 알림 추가
인식 가능한 서비스는 진행 상황을 보고하기 위해 알림 채널에서 진행 중인 알림이 필요합니다. 알림에는 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 는 FJavaWrapper::GameActivityThis를 컨텍스트로 사용하여 해당하는 Java 정적 메서드를 호출하는 내부 JNI 도우미입니다.
서비스 프로세스가 시작될 때 알림이 즉시 표시되도록 하려면 BeginPlay에서 RequestNotificationPermission을 호출합니다.