Modify the default data storage path for Android games

UE源码分析:修改游戏默认的数据存储路径

By default, after packaging a game with UE and installing the Apk on a phone, launching the game will create the game’s data directory under /storage/emulated/0/UE4Game/ (which is at the root of the internal storage). According to Google’s rules, it is best for each app’s data files to be placed in their own private directory. Therefore, I want to put all the game’s data packaged by UE into the directory /storage/emulated/0/Android/data/PACKAGE_NAME (regardless of whether it’s log, ini, or crash information).
This seemingly simple requirement has several different approaches involving UE4’s path management, JNI, Android Manifest, and analysis of UBT code.

Default path:

There are two methods: one is to modify the engine code to implement a change to GFilePathBase, and the other is to simply add manifest in the project settings without changing the engine. Of course, not changing the engine is the best approach. However, since this is an analysis, I’ll explore both methods and also analyze the Project Setting - Android - Use ExternalFilesDir for UE4Game Files option to see why it has no effect.

Modifying Engine Code

After looking through the engine code, I found that this part of the path is written here: AndroidPlatformFile.cpp#L946. It takes GFilePathBase and combines it with UE4Game + PROJECT_NAME to form the path.

In versions of the engine up to UE4.22, this was in the AndroidFile.cpp file, but from 4.23 onwards, it’s in AndroidPlatformFile.cpp.
The initialization of the base path GFilePathBase happens in Launch\Private\Android\AndroidJNI.cpp:

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
// Launch\Private\Android\AndroidJNI.cpp
JNIEXPORT jint JNI_OnLoad(JavaVM* InJavaVM, void* InReserved)
{
FPlatformMisc::LowLevelOutputDebugString(TEXT("In the JNI_OnLoad function"));

JNIEnv* Env = NULL;
InJavaVM->GetEnv((void **)&Env, JNI_CURRENT_VERSION);

// if you have problems with stuff being missing especially in distribution builds then it could be because proguard is stripping things from java
// check proguard-project.txt and see if your stuff is included in the exceptions
GJavaVM = InJavaVM;
FAndroidApplication::InitializeJavaEnv(GJavaVM, JNI_CURRENT_VERSION, FJavaWrapper::GameActivityThis);

FJavaWrapper::FindClassesAndMethods(Env);

// hook signals
if (!FPlatformMisc::IsDebuggerPresent() || GAlwaysReportCrash)
{
// disable crash handler.. getting better stack traces from system for now
// FPlatformMisc::SetCrashHandler(EngineCrashHandler);
}

// Cache path to external storage
jclass EnvClass = Env->FindClass("android/os/Environment");
jmethodID getExternalStorageDir = Env->GetStaticMethodID(EnvClass, "getExternalStorageDirectory", "()Ljava/io/File;");
jobject externalStoragePath = Env->CallStaticObjectMethod(EnvClass, getExternalStorageDir, nullptr);
jmethodID getFilePath = Env->GetMethodID(Env->FindClass("java/io/File"), "getPath", "()Ljava/lang/String;");
jstring pathString = (jstring)Env->CallObjectMethod(externalStoragePath, getFilePath, nullptr);
const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
// Copy that somewhere safe
GFilePathBase = FString(nativePathString);
GOBBFilePathBase = GFilePathBase;

// then release...
Env->ReleaseStringUTFChars(pathString, nativePathString);
Env->DeleteLocalRef(pathString);
Env->DeleteLocalRef(externalStoragePath);
Env->DeleteLocalRef(EnvClass);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Path found as '%s'\n"), *GFilePathBase);

// Get the system font directory
jstring fontPath = (jstring)Env->CallStaticObjectMethod(FJavaWrapper::GameActivityClassID, FJavaWrapper::AndroidThunkJava_GetFontDirectory);
const char * nativeFontPathString = Env->GetStringUTFChars(fontPath, 0);
GFontPathBase = FString(nativeFontPathString);
Env->ReleaseStringUTFChars(fontPath, nativeFontPathString);
Env->DeleteLocalRef(fontPath);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Font Path found as '%s'\n"), *GFontPathBase);

// Wire up to core delegates, so core code can call out to Java
DECLARE_DELEGATE_OneParam(FAndroidLaunchURLDelegate, const FString&);
extern CORE_API FAndroidLaunchURLDelegate OnAndroidLaunchURL;
OnAndroidLaunchURL = FAndroidLaunchURLDelegate::CreateStatic(&AndroidThunkCpp_LaunchURL);

FPlatformMisc::LowLevelOutputDebugString(TEXT("In the JNI_OnLoad function 5"));

char mainThreadName[] = "MainThread-UE4";
AndroidThunkCpp_SetThreadName(mainThreadName);

return JNI_CURRENT_VERSION;
}

