Unreal C++はAndroidでアクセスできるフォルダが制限されていてつらい(回避策あり)

# はじめに

Unreal C++でAndroidの任意のパスにアクセスするのが色々面倒だった話です。

# Victory Pluginの導入でつらい

「[Unreal C++からAndroid API呼び出しのつらさを減らしてみた](http://qiita.com/shiena/items/f07533702e2639a511c3)」でPicturesフォルダのパスをBlueprintから取得できるようになったので、次はそのフォルダにある画像のファイルリストを取ろうとしました。
幸いにも、[Victory Plugin](https://github.com/EverNewJoy/VictoryPlugin)に丁度よさげな関数があります。

![VictoryPlugin.png](https://qiita-image-store.s3.amazonaws.com/0/8995/1399658c-da05-e9c8-921e-06d2be2273e5.png)

ですが、Windowsでは問題なく動くのにAndroid用にビルドするとエラーになってしまいます。
これは公式フォーラムの[Rama's Extra Blueprint Nodes for You as a Plugin, No C++ Required!](https://forums.unrealengine.com/showthread.php?3851-(39)-Rama-s-Extra-Blueprint-Nodes-for-You-as-a-Plugin-No-C-Required!&p=53249&viewfull=1#post53249)に解決策があり、Build.csに設定を追加するとビルドが通るようになりました。
他のプラグインは追加の設定なしでもビルドできるのに何故…

```csharp:CppTest/Source/CppTest/CppTest.Build.cs
// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;
using System.IO;

public class CppTest : ModuleRules
{
    public CppTest(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] {  });
        if (Target.Platform == UnrealTargetPlatform.Android)
        {
            string ProjectPath = Utils.MakePathRelativeTo(ModuleDirectory, BuildConfiguration.RelativeEnginePath);
            AdditionalPropertiesForReceipt.Add(new ReceiptProperty("AndroidPlugin", Path.Combine(ProjectPath, "CppTest_release_UPL.xml")));

            // ここから追加
            PrivateIncludePaths.AddRange(new string[] { "CppTest/Plugins/VictoryPlugin/Source/VictoryBPLibrary/Private" });
            DynamicallyLoadedModuleNames.AddRange(new string[] { "VictoryBPLibrary" });
            // ここまで追加
        }
        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}
```

# FAndroidPlatformFile::PathToAndroidPaths()の罠

Victory Pluginを導入できたので次は適当にpngファイルの一覧を取ってみました。

![JoyFileIOGetFiles.png](https://qiita-image-store.s3.amazonaws.com/0/8995/97cb9e9d-a4e1-9f59-8282-44f92a4d0fad.png)

ですが、`Joy File Get Files`関数の結果、Return Valueはtrueなのにファイル名の配列は0件です。
さすがに原因が分からなくてソースを追ってみました。Android依存部分の[FAndroidPlatformFile::IterateDirectory()](https://github.com/EpicGames/UnrealEngine/blob/4.13.2-release/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp#L1509)でも問題を見付けられなかったので最終手段のデバッグログ有効化です。

```diff
diff --git a/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp b/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp
index 104d4e6..8b8e737 100644
--- a/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp
+++ b/Engine/Source/Runtime/Core/Private/Android/AndroidFile.cpp
@@ -20,7 +20,7 @@

 DEFINE_LOG_CATEGORY_STATIC(LogAndroidFile, Log, All);

-#define LOG_ANDROID_FILE 0
+#define LOG_ANDROID_FILE 1

 #define LOG_ANDROID_FILE_MANIFEST 0
```

上記だけ変更したソースからビルドしたUE4からAndroidアプリをビルドして`adb logcat`でログを確認します。

```text:logcat
D UE4     : FAndroidPlatformFile::IterateDirectory('/storage/emulated/0/Pictures')
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('/storage/emulated/0/Pictures') => AndroidPath = '/storage/emulated/0/Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('/storage/emulated/0/Pictures') => LocalPath = '/storage/emulated/0/UE4Game/CppTest/storage/emulated/0/Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('/storage/emulated/0/Pictures') => AssetPath = '/storage/emulated/0/Pictures'
D UE4     : [2016.11.03-09.15.36:314][  0]LogBlueprintUserMessages: [M_Default_C_1] 0
```

* Picturesフォルダのフルパス: `/storage/emulated/0/Pictures`
* `FAndroidPlatformFile::PathToAndroidPaths`で変換されたPicturesフォルダのフルパス: `/storage/emulated/0/UE4Game/CppTest/storage/emulated/0/Pictures`

えー、と思いながら再度ソースを調べると絶対パスを指定してもゲーム用フォルダをルートとしたパスに変換されてしまうようです…何てこったい…

更に相対パス指定しても以下のように削除されてしまいます。抜け目ない…

```cpp:AndroidFile.cppのPathToAndroitPaths
while (AndroidPath.StartsWith(TEXT("../")))
{
    AndroidPath = AndroidPath.RightChop(3);
}
```

しかも、IAndroidPlatformFileクラスから継承してオーバーライドしたメンバー関数は必ずこの変換が行われるのでFPathsやFFileHelperなどのプラットフォームの違いを吸収するクラスに影響があります。

# 小手先の回避策

絶対パスも相対パスも駄目なことが分かりましたが、1パターンだけ抜け道がありました。
カレントディレクトリを基準にした相対パスです。具体的に以下のようになります。

* 参照したいパス: `/storage/emulated/0/Pictures`
* 引数で渡す相対パス: `./../../Pictures`

これでログを確認してみると…

```text:logcat
D UE4     : FAndroidPlatformFile::IterateDirectory('./../../Pictures')
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('./../../Pictures') => AndroidPath = './../../Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('./../../Pictures') => LocalPath = '/storage/emulated/0/UE4Game/CppTest/./../../Pictures'
D UE4     : FAndroidPlatformFile::PathToAndroidPaths('./../../Pictures') => AssetPath = './../../Pictures'
D UE4     : FAndroidPlatformFile::IterateDirectory('./../../Pictures').. LOCAL Visit: './../../Pictures/Screenshot_20160610-201604.png'
D UE4     : FAndroidPlatformFile::IterateDirectory('./../../Pictures').. LOCAL Visit: './../../Pictures/0.png'
D UE4     : [2016.11.03-09.15.36:315][  0]LogBlueprintUserMessages: [M_Default_C_1] 2
```

取れた！Picturesフォルダのpngファイルのパスを取れました！

つまり、`/storage/emulated/0/UE4Game/CppTest`を起点にして、カレントディレクトリからの相対パスを作ればよいのです。
そしてこの為に用意されているかのような[FPaths::MakePathRelativeTo](https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FPaths/MakePathRelativeTo/index.html)を使えば簡単に相対パスを作ることができます。
(元のソースは[MyBlueprintFunctionLibrary.cpp](http://qiita.com/shiena/items/f07533702e2639a511c3#umyblueprintfunctionlibrarycpp-1)です)

```cpp:MyBlueprintFunctionLibrary.cpp
// Fill out your copyright notice in the Description page of Project Settings.

#include "CppTest_release.h"
#include "MyBlueprintFunctionLibrary.h"

#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#endif

FString UMyBlueprintFunctionLibrary::PicturesPath;

FString UMyBlueprintFunctionLibrary::GetPicturesPath()
{
	return UMyBlueprintFunctionLibrary::PicturesPath;
}

#if PLATFORM_ANDROID
/*
 * AndroidFile.cppに定義されている /storage/emulated/0/UE4Game/CppTest を返す関数
 * AndroidFile.hに定義されていないので、externで宣言しています。
 */
extern const FString &GetFileBasePath();

/*
 * 参照したい絶対パスを渡すと相対パスに変換する
 */
bool AndroidRelativePath(FString InPath, FString& OutPath)
{
    auto Source = InPath;
    auto InRelativeTo = GetFileBasePath();
    auto Result = FPaths::MakePathRelativeTo(Source, *InRelativeTo);
    OutPath = FPaths::Combine(TEXT("."), *Source);

    return Result;
}

extern "C"
{
    JNIEXPORT void JNICALL Java_com_epicgames_ue4_GameActivity_nativeSetPicturesPath(JNIEnv* jenv, jobject, jstring picturesPath)
    {
        const char* javaChars = jenv->GetStringUTFChars(picturesPath, 0);

        // Picturesの絶対パス
        auto RawPicturesPath = FString(UTF8_TO_TCHAR(javaChars));

        /*
         * ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
         * ここでPicturesの絶対パスを相対パスに変換する
         * ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆
         */
        AndroidRelativePath(RawPicturesPath, UMyBlueprintFunctionLibrary::PicturesPath);

        jenv->ReleaseStringUTFChars(picturesPath, javaChars);
    }
}
#endif
```

ただし、この方法はAndroidFile.cppの実装依存の抜け道なので、今後のバージョンアップで使えなくなる可能性があります。

# まとめ

何故このように制限されているのか、GitHubのコミットログにも特に記載されていないので分かりませんが、セーブファイル等を保存する都合上、SDカードのパーミッションが必要でその他のファイルを保護する仕組みがないので簡単にアクセスできないようにしたのではないかと考えられます。

UE4でAndroidアプリを作るのって色々つらい。