22時に寝ようと思って2時に寝る。

備忘録や日記を書いてます。きょうは早く寝よう。

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 を生成する必要があります。具体的には以下のステップを踏みます。

  1. 新しいファイルを生成する
  2. そのファイルを FileProvider.getUriForFile() に渡す
  3. 返された 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 を用いたアプリ間のファイル共有の手順です。

参考文献