【Unity】Androidで情報をローカル保存するときの話

はじめに

ソーシャルゲーム事業部の中島と申します。よろしくお願い致します。
カヤックUnityアドベントカレンダー2018の14日目の記事となります。

Android端末上で動作するアプリで情報をローカル保存する際の注意点について書いてみます。
(Unityは2017.4.8f1を、実機はSHL25 Androidバージョン5.0.2を使用しています)

ローカル保存の方法

Unityで情報を端末のローカル領域に保存するには、大まかに以下の2パターンの方法があります。

  • UnityEngine.PlayerPrefs を利用する
    • key-value形式で保存が可能
  • 自前でファイルを作成し、以下を利用して取得したパスに保存する
    • UnityEngine.Application.persistentDataPath (永続性のある情報向け)
    • UnityEngine.Application.temporaryCachePath (一時的な情報向け)

UnityEngine.Application.persistentDataPath をAndroidで呼び出すとどうなるか

今回は UnityEngine.Application.persistentDataPath (をAndroidで呼び出したとき)の話です。
(ローカル保存のより全体的な話はこちらにまとめられているので、ぜひご活用ください)

UnityEngine.Application.persistentDataPath は永続性のあるデータを保存するためのパスを返します。
早速ですが、 Android上で UnityEngine.Application.persistentDataPath を参照した際に得られる値を見てみます。

Debug.Log(UnityEngine.Application.persistentDataPath); 

上記で、 /storage/emulated/0/Android/data/{アプリのパッケージ名}/files/ の様な出力が得られると思います。 (得られる出力はAndroidのバージョンやUnityでアプリをビルドする際の設定に依存する模様です)

つまりAndroid上で永続性のあるデータを保存するには /storage/emulated/0/Android/data/{アプリのパッケージ名}/files/hoge の様な形でファイルを配置することとなります。

何が問題か

/storage/emulated/0/Android/data/{アプリのパッケージ名}/files/ というパスはAndroidの 外部ストレージ という領域に属しています。
外部ストレージ がどんな特性を持つ領域かは こちら に詳しく記載されていますが、特に注意したいのはその領域がユーザーや他のアプリに対してパブリックであるという点です。

External storage:
*中略*
- It's world-readable, so files saved here may be read outside of your control.

例えばユーザー識別情報をこの領域に保存する作りになっていたとすると、ユーザーや他のアプリはそれを参照が可能ということになります。
これはそのままチート行為やアカウントの譲渡などにつながるリスクとなります。

では実際にアプリで作成したデータをユーザーが読み出せるかを確認してみます。

using System.IO;
using UnityEngine;

public class PdpChecker : MonoBehaviour
{
    private void Start()
    {
        SaveText(
            Application.persistentDataPath,
            "hoge",
            "カヤックUnityアドベントカレンダー2018の14日目"
        );
    }

    private void SaveText(string filePath, string fileName, string textToSave)
    {
        var combinedPath = Path.Combine(filePath, fileName);
        using (var streamWriter = new StreamWriter(combinedPath))
        {
            streamWriter.WriteLine(textToSave);
        }
    }
}

上記を組み込んだapkをAndroid端末にインストールして実行すると、/storage/emulated/0/Android/data/{アプリのパッケージ名}/files/ にhogeというファイルが出力されるはずです。
(パッケージ名は com.hoge.pdpchecker としました)

$ adb shell
shell@SHL25:/ $ cd /storage/emulated/0/Android/data/com.hoge.pdpchecker/files 
/system/bin/sh: cd: /storage/emulated/0/Android/data/com.hoge.pdpchecker/files: No such file or directory

# 目的地の実体は違う場所にある?

shell@SHL25:/ $ cd /storage/emulated
shell@SHL25:/storage/emulated $ ls -la
lrwxrwxrwx root     root              1974-03-13 02:45 legacy -> /mnt/shell/emulated/0

# 実体は /mnt/shell/emulated/0 の模様

shell@SHL25:/storage/emulated $ cd /mnt/shell/emulated/0/Android/data/com.hoge.pdpchecker/files
shell@SHL25:/mnt/shell/emulated/0/Android/data/com.hoge.pdpchecker/files $ ls
hoge

# 目的のファイルを発見

shell@SHL25:/mnt/shell/emulated/0/Android/data/com.hoge.pdpchecker/files $ cat hoge
カヤックUnityアドベントカレンダー201814日目

# 読み出すことが出来た

この様にファイルの内容を読み出すことが可能でした。
以上から UnityEngine.Application.persistentDataPath で取得したパスに配置する情報は、以下に限る必要があります。

  • 永続性がある (ない場合は UnityEngine.Application.temporaryCachePath へ)
  • 秘匿する必要がない

秘匿情報を保存するには

/storage/emulated/0/Android/data/{アプリのパッケージ名}/files/ というパスはAndroidが 外部ストレージ として用意している領域に属しています。

UnityEngine.Application.persistentDataPath で取得できるパスが 外部ストレージ を示すケースがある、と記載しましたが、Androidは対となる 内部ストレージ という領域も提供しています。

Internal storage:
*中略*
- Files saved here are accessible by only your app.

内部ストレージ のパスを取得することができればユーザーに公開せずにファイルを保持することが可能なはずですが、Unityのスクリプトリファレンスを眺めて見てもその様なパスを取得する方法は見つかりませんでした。
Android Nativeではそれが可能な様なので、C#からJavaクラスを制御できる AndroidJavaClass などを利用して取得してみます。
最終的に呼び出すメソッドは Context.getFilesDir() で取得したFileオブジェクトの getCanonicalPath() です。

#if !UNITY_EDITOR && UNITY_ANDROID
    using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
    using (var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
    using (var getFilesDir = currentActivity.Call<AndroidJavaObject>("getFilesDir"))
    {
        string secureDataPathForAndroid = getFilesDir.Call<string>("getCanonicalPath");
        Debug.Log(secureDataPathForAndroid);
    }
#endif

出力は /data/data/{アプリのパッケージ名}/files となりました。
それでは先ほどのスクリプトを以下の様に書き換えて、再度Android実機での読み出しを検証してみます。

using System.IO;
using UnityEngine;

public class PdpChecker : MonoBehaviour
{
    private void Start()
    {
        SaveText(
            GetSecureDataPath(),
            "hoge",
            "カヤックUnityアドベントカレンダー2018の14日目"
        );
    }

    private void SaveText(string filePath, string fileName, string textToSave)
    {
        var combinedPath = Path.Combine(filePath, fileName);
        using (var streamWriter = new StreamWriter(combinedPath))
        {
            streamWriter.WriteLine(textToSave);
        }
    }

    private string GetSecureDataPath()
    {
#if !UNITY_EDITOR && UNITY_ANDROID
        using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
        using (var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
        using (var getFilesDir = currentActivity.Call<AndroidJavaObject>("getFilesDir"))
        {
            string secureDataPathForAndroid = getFilesDir.Call<string>("getCanonicalPath");
            return secureDataPathForAndroid;
        }
#else
        // TODO: 本来は各プラットフォームに対応した処理が必要
        return Application.persistentDataPath;
#endif
    }
}
$ adb shell
shell@SHL25:/ $ cd /data/data/com.hoge.pdpchecker/files
shell@SHL25:/data/data/com.hoge.pdpchecker/files $ ls
opendir failed, Permission denied
255|shell@SHL25:/data/data/com.hoge.pdpchecker/files $ cat hoge
/system/bin/sh: cat: hoge: Permission denied

# アクセス権がない

ユーザーには読み出すことが出来ない様です。

おわりに

Android端末で情報をローカル保存する際の注意点について記載しました。
今回は UnityEngine.Application.persistentDataPath を例に記載しましたが、複数のプラットフォーム上で動作するアプリである以上、これ以外にも注意すべきポイントがたくさんあるものと思います。
そういう罠に嵌る人が減ること、さらにはUnity側でプラットフォーム差分が吸収されていくことを祈りながら本記事は終わりに致します。

明日は小笠原さんによる RGBをHSVに変換して明るさとかを変えるシェーダー の話になります。