Android：SDカード（外部ストレージ）のパスを取得する方法（機種依存対策）

## はじめに
AndroidのSDカードのパスは、機種ごとに異なります。

Android 5.0以上ではそのパスを取得できるAPIがありますが、それより前のOSではそういったAPIは用意されていません。

以前は Environment.getExternalStorageDirectory を使う方法がありましたが、
実際返却されるものは、SDカードのパスとは限らず、機種によっては内部ストレージのパスの可能性もあります。

vold.fstab の中身を見てSDカードのパスを取得する方法もありますが、
私自身試したところ vold.fstab で取得できない機種もありました。

このようにSDカードのパスの取得は、機種依存しやすい所ではありますが、
この記事では機種依存しないようにSDカードのパスを取得する方法を記載しようと思います。
※ ただし、OSごとには取得方法は異なります。

以下は、今回取得するパスの情報になります。

| 項目 | 内容 |
|:-:|:-:|
| 対象OS | Android4.2以上 |
| 取得するパス | /\<SDカードのベースパス\>/Android/data/\<パッケージ名\>/files |

Android4.2以上としているのは、それ未満のOSで動作を確認していないためです。

## Android 5.0 以上
Android 5.0 以上には便利なAPIが用意されているので、それを使います。

```
/**
 * SDカードのfilesディレクトリパスのリストを取得する。
 * Android5.0以上対応。
 *
 * @param context
 * @return SDカードのfilesディレクトリパスのリスト
 */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static List<String> getSdCardFilesDirPathListForLollipop(Context context) {
    List<String> sdCardFilesDirPathList = new ArrayList<>();

    // getExternalFilesDirsはAndroid4.4から利用できるAPI。
    // filesディレクトリのリストを取得できる。
    File[] dirArr = context.getExternalFilesDirs(null);

    for (File dir : dirArr) {
        if (dir != null) {
            String path = dir.getAbsolutePath();

            // isExternalStorageRemovableはAndroid5.0から利用できるAPI。
            // 取り外し可能かどうか（SDカードかどうか）を判定している。
            if (Environment.isExternalStorageRemovable(dir)) {

                // 取り外し可能であればSDカード。
                if (!sdCardFilesDirPathList.contains(path)) {
                    sdCardFilesDirPathList.add(path);
                }

            } else {
                // 取り外し不可能であれば内部ストレージ。
            }
        }
    }
    return sdCardFilesDirPathList;
}
```

## Android 4.2〜4.4
Android 4.2〜4.4にはAPIに良い感じのものがないので、リフレクションを使って取得しています。
4.4系には、Context.getExternalFilesDirs は使えますが、Environment.isExternalStorageRemovable が利用できません。
そのため4.4系も、リフレクションを使ったこちらのほうからSDカードのパスを取得するようにしています。

```
/**
 * SDカードのfilesディレクトリパスのリストを取得する。
 *
 * @param context
 * @return SDカードのfilesディレクトリパスのリスト
 */
public static List<String> getSdCardFilesDirPathList(Context context) {
    List<String> sdCardFilesDirPathList = new ArrayList<>();
    StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
    try {

        // StorageVolumeの一覧を取得する。非公開メソッドなので、リフレクションを使用。
        // Environment.getExternalStorageDirectory を追うと、
        // StorageManagerを使ってStorageVolumeを取得しているのがわかる。
        Method getVolumeListMethod = sm.getClass().getDeclaredMethod("getVolumeList");
        Object[] volumeList = (Object[]) getVolumeListMethod.invoke(sm);

        for (Object volume : volumeList) {

            // getPathFileメソッドは、StorageVolumeのFileオブジェクトを取得するメソッド。
            Method getPathFileMethod = volume.getClass().getDeclaredMethod("getPathFile");
            File file = (File) getPathFileMethod.invoke(volume);
            String storageBasePath = file.getAbsolutePath();

            // isRemovableメソッドは、StorageVolumeが取り外し可能かどうかを判定するメソッド。
            Method isRemovableMethod = volume.getClass().getDeclaredMethod("isRemovable");
            boolean isRemovable = (boolean) isRemovableMethod.invoke(volume);

            // ストレージが取り外し可能かどうか（SDカードかどうか）を判定。
            if (isRemovable) {

                // ベースパスがマウントされているかどうかを判定。
                if (isMountedBasePath(storageBasePath)) {

                    // StorageVolumeの中で、取り外し可能でかつマウント済みのパスは、SDカード。
                    // マウント済みかどうかを確認しないと、機種によっては /mnt/Private などのパスも含まれてしまうことがある。
                    if (!sdCardFilesDirPathList.contains(storageBasePath)) {
                        String sdCardFilesDirPath = storageBasePath + "/Android/data/com.test.h_yama37/files";
                        sdCardFilesDirPathList.add(sdCardFilesDirPath);
                    }

                }
            } else {
                // StorageVolumeの中で、取り外し不可能なパスは、内部ストレージ。
            }
        }

    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

    // Android4.4系のみ、getExternalFilesDirs で一度filesディレクトリを生成する必要がある。
    // Android4.4系は、File.mkdirsなどでfilesディレクトリを生成できないため。
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        context.getExternalFilesDirs(null);
    }

    return sdCardFilesDirPathList;
}

/**
 * 指定したベースパスが、マウントされていれば、trueを返す。
 *
 * @param basePath
 * @return ベースパスがマウントされていればtrue。マウントされていなければfalse。
 */
private static boolean isMountedBasePath(String basePath) {
    boolean isMounted = false;
    BufferedReader br = null;
    File mounts = new File("/proc/mounts");

    // /proc/mountsが存在しなければ処理を終了する
    if (!mounts.exists()) {
        return isMounted;
    }

    try {

        // マウントポイントを取得する
        br = new BufferedReader(new FileReader(mounts));
        String line;

        // マウントポイントに該当するパスがあるかチェックする
        while ((line = br.readLine()) != null) {

            if (line.contains(basePath)) {
                // 該当するパスがあればマウントされているため、処理を終える
                isMounted = true;
                break;
            }

        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    return isMounted;
}
```

## 参考資料
* [アンドロイドのSDカードパスの取得方法](https://www.notice.co.jp/archives/3074)
* [【android】 SDカードのpathを取得する方法](http://nobuo-create.net/sdcard-2/)