外部ストレージへアクセスするアプリは、パーミッション(許可)をユーザーから取得する必要があります。
パーミッションを得たアプリは、許可された権限の範囲内で、ストレージへアクセスが可能です。
これらは、ユーザーデータのプライバシー保護と密接に関係しています。
プライバシー保護をより強固にするために、パーミッションと権限の範囲の仕様は改変されてきました。
今回は、改変の歴史を辿りつつ、「パーミッションと権限の範囲」について、まとめます。
※環境:Android Studio Narwhal | 2025.1.1 Patch 1
目次
パーミッションと権限の範囲
パーミッション
外部ストレージはアプリ間で共有するデータを保存する領域です。
データを「他のアプリと共有する」は「他のアプリに公開する」と同意です。悪意のあるアプリがインストールされた場合に、このアプリを経由してデータ(個人情報)が漏洩します。
ですので、秘匿性の高いデータの保存に適しません。
このセキュリティ上の問題点を承知の上で、外部ストレージへデータを保存するためには、アプリがユーザーからパーミッション(許可)を取得し、外部ストレージへアクセスする権限を持つ必要があります。
権限の範囲
パーミッションを取得したアプリは、パーミッションが示す権限の範囲内で、ストレージのアクセスが可能です。
以下の表はパーミッションと権限の範囲を表しています。
外部ストレージ プライマリ /storage/emulated/ユーザーID | Java / Android API | Media Store API | SAF | |
---|---|---|---|---|
メディアデータ | Pictures | 他:△wrp | 他:◎wrp | 他:△wr |
DCIM | ||||
Movies | ||||
Music | ||||
Alarms | ||||
Notifications | ||||
Podcasts | ||||
Ringtones | ||||
Audiobooks(API≧30) | ||||
Recordings(API≧31) | ||||
ドキュメント 他のファイル | Downloads | 他:○wrp | 他:◎wrp | 他:◎wr |
Documants | ||||
Android (Android/dataを除く) | ||||
ユーザーフォルダ | ||||
ユーザーファイル | ||||
アプリ別 Android/data/パッケージ名/files | 他:△wrp | 他:○wr |
||
※自:自アプリのファイル 他:他アプリのファイル ◎:アクセスできる(最適) ○:アクセスできる △:アクセスできる(不適または制限有) ×:アクセスできない 空:アクセスできない(サポート外) w:書き込みできる r:読み出しできる p:パーミッションが必要 |
外部ストレージ プライマリ /storage/emulated/ユーザーID | Java / Android API | Media Store API | SAF | |
---|---|---|---|---|
メディアデータ | Pictures | 他:× | 他:◎ rp | 他:△wr |
DCIM | ||||
Movies | ||||
Music | ||||
Alarms | ||||
Notifications | ||||
Podcasts | ||||
Ringtones | ||||
Audiobooks(API≧30) | ||||
Recordings(API≧31) | ||||
ドキュメント 他のファイル | Downloads | 他:× | 他:× | 他:◎wr |
Documants | ||||
Android (Android/dataを除く) | ||||
ユーザーフォルダ | ||||
ユーザーファイル | ||||
アプリ別 Android/data/パッケージ名/files | 他:× | 他:○wr |
||
※自:自アプリのファイル 他:他アプリのファイル ◎:アクセスできる(最適) ○:アクセスできる △:アクセスできる(不適または制限有) ×:アクセスできない 空:アクセスできない(サポート外) w:書き込みできる r:読み出しできる p:パーミッションが必要 |
外部ストレージ プライマリ /storage/emulated/ユーザーID | Java / Android API | Media Store API | SAF | |
---|---|---|---|---|
メディアデータ | Pictures | 他:× | 他:◎ rp ※1 | 他:△wr |
DCIM | ||||
Movies | ||||
Music | ||||
Alarms | ||||
Notifications | ||||
Podcasts | ||||
Ringtones | ||||
Audiobooks(API≧30) | ||||
Recordings(API≧31) | ||||
ドキュメント 他のファイル | Downloads | 他:× | 他:× | 他:◎wr |
Documants | ||||
Android (Android/dataを除く) | 他:× | |||
ユーザーフォルダ | ||||
ユーザーファイル | ||||
アプリ別 Android/data/パッケージ名/files | 他:× | 他:× |
||
※自:自アプリのファイル 他:他アプリのファイル ◎:アクセスできる(最適) ○:アクセスできる △:アクセスできる(不適または制限有) ×:アクセスできない 空:アクセスできない(サポート外) w:書き込みできる r:読み出しできる p:パーミッションが必要 ※1:書き込みはファイル毎に権限の取得が必要、通常のパーミッションとは別 |
外部ストレージ プライマリ /storage/emulated/ユーザーID | Java / Android API | Media Store API | SAF | |
---|---|---|---|---|
メディアデータ | Pictures | 他:× | 他:◎ rp ※1 | 他:△wr |
DCIM | ||||
Movies | ||||
Music | ||||
Alarms | ||||
Notifications | ||||
Podcasts | ||||
Ringtones | ||||
Audiobooks(API≧30) | ||||
Recordings(API≧31) | ||||
ドキュメント 他のファイル | Downloads | 他:× | 他:× | 他:◎wr |
Documants | ||||
Android (Android/dataを除く) | 他:× | |||
ユーザーフォルダ | ||||
ユーザーファイル | ||||
アプリ別 Android/data/パッケージ名/files | 他:× | 他:× |
||
※自:自アプリのファイル 他:他アプリのファイル ◎:アクセスできる(最適) ○:アクセスできる △:アクセスできる(不適または制限有) ×:アクセスできない 空:アクセスできない(サポート外) w:書き込みできる r:読み出しできる p:パーミッションが必要(コレクション毎) ※1:書き込みはファイル毎に権限の取得が必要、通常のパーミッションとは別 |
大きな変更
パーミッションと権限の範囲は、APIのバージョンが上がる(API 23以降を想定)中で、大きな変更が2度ありました。
- 対象範囲別ストレージ(API≧29)
- きめ細かいメディア権限(API≧33)
ちなみに、「対象範囲別ストレージ」は「ストレージ名」ではなく「アクセスモデル名」です。
大きな変更前(API≦28)
アプリ自身が所有するファイルへアクセスする時も、他アプリが所有するファイルへアクセスする時も、パーミッションの取得が必要です。
パーミッションの設定&取得
書き込み(WRITE_EXTERNAL_STORAGE)と読み出し(READ_EXTERNAL_STORAGE)パーミッションがあり、権限の範囲は外部ストレージ全体です。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application ... > ... </application> </manifest>
reqPerm(arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE )) ... fun reqPerm(perms: Array<String>) { var _isGranted = true perms.forEach { val _result = PermissionChecker.checkSelfPermission(this, it) _isGranted = _isGranted && (_result == PermissionChecker.PERMISSION_GRANTED) } if (! _isGranted) { requestPermissions(perms, 0) } }
パーミッションの取得を行うと、次のようなダイアログが表示されて、ユーザの許可を求めます。
※詳細は「Runtime Permissionの取得(システムがリクエストを管理)」を参照


