外部ストレージへStorage Access Frameworkでアクセス

投稿日:  更新日:

Storage Access Framework(SAF)は、ファイルピッカーでアクセス対処のファイルを指定する仕組みです。

SAFはアンドロイドシステムに実装されています。SAFを用いれば、ピッカーを自作する必要はありません。

アプリ開発が楽になります。

しかも、指定された外部ストレージ上のファイルは、アプリに対するアクセス許可を付与したものになります。

改めて、許可の取得は不要です。そのまま、ファイルを読み書き出来ます。

今回は「外部ストレージへStorage Access Frameworkでアクセス」を、まとめます。

※環境:Android Studio Narwhal Feature Drop | 2025.1.2 Patch 1

スポンサーリンク

Storage Access Framework(SAF)

Storage Access Frameworkは、ファイルピッカー(GUI)でアクセス対処のファイル(フォルダ)を指定する仕組みです。

API≧19で導入されました。

SAFのサンプル

外部ストレージに保存される全てのデータがアクセス対象です。

主に、「ドキュメントと他のファイル」のアクセスに適しています。メディアデータのアクセスも出来ますが、メディア情報が記録されたデータベースを扱えません。

 外部ストレージ_プライマリ 
API≦28API=29API=30~32API≧33
外部ストレージ プライマリ
/storage/emulated/ユーザーID
Java /
Android
API
Media Store
API
SAF
メディアデータPictures
自:△wrp
他:△wrp
 
自:◎wrp
他:◎wrp
 
自:△wr 
他:△wr 
 
DCIM
Movies
Music
Alarms
Notifications
Podcasts
Ringtones
Audiobooks(API≧30)
Recordings(API≧31)
ドキュメント
他のファイル
Downloads
自:○wrp
他:○wrp
自:◎wrp
他:◎wrp
自:◎wr 
他:◎wr 
Documants
Android
(Android/dataを除く)
ユーザーフォルダ
ユーザーファイル
アプリ別
 Android/data/パッケージ名/files
自:○wr 
他:△wrp
自:○wr 
他:○wr 
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要
外部ストレージ プライマリ
/storage/emulated/ユーザーID
Java /
Android
API
Media Store
API
SAF
メディアデータPictures
自:×   
他:×   
 
自:◎wr 
他:◎ rp
 
自:△wr 
他:△wr 
 
DCIM
Movies
Music
Alarms
Notifications
Podcasts
Ringtones
Audiobooks(API≧30)
Recordings(API≧31)
ドキュメント
他のファイル
Downloads
自:×   
他:×   
自:◎wr 
他:×   
自:◎wr 
他:◎wr 
Documants
Android
(Android/dataを除く)
ユーザーフォルダ
ユーザーファイル
アプリ別
 Android/data/パッケージ名/files
自:○wr 
他:×   
自:○wr 
他:○wr 
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要
外部ストレージ プライマリ
/storage/emulated/ユーザーID
Java /
Android
API
Media Store
API
SAF
メディアデータPictures
自:×   
他:×   
 
自:◎wr 
他:◎ rp
※1
自:△wr 
他:△wr 
 
DCIM
Movies
Music
Alarms
Notifications
Podcasts
Ringtones
Audiobooks(API≧30)
Recordings(API≧31)
ドキュメント
他のファイル
Downloads
自:○wr 
他:×   
自:◎wr 
他:×   
自:◎wr 
他:◎wr 
Documants
Android
(Android/dataを除く)
自:×   
他:×   
ユーザーフォルダ
ユーザーファイル
アプリ別
 Android/data/パッケージ名/files
自:○wr 
他:×   
自:×   
他:×   
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要
※1:書き込みはURI(ファイル)毎にアクセス許可が必要、通常のパーミッションとは別
外部ストレージ プライマリ
/storage/emulated/ユーザーID
Java /
Android
API
Media Store
API
SAF
メディアデータPictures
自:×   
他:×   
 
