はじめに
見て見ぬふりをしてきたScoped Storageにハマったのでメモ書きを残す。
Android Q (10) から導入されたScoped Storage。「端末内部のfileやらのアクセス権限周りや保存領域が変わったんだろうなー」くらいの雑な理解で見て見ぬふりを続けてきたが思わぬところでハマってしまった。
Glideで画像を表示できなくなった
以前は動いていたコードが動かなくなった。具体的にはContentResolver経由で MediaStore.Video.Media.DATA
のStringを取ってきて、Glideのinto()
に渡してもpermissionがないというerrorが出て画像が表示されない。
val PROJECTION = listOf( MediaStore.Video.Media.DATA, ) val cursor = context.contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECTION.toTypedArray(), null, null, MediaStore.Video.Media.DATE_MODIFIED) if (cursor.moveToLast()) { do { val data = cursor.getString(cursor.getColumnIndex(PROJECTION[0])) } while (cursor.moveToPrevious()) } cursor.close()
Glide.with(context.applicationContext) .load(data) .into(imageView) // 表示されない
AndroidManifestに android:requestLegacyExternalStorage="true"
をつけてやると上記のコードでも動くので、Scoped Storage周りで問題が起きているのは間違いない。ちなみに、android:requestLegacyExternalStorage="true"
でとりあえずこの問題は回避できるが、
警告: 来年度のメジャー プラットフォーム リリースでは、ターゲット SDK レベルに関係なく、すべてのアプリで対象範囲別ストレージが必須となります。
とあるので早めに対処することをお勧めする。
^ 上記ページはなぜか言語設定を英語にすると別のページに転送される。同じ内容の英語リソースが見つけられなかった。
対処法
Scoped Storageが導入に伴い、MediaStore.Video.Media.DATA
がdeprecatedになった。代わりにBaseColumns._ID
を取得し、ContentUris.withAppendedId()
を使ってUriを取得すれば行ける。
参考
以下コード例
val PROJECTION = listOf( BaseColumns._ID, ) val cursor = context.contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECTION.toTypedArray(), null, null, MediaStore.Video.Media.DATE_MODIFIED) if (cursor.moveToLast()) { do { val idColumnIndex = cursor.getColumnIndex(PROJECTION[0]) val contentUri = ContentUris.withAppendedId( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, cursor.getLong(idColumnIndex) ) } while (cursor.moveToPrevious()) } cursor.close()
Glide.with(context.applicationContext)
.load(contentUri.toString) // 同じString型を渡しても今後は表示される
.into(imageView)
何が起きているのか
せっかくなのでGlideの内部実装を追ってみた。(version: 4.11.0)
into()
がコールされた後に画像のロードが走るが、その際にload()
で渡されたstringのパターンを見ている。stringが '/'で始まるかどうかで処理が分岐する。
public class StringLoader<Data> implements ModelLoader<String, Data> { @Nullable private static Uri parseUri(String model) { Uri uri; if (TextUtils.isEmpty(model)) { return null; // See https://pmd.github.io/pmd-6.0.0/pmd_rules_java_performance.html#simplifystartswith } else if (model.charAt(0) == '/') { uri = toFileUri(model); } else { uri = Uri.parse(model); String scheme = uri.getScheme(); if (scheme == null) { uri = toFileUri(model); } } return uri; } private static Uri toFileUri(String path) { return Uri.fromFile(new File(path)); }
MediaStore.Video.Media.DATA
でStringを取ってくる場合は/
で始まるパスになっている。
例:
/storage/emulated/0/DCIM/Camera/VID_20200821_00000001.mp4
一方、BaseColumns._ID
からUriを取得する場合は(当たり前だが)パスではなくUriになっている。
例:
content://media/external/video/media/12345
ここでの処理の分岐で画像が出たり出なかったりする。
前者はパスを指定してFileオブジェクトを生成しようとするが、ここでpermissionがないとしてerrorになる。下記リンクは先ほどと同じページだが、パス経由でファイルにアクセスできないと明記してある。繰り返しになるが、この情報は英語リソースで見つけることができない。誰か見つけたらリンク教えてください…
注: 対象範囲別ストレージするアプリは、「/sdcard/DCIM/IMG1024.JPG」のようなパスに対する直接カーネル アクセス権限を有していません。アプリがこのようなファイルにアクセスするには、MediaStore を使用して、openFile() などのメソッドを呼び出す必要があります。