きっとラボ

プログラミングや自然科学などの話題がメイン

Android 7.0 (API 24) への対応 ~(外部)ストレージ内ファイルの共有~

今年最初の技術記事は、開発するアプリを「Android N (API 24) 」へ対応させる過程で苦労したことについて書こうと思います。このバージョンで発生したAndroid側の仕様変更の把握を正確にしていかないと既存のアプリが最新バージョンで安定した動作をできなくなります。弊記事の内容の場合、ビルドのターゲットAPIレベルを「24」としたときに問題が発生します。今回、そのあたりでハマって時間を取られるこを防ぐためにも、備忘録を兼ねて共有していきます。

単純に仕様変更点について知りたいだけなら以下を見ていただければすぐに把握できると思いますが、恐らくそれだけだと対応する際ちょっとした問題にハマる可能性があります。(それとも、そんなのは未熟な私だけですかね?)

Android 7.0 の動作の変更点 | Android Developers

FileProvider | Android Developers

 

追記:基本的にはGoogleの技術ブログに載っていた方法を参考にしています。URLは......忘れました _(┐「ε:)_

概要

Android 7.0 は Android N とも呼ばれ、Android の現在の最新バージョンです。ちなみに「N」とはコードネーム「Nougat(ヌガー) 」のことです。砂糖菓子です。

このバージョンになってから、ファイルシステム関連のアプリの権限が一段と厳しい仕様となりました。ドキュメントを読んでいくと、その仕様が技術者にとっては全然甘くないことがわかります。弊記事では、このあたりに絞ってまとめていき、どう対処すればいいのか、まとめていきたいと思います。

 

ファイル共有に関する変更点について

最新のAPIでは、他のアプリとファイルを共有する場合コンテンツプロバイダを介さなければなりません。プライベートファイルのセキュリティを強化するための変更だそうです。では、どのような変更になったのでしょうか。

file://URI の禁止

これまで、URIを使用するときは file://URI を使って処理を実装していました。他のアプリに共有するときなんかはこれをIntentにのせて送信していたわけです。

しかし、これからはこの方法が使えなくなります。

これを使って他のアプリにURIを渡そうとすると FileUriExposedException がトリガーされます。権限を超えてファイルにアクセスしようとしています~と言うような。

なぜそうなるかというと、「URIを受け取る側がそのパスにアクセスする権限を持たない可能性」があるからです。「越権行為は良くない」ということですね!

これからは content://URI

では、どうするか?結論はコンテンツプロバイダのスキーム content:// を使用するということです。実際には ContentProvider のサブクラスである FileProvider を使用します。

今までは Uri.fromFile(File) で取得できるURIを使用していれば良かったのですが、これからは FileProvider.getUriForFile(Context, String, File) でURIを取得します。これにより、当該URIへの一時的なアクセス権限を付与できます。

こうすることにより、共有をされる側のアプリからもそのファイルにアクセスできるわけです。

 

実装するにはどうするのか

ドキュメント側の記述をよく確認せず実装を試みたことにより、悩みました。

FileProvider を使用してファイルを他のアプリへ提供する場合にはいくつかやらなければならない手順があります。とりあえず、内部ストレージ(getFilesDir()で取得できるディレクトリ内の files/ 以下)のファイルを共有する場合を考えます。

1.FileProvider で共有するファイルのディレクトリを事前に指定

事前に指定されたディレクトリを共有することができます。ですから、まず共有するファイルのディレクトリを指定する必要があります。そのための xmlファイルを作成します。

nameには共有するFileProviderの名前を指定します。好きな名前を指定すれば良いと思います。

ここで使えるタグは以下の3種のみです!それぞれで、設定した path を指定できます。

files-path

これは Context.getFilesDir() で取得できる値 (data/data/{applicationId}/files) を指定する場合に使用します。

cache-path

これは getCacheDir() で取得できる値 (data/data/{applicationId}/cache) を指定する場合に使用します。

external-path

これは Environment.getExternalStorageDirectory() で取得できる値 () を指定する場合に使用します。

生成されるURIはどうなっている?

例えば上記の設定の場合、アプリのドメインが com.kit_lab.android とでも仮定すると

URI

content://com.kit_lab.android.fileprovider/share_name/{ファイル名}

物理パスは

com.kit_lab.android/files/directory_name/{ファイル名}

となります!

2.AndroidManifest に FileProviderタグを追加

まず、アプリケーションタグの中に FileProvider の定義を追加します。

内容について説明していきます。

android:authorities属性

ここで、アプリのドメインを元にしたURI権限を設定しています。この値はユニークな値であることが望ましいため、大抵は「{アプリのドメイン}.fileprovider」の様な形を設定していますし、Googleの技術ブログでもその様に紹介されていました。

android:exported属性

FileProvider の公開設定です。これは false としておきます。これをパブリックにする必要はないとのこと。

android:grantUriPermissions属性

ファイルへの一時的なアクセス権限を許可するために true を設定します。

meta-data

ここには、前述の xmlファイルを指定します。

3.FileProviderを使用してURIの取得する

ここでようやくURIの取得です。今までは

としていました。しかしこれからは

こうします。なお、先程説明した ContentProvider に指定できるパスの設定時のタグ3種ですが、あそこで指定できるパス以外や存在しないパスでURIを取得しようとすると、

java.lang.IllegalArgumentException: Failed to find configured root that contains {問題のパス}

という様な例外がトリガーされます。

getUriForFile の第二引数には先のマニフェストで設定した「android:authorities」の値を指定します。

 

ここに潜む罠

さて、以上の方法で得たURIインテントで送信すれば、無事ファイルの共有ができるのですが、SDカード内のファイルを使いたい場合はどうするのでしょうか?

FileProvider に指定するパスの設定時のタグは前述のドキュメントのリンクより、

external-files-path

を指定すれば良さそうです。しかし、これが実際には使えないのです!

 

qiita.com

では、SDカード内のファイルのURIを取得したくなったらどうすれば良いのでしょうか。

 

SDカード内のファイルのURIを取得するには

この場合、キャッシュを作成して共有させることにより、例外を発生させることなくファイルを共有できました。

 

最後に

今回は Android 7.0 での仕様変更の内、ファイルアクセスに関する実装方法について、紹介しましたが、この他にも多くの変更点があり、それにより既存のアプリが影響を受ける可能背が大きいとのことですので、まだちゃんと確認していないという方は一度目を通されたほうが良いです。

また、SDカード内のファイルURI 取得については、使えるディレクトリにキャッシュという方法で対応可能なのがわかりましたが、もっとマシな方法や適切な方法をご存じの方がいらっしゃったら教えていただけたら嬉しいです!

記事に対する意見や質問等もお待ちしております!

とりあえず、これでファイルをSNSやメールで共有できそうな感じです。

 

では次回投稿をお楽しみに!

 

 

 

改定:04/02/2017