自:◎wr 
他:◎ rp
※1
自:△wr 
他:△wr 
 
DCIM
Movies
Music
Alarms
Notifications
Podcasts
Ringtones
Audiobooks(API≧30)
Recordings(API≧31)
ドキュメント
他のファイル
Downloads
自:○wr 
他:×   
自:◎wr 
他:×   
自:◎wr 
他:◎wr 
Documants
Android
(Android/dataを除く)
自:×   
他:×   
ユーザーフォルダ
ユーザーファイル
アプリ別
 Android/data/パッケージ名/files
自:○wr 
他:×   
自:×   
他:×   
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要(images/video/audioデータ毎)
※1:書き込みはURI(ファイル)毎にアクセス許可が必要、通常のパーミッションとは別
 外部ストレージ_セカンダリ 
AP<29AP=29API≧30
外部ストレージ セカンダリ
/storage/NNN-NNN/
Java /
Android
API
Media Store
API
SAF
ドキュメント
他のファイル
Downloads
自:×   
他:×   
自:◎wr
他:◎wr
Documents
Android
(Android/dataを除く)
ユーザーフォルダ
ユーザーファイル
アプリ別
(Android/data/パッケージ名/files)
自:○wr 
他:△ rp
自:○wr
他:○wr
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要
外部ストレージ セカンダリ
/storage/NNN-NNN/
Java /
Android
API
Media Store
API
SAF
ドキュメント
他のファイル
Downloads
自:×   
他:×   
自:◎wr
他:◎wr
Documents
Android
(Android/dataを除く)
ユーザーフォルダ
ユーザーファイル
アプリ別
(Android/data/パッケージ名/files)
自:○wr 
他:×   
自:○wr
他:○wr
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要
外部ストレージ セカンダリ
/storage/NNN-NNN/
Java /
Android
API
Media Store
API
SAF
ドキュメント
他のファイル
Downloads
自:△wr 
他:×   
自:◎wr
他:◎wr
Documents
Android
(Android/dataを除く)
自:×   
他:×   
ユーザーフォルダ
ユーザーファイル
アプリ別
(Android/data/パッケージ名/files)
自:○wr 
他:×   
自:×  
他:×  
※自:自アプリのファイル
 他:他アプリのファイル
 ◎:アクセスできる(最適)
 ○:アクセスできる
 △:アクセスできる(不適または制限有)
 ×:アクセスできない
 空:アクセスできない(サポート外)
 w:書き込みできる
 r:読み出しできる
 p:パーミッションが必要
スポンサーリンク

パーミッションの取得不要

ファイルの指定にユーザーの操作が介在するので、ファイルを指定した時点でアクセスする許可を得たものと見なされます。

ですので、外部ストレージに対するアクセスであっても、パーミッションの取得は不要です。

スポンサーリンク

ファイル指定の手順

ファイルの指定は、次のような手順で行います。

Content Resolver/Provider経由でアクセスを行います。

SAFの概要

 手順1 

アクセスを行いたいタイミングで、Clientアプリを起動します。

 手順2 

Clinentアプリはピッカーを表示して、ユーザーにアクセス対象のファイル(フォルダ)の指定を求めます。

 手順3 

ピッカーで指定したURI(ファイル)をコールバックで返します。このURIはアクセスする許可を取得済みものです。

手順1~3はアプリケーションコンポーネント(Activity)の連携と同じ動作です。
※詳細は「App component:Activity」を参照

スポンサーリンク

書き込みの例

書き込みの具体的な例です。※Activity Result APIで記述しています。

コールバックの定義と登録

registerForActivityResult(SafPicker)はコールバックの定義と登録を行います。

ラムダ式がコールバックです。コールバックは2段構成にしています。

SAFのコールバック構成

 1段目コールバック 

1段目コールバックでresultからURIを取り出ます。

「ファイル指定の成功・失敗」はresultCodeで判断できます。成功の場合のみ、2段目コールバックの実行を行っています。

