يغطّي هذا الدليل الإعداد والتنفيذ اللازمَين لتشغيل خدمة Android مرئية (FGS) في عملية خاصة من تطبيق Unreal.
1. إعداد دعم الخدمات المرئية
يوضّح هذا القسم كيفية إعداد الأذونات المطلوبة وتعريف الخدمة في بيان مشروعك.
1.1 الأذونات وبيان الخدمة (إضافات UPL)
لا يستبدل Unreal بيان المحرّك، إذ تنشئ أداة UnrealBuildTool ملف AndroidManifest.xml الذي سبق أن تم فيه تعريف GameActivity، لذا لا تحتاج لغة UPL إلا إلى إضافة الأذونات والخدمة، ولا يلزم إعادة بيان مشغّل النشاط. أضِف ما يلي ضمن <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، وإلا سيفشل بدء تشغيل الخدمة.
تستخدم الخدمة الاسم المؤهّل بالكامل com.sample.fgs.DownloadService—إذا بدأ الاسم بنقطة، سيتم حله بالنسبة إلى رقم تعريف التطبيق com.sample.psunreal، ولكن وحدة Java موجودة في حزمة com.sample.fgs، لذا لن يتم العثور عليها.
1.2 إضافة Java لعملية الخدمة إلى مشروع Unreal
يمكنك تجميع رمز Java لتنفيذ الخدمة في ملف jar ونسخه إلى دليل مكتبات التجهيز باستخدام <prebuildCopies> في UPL. تنسخ أداة UEDeployAndroid دليل مكتبات التجهيز إلى 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 والبيان، بدون أي مواقع استدعاء 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. ولها رقم تعريف عملية مختلف عن العملية الرئيسية ويمكنها البقاء بعد انتهاء العملية الرئيسية.
2.1 لا تتضمّن عملية الخدمة أي مكتبات Unreal
لتجنُّب إحضار مكتبات Unreal الأصلية، مثل libUE4.so أو libUE5.so، إلى عملية الخدمة، يجب ألا تستخدم Java من جهة الخدمة أي فئات Unreal، ويجب أن تعتمد فقط على Context وIntent الإضافية وواجهات برمجة التطبيقات للنظام الأساسي في Android. لا يتم تحميل وقت تشغيل محرّك Unreal إلا من خلال GameActivity في العملية الرئيسية، ولا تحمِّل العملية الخاصة :downloader هذه المكتبات، لذا لا يمكن الإشارة إلى فئات Unreal في عملية الخدمة.
2.2 نقاط بدء تشغيل العملية
بعد أن تستدعي العملية الرئيسية startForegroundService، ينشئ النظام عملية الخدمة :downloader:
- يتم إنشاء مثيل لـ
ApplicationواستدعاءApplication.onCreate—يتم تشغيل هذا في كل عملية، لذا يجب إعادة تنفيذ عملية الإعداد التي تحتاجها عملية الخدمة هنا (راجِع القسم 2.3). - يتم إنشاء
DownloadServiceواستدعاءonCreate. هذه هي نقطة دخول عملية الخدمة. - يتم استدعاء
onStartCommandمرة أخرى باستخدامIntentالذي تم إنشاؤه عند بدء التشغيل. تنتقل الخدمة إلى المقدّمة هنا وتبدأ المنفِّذ (راجِع القسم 3.1).
2.3 الحالة موجودة مرة واحدة لكل عملية
تتم مشاركة رمز Dex للقراءة فقط، ولكن حالة وقت التشغيل لا تتم مشاركتها:
- يتم تشغيل
Application.attachBaseContextوApplication.onCreateفي كل عملية تستضيف مكوّنات التطبيق. - تكون عمليات الإعداد الأولية الثابتة والحقول الثابتة مستقلة في كل عملية. لا يؤدي تعيين حقل ثابت في العملية الرئيسية إلى التواصل مع الخدمة.
- تبقى Unreal وC++ و"الأنشطة" في العملية الرئيسية.
3. تنفيذ Java
يغطّي هذا القسم جانب Java من وحدة FGS: 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 هو برنامج التغليف في C++ الذي يستدعي FgsBridge باستخدام JNI.
في ما يلي عمليات تنفيذ الطرق الثلاث:
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 كـ `Context`.
لضمان ظهور الإشعار فورًا عند بدء عملية الخدمة، استدعِ RequestNotificationPermission في BeginPlay.