Unreal을 사용하여 별도의 프로세스에서 감지 가능한 서비스 실행

이 가이드에서는 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를 호출합니다. 이는 서비스 프로세스 진입점입니다.
  • 시작 시 구성된 IntentonStartCommand를 다시 호출합니다. 서비스는 여기에서 포그라운드로 승격되고 작업자를 시작합니다 (3.1 참고).

2.3 프로세스당 한 번 상태 존재

Dex 코드는 읽기 전용으로 공유되지만 런타임 상태는 공유되지 않습니다.

  • Application.attachBaseContextApplication.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"));
}

CallActivityVoidFJavaWrapper::GameActivityThis를 컨텍스트로 사용하여 해당하는 Java 정적 메서드를 호출하는 내부 JNI 도우미입니다. 서비스 프로세스가 시작될 때 알림이 즉시 표시되도록 하려면 BeginPlay에서 RequestNotificationPermission을 호출합니다.