プレビュー再生機能をもったAndroid document pickerがほしい人生だった

8 min read読了の目安(約7200字

概要

Android 4.4で追加されてAndroid 11からごく限られたディレクトリ以外のファイルを読み書きできなくなった上にこれ以外の手段でファイルにアクセスするのが困難になったAndroid Storage Framework(以下SAF)だけど、まあ対応しないわけにもいかない。

java.ioのファイルAPIを使っていた時は、独自のファイルダイアログ的なActivityを実装して、ファイルリストを作ってそこでメディアのプレビュー再生が可能だったので、SAFとDocument Pickerに切り替えた後も当然同じことをやりたくなる。

既存のDocument Pickerライブラリを探す

既存のdocument picker応用としてのプレビュー再生できそうなやつ、あるいはその機能を追加できそうなやつがあるなら、それを使ったほうが手っ取り早い。自分で実装せずに済むのが一番だ。そういうわけで前例を探した。

https://github.com/Turtlebody/android-media-picker

一見それっぽいライブラリだ。早速ビルドして試しに使ってみた。実際に使ってみると…

サンプルアプリの起動画面 サンプルアプリのモード選択画面 ライブラリのMediaPickerActivity(Otherを押す前) SAFのSystem UI (Android-30 emulator)

この最後のやつはAndroidシステムに組み込まれているFilesアプリ(としてemulatorやGoogle Pixelで利用できるようになっているもの)と何も変わらない。これは実際のファイル選択の部分はどうやら既存のコンポーネントに丸投げしていて、そこから選ばれて返ってきたメディアファイルの読み書きが少し簡単になるというやつだった。つまり、このライブラリをいくらいじったところで、肝心のファイル選択の部分に機能追加できるわけではない。

何を実装すれば良いのか?

SAFがやっていることはファイルシステムを独自のドキュメントシステムで置き換えようというものだ。これは実際には複数のコンポーネントが関わってくる。SAFのドキュメンテーションのページの図を見てみよう。

SAFのcontrol flow
SAFを使うと、既存のPOSIXファイルシステムとは違うレベルで抽象化された仮想ファイルシステムのような「ドキュメント」のプロバイダーを実装できるのだけど、ファイルシステムのクライアントとしては、SAFは既存のファイルシステムで出来ることよりも限られている。

ディレクトリとファイルをリストアップしてユーザーに選択させるUIをアプリケーション側が作って提供することができなくなっているのだ。この図でいえば緑の部分がこれに該当する。SAFでは、ドキュメントシステムからドキュメントを選択したいユーザーが、独自の選択ダイアログを作ってユーザーに選択させることが不可能になっているのだ。

ドキュメントプロバイダのクライアントにできることは、所定のDocument Pickerを起動してその結果を受け取ることだけだ。SAFでは Intent.ACTION_OPEN_DOCUMENT を指定した IntentstartActivityForResult() 等を呼び出して、その結果から選択されたファイルならぬドキュメントをする流れになっている。このIntentで呼び出されるのはシステム上にあるFilesでも使われているファイル選択…ならぬドキュメント選択…のActivityだ。このIntentにputExtra()などで追加引数をいろいろ指定することも出来るのだけど、そんなにいろいろなことが出来るようになるわけではない。

packages/apps/DocumentsUI

このActivityはSystem UIなのでAOSPの一部になっていて、実装を覗き見ることができる。自分の調べた範囲でいえば、これはpackages/apps/DocumentsUIだ。

https://android.googlesource.com/platform/packages/apps/DocumentsUI/+/refs/heads/master

通常、何かしらのintentに対応して起動するActivityは、AndroidManifest.xml上に<intent-filter>要素でどのActionに反応するかを指定できる。このDocumentsUIのAndroidManifest.xmlには次のような記述がある。

<intent-filter  android:priority="100">
    <action  android:name="android.intent.action.OPEN_DOCUMENT"  />
    <category  android:name="android.intent.category.DEFAULT"  />
    <category  android:name="android.intent.category.OPENABLE"  />
    <data  android:mimeType="*/*"  />
</intent-filter>

PickActivityという<activity>の内容として宣言されていて、要するにこのActionに対応して起動しているdocument pickerはコレのはずだ。

DocumentsUIに代わるものを自作したい…したくない?

このDocumentsUIアプリで出来ることはかなり限られている。最初に書いた通り、今回やりたいことは「ファイルリスト上でのメディアのプレビュー再生」だ。実のところ静止画像はDocumentsContract.getDocumentThumbnail()などで取得できる。しかし音声や動画についてはこのSystem UIでは何もできない。ファイルリスト上でクリック/タップすると再生用のアプリを選択できる、という以上のことができない。自分のアプリから立ち上げたファイルリストの中で自分のアプリの再生専用Activityを立ち上げるような不毛な構造になってしまうし、その場でプレビューできなくなることによるUX低下は大きい。

AOSPからDocumentsUIのソースを持ってきて機能を追加すればよいのではないだろうか。ActionBarSherlockやThreeTenABPとやっていることは同じだ。AOSPは巨大な依存コンポーネントの集合体だが、アプリなので他のライブラリなどと比べたら相対的に独立性が高いはずだ。

DocumentsUIはどうやってDocument Pickerとして起動しているのだろう? 理屈としては難しくないはずだ。あるIntentに反応できるアプリケーションは、システム上の全てのアプリケーションに含まれる全ての<activity>から、要求されたIntentのactionとして指定されたものにマッチする<intent-filter>があれば、それが対象になる。OPEN_DOCUMENTを対象とするなら、こんな感じになるだろう(実際DocumentsUIのAndroidManifest.xmlをもとにしている)。

<intent-filter>
 <action android:name="android.intent.action.OPEN_DOCUMENT" />  
 <category android:name="android.intent.category.DEFAULT" />  
 <category android:name="android.intent.category.OPENABLE" />  
 <data android:mimeType="*/*" />  
</intent-filter>

システム専用パーミッションの壁

しかしこれを自分のActivityに設定してアプリを実行しても、Intent.ACTION_OPEN_DOCUMENTに対して自分のアプリが選択肢として上がってこない。何かパーミッションが足りないのだろうか。DocumentsUIがどんなパーミッションを要求しているか見てみよう。

<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />  
<uses-permission android:name="android.permission.REMOVE_TASKS" />  
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>  
<uses-permission android:name="android.permission.WAKE_LOCK" />  
<uses-permission android:name="android.permission.CACHE_CONTENT" />  
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />  
<uses-permission android:name="android.permission.CHANGE_OVERLAY_PACKAGES" />  
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />  
<uses-permission android:name="android.permission.MODIFY_QUIET_MODE" />  
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />  
  
<!-- Permissions required for reading and logging compat changes -->  
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>  
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>

(先に小さい問題を解決しておくと、Android 11から他のパッケージに含まれるProviderなどの情報をクエリするアプリケーションでは、<queries>という要素を指定しなければならなくなった。これが無いと他のアプリに含まれるDocument Providerをクエリすることもできないはずなので、自分のアプリにも追加しておく必要がある。)

一番上にあるMANAGE_DOCUMENTSというのが名前からして露骨に怪しい。

冒頭に挙げたSAFのドキュメンテーションの最初のページでは、MANAGE_DOCUMENTSはこう説明されている。

The MANAGE_DOCUMENTS permission. By default a provider is available to everyone. Adding this permission restricts your provider to the system. This restriction is important for security.

デフォルトではこのパーミッションは全ユーザーに与えられていて、このパーミッションを追加するとそのProviderはシステムでしか使えなくなる…一見よくわからないけど、それなら自分がProviderを作る時に指定しなければよいだけではないか…とも読める。

しかしAPIリファレンスを読むと全然違う雰囲気の説明が書かれている:

Allows an application to manage access to documents, usually as part of a document picker.

This permission should only be requested by the platform document management app. This permission cannot be granted to third-party apps.

このパーミッションはドキュメント管理アプリからのみリクエストされるべきであり、サードパーティアプリには与えられない…!?

https://cs.android.com/"MANAGE_DOCUMENTS"を検索すると、DocumentsProviderのソースが引っかかる。引っかかるのはAPIリファレンスにもなっている部分なのだけど、そこにはこう書いてある:

When defining your provider, you must protect it with [android.Manifest.permission#MANAGE_DOCUMENTS](https://developer.android.com/reference/kotlin/android/Manifest.permission.html#MANAGE_DOCUMENTS:kotlin.String), which is a permission only the system can obtain. Applications cannot use a documents provider directly; they must go through [Intent#ACTION_OPEN_DOCUMENT](https://developer.android.com/reference/kotlin/android/content/Intent.html#ACTION_OPEN_DOCUMENT:kotlin.String) or [Intent#ACTION_CREATE_DOCUMENT](https://developer.android.com/reference/kotlin/android/content/Intent.html#ACTION_CREATE_DOCUMENT:kotlin.String) which requires a user to actively navigate and select documents. When a user selects documents through that UI, the system issues narrow URI permission grants to the requesting application.

「アプリケーションではdocuments providerを直接使うことができない。代わりにIntent.ACTION_OPEN_DOCUMENTIntent.ACTION_CREATE_DOCUMENTを使う」と明記されている。

つまりカスタムDocumentsUIを作ることはAndroidシステム上認められないというわけだ。

「できません」っていう結論が出たわけだけど、なぜ出来ないのかを筋道立てて説明することはおよそできたと思う。