UE开发笔记:Android篇

In a previous article, I introduced the development notes and some engineering practices for Mac/iOS: UE4 Development Notes: Mac/iOS Edition. This article serves as a sister piece, documenting the standardized environment, debugging tools, engineering practices, and analysis of related engine code that I used while developing on Android with UE. It records some pitfalls encountered in the project, primarily organized from my previous notes notes/ue. Future Android-related content will also be updated in this article.

Blog Articles

Articles related to Android in the blog:

Compilation Environment

The development environment for Android requires a combination of JDK/NDK/SDK/Ant/Gradle. The UE documentation mentions that NVDIA’s CodeWorks must be installed. However, due to network issues in China, it’s challenging to download, and some components are not necessary, so I packaged an Android development environment for quick deployment.

Component versions:

  • JDK 18077
  • NDK r14b
  • SDK android19-26
  • Ant 1.8.2
  • Gradle 4.1

Download link: AndroidSDK_1R7u1_20190923.7z, after unzipping, there is an AddToPath.bat script to add the necessary environment variables with one click.

1
2
3
4
5
6
7
8
9
10
11
12
@echo off
set "current_dir_name=%~dp0"
setx /M JAVA_HOME "%current_dir_name%jdk18077"
setx /M ANDROID_HOME "%current_dir_name%android-sdk-windows"
setx /M ANDROID_NDK_ROOT "%current_dir_name%android-ndk-r14b"
setx /M ANT_HOME "%current_dir_name%apache-ant-1.8.2"
setx /M GRADLE_HOME "%current_dir_name%gradle-4.1"
setx /M NDK_ROOT "%current_dir_name%android-ndk-r14b"
setx /M NDKROOT "%current_dir_name%android-ndk-r14b"
setx /M NVPACK_NDK_TOOL_VERSION "4.9"
setx /M NVPACK_NDK_VERSION "android-ndk-r14b"
setx /M NVPACK_ROOT "%current_dir_name%"

If you need to update to a newer version of NDK and SDK, some versions might be outdated. You can download the necessary versions yourself:

After downloading, place them in the corresponding directory and modify the values in the environment variables.

After adding the environment variables, there is no need to set the SDK and NDK paths in UE; you can keep the defaults for packaging.

Engine Dependencies and Support

Engine Support for Android Versions

In my previous notes: Android SDK Versions and Android Versions, I listed a table comparing Android system versions and API level versions.

However, different versions of the UE engine support different Android systems. In the Project Setting - Android, you can set the Minimum SDK Version, which is the lowest system version supported for packaging Android in UE.

In UE4.25, the minimum level can be set to 19, which is Android 4.4. In engine versions prior to 4.25, the minimum supported level is 9, which is Android 2.3.

This part of the code can be viewed in Runtime/Android/AndroidRuntimeSettings/Classes/AndroidRuntimeSettings.h and compared for differences between engine versions.

SDK Version to Android Version Mapping Table

You can see the Android SDK Platform on Google’s developer site.
The meaning of Build.VERSION_CODES: Build.VERSION_CODES

AndroidVersion SDK Version Build.VERSION_CODES
Android 11 (API level 30) R
Android 10 (API level 29) Q
Android 9 (API level 28) P
Android 8.1 (API level27) O_MR1
Android 8.0 (API level 26) O
Android 7.1 (API level 25) N_MR1
Android 7.0 (API level 24) N
Android 6.0 (API level 23) M
Android 5.1 (API level 22) LOLLIPOP_MR1
Android 5.0 (API level 21) LOLLIPOP
Android 4.4W (API level 20) KITKAT_WATCH
Android 4.4 (API level 19) KITKAT
Android 4.3 (API level 18) JELLY_BEAN_MR2
Android 4.2 (API level 17) JELLY_BEAN_MR1
Android 4.1 (API level 16) JELLY_BEAN
Android 4.0.3 (API level15) ICE_CREAM_SANDWICH_MR1
Android 4.0 (API level 14) ICE_CREAM_SANDWICH
Android 3.2 (API level 13) HONEYCOMB_MR2
Android 3.1 (API level 12) HONEYCOMB_MR1
Android 3.0 (API level 11) HONEYCOMB
Android 2.3.3 (API level 10) GINGERBREAD_MR1
Android 2.3 (API level 9) GINGERBREAD

The minimum support for Android in UE4 is SDK9, which corresponds to Android 2.3.

Engine Requirements for Android NDK

UE requires the NDK environment installed on the system while packaging Android, but different engine versions have different requirements for the NDK version.

Using an unsupported NDK version will result in the following error:

1
2
UATHelper: Packaging (Android (ETC2)):   ERROR: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended)
PackagingResults: Error: Android toolchain NDK r14b not supported; please use NDK r21 to NDK r23 (NDK r21b recommended)

It indicates that the current NDK version in the system is not supported and displays the supported versions.

The NDK version detection during UE packaging occurs within UBT, specifically in the file UnrealBuildTool/Platform/Android/AndroidToolChain.cs.

It defines the minimum and maximum supported NDK versions for the current engine version:

1
2
3
4
// in ue 4.25
readonly int MinimumNDKToolchain = 210100;
readonly int MaximumNDKToolchain = 230100;
readonly int RecommendedNDKToolchain = 210200;

You can conveniently view the required NDK versions for different engine versions on GitHub: UE_425_AndroidToolChain.cs

The engine’s documentation also elaborates on different engine versions’ NDK requirements: Setting Up Android SDK and NDK for Unreal

Unreal Engine NDK Version
4.25+ NDK r21b, NDK r20b
4.21 - 4.24 NDK r14b
4.19 - 4.20 NDK r12b

NDK Compiler Versions

UE4 supports Android NDK versions r14b-r18b, but in UE4.22.3, setting r18b was recognized by the engine as r18c:

1
2
3
4
5
6
7
8
9
10
UATHelper: Packaging (Android (ETC2)):   Using 'git status' to determine working set for adaptive non-unity build (C:\Users\imzlp\Documents\Unreal Projects\GWorldClient).
UATHelper: Packaging (Android (ETC2)): ERROR: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended)
PackagingResults: Error: Android toolchain NDK r18c not supported; please use NDK r14b to NDK r18b (NDK r14b recommended)
UATHelper: Packaging (Android (ETC2)): Took 7.4575476s to run UnrealBuildTool.exe, ExitCode=5
UATHelper: Packaging (Android (ETC2)): ERROR: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt)
UATHelper: Packaging (Android (ETC2)): (see C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\Log.txt for full exception trace)
PackagingResults: Error: UnrealBuildTool failed. See log for more details. (C:\Users\imzlp\AppData\Roaming\Unreal Engine\AutomationTool\Logs\C+Program+Files+Epic+Games+UE_4.22\UBT-GWorld-Android-Development.txt)
UATHelper: Packaging (Android (ETC2)): AutomationTool exiting with ExitCode=5 (5)
UATHelper: Packaging (Android (ETC2)): BUILD FAILED
PackagingResults: Error: Unknown Error