Our goal is to change the value of GFilePathBase, because by default it gets its value by calling getExternalStorageDirectory, which is the directory of external storage: /storage/emulated/0/. When combined with UE4Game, this gives the default path we usually see.

Since getExternalStorageDirectory and the like are static members of Environment and do not provide the path we want, but Context does, and the UE code does not retrieve it, we need a way to get the App’s Context.

We can get the Context from JNI using the following method:

1
2
3
4
5
6
7
8
9
10
// 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);
}

Then we can use the function getExternalFilesDir under Context to obtain our desired path:

Note: the prototype of getExternalFilesDir is: File getExternalFilesDir(String), so when using JNI to get the jmehodID, it’s important to match the signature exactly, or it will crash; its signature is (Ljava/lang/String;)Ljava/io/File;.

1
2
3
4
5
6
7
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);

The value of nativePathString obtained is:

1
/storage/emulated/0/Android/data/com.imzlp.GWorld/files

Here, com.imzlp.GWorld is your App’s package name.

Then simply assign this value to GFilePathBase, and after repackaging the Apk in the editor and reinstalling it, all the app’s data will be under /storage/emulated/0/Android/data/PACKAGE_NAME/files.

Links for invoking and operating JNI and Android storage paths in UE:

Controlling with Manifest

OK, I’ve generally written about analyzing the modification of GFilePathBase in the engine. There is actually a non-engine modification approach, which is to add manifest in the project settings.

The principle is also in AndoidJNI.cpp, where there is the following code:

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
//This function is declared in the Java-defined class, GameActivity.java: "public native void nativeSetGlobalActivity();"
JNI_METHOD void Java_com_epicgames_ue4_GameActivity_nativeSetGlobalActivity(JNIEnv* jenv, jobject thiz, jboolean bUseExternalFilesDir, jstring internalFilePath, jstring externalFilePath, jboolean bOBBinAPK, jstring APKFilename /*, jobject googleServices*/)
{
if (!FJavaWrapper::GameActivityThis)
{
GGameActivityThis = FJavaWrapper::GameActivityThis = jenv->NewGlobalRef(thiz);
if (!FJavaWrapper::GameActivityThis)
{
FPlatformMisc::LowLevelOutputDebugString(TEXT("Error setting the global GameActivity activity"));
check(false);
}

// This call is only to set the correct GameActivityThis
FAndroidApplication::InitializeJavaEnv(GJavaVM, JNI_CURRENT_VERSION, FJavaWrapper::GameActivityThis);

// @todo split GooglePlay, this needs to be passed in to this function
FJavaWrapper::GoogleServicesThis = FJavaWrapper::GameActivityThis;
// FJavaWrapper::GoogleServicesThis = jenv->NewGlobalRef(googleServices);

// Next we check to see if the OBB file is in the APK
//jmethodID isOBBInAPKMethod = jenv->GetStaticMethodID(FJavaWrapper::GameActivityClassID, "isOBBInAPK", "()Z");
//GOBBinAPK = (bool)jenv->CallStaticBooleanMethod(FJavaWrapper::GameActivityClassID, isOBBInAPKMethod, nullptr);
GOBBinAPK = bOBBinAPK;

const char *nativeAPKFilenameString = jenv->GetStringUTFChars(APKFilename, 0);
GAPKFilename = FString(nativeAPKFilenameString);
jenv->ReleaseStringUTFChars(APKFilename, nativeAPKFilenameString);

const char *nativeInternalPath = jenv->GetStringUTFChars(internalFilePath, 0);
GInternalFilePath = FString(nativeInternalPath);
jenv->ReleaseStringUTFChars(internalFilePath, nativeInternalPath);

const char *nativeExternalPath = jenv->GetStringUTFChars(externalFilePath, 0);
GExternalFilePath = FString(nativeExternalPath);
jenv->ReleaseStringUTFChars(externalFilePath, nativeExternalPath);

if (bUseExternalFilesDir)
{
#if UE_BUILD_SHIPPING
GFilePathBase = GInternalFilePath;
#else
GFilePathBase = GExternalFilePath;
#endif
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("GFilePathBase Path override to'%s'\n"), *GFilePathBase);
}

FPlatformMisc::LowLevelOutputDebugStringf(TEXT("InternalFilePath found as '%s'\n"), *GInternalFilePath);
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("ExternalFilePath found as '%s'\n"), *GExternalFilePath);
}
}

During engine startup, it will be called from JNI, where one of the parameters bUseExternalFilesDir controls whether to modify the value of GFilePathBase. If it’s true, in Shipping packaging mode, GFilePathBase will be set to the value of GInternalFilePath, which is the following path:

1
/data/user/PACKAGE_NAME/files

This is the app’s private data path on Android, which cannot be accessed without ROOT permissions. This also means that files cannot be manually placed in this directory; I believe the design intent is to prevent users from accessing data generated during the release version.

However, UE also provides a configuration to output logs to non-internal storage during Shipping mode. This is done by adding bPublicLogFiles=True under [/Script/AndroidRuntimeSettings.AndroidRuntimeSettings] in DefaultEngine.ini.

In non-Shipping packaging mode, it will be set to the value of GExternalFilePath:

1
/storage/emulated/0/Android/data/PACKAGE_NAME/files

This is the external storage sandbox path for Android apps, which will be automatically cleaned up when the app is uninstalled.

However, the key issue is how to control the bUseExternalFilesDir parameter that comes from JNI.

The answer to this question is to add manifest information! I initially thought it was the ProjectSettings - Android - UseExternalFilesDirForUE4GameFiles option, but selecting it has no effect, as I will analyze later.

Before explaining how to control the bUseExternalFilesDir variable through the manifest, it’s important to know what the Manifest of an APK packaged by UE4 contains by default.

Below is the Manifest file I unpacked from the APK:

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
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="internalOnly" package="com.imzlp.TEST" platformBuildVersionCode="29" platformBuildVersionName="10">
<application android:debuggable="true" android:hardwareAccelerated="true" android:hasCode="true" android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:debuggable="true" android:label="@string/app_name" android:launchMode="singleTask" android:name="com.epicgames.ue4.SplashActivity" android:screenOrientation="landscape" android:theme="@style/UE4SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:configChanges="density|keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|uiMode" android:debuggable="true" android:label="@string/app_name" android:launchMode="singleTask" android:name="com.epicgames.ue4.GameActivity" android:screenOrientation="landscape" android:theme="@style/UE4SplashTheme">
<meta-data android:name="android.app.lib_name" android:value="UE4"/>
</activity>
<activity android:configChanges="density|keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|uiMode" android:name=".DownloaderActivity" android:screenOrientation="landscape" android:theme="@style/UE4SplashTheme"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.EngineVersion" android:value="4.22.3"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.EngineBranch" android:value="++UE4+Release-4.22"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.ProjectVersion" android:value="1.0.0.0"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.DepthBufferPreference" android:value="0"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bPackageDataInsideApk" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bVerifyOBBOnStartUp" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bShouldHideUI" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.ProjectName" android:value="Mobile422"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.AppType" android:value=""/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bHasOBBFiles" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.BuildConfiguration" android:value="Development"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.CookedFlavors" android:value="ETC2"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bValidateTextureFormats" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseDisplayCutout" android:value="false"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bAllowIMU" android:value="true"/>
<meta-data android:name="com.epicgames.ue4.GameActivity.bSupportsVulkan" android:value="false"/>
<meta-data android:name="com.google.android.gms.games.APP_ID" android:value="@string/app_id"/>
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>
<activity android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:name="com.google.android.gms.ads.AdActivity"/>
<service android:name="OBBDownloaderService"/>
<receiver android:name="AlarmReceiver"/>
<receiver android:name="com.epicgames.ue4.LocalNotificationReceiver"/>
<receiver android:exported="true" android:name="com.epicgames.ue4.MulticastBroadcastReceiver">
<intent-filter>
<action android:name="com.android.vending.INSTALL_REFERRER"/>
</intent-filter>
</receiver>
<meta-data android:name="android.max_aspect" android:value="2.1"/>
</application>
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.VIBRATE"/>
</manifest>

