Save 37% off PRO during our Black Friday Sale! »

[DroidKaigi 2021] メディアアクセス古今東西 / Now and Future of Media Access

[DroidKaigi 2021] メディアアクセス古今東西 / Now and Future of Media Access

2021/10/19-21に開催されたDroidKaigi 2021のDay3にて発表した「Now and Future of Media Access 〜メディアアクセス古今東西〜」の発表資料です。

0f50b010cc99988fba8a73008b21f353?s=128

Yoshihiro WADA

October 19, 2021
Tweet

Transcript

  1. Now and Future of Media Access 〜メディアアクセス古今東⻄〜 DroidKaigi 20 2

    1 2 02 1 / 10 / 2 1 Yoshihiro Wada - @e 10 dokup CyberAgent, Inc.
  2. Yoshihiro Wada @ e 10 dokup CyberAgent, Inc. / Ameba

    Photography / Motorsports / Gadget
  3. • 本セッションで扱うトピック‧ゴール • Androidで扱われてきたメディアアクセスの⼿法 • Androidにおけるメディアアクセスで抑えておくべき知識 • ContentResolverとMediaStore APIによるメディアアクセス •

    google/modernstorage を覗き⾒る アジェンダ 3
  4. • モバイルアプリに於けるメディアを扱うユースケース • SNS等のサービスにアップロードするための画像、動画データの取得 • 画像、動画を編集し、その結果を新しいデータとして保存する • 本セッションでは「外部アプリとのメディア共有」について扱う • 外部アプリによって⽣成されたデータの取得

    • ⽣成したデータの外部アプリへの共有 • 取得したデータそのものの扱い(アップロード、加⼯)については扱わ ない 本セッションで扱うトピック 4
  5. • ContentResolverを⽤いたメディアアクセスについて理解する • 画像、動画を始めとしたメディアの扱いの勘所を把握する • google/modernstorageによるメディアアクセス⼿法の変化を垣間⾒る 本セッションのゴール 5 Androidの最新状況に即したメディアアクセスの実装⼿法を知り、 画像、動画を始めとしたメディアの扱いをよりスマートにする

  6. Androidで扱われてきた メディアアクセスの⼿法

  7. • アプリ内で扱うためのメディアファイルのURIを取得するのが⽬的 • 他アプリ、システムを利⽤する • Intent発⾏によるギャラリーアプリ等の呼び出し • アプリ内に実装する • ContentResolverとMediaStore

    APIを利⽤する Androidで扱われてきたメディアアクセスの⼿法 7
  8. • Intent経由でギャラリーアプリ等を呼び出し、結果をonActivityResult で受け取る • ACTION_GET_CONTENT • データの読み取りとインポートのみを⾏う • ACTION_OPEN_DOCUMENT等(Android 4

    . 4 〜) • ドキュメントへの永続的なアクセスを可能にする • 編集等が可能になる • Storage Access Framework(SAF)を利⽤ Intent発⾏によるギャラリーアプリ等の呼び出し 8
  9. • Android 4 . 4 以降で採⽤されたフレームワーク • 標準のシステムUIによるドキュメント選択を提供 することが可能 •

    端末内だけではなくGoogle Drive、Google Photosのようなサービスからも取得することが 可能 Storage Access Framework 9
  10. • 端末内のメディアはMediaStoreに定義されたコレクションに追加される • システムによる⾃動的なスキャンで追加されるようになっている • MediaStore.Images(画像) / MediaStore.Videos(動画) / etc

    … • ContentResolverを使⽤することで、これらのコレクションに抽象的に アクセスし、参照や更新を⾏うことができる ContentResolverとMediaStore APIを利⽤する 10
  11. • ContentResolverはContentProviderへのアクセスを⾏う実装 • ContentProviderはアプリ間をまたがったデータ共有を⾏うセントラ ル‧リポジトリとしてRDBのようにデータ提供を⾏うフレームワーク • MediaStoreではContentProviderに対し、各メディアタイプのテーブル を作成し、そこに共有可能なメディアを格納している • ContentResolverによってContentProvider上のMediaStoreのテーブル

    をクエリしたりすることで、メディアのCRUD処理を⾏うことが可能に なる ContentResolverとMediaStore 11
  12. ContentResolverとMediaStore 12 データストレージ (ここではファイル) ContentProvider ContentResolver ContentResolver を操作する実装

  13. ContentResolverとMediaStore 13 データストレージ (ここではファイル) ContentProvider ContentResolver ContentResolver を操作する実装 MediaStoreによって画像、動画等に分類された テーブルが⽣成されている

  14. ContentResolverとMediaStore 14 データストレージ (ここではファイル) ContentProvider ContentResolver ContentResolver を操作する実装 MediaStoreが⽤意しているカラムに沿って メディアのクエリ、保存といった操作を⾏う

  15. Androidにおける メディアアクセスで 備えておくべき知識

  16. Androidのストレージの区別について(Scoped Storage) 16 アプリ固有のファイル メディア ドキュメント/ファイル • アプリ内でのみ使⽤するファイル • 権限不要だが、外部/内部ストレージ

    で外部アプリへの共有可否が変わる • 共有可能なメディアファイル • MediaStore API経由でアクセス • 外部アプリのメディアにアクセスす るには READ_EXTERNAL_STORAGE か WRITE_EXTERNAL_STORAGE 権限が必要 • Android 9 以前ではすべてのメディア に対して権限が必要 • メディア以外のコンテンツ • SAF経由でアクセス • 権限不要
  17. • FileスキーマURI - eg. file://storage/emulated/0/Pictures/hoge.jpg • ファイルの実体パス • ContentスキーマURI -

    eg. content://media/external/images/media/123 • Content Provider内のデータを特定するURI • MediaStoreに格納される際に抽象化されたURI • 端末ストレージにあるファイルだけでなく、Google Drive等のクラウ ド上の画像もContent URIで扱われる FileスキーマURIとContentスキーマURI 17
  18. • Android 7 . 0 以降、プライベートディレクトリ周りのアクセス制限が強化 • targetSdkVersion 24 以降、File

    URIをIntentに⽤いることができなく なった • FileUriExposedExceptionがthrowされる • 回避策 • FileProvider#getUriForFile によってContent URIに変換する • 対象のファイルがAndroidManifestによって共有指定をした 
 ディレクトリか外部ストレージにあることが条件 File URIとContent URIとアプリ間の共有制限 18
  19. • AndroidManifest上でrequestLegacyExternalStorage をtrueにすること でScoped Storageをオプトアウトすることができた • targetSdkVersion 29 に併せたScoped Storageの対応が間に合わないア

    プリに対しての救済措置 • アプリがアンインストールされるまでオプトアウトが有効になる • targetSdkVersion 30 + Android 1 1 からScoped Storageの対応は必須と なったため、このオプトアウトは無効になるので注意が必要 requestLegacyExternalStorage 19
  20. ContentResolverと MediaStore APIによる メディアアクセスの実装

  21. • 起動後に「Load Images」ボタンをタップするこ とで、端末内の画像のロードが⾏われ、グリッド 表⽰される • ロードされたアイテムをタップすることで、画像 をクロップし、新規に画像を保存する • ここでは以下の項⽬について解説する

    • 端末内の画像のロード • 編集後の画像の新規保存 サンプルアプリの概要 21
  22. ContentResolverによる 端末内の画像ロードの実装

  23. • ContentResolverからContentProviderへのクエリを発⾏する • ContextからContentResolverのインスタンスを取得する • Context#getContentResolverメソッド • Projectionを指定する • Selection

    + args、SortOrderを指定する • ContentResolver#queryメソッドでクエリを発⾏する • クエリ結果からデータを取り出す • 実際に取得したデータを表⽰する ContentResolverによる端末内の画像ロードの実装 23
  24. • ContentResolverからContentProviderへのクエリを発⾏する • ContextからContentResolverのインスタンスを取得する • Context#getContentResolverメソッド • Projectionを指定する • Selection

    + args、SortOrderを指定する • ContentResolver#queryメソッドでクエリを発⾏する • クエリ結果からデータを取り出す • 実際に取得したデータを表⽰する ContentResolverによる端末内の画像ロードの実装 24
  25. • Projectionを指定することによって、ContentResolverが問い合わせて 取得するパラメータを指定することができる • ContentResolverが発⾏するクエリはSQL⽂に相当し、Projectionは 
 カラムの指定に相当する Projectionを指定する 25 val

    projection = arrayOf( MediaStore.Images.Media._ID, // ContentProvider上におけるID MediaStore.Images.Media.DISPLAY_NAME, // ファイル名 MediaStore.Images.Media.SIZE // ファイルサイズ )
  26. • MediaStore.(Images|Video|Audio).Media それぞれに以下のインター フェースの実装を⾏うことで定義している •android.provider.BaseColumns •android.provider.MediaStore.MediaColumns • それぞれのMediaStoreが独⾃に持つ値を定義したColumns Interface NOTE:Projectionの定義について

    26
  27. MediaStore.Images.MediaにおけるProjectionの例 27 カラム名 内容 _ID そのメディアが格納されているContentProvider上のID。 
 ContentスキーマURIで利⽤。 DISPLAY_NAME そのメディアファイルの実際のファイル名に相当

    BUCKET_DISPLAY_NAME そのメディアファイルが属しているバケット(= ディレクトリ)名に相当 HEIGHT/WIDTH そのメディアファイルが持っている縦幅/横幅 MIME_TYPE そのメディアファイルが持っているMimeType DATE_TAKEN そのメディアファイルが撮影された(画像だとEXIF由来)の⽇時
  28. • ContentResolverからContentProviderへのクエリを発⾏する • ContextからContentResolverのインスタンスを取得する • Context#getContentResolverメソッド • Projectionを指定する • Selection

    + args、SortOrderを指定する • ContentResolver#queryメソッドでクエリを発⾏する • クエリ結果からデータを取り出す • 実際に取得したデータを表⽰する ContentResolverによる端末内の画像ロードの実装 28
  29. • Selectionとそこに代⼊するArgsを指定することでSQLにおける 
 WHERE句に相当する条件設定が可能 • SortOrderを指定することでSQLにおけるORDER BY句に相当する順序指 定が可能 • どちらも不要な場合はnullを指定すればOK

    Selection + Args、SortOrderを指定する 29 val selection = “${MediaStore.Images.Media.WIDTH} > ?” val selectionArg = arrayOf(480) val sortOrder = “${MediaStore.Images.Media.DISPLAY_NAME} ASC”
  30. • Selectionとそこに代⼊するArgsを指定することでSQLにおける 
 WHERE句に相当する条件設定が可能 • SortOrderを指定することでSQLにおけるORDER BY句に相当する順序指 定が可能 • どちらも不要な場合はnullを指定すればOK

    Selection + Args、SortOrderを指定する 30 val selection = “${MediaStore.Images.Media.WIDTH} > ?” val selectionArg = arrayOf(480) val sortOrder = “${MediaStore.Images.Media.DISPLAY_NAME} ASC” 横幅480px以上の画像、という条件設定になる
  31. • Selectionとそこに代⼊するArgsを指定することでSQLにおける 
 WHERE句に相当する条件設定が可能 • SortOrderを指定することでSQLにおけるORDER BY句に相当する順序指 定が可能 • どちらも不要な場合はnullを指定すればOK

    Selection + Args、SortOrderを指定する 31 val selection = “${MediaStore.Images.Media.WIDTH} > ?” val selectionArg = arrayOf(480) val sortOrder = “${MediaStore.Images.Media.DISPLAY_NAME} ASC”
  32. • Selectionとそこに代⼊するArgsを指定することでSQLにおける 
 WHERE句に相当する条件設定が可能 • SortOrderを指定することでSQLにおけるORDER BY句に相当する順序指 定が可能 • どちらも不要な場合はnullを指定すればOK

    Selection + Args、SortOrderを指定する 32 val selection = “${MediaStore.Images.Media.WIDTH} > ?” val selectionArg = arrayOf(480) val sortOrder = “${MediaStore.Images.Media.DISPLAY_NAME} ASC” ファイル名の昇順での順序指定をしている
  33. • ContentResolverからContentProviderへのクエリを発⾏する • ContextからContentResolverのインスタンスを取得する • Context#getContentResolverメソッド • Projectionを指定する • Selection

    + args、SortOrderを指定する • ContentResolver#queryメソッドでクエリを発⾏する • クエリ結果からデータを取り出す • 実際に取得したデータを表⽰する ContentResolverによる端末内の画像ロードの実装 33
  34. • ⽤意したProjection、Selection、SortOrderを⽤い、 ContentResolver#queryメソッドをワーカースレッドで実⾏する • 第⼀引数はSQL⽂のFROM句に相当するので、対象となるURIを選ぶ ContentResolver#queryメソッドでクエリを発⾏する 34 val query =

    contentResolver.query( collection, // クエリ対象となるコレクションのURI projection, selection, selectionArgs, sortOrder )
  35. • クエリ対象となるコレクションのURIを指定する • Android 10 以降では MediaStore.VOLUME_EXTERNAL から取得するのが推 奨される クエリ対象となるコレクションのURI⽣成

    35 val collection = if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { MediaStore .Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI }
  36. • ContentResolverからContentProviderへのクエリを発⾏する • ContextからContentResolverのインスタンスを取得する • Context#getContentResolverメソッド • Projectionを指定する • Selection

    + args、SortOrderを指定する • ContentResolver#queryメソッドでクエリを発⾏する • クエリ結果からデータを取り出す • 実際に取得したデータを表⽰する ContentResolverによる端末内の画像ロードの実装 36
  37. • クエリ結果のCursorインスタンスを操作してデータを抽出していく クエリ結果からデータを取り出す 37 query?.use { // itはCursorのインスタンス // カラムインデックスのキャッシュ処理

    val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val displayNameColumn = 
 it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) val sizeColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE) // cursorを行の数だけループさせて結果のリストを作る(次項目) }
  38. • クエリ結果のCursorインスタンスを操作してデータを抽出していく クエリ結果からデータを取り出す 38 query?.use { // itはCursorのインスタンス // カラムインデックスのキャッシュ処理

    val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val displayNameColumn = 
 it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) val sizeColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE) // cursorを行の数だけループさせて結果のリストを作る(次項目) } ⾏ごとにループする際にこのメソッドが毎回呼ばれる のを回避するため、あらかじめキャッシュしておく
  39. クエリ結果からデータを取り出す 39 query?.use { // itはCursorのインスタンス // cursorを行の数だけループさせて結果のリストを作る while (it.moveToNext())

    { val displayName = it.getString(displayNameColumn) val size = it.getInt(sizeColumn) val contentUri = ContentUris.withAppendedId( [コレクションのURI], it.getLong(idColumn) ) // あとはデータ構造を作って、そのリストを結果として返す(省略) } }
  40. クエリ結果からデータを取り出す 40 query?.use { // itはCursorのインスタンス // cursorを行の数だけループさせて結果のリストを作る while (it.moveToNext())

    { val displayName = it.getString(displayNameColumn) val size = it.getInt(sizeColumn) val contentUri = ContentUris.withAppendedId( [コレクションのURI], it.getLong(idColumn) ) // あとはデータ構造を作って、そのリストを結果として返す(省略) } } キャッシュしておいたカラムインデックスを使って 
 実際に各⾏が持っているデータを抽出する
  41. クエリ結果からデータを取り出す 41 query?.use { // itはCursorのインスタンス // cursorを行の数だけループさせて結果のリストを作る while (it.moveToNext())

    { val displayName = it.getString(displayNameColumn) val size = it.getInt(sizeColumn) val contentUri = ContentUris.withAppendedId( [コレクションのURI], it.getLong(idColumn) ) // あとはデータ構造を作って、そのリストを結果として返す(省略) } } 末尾にIDを追加することでそのメディアファイルを 
 ⽰すContentスキーマURIを⽣成する
  42. • ContentResolverからContentProviderへのクエリを発⾏する • ContextからContentResolverのインスタンスを取得する • Context#getContentResolverメソッド • Projectionを指定する • Selection

    + args、SortOrderを指定する • ContentResolver#queryメソッドでクエリを発⾏する • クエリ結果からデータを取り出す • 実際に取得したデータを表⽰する ContentResolverによる端末内の画像ロードの実装 42
  43. • Glideのような画像読み込みライブラリを⽤いてURIからロードする • Bitmapとして読み込んで表⽰させる • 取得したURIをつかってParcelFileDescriptorとして開き、 
 BitmapFactory#DecodeFileDescriptorメソッドでデコードする • 取得したURIをつかってInputStreamとして開き

    
 BitmapFactory#decodeStreamメソッドでデコードする 実際に取得したデータを表⽰する 43
  44. • ギャラリーのサムネイルのような「⼩さいサイズでしか表⽰しないので オリジナルサイズの画像は必要ない」ケース • オリジナル画像だとサイズが⼤きすぎてメモリを圧迫する • ContentResolver#loadThumbnailメソッドを利⽤することで、サムネイ ルとして指定したサイズの画像をBitmapで取得することができる NOTE: ギャラリーのサムネイルについて

    44 val thumbnail: Bitmap = context.contentResolver.loadThumbnail( [ContentスキーマURI], Size(640, 480), null )
  45. • クエリ結果のCursorインスタンスにある⾏の数だけ回す • 対象のファイルが多いほど結果が表⽰されるのは遅くなる • Orderをページング⽤に追加指定することで⼀回の読み込み量を減らす • eg. [もともとのOrder] limit

    100 offset 100 • targetSdkVersion 30 以降 + Android 1 1 以降ではlimit句が使⽤不可 • Selection、Orderと⼀緒にBundleで指定できるオーバーロードが API 26 からあるので、バージョン分岐させてそちらを使うようにする NOTE: クエリのページングについて(1/2) 45
  46. NOTE: クエリのページングについて(2/2) 46 contentResolver.query( uri, projection, bundleOf( ContentResolver.QUERY_ARG_SQL_SORT_ORDER to order,

    ContentResolver.QUERY_ARG_SQL_SELECTION to selection, ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArg, ContentResolver.QUERY_ARG_OFFSET to offset, ContentResolver.QUERY_ARG_LIMIT to limit, ), null )
  47. 編集後の画像の新規保存

  48. • ファイル名等、保存に必要な要素を⽤意する • 画像であればファイル名、MimeTypeなど… • ファイル保存先のURIを取得する • アプリ固有のファイルとして保存するURIを取得する • MediaStoreに登録したURIを取得する

    • 外部のアプリに対して共有できるメディアになる • 取得したURIにファイルの中⾝を書き込む 編集後の画像の新規保存の実装 48
  49. • ファイル名等、保存に必要な要素を⽤意する • 画像であればファイル名、MimeTypeなど… • ファイル保存先のURIを取得する • アプリ固有のファイルとして保存するURIを取得する • MediaStoreに登録したURIを取得する

    • 外部のアプリに対して共有できるメディアになる • 取得したURIにファイルの中⾝を書き込む 編集後の画像の新規保存の実装 49
  50. • Context#getExternalFilesDir の返り値にファイル名を組み合わせてURI を⽣成する • FileスキーマのURIが⽣成されるので、それに画像の中⾝を書き込む アプリ固有のファイルとして保存する 50 val externalFilesDir

    = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) val imageFile = File(externalFilesDir, fileName) val uri = Uri.fromFile(imageFile) // Bitmapの圧縮及びファイルの保存処理
  51. • Context#getExternalFilesDir の返り値にファイル名を組み合わせてURI を⽣成する • FileスキーマのURIが⽣成されるので、それに画像の中⾝を書き込む アプリ固有のファイルとして保存する 51 val externalFilesDir

    = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) val imageFile = File(externalFilesDir, fileName) val uri = Uri.fromFile(imageFile) // Bitmapの圧縮及びファイルの保存処理 アプリ固有のファイルの⽣成と そのファイルのFileスキーマURIの取得
  52. • MediaScannerConnection#scanFile で⽣成したURIをMediaStoreにス キャンするよう、依頼することができる • API 2 8 以前はこれで外部アプリに共有することが可能だった •

    Context#getExternalFilesDir で得られるアプリ固有のファイルは MediaScannerConnectionのスキャン対象にならない API 29 以降のメディアのスキャン周りの制限 52 MediaScannerConnection.scanFile(context, paths, null) { path, uri -> Log.d(TAG, "Success to scan: $path to $uri") }
  53. • MediaScannerConnection#scanFile で⽣成したURIをMediaStoreにス キャンするよう、依頼することができる • API 2 8 以前はこれで外部アプリに共有することが可能だった •

    Context#getExternalFilesDir で得られるアプリ固有のファイルは MediaScannerConnectionのスキャン対象にならない API 29 以降のメディアのスキャン周りの制限 53 MediaScannerConnection.scanFile(context, paths, null) { path, uri -> Log.d(TAG, "Success to scan: $path to $uri") } API 29 以降、uriがnullになるのでスキャンが 失敗したことがわかるようになる
  54. • ファイル名等、保存に必要な要素を⽤意する • 画像であればファイル名、MimeTypeなど… • ファイル保存先のURIを取得する • アプリ固有のファイルとして保存するURIを取得する • MediaStoreに登録したURIを取得する

    • 外部のアプリに対して共有できるメディアになる • 取得したURIにファイルの中⾝を書き込む 編集後の画像の新規保存の実装 54
  55. • ContentResolver#insert メソッドによって、すでにMediaStoreに登録 されたContentスキーマURIを取得する • ContentValuesを⽤意することで、ファイル名やMimeTypeといった 
 メディアに必要なデータを⼀緒にMediaStoreに登録する MediaStoreに登録する 55

  56. MediaStoreに登録する 56 val contentResolver = context.contentResolver val values = ContentValues().apply

    { put(MediaStore.Images.Media.DISPLAY_NAME, format.fileName) put(MediaStore.Images.Media.MIME_TYPE, mimeType) } val collection = [ターゲットとなるコレクションのURI生成] val uri = contentResolver.insert(collection, values)!! // Bitmapの圧縮及びファイルの保存処理
  57. • クエリのときと同様、ターゲットとなるコレクションのURIを指定する • Android 10 以降では MediaStore.VOLUME_EXTERNAL_PRIMARY から取得す るのが推奨される ターゲットとなるコレクションのURI⽣成

    57 val collection = if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { MediaStore .Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI }
  58. • Android 10 以降、VOLUME_EXTERNAL / VOLUME_EXTERNAL_PRIMARY を選択す る必要がある • VOLUME_EXTERNALを⽤いたURI⽣成

    •content://media/external/images/media • すべての共有ストレージを指し、読み取り専⽤のものとして扱う • VOLUME_EXTERNAL_PRIMARYを⽤いたURI⽣成 • content://media/external_primary/images/media になる場合がある • プライマリの共有ストレージを指し、読み書き可能なものとして扱う NOTE: VOLUME_EXTERNALとVOLUME_EXTERNAL_PRIMARY 58
  59. • ファイル名等、保存に必要な要素を⽤意する • 画像であればファイル名、MimeTypeなど… • ファイル保存先のURIを取得する • アプリ固有のストレージを使う • アプリ内のみで扱うファイルになる

    • MediaStoreに登録する • 外部のアプリに対して共有できるメディアになる • 取得したURIにファイルの中⾝を書き込む 編集後の画像の新規保存の実装 59
  60. • 取得したURIに画像を書き込むのは共通した実装 • ContentResolverからOutputStreamやParcelFileDescriptorを開く ファイル保存の実装⼿法 60 context.contentResolver.openOutputStream(uri).use { outputStream ->

    BufferedOutputStream(outputStream, bufferSize).use { os -> try { bitmap.compress(format, 100, os) } catch (e: FileNotFoundException) { Log.e("saveToFile", "Not found target file", e) } } }
  61. • Android 10 以降、 IS_PENDING がContentValuesとして利⽤できる NOTE: IS_PENDINGを利⽤した排他的アクセスの実現 61 //

    ファイル操作前 val values = ContentValues().apply { // 他の値のContentValuesへの追加 put(MediaStore.Images.Media.IS_PENDING, 1) } // ファイル操作後 values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(uri, values, null, null)
  62. • Android 10 以降、 IS_PENDING がContentValuesとして利⽤できる NOTE: IS_PENDINGを利⽤した排他的アクセスの実現 62 //

    ファイル操作前 val values = ContentValues().apply { // 他の値のContentValuesへの追加 put(MediaStore.Images.Media.IS_PENDING, 1) } // ファイル操作後 values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(uri, values, null, null) IS_PENDING が1にすることで他のアプリからの 
 参照を回避することができる
  63. • Android 10 以降、 IS_PENDING がContentValuesとして利⽤できる NOTE: IS_PENDINGを利⽤した排他的アクセスの実現 63 //

    ファイル操作前 val values = ContentValues().apply { // 他の値のContentValuesへの追加 put(MediaStore.Images.Media.IS_PENDING, 1) } // ファイル操作後 values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(uri, values, null, null) ファイル操作後は IS_PENDING を0にして更新すること で他のアプリから参照できるようにする
  64. google/modernstorage を覗き⾒る

  65. • AndroidのDevRelチームがストレージチームと協⼒して開発している 
 ライブラリ群 • ストレージ周りに抽象化レイヤーを提供し、よりストレージアクセス 周りの実装を簡単にするのが狙い • MediaStore •

    Storage Access Framework • 現状のバージョンは1.0.0-alpha 02 google/modernstorage 65
  66. • AndroidのDevRelチームがストレージチームと協⼒して開発している 
 ライブラリ群 • ストレージ周りに抽象化レイヤーを提供し、よりストレージアクセス 周りの実装を簡単にするのが狙い • MediaStore •

    Storage Access Framework • 現状のバージョンは1.0.0-alpha 02 google/modernstorage 66
  67. • MediaStoreの扱いを抽象化したレイヤーを提供するライブラリ • MediaStoreRepositoryが抽象化レイヤーとなる • バージョン毎のURI⽣成などを考慮せずMediaStoreにアクセスできる • (現状)できることは以下 • MediaStoreアクセスに伴うパーミッションチェック

    • MediaStoreに登録されたURIの⽣成 • MediaStoreへのメディアの追加、変更時のスキャン • MediaStoreへの単⼀メディアの問い合わせ modernstorage-mediastore 67
  68. • Scoped Storageに伴う複雑なパーミッション周りのチェックを⾏う 
 ヘルパーメソッドが⽤意されている •can(Read|Write)OwnEntries() • MediaStoreにあるアプリ⾃⾝が作成したメディアの読み書きの 
 パーミッションチェック

    •can(Read|Write)SharedEntries() • MediaStoreにある外部アプリが作成したメディアの読み書きの 
 パーミッションチェック MediaStoreアクセスに伴うパーミッションチェック 68
  69. • MediaStoreに登録されたURIを払い出すことができる • ACTION_IMAGE_CAPTURE Intentでカメラを起動する場合等で使⽤できる EXTRA_OUTPUT に扱うURIとして使⽤可能 MediaStoreに登録されたURIの⽣成 69 val

    photoUri = mediaStoreRepository.createMediaUri( filename = "new-image.jpg", type = FileType.IMAGE, location = SharedPrimary )
  70. • InputStreamを渡してMediaStoreにメディアを登録したり、登録済みの メディアを編集後にスキャンさせて更新させることができる MediaStoreへのメディアの追加、変更時のスキャン 70 val photoUri = mediaStore.addMediaFromStream( filename

    = "new-image.jpg", type = FileType.IMAGE, mimeType = "image/jpg", inputStream = sample, location = SharedPrimary ) // メディアの追加 mediaStore.scanUri(updatedPhotoUri, “image/png”) // メディアのスキャン
  71. • URIを使ってMediaStoreに登録されたメディアを問い合わせて、ファイ ル名やファイルサイズといった情報を取得することができる MediaStoreへの単⼀メディアの問い合わせ 71 val mediaDetails = mediaStore.getResourceByUri(mediaUri) mediaDetails.uri

    // content://media/external/images/media/123 mediaDetails.mimeType // image/jpeg mediaDetails.path // ファイルの実体のパス
  72. まとめ

  73. • Android 10 以降で実装されたScoped Storageによってメディアアクセス 周りの複雑性が増した • ストレージの場所からファイルの利⽤⽬的への権限の基準の変更が発 ⽣した •

    しかし、メディアアクセスにはMediaStoreとContentResolverを駆 使する必要があるのは以前と変わらない • google/modernstorageは2021年の後半をターゲットに開発を進めてい る、と書かれており、いずれはストレージアクセスのスタンダードにな りそう まとめ 73
  74. • https://developer.android.com/training/data-storage?hl=ja#scoped-storage • データ ストレージとファイル ストレージの概要|Android Developers • https://developer.android.com/training/data-storage/shared/media •

    共有ストレージからメディアにアクセスする|Android Developers • https://google.github.io/modernstorage/ • google/modernstorage Reference 74