The reason for needing to change the NDK version is that the compilers included in different NDK versions have varying levels of support for the C++11 standard.

NDK clang version
r14b clang 3.8.275480 (based on LLVM 3.8.275480)
r17c clang version 6.0.2
r18b clang version 7.0.2
r20b clang version 8.0.7

Tools

ADB

ADB is a powerful debugging tool for Android, and familiarizing yourself with some ADB commands can greatly enhance efficiency. First, download ADB.

Install APK

1
$ adb install APK_FILE_NAME.apk

Launch App

The installed renderdoccmd does not have a desktop icon. To start it yourself, you can only use the following ADB command:

1
adb shell am start org.renderdoc.renderdoccmd.arm64/.Loader -e renderdoccmd "remoteserver"

ADB command template for launching the app:

1
adb shell am start PACKAGE_NAME/.ActivityName

This method requires knowing the app’s package name and activity name. The package name can be easily found, but if you don’t know the activity, you can obtain it through the following procedure:

First, use a decompiling tool to unpack the APK (you can use the earlier mentioned apktools):

1
apktool.bat d -o ./renderdoccmd_arm64 org.renderdoc.renderdoccmd.arm64.apk

Then, open the AndroidManifest.xml file in the org.renderdoc.renderdoccmd.arm64 directory and find the Application entry:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.renderdoc.renderdoccmd.arm64" platformBuildVersionCode="26" platformBuildVersionName="8.0.0">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<application android:debuggable="true" android:hasCode="true" android:icon="@drawable/icon" android:label="RenderDocCmd">
<activity android:configChanges="keyboardHidden|orientation" android:exported="true" android:label="RenderDoc" android:name=".Loader" android:screenOrientation="landscape">
<meta-data android:name="android.app.lib_name" android:value="renderdoccmd"/>
</activity>
</application>
</manifest>

This contains all registered Activity entries. For an APK without a UI, there is typically only one activity; hence the main activity for the renderdoccmd is .Loader.

If it’s a UI app, there may be multiple activities. You can check the Category in AndroidManifest.xml or identify the main activity by looking for the one with main in its name. Typically, it starts with the launcher, leads to the main activity, or some may navigate to a login interface.

PS: The main activity for a game packaged with UE is com.epicgames.ue4.SplashActivity, which can be launched using the following command.

1
adb shell am start com.imzlp.GWorld/com.epicgames.ue4.SplashActivity

Transfer Files

To transfer files from your computer to the mobile device using ADB:

1
2
# adb push 1.0_Android_ETC2_P.pak /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks
$ adb push FILE_NAME REMOATE_PATH

To transfer files from the mobile device to your computer:

1
2
# adb pull /sdcard/Android/data/com.imzlp.TEST/files/UE4GameData/Mobile422/Mobile422/Saved/Paks/1.0_Android_ETC2_P.pak A.Pak
$ adb pull REMOATE_FILE_PATH LOCAL_PATH

Logcat

Using logcat allows you to see log information from the Android device.

1
$ adb logcat

This will print out all information from the current device, but during app debugging, we don’t need to see so much. We can use find to filter results (case sensitive):

1
2
# adb logcat | find "GWorld"
$ adb logcat | find "KEY_WORD"

To filter logs from the packaged UE app:

1
$ adb logcat | find "UE4"

If there are too many accumulated logs from frequent runs, you can clear them:

1
adb logcat -c

Extract Installed APKs from the Device

Note: When executing the following commands, ensure that developer permissions are enabled on your phone. You must allow the verification fingerprint prompts.

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
# Check connected devices
$ adb devices
List of devices attached
b2fcxxxx unauthorized
# List all installed apps on the phone
$ adb shell pm list package
# If prompted with the error below, execute adb kill-server
error: device unauthorized.
This adb servers $ADB_VENDOR_KEYS is not set
Try 'adb kill-server' if that seems wrong.
Otherwise check for a confirmation dialog on your device.
# Normally, it should list many entries like this
C:\Users\imzlp>adb shell pm list package
package:com.miui.screenrecorder
package:com.amazon.mShop.android.shopping
package:com.mobisystems.office
package:com.weico.international
package:com.github.shadowsocks
package:com.android.cts.priv.ctsshim
package:com.sorcerer.sorcery.iconpack
package:com.google.android.youtube

# Find the APK location for the specified app
$ adb shell pm path com.github.shadowsocks
package:/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
# Then pull that file to your local machine
$ adb pull /data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/base.apk
/data/app/com.github.shadowsocks-iBtqbmLo8rYcq2BqFhJtsA==/...se.apk: 1 file pulled. 21.5 MB/s (4843324 bytes in 0.215s)

Flash Recovery

Download ADB, and use the following commands based on your circumstances (if you’re already in bootloader, you don’t need to execute the first command).

1
2
3
4
5
6
adb reboot bootloader
# Write img to the device
fastboot flash recovery recovery.img
fastboot flash boot boot.img
# Boot img
fastboot boot recovery.img

Port Forwarding

You can specify port forwarding with ADB commands:

1
2
3
4
# PC to Device
adb reverse tcp:1985 tcp:1985
# Device to PC
adb forward tcp:1985 tcp:1985

Check APK Location by Package Name

You can use the following ADB commands:

1
2
$ adb shell pm list package -f com.tencent.tmgp.fm
package:/data/app/com.tencent.tmgp.fm-a_cOsX8G3VClXwiI-RD9wQ==/base.apk=com.tencent.tmgp.fm

The last parameter is the package name, and the output will be the APK’s path.

View Package Name of the Current Window’s App

Use the following ADB command:

1
2
3
4
5
6
7
8
$ adb shell dumpsys window w | findstr \/ | findstr name=
mSurface=Surface(name=SideSlideGestureBar-Bottom)/@0xa618588
mSurface=Surface(name=SideSlideGestureBar-Right)/@0x619b646
mSurface=Surface(name=SideSlideGestureBar-Left)/@0xea02007
mSurface=Surface(name=StatusBar)/@0x7e4962d
mAnimationIsEntrance=true mSurface=Surface(name=com.tencent.tmgp.fm/com.epicgames.ue4.GameActivity)/@0x43b30a0
mSurface=Surface(name=com.tencent.tmgp.fm/com.epicgames.ue4.GameActivity)/@0xa3481e
mAnimationIsEntrance=true mSurface=Surface(name=com.vivo.livewallpaper.monster.bmw.MonsterWallpaperService)/@0x53e44ae

