苦しんだScopedStorageについて

この記事は、FOLIO Advent Calendar 2019の22日目の記事です。

11月に北海道を飛び出しFOLIOに入社してからはや2ヶ月が経とうとしています。この2ヶ月は本当にあっという間でしたが、北海道で経験できることの半年分を経験できているような気がします!

特にGooglePlayベストオブ2019の自己改善部門で大賞をとったことは特に衝撃的でした🎉🎉🎉(11月入社なので受賞に関しては何もコミットしていないことには目を瞑りつつ…)

そんな2ヶ月だったのですが、11月の後半は、Scoped Storage対応に苦しめられ続けていました… 

Scoped Storageに対応しなければいけない箇所が結構あったことと、そもそもFOLIOのコード自体もあまり理解できていない状態だったので、ものすごく時間がかかってしまいました…

なので、今回はその知見を記事として残しておきたいと思います。

Scoped Storageについて

Android 10からは、外部ストレージに関する特別なアクセス権限が自動的に付与され、アプリに割り当てられるストレージに対して読み込み/書き込みする場合は、権限が不要になりました。

また、メディアファイルにアクセスする場合は、MediaStore経由でアクセスすることが必要となり、ストレージ内への直接のパスは取得することができなくなっています。

ダウンロードディレクトリに関しては、ストレージアクセスフレームワークを使用する必要があります。

スクリーンショット 2019-12-18 21.16.01

行った対応もアプリ外のストレージに画像を保存する対応だったので、本記事ではアプリ外の画像へアクセスする場合について、まとめていきたいと思います。

画像の読み込みについて

画像を読み込む場合は、READ_EXTERNAL_STORAGE権限と、MediaStore + ContentResolver経由でURIを取得する必要があります。

MediaStoreから画像が保存されているストレージのURIを取得し、そのURIを使用しContentResolverのquery()を実行し、対象のメディアディレクトリ内にあるファイル一覧から、特定のファイルのURIを取得します。

MediaStore.Images.Media._IDで、画像のIDを取得し、そのIDとContentUris.withAppendedId()を使用して、画像のURIを取得します。

ContentResolverからInputStreamを取得し、そのInputStreamをBitmapFactoryでデコードすることでBitmapを取得することができます。Android 10の場合は、ImageDecoderも使用することができます。

BitmapOptionなども使用しながら画像を読み込みたい場合は、ContentResolverからFileDescriptorを取得し、BitmapFactory.decodeFileDescriptor()を使用することで書き込むことができます。

ファイルのサイズを取得する場合は、読み込み時のProjectionにMediaStore.Image.Media.Sizeを追加することで取得することができます。ですが、書き込み直後に取得した場合は、システムへのファイルサイズの反映が終わっておらず、正しい値を取得できない場合があります。そのため、書き込み後にファイルサイズを取得する場合は、FileDescriptorを取得し、statSizeを取得することをおすすめします。

アプリ外のストレージへの画像の書き込み

画像を書き込む場合は、WRITE_EXTERNAL_STORAGE権限と、ContentResolver + MediaStoreを使用する必要があります。

まずは、書き込むファイルの情報をContentValuesに設定していきます。MediaStoreから画像が保存されているストレージのURIを取得し、ContentResolver.insert()を呼び出すことで、書き込み用のURIを取得することができます。

ContentValuesにIS_PENDINGをtrueにし、システムにまだ画像を挿入中であるということを知らせることで、処理を行っているアプリ以外からのアクセスが拒否される状態にすることができます。画像を書き込んだあとに、IS_PENDINGをfalseに更新することで、他のアプリからもアクセスできるようになります。

取得したURIへ実際に書き込みを行うには、ContentResolverからOutputStreamを取得します。

書き込み用のURIを取得したタイミングで、システムには画像を書き込んだ状態であると認識されるため、IS_PENDINGを設定しない状態で、ギャラリーなどから見ると、すでに画像が認識された状態になります。ですが、画像の書き込みを行っていない場合は、実体がないので、なにも画像を読み込みません。そのまま書き込みを行わないと、永遠に読み込むことができない画像がギャラリーに表示され続けてしまうので注意が必要です。

その他の注意点について

Scoped Storageについては、ManifestファイルでrequestLegacyExternalStorageをtrueにして追加することで、オプトアウトすることができます。

一度オプトアウトすると、アプリをアンインストールするまでScoped Storageが有効にならないので、注意が必要です。また、Android 11では、Scoped Storage対応が必須になるため、一時的にオプトアウトしたとしても、本対応は必ず行っておくようにしましょう。

まとめ

Android 10から追加された仕組みなため、随所でバージョン分けをする必要があり、ちょっとめんどくさいです。パスを直接取得しているアプリの場合は、そこそこ修正する必要があるため、なるべく早めの対応をおすすめしておきます。

また、試行錯誤しながら対応を行ったため、もっといいやり方がある場合は、ぜひ教えていただけると大変助かります🙏

参考にしたWebページ

https://proandroiddev.com/working-with-scoped-storage-8a7e7cafea3

この記事が気に入ったらサポートをしてみませんか?