Slide 1

Slide 1 text

Now and Future of Media Access 〜メディアアクセス古今東⻄〜 DroidKaigi 20 2 1 2 02 1 / 10 / 2 1 Yoshihiro Wada - @e 10 dokup CyberAgent, Inc.

Slide 2

Slide 2 text

Yoshihiro Wada @ e 10 dokup CyberAgent, Inc. / Ameba Photography / Motorsports / Gadget

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

• モバイルアプリに於けるメディアを扱うユースケース • SNS等のサービスにアップロードするための画像、動画データの取得 • 画像、動画を編集し、その結果を新しいデータとして保存する • 本セッションでは「外部アプリとのメディア共有」について扱う • 外部アプリによって⽣成されたデータの取得 • ⽣成したデータの外部アプリへの共有 • 取得したデータそのものの扱い(アップロード、加⼯)については扱わ ない 本セッションで扱うトピック 4

Slide 5

Slide 5 text

• ContentResolverを⽤いたメディアアクセスについて理解する • 画像、動画を始めとしたメディアの扱いの勘所を把握する • google/modernstorageによるメディアアクセス⼿法の変化を垣間⾒る 本セッションのゴール 5 Androidの最新状況に即したメディアアクセスの実装⼿法を知り、 画像、動画を始めとしたメディアの扱いをよりスマートにする

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

• Intent経由でギャラリーアプリ等を呼び出し、結果をonActivityResult で受け取る • ACTION_GET_CONTENT • データの読み取りとインポートのみを⾏う • ACTION_OPEN_DOCUMENT等(Android 4 . 4 〜) • ドキュメントへの永続的なアクセスを可能にする • 編集等が可能になる • Storage Access Framework(SAF)を利⽤ Intent発⾏によるギャラリーアプリ等の呼び出し 8

Slide 9

Slide 9 text

• Android 4 . 4 以降で採⽤されたフレームワーク • 標準のシステムUIによるドキュメント選択を提供 することが可能 • 端末内だけではなくGoogle Drive、Google Photosのようなサービスからも取得することが 可能 Storage Access Framework 9

Slide 10

Slide 10 text

• 端末内のメディアはMediaStoreに定義されたコレクションに追加される • システムによる⾃動的なスキャンで追加されるようになっている • MediaStore.Images(画像) / MediaStore.Videos(動画) / etc … • ContentResolverを使⽤することで、これらのコレクションに抽象的に アクセスし、参照や更新を⾏うことができる ContentResolverとMediaStore APIを利⽤する 10

Slide 11

Slide 11 text

• ContentResolverはContentProviderへのアクセスを⾏う実装 • ContentProviderはアプリ間をまたがったデータ共有を⾏うセントラ ル‧リポジトリとしてRDBのようにデータ提供を⾏うフレームワーク • MediaStoreではContentProviderに対し、各メディアタイプのテーブル を作成し、そこに共有可能なメディアを格納している • ContentResolverによってContentProvider上のMediaStoreのテーブル をクエリしたりすることで、メディアのCRUD処理を⾏うことが可能に なる ContentResolverとMediaStore 11

Slide 12

Slide 12 text

ContentResolverとMediaStore 12 データストレージ (ここではファイル) ContentProvider ContentResolver ContentResolver を操作する実装

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Androidのストレージの区別について(Scoped Storage) 16 アプリ固有のファイル メディア ドキュメント/ファイル • アプリ内でのみ使⽤するファイル • 権限不要だが、外部/内部ストレージ で外部アプリへの共有可否が変わる • 共有可能なメディアファイル • MediaStore API経由でアクセス • 外部アプリのメディアにアクセスす るには READ_EXTERNAL_STORAGE か WRITE_EXTERNAL_STORAGE 権限が必要 • Android 9 以前ではすべてのメディア に対して権限が必要 • メディア以外のコンテンツ • SAF経由でアクセス • 権限不要

Slide 17

Slide 17 text

• 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

Slide 18

Slide 18 text

• Android 7 . 0 以降、プライベートディレクトリ周りのアクセス制限が強化 • targetSdkVersion 24 以降、File URIをIntentに⽤いることができなく なった • FileUriExposedExceptionがthrowされる • 回避策 • FileProvider#getUriForFile によってContent URIに変換する • 対象のファイルがAndroidManifestによって共有指定をした 
 ディレクトリか外部ストレージにあることが条件 File URIとContent URIとアプリ間の共有制限 18

