Using scoped storage in debug Unreal Engine builds on Android API 29+

Android API 29 (Android 10) introduced the concept of scoped storage. And even more, apps targeting Android API 30 (Android 11 or higher) cannot opt out of scoped storage. Scoped storage limits app access to external storage. With scoped storage, the application can only access its own files in a restricted directory on external storage. It cannot read from or write to arbitrary places.

It can be a problem if you are making a debug build with the “for distribution” checkbox off. The thing is, in this case, Unreal Engine behaves differently with regard to the “Use ExternalFilesDir for UE4Game files” (bUseExternalFilesDir) setting. In distribution builds Unreal Engine stores game files in a directory obtained from getFilesDir() or getExternalFilesDir(). In contrast, in a non-distribution build Unreal Engine uses the directory returned from getExternalStorageDirectory(), which is top-level and thus is not intended for direct usage by applications. This directory is not an ideal place for storing game files.

There are several ways around this. If our game is targeting Android API 29, we can opt out of scoped storage for this particular debug build. If our game built for Android API 30, in principle we could give our app all-files access to storage by requesting MANAGE_EXTERNAL_STORAGE permission. Both these solutions are quite crude workarounds in a way they don’t fix the problem, they just fix the symptoms. What we want to do is tell Unreal Engine not to store savegames and other data in the root of external storage.

The first thing that comes to mind is to alter the code that decides where to put savegames on disk. This is a quick and easy fix. Let’s try this by editing the GetSaveGamePath method in SaveGameSystem.h. Our goal is to store savegames in GInternalFilePath, so it could look something like this:

virtual FString GetSaveGamePath(const TCHAR* Name)
{
  FString SaveDir;
  #ifdef USE_ANDROID_FILE
  extern FString GInternalFilePath;
  SaveDir = FString::Printf(TEXT("%s/SaveGames/%s.sav"), *GInternalFilePath, Name);
  #else
  SaveDir = FString::Printf(TEXT("%sSaveGames/%s.sav"), *FPaths::ProjectSavedDir(), Name);
  #endif
  UE_LOG(LogTemp, Warning, TEXT("Save game path: %s"), *SaveDir);
  return SaveDir;
}

And indeed it works! But this way has some drawbacks: it requires modifying the engine, which almost always incurs additional troubles of recompilation, distributing the engine to the rest of the team, and many more. I would prefer to override the default Unreal Engine behavior without modifying the engine code.

Why doesn’t Unreal Engine respect bUseExternalFilesDir in debug building in the first place? Let’s find out. In nativeSetGlobalActivity C++ thunk (AndroidJNI.cpp) we can see that GFilePathBase gets reassigned to one of GInternalFilePath or GExternalFilePath depending on whether the build is in shipping or development configuration, but only when bUseExternalFilesDir is true. This parameter comes from a Java call in GameActivity, which in turn reads it from com.epicgames.ue4.GameActivity.bUseExternalFilesDir meta-data in ApplicationInfo. This is an object that stores application meta-data collected from the AndroidManifest.xml <application> tag. This tag gets populated with metadata during the build phase by Unreal Build Tool (UBT). There we can see that MakeApk calls simply ignore the bUseExternalFilesDir setting when bForDistribution is set to false. This is why Unreal Engine tries to store game files in external storage root directory, which isn’t a good practice in modern Android.

UPL (Unreal Plugin Language) comes to the rescue. With UPL we can manipulate AndroidManifest.xml contents and bend the default behavior to our will. Since two meta-data elements inside a single <application> cannot have the same android:name attribute, we must remove meta-data generated by Unreal Engine and add our own with the desired value. Let’s try this approach and modify our project’s UPL file:

<loopElements tag="meta-data">
  <setStringFromAttribute  result="NameTagValue" tag="$" name="android:name"/>
  <setBoolIsEqual result="UseExternalFilesDirElement" arg1="$S(NameTagValue)" arg2="com.epicgames.ue4.GameActivity.bUseExternalFilesDir"/>
  <if condition="UseExternalFilesDirElement">
    <true>
      <removeElement tag="$"/>
    </true>
  </if>
</loopElements>

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

Package the game for Android and check that bUseExternalFilesDir is now true even in debug non-distribution builds. Success! Save games now get saved to internal storage directory and work flawlessly.