In this output, the string following mAnimationIsEntrance=true mSurface=Surface(name= and before / is our app’s package name.

View Symbols in SO Files

You can use the objdump tool, which has a variety of executable programs compatible with different platforms in NDK:

1
2
3
x86_64-linux-android-objdump.exe
aarch64-linux-android-objdump.exe
arm-linux-androideabi-objdump.exe

Choose the one you need.

You can use the command objdump -tT libUE4.so to output the contents of the SO symbol table.

SessionFrontEnd Real-Time Analysis

There are several prerequisites:

  1. USB connection between PC and phone
  2. ADB installed

First, port mapping is needed because SessionFrontEnd communicates with the game through listening ports, and the phone and PC are not in the same subnet. Therefore, you need to forward the PC’s listening port to the phone’s port through ADB.

The listening port for SessionFrontEnd can be obtained via port analysis on UE4Editor.exe:

1
2
3
4
5
6
7
8
9
C:\Users\lipengzha>netstat -ano | findstr "231096"  
TCP 0.0.0.0:1985 0.0.0.0:0 LISTENING 231096
TCP 0.0.0.0:3961 0.0.0.0:0 LISTENING 231096
TCP 0.0.0.0:3963 0.0.0.0:0 LISTENING 231096
TCP 127.0.0.1:4014 127.0.0.1:12639 ESTABLISHED 231096
TCP 127.0.0.1:4199 127.0.0.1:12639 ESTABLISHED 231096
UDP 0.0.0.0:6666 *:* 231096
UDP 0.0.0.0:24024 *:* 231096
UDP 0.0.0.0:58101 *:* 231096

You need to map port 1985 on the PC to port 1985 on Android, so when the mobile app connects to 0.0.0.0 on port 1985, it connects to the port on the PC.

Use the following ADB command:

1
adb reverse tcp:1985 tcp:1985 

Then specify the launch parameters for the app on the phone:

1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device"  

Save these texts as a UE4Commandline.txt file in the project data directory, specifically at:

1
/sdcard/UE4Game/PROJECT_NAME/ 

Afterward, directly start the app, and you will see the device’s data in SessionFrontEnd on the PC.

Unreal Insights Real-Time Analysis

The previous section introduced using SessionFrontEnd for real-time analysis on Android. During actual testing, it was found to be somewhat unstable, potentially causing crashes in the game. In newer versions of the engine, UE also provides a new performance analysis tool called Unreal Insights, which allows for more convenient and intuitive profiling.
Documentation:

Port mapping is also required, mapping port 1980 on the PC to the device:

1
adb reverse tcp:1980 tcp:1980 

Next, add startup commands to the Android device:

1
../../../FGame/FGame.uproject -Messaging -SessionOwner="lipengzha" -SessionName="Launch On Android Device" -iterative -tracehost=127.0.0.1 -Trace=CPU 

Enable Unreal Insights on the PC and start the game on the mobile device to begin real-time capturing:

Unreal Insights can also capture data in PIE mode in real time by adding the -trace parameter when starting the Editor:

1
UE4Editor.exe PROJECT_NAME.uproject -trace=counters,cpu,frame,bookmark,gpu

After launching the game, listen to 127.0.0.1 in Unreal Insights.

Engineering Practices

When developing for Android, there is a requirement to support both arm64 and armv7, and both libraries need to be added to PublicAdditionalLibraries in the Build.cs:

1
2
3
4
5
PublicAdditionalLibraries.AddRange(new string[]
{
Path.Combine(ThirdPartyFolder, "Android_armeabi-v7a", AkConfigurationDir),
Path.Combine(ThirdPartyFolder, "Android_arm64-v8a", AkConfigurationDir),
});

However, there isn’t a method in UE’s ModuleRules to determine the architecture currently being compiled (the ModuleRules constructor only runs once during compilation), leading to linker errors when compiling for arm64, as it finds the armv7 libraries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
14>ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkAudioLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkLEngine.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkAudioLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSoundEngine.a(AkLEngine.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgrBase.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMemoryMgr.a(AkMemoryMgrBase.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkStreamMgr.a(AkStreamMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkStreamMgr.a(AkStreamMgr.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMusicEngine.a(AkMusicRenderer.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMusicEngine.a(AkMusicRenderer.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSpatialAudio.a(AkSpatialAudio.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkSpatialAudio.a(AkSpatialAudio.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkAudioInputSource.a(AkFXSrcAudioInput.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkAudioInputSource.a(AkFXSrcAudioInput.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkVorbisDecoder.a(AkVorbisLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkVorbisDecoder.a(AkVorbisLib.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMeterFX.a(InitAkMeterFX.o) is incompatible with aarch64linux
ld.lld: error: D:/Client/Plugins/WWise/ThirdParty/Android_armeabi-v7a/Profile/lib\libAkMeterFX.a(InitAkMeterFX.o) is incompatible with aarch64linux
ld.lld: error: too many errors emitted, stopping now (use -error-limit=0 to see all errors)
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
14>Execution failed. Error: 1 (0x01) Target: 'C:\BuildAgent\workspace\FGameClientBuild\Client\Binaries\Android\FGame-arm64.so'

Thus, a method is needed to allow the compiler to automatically match the correct path for linking when using PublicAdditionalLibraries with both the arm64 and armv7 libraries.

Looking through the UBT code, it’s discovered that UE performs pattern matching on the paths of link libraries for Android:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Engine\Source\Programs\UnrealBuildTool\Platform\Android\AndroidToolChain.cs
static private Dictionary<string, string[]> AllArchNames = new Dictionary<string, string[]> {
{ "-armv7", new string[] { "armv7", "armeabi-v7a", } },
{ "-arm64", new string[] { "arm64", "arm64-v8a", } },
{ "-x86", new string[] { "x86", } },
{ "-x64", new string[] { "x64", "x86_64", } },
};

static bool IsDirectoryForArch(string Dir, string Arch)
{
// make sure paths use one particular slash
Dir = Dir.Replace("\\", "/").ToLowerInvariant();

// look for other architectures in the Dir path, and fail if it finds it
foreach (KeyValuePair<string, string[]> Pair in AllArchNames)
{
if (Pair.Key != Arch)
{
foreach (string ArchName in Pair.Value)
{
// if there's a directory in the path with a bad architecture name, reject it
if (Regex.IsMatch(Dir, "/" + ArchName + "$") || Regex.IsMatch(Dir, "/" + ArchName + "/") || Regex.IsMatch(Dir, "/" + ArchName + "_API[0-9]+_NDK[0-9]+", RegexOptions.IgnoreCase))
{
return false;
}
}
}
}

// if nothing was found, we are okay
return true;
}

public override FileItem[] LinkAllFiles(LinkEnvironment LinkEnvironment, bool bBuildImportLibraryOnly, IActionGraphBuilder Graph)
{
// ...
// Add the library paths to the additional path list
foreach (DirectoryReference LibraryPath in LinkEnvironment.LibraryPaths)
{
// LinkerPaths could be relative or absolute
string AbsoluteLibraryPath = Utils.ExpandVariables(LibraryPath.FullName);
if (IsDirectoryForArch(AbsoluteLibraryPath, Arch))
{
// environment variables aren't expanded when using the $( style
if (Path.IsPathRooted(AbsoluteLibraryPath) == false)
{
AbsoluteLibraryPath = Path.Combine(LinkerPath.FullName, AbsoluteLibraryPath);
}
AbsoluteLibraryPath = Utils.CollapseRelativeDirectories(AbsoluteLibraryPath);
if (!AdditionalLibraryPaths.Contains(AbsoluteLibraryPath))
{
AdditionalLibraryPaths.Add(AbsoluteLibraryPath);
}
}
}

// ...
}

The key part is this line:

1
if (Regex.IsMatch(Dir, "/" + ArchName + "$") || Regex.IsMatch(Dir, "/" + ArchName + "/") || Regex.IsMatch(Dir, "/" + ArchName + "_API[0-9]+_NDK[0-9]+", RegexOptions.IgnoreCase))

Based on the regex rule above, you can change the paths of the linked libraries to:

1
2
XXXX/armeabi-v7a/
XXXX/armeabi-v8a/

As long as the paths for the libraries added for Android match this rule, UE will automatically use the corresponding architecture’s libraries (.a and .so can both use this rule).

UE is quite tricky here; this requirement isn’t documented, making it a completely unspoken rule.

Android Runtime Permission Requests

ActivityCompact has a requestPermission method that can handle such situations.

Get Package Name on Android

By adding the following code in UPL in Java:

1
2
3
4
5
public String AndroidThunkJava_GetPackageName()
{
Context context = getApplicationContext();
return context.getPackageName();
}

You can call it from C++ using JNI:

1
2
3
4
5
6
7
8
9
10
11
12
FString UFlibAppHelper::GetAppPackageName()
{
FString result;
#if PLATFORM_ANDROID
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetPackageName", "()Ljava/lang/String;", false);
result = FJavaHelper::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis, GetInstalledPakPathMethodID));
}
#endif
return result;
}

Get External Storage Path on Android

To get the App’s sandbox path:

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
// /storage/emulated/0/Android/data/com.xxxx.yyyy.zzzz/files
FString FAndroidGCloudPlatformMisc::getExternalStorageDirectory()
{
FString result;
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
// get context
jobject JniEnvContext;
{
jclass activityThreadClass = Env->FindClass("android/app/ActivityThread");
jmethodID currentActivityThread = FJavaWrapper::FindStaticMethod(Env, activityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;", false);
jobject at = Env->CallStaticObjectMethod(activityThreadClass, currentActivityThread);
jmethodID getApplication = FJavaWrapper::FindMethod(Env, activityThreadClass, "getApplication", "()Landroid/app/Application;", false);

JniEnvContext = FJavaWrapper::CallObjectMethod(Env, at, getApplication);
}
jmethodID getExternalFilesDir = Env->GetMethodID(Env->GetObjectClass(JniEnvContext), "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;");
// get File
jobject ExternalFileDir = Env->CallObjectMethod(JniEnvContext, getExternalFilesDir, nullptr);
// getPath method in File class
jmethodID getFilePath = Env->GetMethodID(Env->FindClass("java/io/File"), "getPath", "()Ljava/lang/String;");
jstring pathString = (jstring)Env->CallObjectMethod(ExternalFileDir, getFilePath, nullptr);
const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
result = ANSI_TO_TCHAR(nativePathString);
}
return result;
}

Get Installed App’s APK Path on Android

To retrieve the APK path at runtime, since UE does not have existing interfaces, JNI must be used to get it from Java.
By checking the Android Developer documentation, it is found that ApplicationInfo contains the sourceDir property, which records the APK path. This can be fetched using the PackageManager call to getApplicationInfo for a specific package name.

Thus, Java code in UPL:

1
2
3
4
5
6
7
8
9
10
11
12
public String AndroidThunkJava_GetInstalledApkPath()
{
Context context = getApplicationContext();
PackageManager packageManager = context.getPackageManager();
ApplicationInfo appInfo;
try{
appInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
return appInfo.sourceDir;
}catch (PackageManager.NameNotFoundException e){
return "invalid";
}
}

Then call it in UE using JNI:

1
2
3
4
5
if (JNIEnv* Env = FAndroidApplication::GetJavaEnv())
{
jmethodID GetInstalledPakPathMethodID = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, "AndroidThunkJava_GetInstalledApkPath", "()Ljava/lang/String;", false);
FString ResultApkPath = FJavaHelperEx::FStringFromLocalRef(Env, (jstring)FJavaWrapper::CallObjectMethod(Env, FJavaWrapper::GameActivityThis, GetInstalledPakPathMethodID));
}

Here FJavaHelperEx::FStringFromLocalRef is my wrapper function for converting from jstring to FString:

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
namespace FJavaHelperEx
{
FString FStringFromParam(JNIEnv* Env, jstring JavaString)
{
if (!Env || !JavaString || Env->IsSameObject(JavaString, NULL))
{
return {};
}

const auto chars = Env->GetStringUTFChars(JavaString, 0);
FString ReturnString(UTF8_TO_TCHAR(chars));
Env->ReleaseStringUTFChars(JavaString, chars);
return ReturnString;
}

FString FStringFromLocalRef(JNIEnv* Env, jstring JavaString)
{
FString ReturnString = FStringFromParam(Env, JavaString);

if (Env && JavaString)
{
Env->DeleteLocalRef(JavaString);
}

return ReturnString;
}
}

The retrieved result:

Upgrade to AndroidX

Use UPL to intervene in the packaging process:

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
30
31
32
33
34
35
36
37
<gradleProperties>
<insert>
android.useAndroidX=true
android.enableJetifier=true
</insert>
</gradleProperties>

<baseBuildGradleAdditions>
<insert>
<!-- Here goes the gradle code -->
allprojects {
def mappings = [
'android.support.annotation': 'androidx.annotation',
'android.arch.lifecycle': 'androidx.lifecycle',
'android.support.v4.app.NotificationCompat': 'androidx.core.app.NotificationCompat',
'android.support.v4.app.ActivityCompat': 'androidx.core.app.ActivityCompat',
'android.support.v4.content.ContextCompat': 'androidx.core.content.ContextCompat',
'android.support.v13.app.FragmentCompat': 'androidx.legacy.app.FragmentCompat',
'android.arch.lifecycle.Lifecycle': 'androidx.lifecycle.Lifecycle',
'android.arch.lifecycle.LifecycleObserver': 'androidx.lifecycle.LifecycleObserver',
'android.arch.lifecycle.OnLifecycleEvent': 'androidx.lifecycle.OnLifecycleEvent',
'android.arch.lifecycle.ProcessLifecycleOwner': 'androidx.lifecycle.ProcessLifecycleOwner',
]

beforeEvaluate { project ->
project.rootProject.projectDir.traverse(type: groovy.io.FileType.FILES, nameFilter: ~/.*\.java$/) { f ->
mappings.each { entry ->
if (f.getText('UTF-8').contains(entry.key)) {
println "Updating ${entry.key} to ${entry.value} in file ${f}"
ant.replace(file: f, token: entry.key, value: entry.value)
}
}
}
}
}
</insert>
</baseBuildGradleAdditions>

Not supporting AndroidX on UE 4.27 will lead to a packing failure.

In these two lines of code in SplashActivity.java, there are errors:

1
2
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

Error log: Task :app:compileDebugJavaWithJavac FAILED, and following the method in this article to support AndroidX allows packing to succeed.

References:

Adding External Storage Read/Write Permissions for APK

In Project Settings - Platform - Android - Advanced APK Packaging - Extra Permissions, add:

1
2
android.permission.WRITE_EXTERNAL_STORAGE
android.permission.READ_EXTERNAL_STORAGE

HTTP Request Error on AndroidP

Using HTTP requests to upload data on Android P will show the following error message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo W/CrashReport: java.io.IOException: Cleartext HTTP traffic to android.bugly.qq.com not permitted
at com.android.okhttp.HttpHandler$CleartextURLFilter.checkURLPermitted(HttpHandler.java:115)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:458)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:127)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:258)
at com.tencent.bugly.proguard.ai.a(BUGLY:265)
at com.tencent.bugly.proguard.ai.a(BUGLY:114)
at com.tencent.bugly.proguard.al.run(BUGLY:355)
at com.tencent.bugly.proguard.ak$1.run(BUGLY:723)
at java.lang.Thread.run(Thread.java:764)
2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo E/CrashReport: Failed to upload, please check your network.
2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo D/CrashReport: Failed to execute post.
2018-10-10 16:39:21.312 31611-31646/com.xfhy.tinkerfirmdemo E/CrashReport: [Upload] Failed to upload(1): Failed to upload for no response!
2018-10-10 16:39:21.313 31611-31646/com.xfhy.tinkerfirmdemo E/CrashReport: [Upload] Failed to upload(1) userinfo: failed after many attempts

You need to configure the specified domain name as a whitelist during packaging.

The method is as follows:

Create a network_security_config.xml file in res/xml with the following content (modify the URL as needed):

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">android.bugly.qq.com</domain>
</domain-config>
</network-security-config>

Then reference this file in AndroidManifest.xml:

1
<application android:networkSecurityConfig="@xml/network_security_config"/>

Re-packaging will bring the following log at runtime:

1
02-25 21:09:15.831 27760 27791 D NetworkSecurityConfig: Using Network Security Config from resource network_security_config debugBuild: true

Writing Files on Android

When calling FFileHelper::SaveArrayToFile:

1
FFileHelper::SaveArrayToFile(TArrayView<const uint8>(data, delta), *path, &IFileManager::Get(), EFileWrite::FILEWRITE_Append));

This function internally creates an FArchive object to manage the current file, which has an IFileHandle object called Handle. On the Android platform, it’s FFileHandleAndroid.

Writing to files in FArchive calls Serialize, which in turn calls Handle‘s Write:

1
2
3
4
bool FArchiveFileWriterGeneric::WriteLowLevel( const uint8* Src, int64 CountToWrite )
{
return Handle->Write( Src, CountToWrite );
}

The Android Write implementation is:

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
30
31
// Runtime/Core/Private/Android/AndroidFile.h
virtual bool Write(const uint8* Source, int64 BytesToWrite) override
{
CheckValid();
if (nullptr != File->Asset)
{
// Can't write to assets.
return false;
}

bool bSuccess = true;
while (BytesToWrite)
{
check(BytesToWrite >= 0);
int64 ThisSize = FMath::Min<int64>(READWRITE_SIZE, BytesToWrite);
check(Source);
if (__pwrite(File->Handle, Source, ThisSize, CurrentOffset) != ThisSize)
{
bSuccess = false;
break;
}
CurrentOffset += ThisSize;
Source += ThisSize;
BytesToWrite -= ThisSize;
}

// Update the cached file length
Length = FMath::Max(Length, CurrentOffset);

return bSuccess;
}

You can see it writes to a file in chunks of 1MB each time.

Android File Write Error Code Correspondence

Look up error string by error code number.

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// bionic_errdefs.h
#ifndef __BIONIC_ERRDEF
#error "__BIONIC_ERRDEF must be defined before including this file"
#endif
__BIONIC_ERRDEF( 0 , 0, "Success" )
__BIONIC_ERRDEF( EPERM , 1, "Operation not permitted" )
__BIONIC_ERRDEF( ENOENT , 2, "No such file or directory" )
__BIONIC_ERRDEF( ESRCH , 3, "No such process" )
__BIONIC_ERRDEF( EINTR , 4, "Interrupted system call" )
__BIONIC_ERRDEF( EIO , 5, "I/O error" )
__BIONIC_ERRDEF( ENXIO , 6, "No such device or address" )
__BIONIC_ERRDEF( E2BIG , 7, "Argument list too long" )
__BIONIC_ERRDEF( ENOEXEC , 8, "Exec format error" )
__BIONIC_ERRDEF( EBADF , 9, "Bad file descriptor" )
__BIONIC_ERRDEF( ECHILD , 10, "No child processes" )
__BIONIC_ERRDEF( EAGAIN , 11, "Try again" )
__BIONIC_ERRDEF( ENOMEM , 12, "Out of memory" )
__BIONIC_ERRDEF( EACCES , 13, "Permission denied" )
__BIONIC_ERRDEF( EFAULT , 14, "Bad address" )
__BIONIC_ERRDEF( ENOTBLK , 15, "Block device required" )
__BIONIC_ERRDEF( EBUSY , 16, "Device or resource busy" )
__BIONIC_ERRDEF( EEXIST , 17, "File exists" )
__BIONIC_ERRDEF( EXDEV , 18, "Cross-device link" )
__BIONIC_ERRDEF( ENODEV , 19, "No such device" )
__BIONIC_ERRDEF( ENOTDIR , 20, "Not a directory" )
__BIONIC_ERRDEF( EISDIR , 21, "Is a directory" )
__BIONIC_ERRDEF( EINVAL , 22, "Invalid argument" )
__BIONIC_ERRDEF( ENFILE , 23, "File table overflow" )
__BIONIC_ERRDEF( EMFILE , 24, "Too many open files" )
__BIONIC_ERRDEF( ENOTTY , 25, "Not a typewriter" )
__BIONIC_ERRDEF( ETXTBSY , 26, "Text file busy" )
__BIONIC_ERRDEF( EFBIG , 27, "File too large" )
__BIONIC_ERRDEF( ENOSPC , 28, "No space left on device" )
__BIONIC_ERRDEF( ESPIPE , 29, "Illegal seek" )
__BIONIC_ERRDEF( EROFS , 30, "Read-only file system" )
__BIONIC_ERRDEF( EMLINK , 31, "Too many links" )
__BIONIC_ERRDEF( EPIPE , 32, "Broken pipe" )
__BIONIC_ERRDEF( EDOM , 33, "Math argument out of domain of func" )
__BIONIC_ERRDEF( ERANGE , 34, "Math result not representable" )
__BIONIC_ERRDEF( EDEADLK , 35, "Resource deadlock would occur" )
__BIONIC_ERRDEF( ENAMETOOLONG , 36, "File name too long" )
__BIONIC_ERRDEF( ENOLCK , 37, "No record locks available" )
__BIONIC_ERRDEF( ENOSYS , 38, "Function not implemented" )
__BIONIC_ERRDEF( ENOTEMPTY , 39, "Directory not empty" )
__BIONIC_ERRDEF( ELOOP , 40, "Too many symbolic links encountered" )
__BIONIC_ERRDEF( ENOMSG , 42, "No message of desired type" )
__BIONIC_ERRDEF( EIDRM , 43, "Identifier removed" )
__BIONIC_ERRDEF( ECHRNG , 44, "Channel number out of range" )
__BIONIC_ERRDEF( EL2NSYNC , 45, "Level 2 not synchronized" )
__BIONIC_ERRDEF( EL3HLT , 46, "Level 3 halted" )
__BIONIC_ERRDEF( EL3RST , 47, "Level 3 reset" )
__BIONIC_ERRDEF( ELNRNG , 48, "Link number out of range" )
__BIONIC_ERRDEF( EUNATCH , 49, "Protocol driver not attached" )
__BIONIC_ERRDEF( ENOCSI , 50, "No CSI structure available" )
__BIONIC_ERRDEF( EL2HLT , 51, "Level 2 halted" )
__BIONIC_ERRDEF( EBADE , 52, "Invalid exchange" )
__BIONIC_ERRDEF( EBADR , 53, "Invalid request descriptor" )
__BIONIC_ERRDEF( EXFULL , 54, "Exchange full" )
__BIONIC_ERRDEF( ENOANO , 55, "No anode" )
__BIONIC_ERRDEF( EBADRQC , 56, "Invalid request code" )
__BIONIC_ERRDEF( EBADSLT , 57, "Invalid slot" )
__BIONIC_ERRDEF( EBFONT , 59, "Bad font file format" )
__BIONIC_ERRDEF( ENOSTR , 60, "Device not a stream" )
__BIONIC_ERRDEF( ENODATA , 61, "No data available" )
__BIONIC_ERRDEF( ETIME , 62, "Timer expired" )
__BIONIC_ERRDEF( ENOSR , 63, "Out of streams resources" )
__BIONIC_ERRDEF( ENONET , 64, "Machine is not on the network" )
__BIONIC_ERRDEF( ENOPKG , 65, "Package not installed" )
__BIONIC_ERRDEF( EREMOTE , 66, "Object is remote" )
__BIONIC_ERRDEF( ENOLINK , 67, "Link has been severed" )
__BIONIC_ERRDEF( EADV , 68, "Advertise error" )
__BIONIC_ERRDEF( ESRMNT , 69, "Srmount error" )
__BIONIC_ERRDEF( ECOMM , 70, "Communication error on send" )
__BIONIC_ERRDEF( EPROTO , 71, "Protocol error" )
__BIONIC_ERRDEF( EMULTIHOP , 72, "Multihop attempted" )
__BIONIC_ERRDEF( EDOTDOT , 73, "RFS specific error" )
__BIONIC_ERRDEF( EBADMSG , 74, "Not a data message" )
__BIONIC_ERRDEF( EOVERFLOW , 75, "Value too large for defined data type" )
__BIONIC_ERRDEF( ENOTUNIQ , 76, "Name not unique on network" )
__BIONIC_ERRDEF( EBADFD , 77, "File descriptor in bad state" )
__BIONIC_ERRDEF( EREMCHG , 78, "Remote address changed" )
__BIONIC_ERRDEF( ELIBACC , 79, "Can not access a needed shared library" )
__BIONIC_ERRDEF( ELIBBAD , 80, "Accessing a corrupted shared library" )
__BIONIC_ERRDEF( ELIBSCN , 81, ".lib section in a.out corrupted" )
__BIONIC_ERRDEF( ELIBMAX , 82, "Attempting to link in too many shared libraries" )
__BIONIC_ERRDEF( ELIBEXEC , 83, "Cannot exec a shared library directly" )
__BIONIC_ERRDEF( EILSEQ , 84, "Illegal byte sequence" )
__BIONIC_ERRDEF( ERESTART , 85, "Interrupted system call should be restarted" )
__BIONIC_ERRDEF( ESTRPIPE , 86, "Streams pipe error" )
__BIONIC_ERRDEF( EUSERS , 87, "Too many users" )
__BIONIC_ERRDEF( ENOTSOCK , 88, "Socket operation on non-socket" )
__BIONIC_ERRDEF( EDESTADDRREQ , 89, "Destination address required" )
__BIONIC_ERRDEF( EMSGSIZE , 90, "Message too long" )
__BIONIC_ERRDEF( EPROTOTYPE , 91, "Protocol wrong type for socket" )
__BIONIC_ERRDEF( ENOPROTOOPT , 92, "Protocol not available" )
__BIONIC_ERRDEF( EPROTONOSUPPORT, 93, "Protocol not supported" )
__BIONIC_ERRDEF( ESOCKTNOSUPPORT, 94, "Socket type not supported" )
__BIONIC_ERRDEF( EOPNOTSUPP , 95, "Operation not supported on transport endpoint" )
__BIONIC_ERRDEF( EPFNOSUPPORT , 96, "Protocol family not supported" )
__BIONIC_ERRDEF( EAFNOSUPPORT , 97, "Address family not supported by protocol" )
__BIONIC_ERRDEF( EADDRINUSE , 98, "Address already in use" )
__BIONIC_ERRDEF( EADDRNOTAVAIL , 99, "Cannot assign requested address" )
__BIONIC_ERRDEF( ENETDOWN , 100, "Network is down" )
__BIONIC_ERRDEF( ENETUNREACH , 101, "Network is unreachable" )
__BIONIC_ERRDEF( ENETRESET , 102, "Network dropped connection because of reset" )
__BIONIC_ERRDEF( ECONNABORTED , 103, "Software caused connection abort" )
__BIONIC_ERRDEF( ECONNRESET , 104, "Connection reset by peer" )
__BIONIC_ERRDEF( ENOBUFS , 105, "No buffer space available" )
__BIONIC_ERRDEF( EISCONN , 106, "Transport endpoint is already connected" )
__BIONIC_ERRDEF( ENOTCONN , 107, "Transport endpoint is not connected" )
__BIONIC_ERRDEF( ESHUTDOWN , 108, "Cannot send after transport endpoint shutdown" )
__BIONIC_ERRDEF( ETOOMANYREFS , 109, "Too many references: cannot splice" )
__BIONIC_ERRDEF( ETIMEDOUT , 110, "Connection timed out" )
__BIONIC_ERRDEF( ECONNREFUSED , 111, "Connection refused" )
__BIONIC_ERRDEF( EHOSTDOWN , 112, "Host is down" )
__BIONIC_ERRDEF( EHOSTUNREACH , 113, "No route to host" )
__BIONIC_ERRDEF( EALREADY , 114, "Operation already in progress" )
__BIONIC_ERRDEF( EINPROGRESS , 115, "Operation now in progress" )
__BIONIC_ERRDEF( ESTALE , 116, "Stale NFS file handle" )
__BIONIC_ERRDEF( EUCLEAN , 117, "Structure needs cleaning" )
__BIONIC_ERRDEF( ENOTNAM , 118, "Not a XENIX named type file" )
__BIONIC_ERRDEF( ENAVAIL , 119, "No XENIX semaphores available" )
__BIONIC_ERRDEF( EISNAM , 120, "Is a named type file" )
__BIONIC_ERRDEF( EREMOTEIO , 121, "Remote I/O error" )
__BIONIC_ERRDEF( EDQUOT , 122, "Quota exceeded" )
__BIONIC_ERRDEF( ENOMEDIUM , 123, "No medium found" )
__BIONIC_ERRDEF( EMEDIUMTYPE , 124, "Wrong medium type" )
__BIONIC_ERRDEF( ECANCELED , 125, "Operation Canceled" )
__BIONIC_ERRDEF( ENOKEY , 126, "Required key not available" )
__BIONIC_ERRDEF( EKEYEXPIRED , 127, "Key has expired" )
__BIONIC_ERRDEF( EKEYREVOKED , 128, "Key has been revoked" )
__BIONIC_ERRDEF( EKEYREJECTED , 129, "Key was rejected by service" )
__BIONIC_ERRDEF( EOWNERDEAD , 130, "Owner died" )
__BIONIC_ERRDEF( ENOTRECOVERABLE, 131, "State not recoverable" )

#undef __BIONIC_ERRDEF
```### Get GPU and OpenGL ES Version Information

```bash
$ adb shell dumpsys | grep GLES
GLES: Qualcomm, Adreno (TM) 650, OpenGL ES 3.2 V@0502.0 (GIT@191610ae03, Ic907de5ed0, 1600323700) (Date:09/17/20)

UE Project Startup Parameters

Looking at the code in the engine, under the Launch module in Launch\Private\Android\LaunchAndroid.cpp, there is the InitCommandLine function:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Launch\Private\Android\LaunchAndroid.cpp
static void InitCommandLine()
{
static const uint32 CMD_LINE_MAX = 16384u;

// initialize the command line to an empty string
FCommandLine::Set(TEXT(""));

AAssetManager* AssetMgr = AndroidThunkCpp_GetAssetManager();
AAsset* asset = AAssetManager_open(AssetMgr, TCHAR_TO_UTF8(TEXT("UE4CommandLine.txt")), AASSET_MODE_BUFFER);
if (nullptr != asset)
{
const void* FileContents = AAsset_getBuffer(asset);
int32 FileLength = AAsset_getLength(asset);

char CommandLine[CMD_LINE_MAX];
FileLength = (FileLength < CMD_LINE_MAX - 1) ? FileLength : CMD_LINE_MAX - 1;
memcpy(CommandLine, FileContents, FileLength);
CommandLine[FileLength] = '\0';

AAsset_close(asset);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("APK Commandline: %s"), FCommandLine::Get());
}

// read in the command line text file from the sdcard if it exists
FString CommandLineFilePath = GFilePathBase + FString("/UE4Game/") + (!FApp::IsProjectNameEmpty() ? FApp::GetProjectName() : FPlatformProcess::ExecutableName()) + FString("/UE4CommandLine.txt");
FILE* CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r");
if(CommandLineFile == NULL)
{
// if that failed, try the lowercase version
CommandLineFilePath = CommandLineFilePath.Replace(TEXT("UE4CommandLine.txt"), TEXT("ue4commandline.txt"));
CommandLineFile = fopen(TCHAR_TO_UTF8(*CommandLineFilePath), "r");
}

if(CommandLineFile)
{
char CommandLine[CMD_LINE_MAX];
fgets(CommandLine, ARRAY_COUNT(CommandLine) - 1, CommandLineFile);

fclose(CommandLineFile);

// chop off trailing spaces
while (*CommandLine && isspace(CommandLine[strlen(CommandLine) - 1]))
{
CommandLine[strlen(CommandLine) - 1] = 0;
}

// initialize the command line to an empty string
FCommandLine::Set(TEXT(""));

FCommandLine::Append(UTF8_TO_TCHAR(CommandLine));
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Override Commandline: %s"), FCommandLine::Get());
}

#if !UE_BUILD_SHIPPING
if (FString* ConfigRulesCmdLineAppend = FAndroidMisc::GetConfigRulesVariable(TEXT("cmdline")))
{
FCommandLine::Append(**ConfigRulesCmdLineAppend);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ConfigRules appended: %s"), **ConfigRulesCmdLineAppend);
}
#endif
}

In simple terms, this means writing startup parameters into UE4Game/ProjectName/ue4commandline.txt, and the engine will read from this file during startup and add it to FCommandLine.

Android Set Aspect Ratio

The value under Project Settings - Platforms - Android - Maximum support aspect ratio defaults to 2.1, but in full screen situations, there may be black edges.

This value controls the value in AndroidManifest.xml:

1
<meta-data android:name="android.max_aspect" android:value="2.1"/>

I currently set the value to 2.5.

Note: Enable FullScreen Immersive on KitKat and above devices controls whether to hide the virtual keys when entering the game.

UPL for Android

In UE, when adding third-party modules or modifying configuration files for mobile, AdditionalPropertiesForReceipt is often used, where ReceiptProperty is created with the xml file being the Unreal Plugin Language (UPL) script.

The platform names for ReceiptProperty are fixed for iOS and Android, namely IOSPlugin and AndroidPlugin, and cannot specify other names (see code UEDeployIOS.cs#L1153 and UEDeployAndroid.cs#L4303).

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

All UPL in Android Project

You can find all UPL file paths in the project path under Intermediate/Android/ActiveUPL.xml:

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

Deleting Items from AndroidManifest.xml

Since UE automatically adds items to AndroidManifest.xml, manually adding items can cause errors indicating they already exist:

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

To modify or delete items generated by UE in AndroidManifest.xml, you can use the method of deleting first and then adding them back.

Taking the deletion of the following item as an example:

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

You can write the following code in the androidManifestUpdates of the UPL:

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>

This traverses through AndroidManifest.xml to remove the meta-data where android:name equals com.epicgames.ue4.GameActivity.bUseExternalFilesDir.

JNI Call to Receive ActivityResult

Sometimes you need to create an Intent using startActivityForResult to perform operations such as opening the camera or selecting an image from the album.

However, Android does not block the current function during these operations, so data cannot be directly received in the calling function. The result from actions executed via startActivityForResult will be called in the Activity’s onActivityResult.

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

UE provides a way to append Java code to OnActivityResult in UPL:

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

Using this approach, added Java code will be appended to the end of the OnActivityResult function. However, there is a problem: after executing the custom appended code, the received result still needs to be handled and passed back to UE, which can be cumbersome.

After reviewing the code, I found that UE offers a multicast delegate event for the Java side’s OnActivityResult, allowing you to listen for OnActivityResult events in UE using C++ to handle it.

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

// The delegate is defined in `FJavaWrapper`
// Delegate that can be registered to that is called when an activity is finished
static FOnActivityResult OnActivityResultDelegate;

In UE, you can bind this multicast delegate to listen for calls to Java’s OnActivityResult, allowing you to handle them accordingly.

It is called from the Java side in AndroidJNI.cpp through the Java_com_epicgames_ue4_GameActivity_nativeOnActivityResult function, and the calling mechanism is documented in the previous notes.

Java Calls C++

Some needs and implementations require calling from Java to C++. You can do so as follows:
First, declare a native Java function in GameActivity (no need to define it):

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

Then, define a corresponding C++ function as follows:

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);
}

You can see the rules for the function name are as follows:

  1. The function must be prefixed with JNI_METHOD, which is a macro __attribute__ ((visibility ("default"))) extern "C".
  2. The function name must start with Java_ followed by com_epicgames_ue4_GameActivity_, indicating it is defined in GameActivity.
  3. The following part must match the Java function name.

The rules for accepting parameters are:

  1. The first parameter is the Java Env.
  2. The second parameter is the Java this.
  3. The subsequent parameters correspond to the parameters passed from Java.

    Full-Screen Adaptation for Android P

When packaging in UE4, the project will generate a GameActivity.java file, which contains the OnCreate code for full-screen adaptation:

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 and later systems do not support this, so you will need to write JNI calls to forcibly enable compatibility for P and later system versions. In UE4.23 and later engine versions, you can use UPL to add code to the OnCreate function, allowing you to directly insert the code into GameActivity.java:

1
2
3
4
5
6
7
8
9
10
<gameActivityOnCreateFinalAdditions>
<insert>
// Allow the use of cutouts on P versions
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>

In UE4.23 and prior versions, you cannot add code to OnCreate; instead, you must write a JNI call:

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");
}
}
});
}

