UPL for Android

在 UE 中为移动端添加第三方模块或者修改配置文件时经常会用到 AdditionalPropertiesForReceipt,里面创建ReceiptProperty 传入的 xml 文件就是 UE 的 Unreal Plugin Language 脚本。

ReceiptProperty的平台名称在 IOS 和 Android 上是固定的,分别是 IOSPluginAndroidPlugin,不可以指定其他的名字(详见代码 UEDeployIOS.cs#L1153UEDeployAndroid.cs#L4303)。

1
AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ThirdPartyPath, "Android/PlatformUtils_UPL_Android.xml")));

Android 项目中所有的 UPL

可以在项目路径下Intermediate/Android/ActiveUPL.xml,里面列出了当前项目中所有的 UPL 文件路径:

1
2
3
4
5
6
7
Plugins\Online\Android\OnlineSubsystemGooglePlay\Source\OnlineSubsystemGooglePlay_UPL.xml
Plugins\Runtime\AndroidPermission\Source\AndroidPermission\AndroidPermission_APL.xml
Plugins\Runtime\GoogleCloudMessaging\Source\GoogleCloudMessaging\GoogleCloudMessaging_UPL.xml
Plugins\Runtime\GooglePAD\Source\GooglePAD\GooglePAD_APL.xml
Plugins\Runtime\Oculus\OculusVR\Source\OculusHMD\OculusMobile_APL.xml
Source\Runtime\Online\Voice\AndroidVoiceImpl_UPL.xml
Source\ThirdParty\GoogleGameSDK\GoogleGameSDK_APL.xml

删除 AndroidManifest.xml 中的项

因为 UE 默认会给 AndroidManifest.xml 添加项,如果其中的项我们想要手动控制,直接添加的话会产生错误,提示已经存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
UATHelper: Packaging (Android (ASTC)):   > Task :app:processDebugManifest FAILED
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): Z:\app\src\main\AndroidManifest.xml:47:5-106 Error:
UATHelper: Packaging (Android (ASTC)): Element meta-data#com.epicgames.ue4.GameActivity.bUseExternalFilesDir at AndroidManifest.xml:47:5-106 duplicated with element declared at AndroidManifest.xml:27:5-107
UATHelper: Packaging (Android (ASTC)): Z:\app\src\main\AndroidManifest.xml Error:
UATHelper: Packaging (Android (ASTC)): Validation failed, exiting
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): FAILURE: Build failed with an exception.
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): * What went wrong:
UATHelper: Packaging (Android (ASTC)): Execution failed for task ':app:processDebugManifest'.
UATHelper: Packaging (Android (ASTC)): > Manifest merger failed with multiple errors, see logs
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): See http://g.co/androidstudio/manifest-merger for more information about the manifest merger.
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): * Try:
UATHelper: Packaging (Android (ASTC)): Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): * Get more help at https://help.gradle.org
UATHelper: Packaging (Android (ASTC)):
UATHelper: Packaging (Android (ASTC)): BUILD FAILED in 10s
UATHelper: Packaging (Android (ASTC)): 189 actionable tasks: 1 executed, 188 up-to-date
UATHelper: Packaging (Android (ASTC)): ERROR: cmd.exe failed with args /c "C:\Users\lipengzha\Documents\Unreal Projects\GCloudExample\Intermediate\Android\armv7\gradle\rungradle.bat" :app:assembleDebug
PackagingResults: Error: cmd.exe failed with args /c "C:\Users\lipengzha\Documents\Unreal Projects\GCloudExample\Intermediate\Android\armv7\gradle\rungradle.bat" :app:assembleDebug
UATHelper: Packaging (Android (ASTC)): Took 13.3060694s to run UnrealBuildTool.exe, ExitCode=6
UATHelper: Packaging (Android (ASTC)): UnrealBuildTool failed. See log for more details. (C:\Users\lipengzha\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.25\UBT-.txt)
UATHelper: Packaging (Android (ASTC)): AutomationTool exiting with ExitCode=6 (6)
UATHelper: Packaging (Android (ASTC)): BUILD FAILED
PackagingResults: Error: Unknown Error

如果想要修改或者删除 UE 默认生成的 AndroidManifest.xml 中的项,可以通过先删除再添加的方式。

以删除以下项为例:

1
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="false" />

在 UPL 的 androidManifestUpdates 中编写以下代码:

1
2
3
4
5
6
7
8
9
10
11
<androidManifestUpdates>
<loopElements tag="meta-data">
<setStringFromAttribute result="ApplicationSectionName" tag="$" name="android:name"/>
<setBoolIsEqual result="bUseExternalFilesDir" arg1="$S(ApplicationSectionName)" arg2="com.epicgames.ue4.GameActivity.bUseExternalFilesDir"/>
<if condition="bUseExternalFilesDir">
<true>
<removeElement tag="$"/>
</true>
</if>
</loopElements>
</androidManifestUpdates>

就是去遍历 AndroidManfest.xml 中已经存在 meta-data 中,android:namecom.epicgames.ue4.GameActivity.bUseExternalFilesDir 的项给删除。

JNI 调用接收 ActivityResult

有时需要通过 startActivityForResult 来创建 Intent 来执行一些操作,如打开摄像头、打开相册选择图片等。