対象範囲別ストレージ(API≧29)
対象範囲別ストレージとは、ドキュメントに「アプリとユーザーデータのプライバシー保護を目的に、アプリの外部ストレージに対するアクセスを制限します。」と説明されています。
変更点
具体的には、次の点が変更になります。
★Java/Android APIを使ったアクセスを禁止 ★Media Store APIを使ったアクセスを強制 ・アプリ自身が所有するファイルへアクセス ... ※1 -パーミッションは不要 ・他アプリが所有するファイルへアクセス -Readアクセスにパーミッションが必要 -Writeアクセスは個別にURIへ許可を出す ... ※2 (通常のパーミッションとは別) ★サンドボックスと同等な領域(アプリ別)を確保 ※サンドボックス:内部ストレージにあるアプリ固有の領域
プライバシー保護で問題になる「他のアプリが所有するファイルへアクセスする方法」は残ります。無くしてしまったら、「アプリ間で共有するデータを保存する」という外部ストレージ本来の目的を失うからです。
パーミッションの設定&取得
読み出し(READ_EXTERNAL_STORAGE)パーミッションがあり、権限の範囲はメディアデータです。
※1,2の変更により、書き込み(WRITE_EXTERNAL_STORAGE)パーミッションは不要になりました。
ただし、API≦28では必要なので、API≦32(=23~32)の全てに対応したければ、maxSdkVersionを付けて指定します。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application ... > ... </application> </manifest>
reqPerm(arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, )) ... fun reqPerm(perms: Array<String>) { var _isGranted = true perms.forEach { val _result = PermissionChecker.checkSelfPermission(this, it) _isGranted = _isGranted && (_result == PermissionChecker.PERMISSION_GRANTED) } if (! _isGranted) { requestPermissions(perms, 0) } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application ... > ... </application> </manifest>
reqPerm(arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE )) ... fun reqPerm(perms: Array<String>) { var _isGranted = true perms.forEach { val _result = PermissionChecker.checkSelfPermission(this, it) _isGranted = _isGranted && (_result == PermissionChecker.PERMISSION_GRANTED) } if (! _isGranted) { requestPermissions(perms, 0) } }
パーミッションの取得を行うと、次のようなダイアログが表示されて、ユーザの許可を求めます。


