Androidでストレージの物理パスを取得する

# はじめに

Androidにおける、ストレージボリュームへの物理パスを取得する方法について述べます。
ただし、全てのAndroid端末で動作を保証するものではありません。

# コード例

開発環境: Android Studio 4.0.1
動作端末: 各バージョンのエミュレータ端末
言語: Kotlin

例として、ストレージの物理パスをキー、ストレージのラベルを値とするMapデータを出力するコードは次のようになります。 

```kotlin
val map: Map<String, String> = when {
    Build.VERSION.SDK_INT>= Build.VERSION_CODES.R -> {
        // Android 11- (API 30)
        sm.storageVolumes.mapNotNull { volume ->
            val path = volume.directory?.absolutePath ?: return@mapNotNull null
            val label = volume.getDescription(this) ?: return@mapNotNull null
            path to label
        }.toMap()
    }
    Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
        // Android 7-10 (API 24-29)
        val getPath = StorageVolume::class.java.getDeclaredMethod("getPath")
        sm.storageVolumes.mapNotNull { volume ->
            val path = (getPath.invoke(volume) as String?) ?: return@mapNotNull null
            val label = volume.getDescription(this) ?: return@mapNotNull null
            path to label
        }.toMap()
    }
    else -> {
        // Android 4-6 (API 14-23)
        val getVolumeList = sm.javaClass.getDeclaredMethod("getVolumeList")
        (getVolumeList.invoke(sm) as Array<*>).filterNotNull().mapNotNull { volume ->
            val getPath = volume.javaClass.getDeclaredMethod("getPath") ?: return@mapNotNull null
            val getLabel = volume.javaClass.getDeclaredMethod("getDescription", Context::class.java)
            val path = (getPath.invoke(volume) as String?) ?: return@mapNotNull null
            val label = (getLabel.invoke(volume, this) as String?) ?: return@mapNotNull null
            path to label
        }.toMap()
    }
}
```

Mapをテキストとして出力すると、以下のようなデータになります。

```
{/storage/emulated/0=Internal shared storage, /storage/0F08-1A0B=SDCARD}
```

## 簡単な解説

ストレージデバイスの情報はStorageManagerから取得することが出来ます。StorageManagerのgetStorageVolumes()メソッドから、StorageVolumeの一覧を取得できます。StorageVolumeにはストレージ毎の各種パラメータが定義されており、物理パスもその中に含まれています。
ただし、Androidのバージョンによってパラメータの取得メソッドが公開されていないため、リフレクションを使って強引に取得する必要があります。

### Android 11-

意外なことに、Android11からはStorageVolumeに物理パスを示すFileオブジェクトを取得するための正式なメソッド(getDirectory)が新たに用意されています。物理パスへのアクセスが制限されつつある最近の動向と逆行するような変更に驚きです。なので、Android 11でストレージの物理パスを取得するのは非常に簡単です。
ちなみに、旧バージョンと同様にリフレクションによってアクセスしようとするとエラーが発生します。(参考: [非 SDK インターフェースの制限  \|  Google Play  \|  Android Developers](https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces))

### Android 7-10

Android 7-10では、StorageVolumeに物理パスを取得するためのprivateなフィールド(mPath)、メソッド(getPath)がありますが、公開はされていません。そのため、リフレクションによって取得する必要があります。

### Android 4-6

Android 4-6では、StorageVolumeオブジェクト自体が外からアクセスできません。なので、ストレージの一覧を取得するgetVolumeList()メソッドをリフレクションで呼び出す必要があります。また、取得したStorageVolumeはObject型(Any型)として扱い、各種メソッドをリフレクションで呼び出します。

### それより前

ちなみに、Android 4より前でも、Android 4-6と同様にリフレクションを用いることでアクセスできますが、getDescriptionの引数Contextが無いという違いがあります。まぁ今さらそんな古い機種を対象にする人はいないと思うので省略します。

# さいごに

Androidは年々ストレージアクセスへのアクセス制限を強めており、ファイルへのアクセスも物理パスを隠蔽し、Storage Access Frameworkを用いたり、MediaStoreを用いたりして、content://スキームのURIによってファイルアクセスをするようになっています。なので、物理パスはできる限り使わない(ファイルの所在を意識しない)作りが望ましいのですが、アプリによっては依然として物理パスを使う場面はあります。
そのため、物理パスが必要となる場合は上記のようなコードが参考になるかと思います。