Add this to GameActivity.java via UPL using <gameActivityClassAdditions>, and invoke it through JNI when the game starts.

Note:

layoutInDisplayCutoutMode options include:

Restarting the App on Android

Sometimes, after a game update, you need to restart the App for changes to take effect. In UE, you can write the following Java code using UPL:

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);
}

Trigger this through JNI when you need to restart:

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
}

Common Issues

app:assembleDebug Error

1
UATHelper: Packaging (Android…) ERROR: cmd.exe failed with args /c “[ProjectPath]\Intermediate/Android/APK/gradle/rungradle.bat” :app:assembleDebug

Uncheck the following option:

The Enable Gradle instead of Ant option is no longer present in 4.24+, and the logs indicate that UE failed to download Grade from the internet. You can enable global proxy during packaging. Alternatively, you can use my packaged Gradle version: gradle-5.4.1.7z and extract it to the following path:

1
C:\Users\lipengzha\.gradle\wrapper\dists\gradle-5.4.1-all\3221gyojl5jsh0helicew7rwx\gradle-5.4.1

Then create an environment variable GRADLE_HOME that points to this path.

In certain cases, cleaning the project’s Intermediate directory can also resolve the issue and is worth a try.

There are also related articles that update Android Tools. The steps are:

  1. run NVPACK/android-sdk-windows/tools/android.bat
  2. click on “Deselect All”
  3. update Extras/Android Support Repository

Articles:

app:packagedebug Error

If you encounter the following error log during packaging:

1
2
3
execution failed for task ':app:packagedebug'.  
 > a failure occurred while executing com.android.build.gradle.internal.tasks.workers$actionfacade 
> out of range: 3185947123

Solution: Delete Intermediate/Android and Intermediate/Build/Android from your project.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE开发笔记:Android篇
Author:LIPENGZHA
Publish Date:2021/04/15 19:54
World Count:18k Words
Link:https://en.imzlp.com/posts/17996/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!