DocumentsProviderについて調べた

# 概要

この記事は、AndroidのDocumentsProviderについての概要をまとめた内容です。

SMB/CIFSの共有フォルダにDocumentsProviderを介してアクセスするためのアプリ、「[CIFS Documents Provider](https://play.google.com/store/apps/details?id=com.wa2c.android.cifsdocumentsprovider)」を開発するに当たり、調べた内容になります。

# Storage Access Framework とは

Androidには、Android4.4より導入された[Storage Access Framework (SAF)](https://developer.android.com/guide/topics/providers/document-provider)という仕組みがあります。SAFは、ストレージサービスに対する共通のインターフェースを提供する仕組みです。このSAFを介することで、クライアント(アプリ)はローカルでもリモートでも共通のインターフェースを使って、ファイルにアクセスすることができるようになります。

# DocumentsProviderとは

DocumentsProviderとは、SAFにおいてファイルを提供する側です。ドキュメントでは以下のように書かれています。

> サービスをカプセル化する DocumentsProvider を実装することで、クラウドやローカル ストレージ サービスをエコシステムに参加させることができます。プロバイダのドキュメントへのアクセスが必要なクライアント アプリは、数行のコードだけで SAF と統合できます。

DocumentsProviderを実装することでストレージサービスのアクセスを抽象化し、SAFの仕組みを使って同一のコードでディレクトリやファイルが扱えるようになります。これによってユーザがサービスの仕組みを意識せず、透過的にファイルを操作することができるようになります。
例えば、[Google Drive](https://play.google.com/store/apps/details?id=com.google.android.apps.docs)や[DropBox](https://play.google.com/store/apps/details?id=com.dropbox.android)といったストレージサービスのアプリをインストールすると、SAFを介して各サービスに保存されたファイルをクライアントから操作できるようになります。これは、各アプリがファイルにアクセスするためのDocumentsProviderを実装しているためです。

# クラスの概要

SAFを扱う上でよく登場するのが次のようなクラスです。

## [DocumentFile](https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile?hl=ja)

DocumentFileは、SAFのクライアント側で扱われるファイルやディレクトリの抽象クラスとなります。ファイルやディレクトリとして扱うためのいくつかのプロパティ(名前やサイズ、更新日時など)やメソッドが用意されています。実装自体は、JavaのFileクラス(java.io.File)によく似ており、同じような使い方ができます(このDocumentFileは、Fileクラスからインスタンスを作成することもできますが、クラスやインターフェースを継承しているわけではありません)。
ファイルやディレクトリは、DocumentsProviderより発行されるURIによって指定します。URIにアクセスするためには、事前にDocumentsProvierからアクセス権限を取得しておく必要があります。


## [DocumentsProvider](https://developer.android.com/reference/android/provider/DocumentsProvider)

DocumentsProviderは名の通り、DocumentsProviderを定義するクラスです。DocumentsProviderは、クライアント側がDocumentFileで扱えるファイルの情報を返したり、DocumentProvider自身の情報を返したり、発行するURIとファイルのマッピングを行ったりします。

ちなみに、DocumentsProviderは従来から存在したContentProviderを継承しており、ContentProviderと同様にクエリを使う事もできるようです(使った事ないですが)。
余談ですが、Android 10以降では、Android端末のローカルコンテンツを管理するMediaStoreにDocumentsProvider用のカラムが追加されているため、MediaStoreのコンテンツURIから直接DocumentFileとして扱うことが可能となっています。

### 標準で備わっているDocumentsProvider

Androidには、標準でいくつかのDocumentsProviderが備わっています。これらは、内蔵のストレージやSDカードにアクセスする際などに利用されます。次のようなDocumentsProviderが提供されています。(他にもあるかもしれないですが、よく知らないです。)

<dt>内蔵ストレージ/SDカード ( ExternalStorageProvider ) </dt>
<dd>端末の内蔵ストレージ、SDカードなどの端末のストレージへのアクセスを提供します。</dd>
<dt>メディアデータ ( MediaDocumentsProvider )</dt>
<dd>端末に保存されたメディアファイルを管理するMediaStore上で扱うファイルへのアクセスを提供します。</dd>
<dt>ダウンロードデータ ( DownloadStorageProvider )</dt>
<dd>端末でダウンロードされたファイルへのアクセスを提供します。</dd>

# ピッカー

クライアントがドキュメントプロバイダからファイルのアクセス権限を獲得し、DocumentFileの情報を取得するためには、SAFのピッカーを用いて、ユーザにファイルを選択させる必要があります。
Androidアプリで外部ファイルに対する操作を行う際、選択画面(ピッカー)を目にしたことがあるかと思いますが、これらはSAFの仕組みで提供されています。ピッカーでは、アプリから利用可能なDocumentsProviderと、それぞれが管理するファイルの一覧を表示し、ユーザはそこから目的のファイルやディレクトリを選択します。ピッカーでDocumentsProviderを選択する画面は次のようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/84092157-2e52-7763-a177-0044313f94ca.png" width="400px" />

また、DocumentsProviderのピッカーには、3種類(ファイル読込み、ファイル書込み、ディレクトリ選択)があります。

|ファイル読込み|ファイル書込み|ディレクトリ選択|
|:-:|:-:|:-:|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/f8994e8a-d9ae-ea15-86dc-0444a4c40679.png" width="200px" />|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/ece59fbb-bd7e-1f1c-2847-eaf1e5f92c21.png" width="200px" />|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78503/0d71e951-d8f8-2923-e46a-21b04adc7f83.png" width="200px" />|

クライアントアプリのActivityで、各々のピッカーで選択したファイル・ディレクトリのDocumentFileを作成する実装コードは次のようになります。現在では、従来のonActivityResultではなく、activity-ktxやfragment-ktxに含まれるregisterForActivityResultを用いて選択結果を受け取る実装が推奨されます。

```kotlin:ファイル読込み
    val openLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
        uri ?: return@registerForActivityResult
        // 権限の永続化
        contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        // その他、UR
        val documentFile = DocumentFile.fromSingleUri(this, uri)
        // 以降、ファイル読込み処理
    }
    // 呼び出しコード例: openLauncher.launch(arrayOf("*/*"))
```

```kotlin:ファイル書込み
    val createLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
        uri ?: return@registerForActivityResult
        // 権限の永続化
        contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        // DocumentFile作成
        val documentFile = DocumentFile.fromSingleUri(this, uri)
        // 以降、ファイル書込み処理
    }
    // 呼び出しコード例: createLauncher.launch("*/*")
```

```kotlin:ディレクトリ選択
    val treeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
        uri ?: return@registerForActivityResult
        // 権限の永続化
        contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        // DocumentFile作成
        val documentFile = DocumentFile.fromTreeUri(this, uri)
        // 以降、ディレクトリ処理
    }
    // 呼び出しコード例: treeLauncher.launch(null)
```

選択したファイルやディレクトリの永続的な権限が必要な場合は、戻ってきたURIに対して、takePersistableUriPermission を実行する必要があります。

ただし、DocumentsProviderがこれらをすべてサポートしているとは限りません。例えば、Google DriveのDocumentsProviderはディレクトリの選択ができません。

# さいごに

ここでは、Storage Access Frameworkの仕組みと、簡単な概要について紹介しました。これを用いる事で、様々なストレージサービスを同じ操作で扱えるようになることが分かります。近年のAndroidは外部ストレージへのアクセスが厳しくなっており、今後アプリが共通のストレージにアクセスする場合は、SAFを用いる必要があるため、開発者はこれらの情報を認識しておく必要があるかと思います。

さらに、ここで調べた知識を踏まえた上で、[CIFS Documents Provider](https://play.google.com/store/apps/details?id=com.wa2c.android.cifsdocumentsprovider)というアプリを作成したのですが。それについてはまた別の機会に書きます。