SAFのACTION_OPEN_DOCUMENT_TREEでフォルダを指定

投稿日:  更新日:

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段構成にしています。

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段目コールバックでフォルダに対する処理を行います。

そして、このコールバックを利用するコンポーネントのランチャー(_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)があります。

理由は不明です。ドキュメントに理由の説明が見つかりません。

指定が出来ないフォルダ

指定が出来ないフォルダABC

ただし、フォルダの作成は可能です。そして、作成したフォルダ(a,b)は指定が出来ます。

スポンサーリンク

関連記事:

ストレージのリソースは、内部メモリー、外部メモリー、クラウドの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)は、ファイルピッカーでアクセス対処のファイルを指定する仕組みです。 SAFはアンドロイドシステムに実装されています。SAFを用いれば、ピッカーを自作する必要はありません。 アプリ開発が楽になります。 しかも、指定された外部ストレージ上のファイルは、アプリに対するアクセス許可を付与したものになります。 改めて、許可の取得は不要です。そのまま、ファイルを読み書き出来ます。 今回は「外部ストレージへStorage Access Frameworkでアクセス」を、まとめます。 ※環境:Android Studio Narwhal Feature Drop | 2025.1.2 Patch 1 ...
スポンサーリンク