Slide 19

Slide 19 text

• AndroidManifest上でrequestLegacyExternalStorage をtrueにすること でScoped Storageをオプトアウトすることができた • targetSdkVersion 29 に併せたScoped Storageの対応が間に合わないア プリに対しての救済措置 • アプリがアンインストールされるまでオプトアウトが有効になる • targetSdkVersion 30 + Android 1 1 からScoped Storageの対応は必須と なったため、このオプトアウトは無効になるので注意が必要 requestLegacyExternalStorage 19

Slide 20

Slide 20 text

ContentResolverと MediaStore APIによる メディアアクセスの実装

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

ContentResolverによる 端末内の画像ロードの実装

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

• 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 // ファイルサイズ )

Slide 26

Slide 26 text

• MediaStore.(Images|Video|Audio).Media それぞれに以下のインター フェースの実装を⾏うことで定義している •android.provider.BaseColumns •android.provider.MediaStore.MediaColumns • それぞれのMediaStoreが独⾃に持つ値を定義したColumns Interface NOTE:Projectionの定義について 26

Slide 27

Slide 27 text

MediaStore.Images.MediaにおけるProjectionの例 27 カラム名 内容 _ID そのメディアが格納されているContentProvider上のID。 
 ContentスキーマURIで利⽤。 DISPLAY_NAME そのメディアファイルの実際のファイル名に相当 BUCKET_DISPLAY_NAME そのメディアファイルが属しているバケット(= ディレクトリ)名に相当 HEIGHT/WIDTH そのメディアファイルが持っている縦幅/横幅 MIME_TYPE そのメディアファイルが持っているMimeType DATE_TAKEN そのメディアファイルが撮影された(画像だとEXIF由来)の⽇時

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

• 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”

Slide 30

Slide 30 text

• 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以上の画像、という条件設定になる

Slide 31

Slide 31 text

• 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”

Slide 32

Slide 32 text

• 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” ファイル名の昇順での順序指定をしている

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

• ⽤意したProjection、Selection、SortOrderを⽤い、 ContentResolver#queryメソッドをワーカースレッドで実⾏する • 第⼀引数はSQL⽂のFROM句に相当するので、対象となるURIを選ぶ ContentResolver#queryメソッドでクエリを発⾏する 34 val query = contentResolver.query( collection, // クエリ対象となるコレクションのURI projection, selection, selectionArgs, sortOrder )

Slide 35

Slide 35 text

• クエリ対象となるコレクションの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 }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

• クエリ結果の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を行の数だけループさせて結果のリストを作る(次項目) }

Slide 38

Slide 38 text

• クエリ結果の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を行の数だけループさせて結果のリストを作る(次項目) } ⾏ごとにループする際にこのメソッドが毎回呼ばれる のを回避するため、あらかじめキャッシュしておく

Slide 39

Slide 39 text

クエリ結果からデータを取り出す 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) ) // あとはデータ構造を作って、そのリストを結果として返す(省略) } }

Slide 40

Slide 40 text

クエリ結果からデータを取り出す 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) ) // あとはデータ構造を作って、そのリストを結果として返す(省略) } } キャッシュしておいたカラムインデックスを使って 
 実際に各⾏が持っているデータを抽出する

Slide 41

Slide 41 text

クエリ結果からデータを取り出す 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を⽣成する

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

• Glideのような画像読み込みライブラリを⽤いてURIからロードする • Bitmapとして読み込んで表⽰させる • 取得したURIをつかってParcelFileDescriptorとして開き、 
 BitmapFactory#DecodeFileDescriptorメソッドでデコードする • 取得したURIをつかってInputStreamとして開き 
 BitmapFactory#decodeStreamメソッドでデコードする 実際に取得したデータを表⽰する 43

Slide 44

Slide 44 text

• ギャラリーのサムネイルのような「⼩さいサイズでしか表⽰しないので オリジナルサイズの画像は必要ない」ケース • オリジナル画像だとサイズが⼤きすぎてメモリを圧迫する • ContentResolver#loadThumbnailメソッドを利⽤することで、サムネイ ルとして指定したサイズの画像をBitmapで取得することができる NOTE: ギャラリーのサムネイルについて 44 val thumbnail: Bitmap = context.contentResolver.loadThumbnail( [ContentスキーマURI], Size(640, 480), null )

