Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[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 〜メディアアクセス古今東西〜」の発表資料です。

Yoshihiro WADA

October 19, 2021
Tweet

More Decks by Yoshihiro WADA

Other Decks in Programming

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