注意点:API29における無効化
API29における対象範囲別ストレージは、デフォルトで有効ですが、設定で無効に出来ます。
ただし、API≧30では強制的に有効です。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:requestLegacyExternalStorage="true" ... > ... </application> </manifest>
API29とAPI≧30でアクセスする権限の範囲に違いがあります。注意が必要です。
ですので、「対象範囲別ストレージの実質的な実装はAPI≧30」と捉えて、API29は無効化が妥当だと思います。
注意点:URIのアクセス許可は別物
対象範囲別ストレージは、URI(ストレージ上のファイルを一意に表す)毎にアクセス許可を出せます。
許可を持つアプリは、そのURIにアクセスが可能です。
この「URI毎に出されるアクセス許可」と「パーミッションシステムの出すアクセス許可」は独立しています。両者の関係性は無く、全くの別物です。
対象範囲別ストレージにおいて、他アプリが所有するファイルへ書き込みたい(更新や編集、削除を含む)場合は、URIのアクセス許可を取得する方法を使います。
きめ細かいメディア権限(API≧33)
きめ細かいメディア権限では、メディアデータに対するパーミッションがコレクション毎に細分化されます。
変更点
具体的には、次の点が変更になります。
★読み出し(READ_EXTERNAL_STORAGE)パーミッションを廃止 ... ※3 ★メディアデータに対するパーミッション新設 ... ※4 ・コレクション毎に細分化 -READ_MEDIA_IMAGES ⇒ imagesコレクションのアクセス許可 -READ_MEDIA_VIDEO ⇒ videoコレクションのアクセス許可 -READ_MEDIA_AUDIO ⇒ audioコレクションのアクセス許可
例えば、READ_MEDIA_IMAGESパーミッションを取得したアプリは、imagesコレクションにアクセスする権限をもちます。videoとaudioコレクションへのアクセスは出来ません。
アクセスしたければ、別途、パーミッションを取得する必要があります。
パーミッションの設定&取得
imeges/video/audio毎の読み出しパーミッションがあり、権限の範囲はコレクションです。
パーミッションは※3から※4へ切り替わり、READ_EXTERNAL_STORAGEパーミッションは不要になりました。
ただし、API≦32では必要なので、全てのAPIに対応したれければ、残す必要があります。残してもAPI≧33では無視されます。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <application ... > ... </application> </manifest>
reqPerm(arrayOf( Manifest.permission.READ_MEDIA_IMAGES, // Manifest.permission.READ_MEDIA_VIDEO, // Manifest.permission.READ_MEDIA_AUDIO )) ... fun reqPerm(perms: Array<String>) { var _isGranted = true perms.forEach { val _result = PermissionChecker.checkSelfPermission(this, it) _isGranted = _isGranted && (_result == PermissionChecker.PERMISSION_GRANTED) } if (! _isGranted) { requestPermissions(perms, 0) } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application ... > ... </application> </manifest>
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) reqPerm(arrayOf( Manifest.permission.READ_MEDIA_IMAGES, // Manifest.permission.READ_MEDIA_VIDEO, // Manifest.permission.READ_MEDIA_AUDIO )) else reqPerm(arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE )) ... fun reqPerm(perms: Array<String>) { var _isGranted = true perms.forEach { val _result = PermissionChecker.checkSelfPermission(this, it) _isGranted = _isGranted && (_result == PermissionChecker.PERMISSION_GRANTED) } if (! _isGranted) { requestPermissions(perms, 0) } }
パーミッションの取得を行うと、次のようなダイアログが表示されて、ユーザの許可を求めます。imagesとvideoコレクションは同じダイアログです。

※エミュレータ―(API33)の表示


※エミュレータ―(API33)の表示

関連記事: