Androidのファイルストレージに関する仕様の整理

# 概要

モバイルアプリにおいてはしばしば画像・動画・音声といったメディアファイルや、ドキュメントファイルを扱う場面が多く存在します。
ことAndroidにおいては比較的柔軟にファイルを扱うことができるものの、OSバージョンアップの度に大きく仕様変更が入ることが多々あり開発者泣かせになっている節があります。
本記事においてはそんな複雑化している Android アプリのファイルストレージに関する近年の仕様の変遷と、執筆時点でのベストプラクティスについて記述しています。

# Androidのファイルストレージのユースケース

本記事執筆時点での最新OSをターゲットにする場合のファイル操作のベストプラクティスについてです。

参考:

https://developer.android.com/training/data-storage

https://developer.android.com/training/data-storage/use-cases

Android アプリで扱うファイルには主に以下の2種があります
(ここでいう"ファイル"には構造化DBやKey-Value Preferenceのファイルは含まれません。)

- メディアファイル
    - 画像、動画、オーディオ、ダウンロードファイル
- メディア以外のファイル
    - 上記以外ですべてのファイル
    - 構造化DBやPreferenceのファイルは除く

## メディアファイル

Media Store API を利用します。

https://developer.android.com/training/data-storage/shared/media

Media Store APIの使い方については[@e10dokup](https://twitter.com/e10dokup)さんの以下の資料が非常に分かりやすいのでこちらを参照ください🙇

https://speakerdeck.com/e10dokup/now-and-future-of-media-access
 
## メディア以外のファイル

Storage Access Framework を利用します。

https://developer.android.com/guide/topics/providers/document-provider#client

例: ACTION_OPEN_DOCUMENT_TREE で任意のディレクトリ内のファイルやフォルダを操作する

```kotlin
val REQUEST_CODE = 100

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)
    prefSetting = PreferenceManager.getDefaultSharedPreferences(this)
    path_button.setOnClickListener {
        // Intent 起動 
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
        startActivityForResult(intent, REQUEST_CODE)
    }

}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        val uri = data?.data ?: return

        // 権限の永続化
        contentResolver.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
         
        // ファイルの保存 or 読み出しの処理
        // もしくは URI を SharedPreference 等に保存して他クラスで処理することも可能
    }
}
```

# ファイルストレージに関する仕様の変遷

公式ドキュメントの情報を基に、Android 4.4 以降におけるファイルストレージに関する仕様の変遷をまとめました。
(紙面の都合上、Android 4.4 以前についての情報は割愛させていただいております...)

なお、Androidには「内部ストレージ」と「外部ストレージ」という概念があります。
これはそれぞれ以下のような意味合いになります:

- 内部ストレージ(Internal storage): アプリ専用のファイルを保存する領域
- 外部ストレージ(External storage): 端末やSDカード内にある他アプリと共有されたファイルの保存領域

内部ストレージ/外部ストレージは、端末の内臓ストレージ/SDカードのような区分とは直接関係していません。
また、「アプリ固有のストレージ」「共有ストレージ」という概念もあり、それぞれ以下のような意味合いになります。

- アプリ固有のストレージ(App-specific storage): 内部ストレージと外部ストレージにそれぞれ設けられたアプリ専用の保存領域
- 共有ストレージ(Shared storage): 外部ストレージに設けられている、画像などのメディアファイルやドキュメントファイルのための保存領域

## Android 4.4 

https://developer.android.com/about/versions/android-4.4#StorageAccess

- 外部ストレージの読み込みには [READ_EXTERNAL_STORAGE](https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE) 権限が必要になりました。
    - 内部ストレージの読み書きに権限は不要です。
- [Storage Access Framework(SAF)](https://developer.android.com/guide/topics/providers/document-provider) が実装されました。
    - 以下の Intent でAndroid標準のFile Pickerが起動し、ファイル単位で読み取り、書き込みの権限を付与できるようになりました。
    - [ACTION_OPEN_DOCUMENT](https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT) 
    - [ACTION_CREATE_DOCUMENT](https://developer.android.com/reference/android/content/Intent#ACTION_CREATE_DOCUMENT) 

## Android 5.0

https://developer.android.com/about/versions/android-5.0#Storage

- SAFの機能が拡張され、[ACTION_OPEN_DOCUMENT_TREE](https://developer.android.com/reference/android/content/Intent?hl=ja#ACTION_OPEN_DOCUMENT_TREE) Intent を用いてディレクトリ全体に権限を付与することができるようになりました。

## Android 6.0

https://developer.android.com/about/versions/marshmallow/android-6.0-changes?hl=ja#behavior-runtime-permissions

- 実行時パーミッションが実装されました。
- それに伴い、[WRITE_EXTERNAL_STORAGE](https://developer.android.com/reference/android/Manifest.permission#WRITE_EXTERNAL_STORAGE) 権限を付与するためには実行時パーミッションによる権限付与が必要になりました。

## Android 7.0

https://developer.android.com/about/versions/nougat/android-7.0-changes#permfilesys

- ファイルのパーミッションに関していくつか追加で制限が入りました。
- アプリのプライベートディレクトリのパーミッションが `0700` になりました。
- `file://` で始まるURIを使用したファイルの共有に [FileProvider](https://developer.android.com/training/secure-file-sharing/setup-sharing) の利用が推奨されるようになりました。
- [DownloadManager.COLUMN_LOCAL_FILENAME](https://developer.android.com/reference/android/app/DownloadManager#COLUMN_LOCAL_FILENAME) が使用不可になりました。

## Android 8.0

https://developer.android.com/about/versions/oreo/android-8.0-changes

ファイルストレージに関する仕様の変更はありません。

## Android 9.0

https://developer.android.com/about/versions/pie/android-9.0-changes-28#per-app-selinux

- アプリ固有のファイルへの外部のアプリからのアクセスに制限が入りました。
- 他のアプリとのファイル共有については [ContentProvider](https://developer.android.com/guide/topics/providers/content-provider-basics) の使用が推奨されるようになりました。

## Android 10

https://developer.android.com/about/versions/10/privacy/changes#scoped-storage

- [対象範囲別ストレージ](https://developer.android.com/training/data-storage#scoped-storage)　が実装されました。
 - 共有ストレージ内のファイルは Storage Access Framework か Media Store API を通してしかアクセスできなくなりました。
 - ただし、移行措置として TargetSdkVersion <= 29 のアプリにおいては [requestLegacyExternalStorage](https://developer.android.com/reference/android/R.attr#requestLegacyExternalStorage) オプションを `true` にしていれば対象範囲別の利用を回避できるようになっています。
- Media Store API に `MediaStore.Downloads` テーブルが追加されました。
    - この変更により、Media Store API の テーブルは以下の4種となりました。
    - [MediaStore.Images](https://developer.android.com/reference/android/provider/MediaStore.Images)
    - [MediaStore.Video](https://developer.android.com/reference/android/provider/MediaStore.Video)
    - [MediaStore.Audio](https://developer.android.com/reference/android/provider/MediaStore.Audio)
    - [MediaStore.Downloads](https://developer.android.com/reference/android/provider/MediaStore.Downloads)

## Android 11

https://developer.android.com/about/versions/11/privacy/storage

- Android 11では、ファイルストレージの仕様に関して大幅な変更が加えられました。(寧ろここがメインなのではというくらいに)
- 対象範囲別ストレージの強制化
    - TargetSdkVersion >= 30 のアプリにおいては `requestLegacyExternalStorage` プロパティが使用不可になりました。
    - `requestLegacyExternalStorage=false` が記述されていても無視されます。
- アプリ固有のディレクトリを [Context#getExternalFilesDirs()](https://developer.android.com/reference/android/content/Context#getExternalFilesDirs(java.lang.String)) メソッドによって得られるパス以外の場所に保存できなくなりました。
- メディアファイルに `Java File API` と `fopen()` などのネイティブメソッド経由でアクセスできるようになりました。
- Android 9.0 で制限が入った他アプリからのアプリ固有データへのアクセスの制限が強化され、外部/内部ストレージともに外部アプリからのアクセスが一切不可になりました。
- Storage Access Framework でアクセス権を付与できるディレクトリに制限が入りました。なお、これらの制限は TargetSdkVersion >= 30 のときにのみ有効になります。
 - `ACTION_OPEN_DOCUMENT_TREE` Intent を使用した場合、以下のディレクトリには権限を付与できなくなりました。
    - 内部ストレージのルートディレクトリ
    - SDカードのルートディレクトリ
    - Download ディレクトリ
- `ACTION_OPEN_DOCUMENT_TREE` または `ACTION_OPEN_DOCUMENT` を使用した場合、以下のディレクトリ配下のファイルには権限を付与できなくなりました。
    - `Android/data/` とその配下のディレクトリ
    - `Android/obb/` とその配下のディレクトリ
- SAF以外の方法でメディア以外のファイルにアクセス可能となる権限として、`MANAGE_EXTERNAL_STORAGE` が追加されました。
    - Play Store にこの権限を付与したアプリをリリースするには、[一定の基準](https://developer.android.com/training/data-storage/manage-all-files#all-files-access-google-play)をクリアする必要があります。
- Media Store API を用いる場合は `WRITE_EXTERNAL_STORAGE` 権限と `WRITE_MEDIA_STORAGE` が不要となりました。
    - これらの権限はもはや不要？

## Android 12

https://developer.android.com/about/versions/12/summary

- 新機能追加が主で、既存の実装に対していますぐ修正が必要になるような変更は含まれていません。
- Media Store API の MediaStore.Audio テーブルに `Recordings/` ディレクトリが追加されました。
- Media Store API の [getMediaUri](https://developer.android.com/reference/android/provider/MediaStore#getMediaUri(android.content.Context,%20android.net.Uri)) メソッドにおいて [MediaDocumentsProvider](https://cs.android.com/android/platform/superproject/+/master:packages/providers/MediaProvider/src/com/android/providers/media/MediaDocumentsProvider.java) 形式のURIがサポートされるようになりました。
- [MANAGE_MEDIA](https://developer.android.com/reference/android/Manifest.permission#MANAGE_MEDIA) 権限が追加されました。
    - `MANAGE_EXTERNAL_STORAGE` 権限が付与されている場合に、追加の確認ダイアログを表示させないようにする権限のようです。
- StorageManager に [getManageSpaceActivityIntent](https://developer.android.com/reference/android/os/storage/StorageManager#getManageSpaceActivityIntent(java.lang.String,%20int)) が追加されました。
    - 他のファイルマネージャーなどにアクティビティを公開できるようです。

## Android 13

https://developer.android.com/about/versions/13/behavior-changes-13

* メディアファイルに対するアクセス権限の仕様が一部変更され、`READ_EXTERNAL_STORAGE` 権限の代わりに、扱うメディアファイルごとに以下の3種類の権限の付与が必要となりました。
  * 画像及び写真: `READ_MEDIA_IMAGES`
  * 動画: `READ_MEDIA_VIDEO`
  * 音声ファイル: `READ_MEDIA_AUDIO`

## Android 14

https://developer.android.com/about/versions/14/behavior-changes-14

- `Selected Photos Access` という仕組みが導入され、iOSのように特定のメディアへ個別にアクセス権限を付与できるようになりました。
   - 詳細については[ドキュメント](https://developer.android.com/about/versions/14/changes/partial-photo-video-access) を参照してください。
- SAFなどの手段で読み込もうとしているZipファイルのエントリに「..」や「/」が含まれていると、[ZipException](https://developer.android.com/reference/java/util/zip/ZipException) が発生するようになりました。

## Android 15

https://developer.android.com/about/versions/15/behavior-changes-15

ベータ版 4.1（2024 年 7 月）の時点では、
ストレージに影響のある仕様変更はありません。

# まとめ

本記事では、Android OS のファイルストレージに関して、ユースケースに応じた実装と、過去から現在までにおける仕様の変遷についてご紹介しました。
ファイルストレージに関する仕様はOSのバージョンアップによる影響が大きく、設計に頭を悩ませがちな部分ではありますが、本記事が読者の皆様の一助となりましたら幸いです。