This file is generated in UnrealBuildTool\Platform\Android\UEDeployAdnroid.cs‘s GenerateManifest function.

It controls the permissions and attributes configuration required after APK installation. We can see that one entry is:

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

The value of bUseExternalFilesDir is false! How can we set it to true?

You need to open Project Settings - Android - Advanced APK Packaging and find the Extra Tags for <application> node. Since <meta-data /> is under Application, you need to add content here.

The content to add is:

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

That’s right! Just paste this line with meta-data and modify the value, and UE will automatically append this content to the Manifest in the Application section during packing, overriding the default false value.

Then, after repackaging, you will see that the bUseExternalFilesDir option takes effect.

Controlling bUseExternalFilesDir via UPL

Since UE automatically adds com.epicgames.ue4.GameActivity.bUseExternalFilesDir to AndroidManifest.xml, if we want to manually control it, directly adding it will cause errors, prompting that it already exists:

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

If you want to modify or delete the entry in the UE-generated AndroidManifest.xml, you can do so by first deleting it before adding it.

For example, to delete the following entry:

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

You can write the following code in the UPL’s androidManifestUpdates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

<addElements tag="application">
<meta-data android:name="com.epicgames.ue4.GameActivity.bUseExternalFilesDir" android:value="true" />
</addElements>
</androidManifestUpdates>

This code iterates through the AndroidManifest.xml, locating and deleting the meta-data entry with the android:name as com.epicgames.ue4.GameActivity.bUseExternalFilesDir.

Analysis of the Ineffectiveness of the bUseExternalFilesDir Option in Project Settings

Now let’s analyze why the Project Settings - Android - Use ExternalFilesDir for UE4Game Files option is ineffective. In fact, this option does control the value of bUseExternalFilesDir in the manifest, manipulated within UBT, as previously mentioned, the manifest file is generated within UBT. However, although UE provides this parameter, in the current engine (4.22.3) this option has no effect because it is disabled by default.

First, the UBT build call stack is as follows:

  1. Deploy in AndroidPlatform (UEBuildAndroid.cs)
  2. PrepTargetForDeployment in UEDeployAndroid (UEDeployAndroid.cs)
  3. MakeApk in UEDeployAndroid (UEDeployAndroid.cs) (the most critical function)

The MakeApk function receives a special control parameter bDisallowExternalFilesDir:

1
2
// UEDeployAndroid.cs
private void MakeApk(AndroidToolChain ToolChain, string ProjectName, TargetType InTargetType, string ProjectDirectory, string OutputPath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bMakeSeparateApks, bool bIncrementalPackage, bool bDisallowPackagingDataInApk, bool bDisallowExternalFilesDir);

It is used to control whether to enable the Use ExternalFilesDir for UE4Game Files option in project settings.

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
// UEDeployAndroid.cs
private void MakeApk(AndroidToolChain ToolChain, string ProjectName, TargetType InTargetType, string ProjectDirectory, string OutputPath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bMakeSeparateApks, bool bIncrementalPackage, bool bDisallowPackagingDataInApk, bool bDisallowExternalFilesDir)
{
// ...
bool bUseExternalFilesDir = UseExternalFilesDir(bDisallowExternalFilesDir);
// ...
}