fun ComponentActivity.SafPicker(callback: (Uri) -> Unit) =
    registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if(result.resultCode == RESULT_OK) {  // ファイル指定が成功
            val _intent = result.data
            _intent?.let {
                val _uri = it.data
                _uri?.let { callback(it) }    // 2段目コールバックの実行
            }
        }
        else { }                              // ファイル指定が失敗
    }
 2段目コールバック 

2段目コールバックで書き込み処理を行います。

そして、このコールバックを利用するコンポーネントのランチャー(_safWritePicker : ActivityResultLauncher)を返します。

        val _safWritePicker = SafPicker { uri ->
            writeToUri_Resolver(_data, uri)
        }

registerForActivityResult(SafPicker)は、LifecycleOwnerのcurrentStateが「STARTED」より前のタイミングで実行して下さい。Activityのライフサイクルと密接に関係しています。
※LifecycleOwnerについては「ライフサイクル対応コンポーネント作成」を参照

スポンサーリンク

ピッカーの起動

Clientアプリを起動(launch)し、ピッカーを表示させます。

launchの引数はアクセスのリクエスト(Intent)です。書き込みを行いたいのでIntent.ACTION_CREATE_DOCUMENTアクションを使って、リクエストを構築しています。

_safWritePicker.launch(FileCreateIntent("sample.txt"))
fun FileCreateIntent(filename: String) =
    Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        putExtra(Intent.EXTRA_TITLE, filename)  // デフォルトのファイル名
        type = "*/*"                            // 表示するファイルのMIME
    }

書き込みの実行

ピッカーが表示されて、ユーザにフォルダの場所とファイル名の指定を求めます。

図(エミュレータ―で実行、API33)は、外部ストレージへ「UserDir(フォルダ)」を作り、その中に「sample.txt」を書き込む例です。

ピッカーの書き込み操作例

指定されたURI(ファイル)は、このアプリによる書き込みが可能です。

例えば書き込みするのであれば、通常のアクセスと同様に、下記の拡張関数で行えます。

fun Context.writeToUri_Resolver(data: ByteArray, uri: Uri) {
    try {
        contentResolver.openOutputStream(uri, "w")?.use { outputStream ->
            outputStream.write(data)
            outputStream.flush()
        }
    } catch (e: Exception) { Log.i(TAG, "[writeToUri] ${e}") }
}
# pwd
/storage/emulated/0
# ls -ld * UserDir/*
drwxrws--- 2 u0_a166  media_rw 4096 2025-08-24 23:56 Alarms
drwxrws--x 5 media_rw media_rw 4096 2025-08-24 23:55 Android
drwxrws--- 2 u0_a166  media_rw 4096 2025-08-24 23:56 Audiobooks
drwxrws--- 2 u0_a166  media_rw 4096 2025-08-24 23:56 DCIM
...
drwxrws--- 2 u0_a166  media_rw 4096 2025-08-24 23:56 Recordings
drwxrws--- 2 u0_a166  media_rw 4096 2025-08-24 23:56 Ringtones
drwxrws--- 2 u0_a166  media_rw 4096 2025-08-25 02:20 UserDir
-rw-rw---- 1 u0_a166  media_rw   30 2025-08-25 02:20 UserDir/sample.txt

※エミュレータ(API 33)で実行
スポンサーリンク

読み出しの例

読み出しの具体的な例です。※Activity Result APIで記述しています。

コールバックの定義と登録

1段目コールバックは書き込み時と同じです。

2段目コールバックで読み出し処理を行います。

そして、このコールバックを利用するコンポーネントのランチャー(_safReadPicker : ActivityResultLauncher)を返します。

        val _safReadPicker = SafPicker { uri ->
            val _data = readFromUri_Resolver(uri)
            Log.i(TAG, "Data = ${_data.decodeToString()}")
        }

ピッカーの起動

Clientアプリを起動(launch)し、ピッカーを表示させます。