Slide 45

Slide 45 text

• クエリ結果のCursorインスタンスにある⾏の数だけ回す • 対象のファイルが多いほど結果が表⽰されるのは遅くなる • Orderをページング⽤に追加指定することで⼀回の読み込み量を減らす • eg. [もともとのOrder] limit 100 offset 100 • targetSdkVersion 30 以降 + Android 1 1 以降ではlimit句が使⽤不可 • Selection、Orderと⼀緒にBundleで指定できるオーバーロードが API 26 からあるので、バージョン分岐させてそちらを使うようにする NOTE: クエリのページングについて(1/2) 45

Slide 46

Slide 46 text

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 )

Slide 47

Slide 47 text

編集後の画像の新規保存

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

• 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の取得

Slide 52

Slide 52 text

• 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") }

Slide 53

Slide 53 text

• 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になるのでスキャンが 失敗したことがわかるようになる

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

• ContentResolver#insert メソッドによって、すでにMediaStoreに登録 されたContentスキーマURIを取得する • ContentValuesを⽤意することで、ファイル名やMimeTypeといった 
 メディアに必要なデータを⼀緒にMediaStoreに登録する MediaStoreに登録する 55

Slide 56

Slide 56 text

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の圧縮及びファイルの保存処理

Slide 57

Slide 57 text

• クエリのときと同様、ターゲットとなるコレクションの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 }

Slide 58

Slide 58 text

• 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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

• 取得した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) } } }

Slide 61

Slide 61 text

• 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)

Slide 62

Slide 62 text

• 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にすることで他のアプリからの 
 参照を回避することができる

Slide 63

Slide 63 text

• 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にして更新すること で他のアプリから参照できるようにする

Slide 64

Slide 64 text

google/modernstorage を覗き⾒る

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

• MediaStoreの扱いを抽象化したレイヤーを提供するライブラリ • MediaStoreRepositoryが抽象化レイヤーとなる • バージョン毎のURI⽣成などを考慮せずMediaStoreにアクセスできる • (現状)できることは以下 • MediaStoreアクセスに伴うパーミッションチェック • MediaStoreに登録されたURIの⽣成 • MediaStoreへのメディアの追加、変更時のスキャン • MediaStoreへの単⼀メディアの問い合わせ modernstorage-mediastore 67

Slide 68

Slide 68 text

• Scoped Storageに伴う複雑なパーミッション周りのチェックを⾏う 
 ヘルパーメソッドが⽤意されている •can(Read|Write)OwnEntries() • MediaStoreにあるアプリ⾃⾝が作成したメディアの読み書きの 
 パーミッションチェック •can(Read|Write)SharedEntries() • MediaStoreにある外部アプリが作成したメディアの読み書きの 
 パーミッションチェック MediaStoreアクセスに伴うパーミッションチェック 68

Slide 69

Slide 69 text

• 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 )

Slide 70

Slide 70 text

• 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”) // メディアのスキャン

Slide 71

Slide 71 text

• URIを使ってMediaStoreに登録されたメディアを問い合わせて、ファイ ル名やファイルサイズといった情報を取得することができる MediaStoreへの単⼀メディアの問い合わせ 71 val mediaDetails = mediaStore.getResourceByUri(mediaUri) mediaDetails.uri // content://media/external/images/media/123 mediaDetails.mimeType // image/jpeg mediaDetails.path // ファイルの実体のパス

Slide 72

Slide 72 text

まとめ

Slide 73

Slide 73 text

• Android 10 以降で実装されたScoped Storageによってメディアアクセス 周りの複雑性が増した • ストレージの場所からファイルの利⽤⽬的への権限の基準の変更が発 ⽣した • しかし、メディアアクセスにはMediaStoreとContentResolverを駆 使する必要があるのは以前と変わらない • google/modernstorageは2021年の後半をターゲットに開発を進めてい る、と書かれており、いずれはストレージアクセスのスタンダードにな りそう まとめ 73

Slide 74

Slide 74 text

• 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