// func UseExternalFilesDir
public bool UseExternalFilesDir(bool bDisallowExternalFilesDir, ConfigHierarchy Ini = null)
{
if (bDisallowExternalFilesDir)
{
return false;
}

// make a new one if one wasn't passed in
if (Ini == null)
{
Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
}

// we check this a lot, so make it easy
bool bUseExternalFilesDir;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bUseExternalFilesDir", out bUseExternalFilesDir);

return bUseExternalFilesDir;
}

As can be seen, if bDisallowExternalFilesDir is true, it will not read the configuration in project settings at all.

The critical point is that, when calling MakeApk in PrepTargetForDeployment, the default parameter passed is true:

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
// UEDeployAndroid.cs
public override bool PrepTargetForDeployment(UEBuildDeployTarget InTarget)
{
//Log.TraceInformation("$$$$$$$$$$$$$$ PrepTargetForDeployment $$$$$$$$$$$$$$$$$ {0}", InTarget.TargetName);
AndroidToolChain ToolChain = new AndroidToolChain(InTarget.ProjectFile, false, InTarget.AndroidArchitectures, InTarget.AndroidGPUArchitectures);

// we need to strip architecture from any of the output paths
string BaseSoName = ToolChain.RemoveArchName(InTarget.OutputPaths[0].FullName);

// get the receipt
UnrealTargetPlatform Platform = InTarget.Platform;
UnrealTargetConfiguration Configuration = InTarget.Configuration;
string ProjectBaseName = Path.GetFileName(BaseSoName).Replace("-" + Platform, "").Replace("-" + Configuration, "").Replace(".so", "");
FileReference ReceiptFilename = TargetReceipt.GetDefaultPath(InTarget.ProjectDirectory, ProjectBaseName, Platform, Configuration, "");
Log.TraceInformation("Receipt Filename: {0}", ReceiptFilename);
SetAndroidPluginData(ToolChain.GetAllArchitectures(), CollectPluginDataPaths(TargetReceipt.Read(ReceiptFilename, UnrealBuildTool.EngineDirectory, InTarget.ProjectDirectory)));

// make an apk at the end of compiling, so that we can run without packaging (debugger, cook on the fly, etc)
string RelativeEnginePath = UnrealBuildTool.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory());
MakeApk(ToolChain, InTarget.TargetName, InTarget.ProjectDirectory.FullName, BaseSoName, RelativeEnginePath, bForDistribution: false, CookFlavor: "",
bMakeSeparateApks: ShouldMakeSeparateApks(), bIncrementalPackage: true, bDisallowPackagingDataInApk: false, bDisallowExternalFilesDir: true);

// if we made any non-standard .apk files, the generated debugger settings may be wrong
if (ShouldMakeSeparateApks() && (InTarget.OutputPaths.Count > 1 || !InTarget.OutputPaths[0].FullName.Contains("-armv7-es2")))
{
Console.WriteLine("================================================================================================================================");
Console.WriteLine("Non-default apk(s) have been made: If you are debugging, you will need to manually select one to run in the debugger properties!");
Console.WriteLine("================================================================================================================================");
}
return true;
}

This is really a tricky point… I see that the UBT source code for UE4.18 is the same, it is still disabled by default. Clearly, there is this option, yet it is closed by default without any prompt, which is quite frustrating.

Conclusion

In fact, modifying the engine code and using the manifest both have their advantages:

  • The advantage of modifying the code is that you can specify any path (not necessarily reasonable), but the downside is that you need the source version of the engine.
  • The advantage of using the Manifest is that you don’t need the source version of the engine, but can only use InternalFilesDir (Shipping) or ExternalFilesDir (not-shipping).

By the way, let’s complain a bit about UE; why expose an option that doesn’t work in the settings?

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

Scan the QR code on WeChat and follow me.

Title:Modify the default data storage path for Android games
Author:LIPENGZHA
Publish Date:2020/01/22 09:10
Word Count:7k Words
Link:https://en.imzlp.com/posts/20367/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!