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
目次
ACTION_OPEN_DOCUMENT_TREE
Storage Access Framework(SAF)にAPI≧21で「ACTION_OPEN_DOCUMENT_TREE」というアクションが追加されました。
アクション(Intent.*) | リクエスト | コメント |
---|---|---|
ACTION_CREATE_DOCUMENT | 新しくファイルを作成 | 書き込み可能(新規作成) ファイル名の重複は番号を付けて回避 例:sample(1).txt |
ACTION_OPEN_DOCUMENT | 既存のファイルを開く | 書き込み可能(上書き) 読み出し可能 |
ACTION_OPEN_DOCUMENT_TREE (API≧21) | フォルダを開く | フォルダ全体のアクセス権を持つ |
このアクションを使うと、ファイルピッカーでアクセス対処のフォルダを指定できます。
指定したフォルダ全体に、アプリのアクセス許可が付与されます。ですので、外部ストレージ上のファイルであっても、パーミッションの取得に関係なく、複数のファイルをまとめて処理できます。
フォルダ指定の手順は他のアクションと同じです。
※詳細は「外部ストレージへStorage Access Frameworkでアクセス」を参照。
フォルダ指定の例
フォルダ指定の具体的な例です。※Activity Result APIで記述しています。
コールバックの定義と登録
registerForActivityResult(SafPicker)はコールバックの定義と登録を行います。
ラムダ式がコールバックです。コールバックは2段構成にしています。
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段目コールバックでフォルダに対する処理を行います。
そして、このコールバックを利用するコンポーネントのランチャー(_safWriteTreePicker : ActivityResultLauncher)を返します。
val _safWriteTreePicker = SafPicker { uri -> val _docFile = DocumentFile.fromTreeUri(this@TesSafActivity, uri) /* フォルダに対する処理 ~ここから~ */ // ↓↓ サンプル:5つのファイルをフォルダ内に作成 ↓↓ for(i in 1..3) { val _data = "${_textData} (${System.currentTimeMillis()})".toByteArray() val _dFile = _docFile.createFile("text/plain", "text${i}.txt") writeToUri_Resolver(_data, _dFile.uri) } /* ~ここまで~ */ }
registerForActivityResult(SafPicker)は、LifecycleOwnerのcurrentStateが「STARTED」より前のタイミングで実行して下さい。Activityのライフサイクルと密接に関係しています。
※LifecycleOwnerについては「ライフサイクル対応コンポーネント作成」を参照
ピッカーの起動
Clientアプリを起動(launch)し、ピッカーを表示させます。
launchの引数はアクセスのリクエスト(Intent)です。フォルダ指定を行いたいのでIntent.ACTION_OPEN_DOCUMENT_TREEアクションを使って、リクエストを構築しています。
_safWriteTreePicker.launch(TreeOpenIntent())
fun TreeOpenIntent() = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
フォルダに対する処理
ピッカーが表示されて、ユーザにフォルダの指定を求めます。
図(エミュレータ―で実行、API33)は、外部ストレージへ「UserDir(フォルダ)」を作り、そのフォルダを指定する例です。
指定されたURIはフォルダを表します。そして、このフォルダ全体(階下の子フォルダやファイル)に対して、このアプリによるアクセスが可能です。
フォルダに対する処理を簡単にするために、UriをDocumentFileに置き換えています。
※DocumentFileクラスの詳細は後述します。
val _docFile = DocumentFile.fromTreeUri(this@Activity, uri) // DocumentFileへ置き換え /* フォルダに対する処理 ~ここから~ */ // ↓↓ サンプル:3つのファイルをフォルダ内に作成 ↓↓ for(i in 1..3) { val _data = "${_textData} (${System.currentTimeMillis()})".toByteArray() val _dFile = _docFile.createFile("text/plain", "text${i}.txt") writeToUri_Resolver(_data, _dFile.uri) Log.i(TAG, "${_dFile.name} ${_dFile.isDirectory} ${_dFile.isFile} ${_dFile.uri}") } /* ~ここまで~ */
DocumentFileを通して得られた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_a163 media_rw 4096 2025-08-28 00:33 Alarms
drwxrws--x 5 media_rw media_rw 4096 2025-08-28 00:32 Android
drwxrws--- 2 u0_a163 media_rw 4096 2025-08-28 00:33 Audiobooks
drwxrws--- 2 u0_a163 media_rw 4096 2025-08-28 00:33 DCIM
...
drwxrws--- 2 u0_a163 media_rw 4096 2025-08-28 00:33 Recordings
drwxrws--- 2 u0_a163 media_rw 4096 2025-08-28 00:33 Ringtones
drwxrws--- 2 u0_a163 media_rw 4096 2025-08-28 00:47 UserDir
-rw-rw---- 1 u0_a163 media_rw 30 2025-08-28 00:47 UserDir/text1.txt
-rw-rw---- 1 u0_a163 media_rw 30 2025-08-28 00:47 UserDir/text2.txt
-rw-rw---- 1 u0_a163 media_rw 30 2025-08-28 00:47 UserDir/text3.txt
※エミュレータ(API 33)で実行
DocumentFileクラス
DocumentFileはFileクラスの機能をエミュレートしたユーティリティクラスです。
利用に際して環境設定が必要です。
dependencies { ... implementation "androidx.documentfile:documentfile:1.1.0" ... }
Fileと同じ操作感でフォルダを扱えるようになります。
ただし、ファイルの読み書きはDocumentFileからUriを取り出して、Uriを使って行います。
以下は、フォルダ内のファイルを読み出す例です。
val _docFile = DocumentFile.fromTreeUri(this@Activity, uri) // DocumentFileへ置き換え /* フォルダに対する処理 ~ここから~ */ // ↓↓ サンプル:フォルダ内の全ファイルを読み出し ↓↓ val _list = _docFile.listFiles() _list.forEach { val _data = readFromUri_Resolver(it.uri) Log.i(TAG, "Data = ${_data.decodeToString()}") } /* ~ここまで~ */
Data = Hello World !! (1756342025161) Data = Hello World !! (1756342025253) Data = Hello World !! (1756342025310)
DocumentFileはオーバヘッドの大きい(処理が遅い)クラスです。
ですので、多数のファイルを一括で処理する用途に向きません。その場合は、DocumentsContractのメソッドと定数を使って処理することが推奨されています。
指定が出来ないフォルダ(API≧30)
外部ストレージ(セカンダリも含む)に、ACTION_OPEN_DOCUMENT_TREEアクションを使って、指定が出来ないフォルダ(A,B,C)があります。
理由は不明です。ドキュメントに理由の説明が見つかりません。
ただし、フォルダの作成は可能です。そして、作成したフォルダ(a,b)は指定が出来ます。
関連記事: