Android - FileProvider で外部アプリとファイルを共有する
FileProvider とは
Android7.0(API24, Android N)から、ファイルシステムに大きな変更が加わり、権限の仕様がより厳しいものへと変更されました。
API24 以降向けのアプリにおいて、プライベートディレクトリにアクセス制限が加わり、外部から存在、サイズ、メタデータなどの漏洩を防ぐことができます。
この権限の変更により以下のような副作用があります。
- プライベートファイルの所有者は
MODE_WORLD_READABLE
およびMODE_WORLD_WRITABLE
を使用したパーミッションの緩和ができず、実行しようとするとSecurityException
が発生する - 開発しているアプリのパッケージドメイン以外の
file://
URIを渡すと、受け取り手がアクセスできないパスとなるため、 外部のアプリとのプライベートなファイルの共有には FileProvider の使用が推奨される
このように Android7.0 以降向けのアプリでは、Androidフレームワークによって自身のアプリ以外への file://
URIの公開ができず、 content://
URIへ変換し一時的なパーミッションを付与した上で URI をやりとりする必要があります。
ファイルに対してパーミッションを付与したり、 file://
から content://
へURIへ変換する最も簡単な方法は FileProvider クラスを使用することです。
FileProvider を用いてアプリ間のファイル共有を実現する
繰り返しとなりますが、自身のアプリから別のアプリにファイルを安全に共有するには、 content://
URIの形式でファイルをハンドルできるようにアプリを構成する必要があります。
Android フレームワークの FileProvider コンポーネントは、XML で指定した仕様に基づいてファイルのコンテンツ URI を生成します。具体的な手順を以下で説明していきます。
FileProvider の使用を AndroidManifest で宣言する
まず、 AndroidManifest.xml
にエントリーを追加します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.mobileapp"> <application ...> <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.provider" android:grantUriPermissions="true" android:exported="false"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider" /> </provider> ... </application> </manifest>
指定している各属性について、説明します。
android:authorities 属性
アプリのパッケージドメインをプレフィックスとした命名でURIの権限を設定します。ユニークである必要があるため、 ${applicationId}.provider
などの名前が良いと思います。
android:exported 属性
FileProvider は公開する必要がないため false
とします。
android:grantUriPermissions 属性
外部からのファイルへのアクセスを一時的に許可できるようにします。今回は、外のアプリとプライベートファイルを共有したいため、 true
とします。
共有するディレクトリを宣言する
共有するファイルを配置するディレクトリを指定します。 res/xml
以下に file_provider.xml
ファイルを作成し、以下の内容で記述します。
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="photos" path="photos/"/> </paths>
この例では、 <files-path>
タグでアプリの内部ストレージである files/
ディレクトリ以下のサブディレクトリを共有するようにしてます。このサブディレクトリのパスは、 Context.filesDir
で取得できます。この他にも、以下のようなタグが指定可能です。
<cache-path>
- 内部ストレージのキャッシュを共有でき、パスは
Context.cacheDir
で取得できる
- 内部ストレージのキャッシュを共有でき、パスは
<external-path>
- 外部ストレージのルートにあるファイルを共有でき、ルートパスは
Environment.getExternalStorageDirectory()
で取得できる
- 外部ストレージのルートにあるファイルを共有でき、ルートパスは
<external-files-path>
- 外部ストレージのルートにあるディレクトリを共有でき、パスは
Context.getExternalFilesDir()
で取得できる
- 外部ストレージのルートにあるディレクトリを共有でき、パスは
<external-cache-path>
- 外部ストレージにあるキャッシュを共有でき、パスは
Context.externalCacheDir
で取得できる
- 外部ストレージにあるキャッシュを共有でき、パスは
<external-media-path>
- 外部メディアにあるディレクトリを共有でき、パスは
Context.externalMediaDirs
で取得できる
- 外部メディアにあるディレクトリを共有でき、パスは
また、タグに含まれる属性については説明します。
name="name"
URIパスのセグメント。この値は、生成されるURIのパスに含まれるものです。
path="path"
共有するサブディレクトリ。値はサブディレクトリ名であり、個々のファイル名ではないことに注意します。ファイル名で単一のファイルを共有したり、ワイルドカードを使用して指定することもできません。
生成されるURIを見てみる
以下のように、複数のパスを指定することもできます。下記の定義の場合、生成されるURIと実態のあるコンテンツのパスを見てみます。(Providerの指定は冒頭でAndroidManifestファイルで定義したものを想定)
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="my_images" path="images/"/> <files-path name="my_docs" path="docs/"/> </paths>
共有のコンテンツのURIは、
content://com.example.mobileapp.provider/my_images/example.jpg
content://com.example.mobileapp.provider/my_docs/example.pdf
コンテンツの実態のパスは、
com.example.mobileapp/files/images/example.jpg
com.example.mobileapp/files/docs/example.pdf
となります。
ファイルからコンテンツURIを生成する
コンテンツURIを使用してファイルを他のアプリと共有する場合は、FileProvider を使用して URI を生成する必要があります。具体的には以下のステップを踏みます。
具体的なコードは以下のようになります。
val captureFile = this.createOutputFile() val contentUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".provider", captureFile)
getUriForFile(Context context, String authority, File file)
によって、共有コンテンツURIを生成してくれます。
例えば、画像ファイルを共有したい場合は createOutPutFile()
の中身は以下のようになります。
fun createOutputFile(): File { val timeStamp = DateFormat.format("yyyyMMdd_HHmmss", Date()).toString() val tempFile = File( this.activity.filesDir, "/my_images/$timeStamp.jpg") if (!tempFile.exists()) { try { tempFile.parentFile.mkdirs() tempFile.createNewFile() } catch (e: IOException) { e.printStackTrace() } } return tempFile }
ここで注意したいのは、File はインスタンス化した後、実態としてストレージに空の状態で一時保存することです。
そのためにこの関数内では /my_images
ディレクトリの存在を確認し、なければ親ディレクトリとして生成し、その配下にタイムスタンプをファイル名として tempFile.createNewFile()
しています。
外部アプリへコンテンツURIを渡す
今回は、前項で生成した画像形式の一時ファイルをカメラアプリへ共有してみます。具体的なコードは以下です。
val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) this.cameraContentUri = createOutputUri() cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, this.cameraContentUri) cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) this.activity.startActivityForResult(cameraIntent, REQUEST_CODE)
重要なのは、外部アプリに対して共有コンテンツへの一時的なアクセス許可を与えてあげることです。今回は書き込み権限を与えるために Intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
を指定しています。
以上が、FileProvider を用いたアプリ間のファイル共有の手順です。