launchの引数はアクセスのリクエスト(Intent)です。読み出しを行いたいのでIntent.ACTION_OPEN_DOCUMENTアクションを使って、リクエストを構築しています。

_safReadPicker.launch(FileOpenIntent())
fun FileOpenIntent() =
    Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "*/*"                            // 表示するファイルのMIME
    }
スポンサーリンク

読み出しの実行

ピッカーが表示されて、ユーザにフォルダの場所とファイル名の指定を求めます。

図(エミュレータ―で実行、API33)は、外部ストレージへ「UserDir(フォルダ)」を開き、その中から「sample.txt」を読み出す例です。

ピッカーの読み出し操作例

指定されたURI(ファイル)は、このアプリによる読み出しが可能です。

例えば読み出しするのであれば、通常のアクセスと同様に、下記の拡張関数で行えます。

fun Context.readFromUri_Resolver(uri: Uri): ByteArray  {
    val _os = ByteArrayOutputStream()
    contentResolver.openInputStream(uri).use { stream ->
        _os.use {
            stream?.copyTo(_os)
        }
    }
    return _os.toByteArray()
}
Data = Hello World !! (1756087962165)

※エミュレータ(API 33)で実行
スポンサーリンク

リクエストで利用するアクション

リクエスト(Intent)で指定するアクションは、表のような意味が有ります。

アクション(Intent.*)リクエストコメント
ACTION_CREATE_DOCUMENT新しくファイルを作成書き込み可能(新規作成)
ファイル名の重複は番号を付けて回避
 例:sample(1).txt
ACTION_OPEN_DOCUMENT既存のファイルを開く書き込み可能(上書き)
読み出し可能
ACTION_OPEN_DOCUMENT_TREE
(API≧21)
フォルダを開くフォルダ全体のアクセス権を持つ

※ACTION_OPEN_DOCUMENT_TREEはAPI≧21で利用可能です。

スポンサーリンク

関連記事:

ストレージのリソースは、内部メモリー、外部メモリー、クラウドの3つがあります。 外部メモリーは、主にSDカードです。 このSDカードはAndroid端末の世代が進むにつれて、扱いを変えてきました。 リソースとSDカードを主眼に置いたストレージの変遷について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
Adoptable Storage(API≧23、Android 6.0)が導入されて、SDカードの扱い方をユーザ側で指定できるようになりました。 「Adoptable Storage」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
ストレージはデータの用途別に記憶領域が分けられます。 保存先を守らないと、セキュリティリスクが発生したり、他のアプリと協調した動作が出来なくなったり、します。 ですので、適切な場所へデータを保存しましょう。 今回は「用途別記憶領域とボリューム」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
ストレージへアクセスする方法は、「扱うデータの種類」「アクセス先」「セキュリティの確保」などの要件により、最適なアクセス方法が存在するので、使い分けが必要です。 今回は「アクセスする方法」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
外部ストレージへアクセスするアプリは、パーミッション(許可)をユーザーから取得する必要があります。 パーミッションを得たアプリは、許可された権限の範囲内で、ストレージへアクセスが可能です。 これらは、ユーザーデータのプライバシー保護と密接に関係しています。 プライバシー保護をより強固にするために、パーミッションと権限の範囲の仕様は改変されてきました。 今回は、改変の歴史を辿りつつ、「パーミッションと権限の範囲」について、まとめます。 ※環境:Android Studio Narwhal | 2025.1.1 Patch 1 ...
Storage Access Framework(SAF)は、ファイルピッカーでアクセス対処のファイルを指定する仕組みです。 API≧21で、ACTION_OPEN_DOCUMENT_TREEアクションが追加され、フォルダを指定するリクエストが構築できるようになりました。 今回は「SAFのACTION_OPEN_DOCUMENT_TREEでフォルダを指定」について、まとめます。 ※環境:Android Studio Narwhal Feature Drop | 2025.1.2 Patch 1     androidx.documentfile:documentfile:1.1.0 ...
スポンサーリンク