但是 Android 做这些操作的时候不是阻塞在当前的函数中的,所以不能直接在调用的函数里接收这些数据。而通过 startActivityForResult 执行的 Action 的结果都会调用到 Activity 的 onActivityResult 中。

1
2
3
// GameActivity.java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){}

UE 在 UPL 中提供了往 OnActivityResult 追加 Java 代码的用法:

1
2
<!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
<gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>

使用这种方式添加的 Java 代码会追加到 OnActivityResult 函数的末尾,但是这种方式有一个问题,那就是执行了自己追加到 OnActivityResult 的代码之后,还要处理接收到的结果,并且传递到 UE 端来,有点麻烦。

经过翻阅代码,发现 UE 提供了 Java 端的 OnActivityResult 的多播代理事件,这样就可以直接在 UE 里用 C++ 来监听 OnActivityResult 的事件,自己做处理。

1
2
3
4
5
6
// Launch/Puclic/Android/AndroidJNI.h
DECLARE_MULTICAST_DELEGATE_SixParams(FOnActivityResult, JNIEnv *, jobject, jobject, jint, jint, jobject);

// 该代理是定义在 `FJavaWrapper` 里的
// Delegate that can be registered to that is called when an activity is finished
static FOnActivityResult OnActivityResultDelegate;

在 UE 侧就可以通过绑定这个多播代理来监听 Java 端的 OnActivityResult 调用,可以在其中做分别的处理。

它由 AndroidJNI.cpp 中的 Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult 函数从 Java 那边调用过来,调用机制在上个笔记中有记录。

Java 调 C++

有些需求和实现需要从 Java 调到 C++ 这边,可以通过下面这种方式:
首先,在 GameActivity 中新建一个 native 的 java 函数声明(不需要定义):

1
public native void nativeOnActivityResult(GameActivity activity, int requestCode, int resultCode, Intent data);

然后在 C++ 端按照下面的规则定义一个 C++ 函数:

1
2
3
4
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult(JNIEnv* jenv, jobject thiz, jobject activity, jint requestCode, jint resultCode, jobject data)
{
FJavaWrapper::OnActivityResultDelegate.Broadcast(jenv, thiz, activity, requestCode, resultCode, data);
}

可以看到函数名字的规则为:

  1. 函数前需要加 JNI_METHOD 修饰,它是一个宏__attribute__ ((visibility ("default"))) extern "C"
  2. 函数名需要以 Java_ 开头,并且后面跟上 com_epicgames_ue4_GameActivity_,标识是定义在GameActivity 中的
  3. 然后再跟上 java 中的函数名

接受参数的规则:

  1. 第一个参数是 Java 的 Env
  2. 第二个是 java 里的 this
  3. 后面的参数以此是从 java 里传递参数

AndroidP 的全面屏适配

在 UE4 打包的时候,会给项目生成 GameActivity.java 文件,里面的 OnCreate 具有适配全面屏的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onCreate(Bundle savedInstanceState)
{
// ...
if (UseDisplayCutout)
{
// will not be true if not Android Pie or later
WindowManager.LayoutParams params = getWindow().getAttributes();
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(params);
}
// ...
}

Android P 和之后的系统不支持,所以就要自己写 Jni 调用来强制让 P 和之后系统版本支持。
在 UE4.23+ 以后的引擎版本,支持了通过 UPL 往 OnCreate 函数添加代码,就可以直接把代码插入到 GameActivity.java 了:

1
2
3
4
5
6
7
8
9
10
<gameActivityOnCreateFinalAdditions>
<insert>
// P 版本允许使用刘海
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams lp = this.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
this.getWindow().setAttributes(lp);
}
</insert>
</gameActivityOnCreateFinalAdditions>

在 UE4.23 之前不可以给 OnCreate 添加代码,只能自己写个 JNI 调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void AndroidThunkJava_SetFullScreenDisplayForP()
{
final GameActivity Activity = this;
runOnUiThread(new Runnable()
{
private GameActivity InActivity = Activity;
public void run()
{
WindowManager windowManager = InActivity.getWindowManager();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
{

WindowManager.LayoutParams lp = InActivity.getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
InActivity.getWindow().setAttributes(lp);
Log.debug("call AndroidThunkJava_SetFullScreenDisplayForP");
}
}
});
}

将其在 UPL 中通过 <gameActivityClassAdditions> 添加到 GameActivity.java 中,在游戏启动时通过 JNI 调用即可。

注:

layoutInDisplayCutoutMode的可选项为:

外部资料

Android 重启 App

当游戏更新完毕之后有时候需要重启 App 才可以生效,在 UE 中可以使用 UPL 写入以下 java 代码:

1
2
3
4
5
6
7
8
9
10
public void AndroidThunkJava_AndroidAPI_RestartApplication() {
Context context = getApplicationContext();
PackageManager pm = context.getPackageManager();
Intent intent = pm.getLaunchIntentForPackage(context.getPackageName());
int delayTime = 500;
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent restartIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + delayTime, restartIntent);
System.exit(0);
}

在需要重启的时候通过 jni 调用来触发:

1
2
3
4
5
6
7
8
9
void RestartApplication()
{
#if PLATFORM_ANDROID
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true))
{
FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, AndroidThunkJava_AndroidAPI_RestartApplication);
}
#endif
}