$30 off During Our Annual Pro Sale. View Details »

[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.

    View Slide

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

    View Slide

  3. • 本セッションで扱うトピック‧ゴール


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


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


    • ContentResolverとMediaStore APIによるメディアアクセス


    • google/modernstorage を覗き⾒る
    アジェンダ
    3

    View Slide

  4. • モバイルアプリに於けるメディアを扱うユースケース


    • SNS等のサービスにアップロードするための画像、動画データの取得


    • 画像、動画を編集し、その結果を新しいデータとして保存する


    • 本セッションでは「外部アプリとのメディア共有」について扱う


    • 外部アプリによって⽣成されたデータの取得


    • ⽣成したデータの外部アプリへの共有


    • 取得したデータそのものの扱い(アップロード、加⼯)については扱わ
    ない
    本セッションで扱うトピック
    4

    View Slide

  5. • ContentResolverを⽤いたメディアアクセスについて理解する


    • 画像、動画を始めとしたメディアの扱いの勘所を把握する


    • google/modernstorageによるメディアアクセス⼿法の変化を垣間⾒る
    本セッションのゴール
    5
    Androidの最新状況に即したメディアアクセスの実装⼿法を知り、


    画像、動画を始めとしたメディアの扱いをよりスマートにする

    View Slide

  6. Androidで扱われてきた


    メディアアクセスの⼿法

    View Slide

  7. • アプリ内で扱うためのメディアファイルのURIを取得するのが⽬的


    • 他アプリ、システムを利⽤する


    • Intent発⾏によるギャラリーアプリ等の呼び出し


    • アプリ内に実装する


    • ContentResolverとMediaStore APIを利⽤する
    Androidで扱われてきたメディアアクセスの⼿法
    7

    View Slide

  8. • Intent経由でギャラリーアプリ等を呼び出し、結果をonActivityResult
    で受け取る


    • ACTION_GET_CONTENT


    • データの読み取りとインポートのみを⾏う


    • ACTION_OPEN_DOCUMENT等(Android
    4
    .
    4
    〜)


    • ドキュメントへの永続的なアクセスを可能にする


    • 編集等が可能になる


    • Storage Access Framework(SAF)を利⽤
    Intent発⾏によるギャラリーアプリ等の呼び出し
    8

    View Slide

  9. • Android
    4
    .
    4
    以降で採⽤されたフレームワーク


    • 標準のシステムUIによるドキュメント選択を提供
    することが可能


    • 端末内だけではなくGoogle Drive、Google
    Photosのようなサービスからも取得することが
    可能
    Storage Access Framework
    9

    View Slide

  10. • 端末内のメディアはMediaStoreに定義されたコレクションに追加される


    • システムによる⾃動的なスキャンで追加されるようになっている


    • MediaStore.Images(画像) / MediaStore.Videos(動画) / etc


    • ContentResolverを使⽤することで、これらのコレクションに抽象的に
    アクセスし、参照や更新を⾏うことができる
    ContentResolverとMediaStore APIを利⽤する
    10

    View Slide

  11. • ContentResolverはContentProviderへのアクセスを⾏う実装


    • ContentProviderはアプリ間をまたがったデータ共有を⾏うセントラ
    ル‧リポジトリとしてRDBのようにデータ提供を⾏うフレームワーク


    • MediaStoreではContentProviderに対し、各メディアタイプのテーブル
    を作成し、そこに共有可能なメディアを格納している


    • ContentResolverによってContentProvider上のMediaStoreのテーブル
    をクエリしたりすることで、メディアのCRUD処理を⾏うことが可能に
    なる
    ContentResolverとMediaStore
    11

    View Slide

  12. ContentResolverとMediaStore
    12
    データストレージ


    (ここではファイル)
    ContentProvider
    ContentResolver
    ContentResolver


    を操作する実装

    View Slide

  13. ContentResolverとMediaStore
    13
    データストレージ


    (ここではファイル)
    ContentProvider
    ContentResolver
    ContentResolver


    を操作する実装
    MediaStoreによって画像、動画等に分類された


    テーブルが⽣成されている

    View Slide

  14. ContentResolverとMediaStore
    14
    データストレージ


    (ここではファイル)
    ContentProvider
    ContentResolver
    ContentResolver


    を操作する実装
    MediaStoreが⽤意しているカラムに沿って


    メディアのクエリ、保存といった操作を⾏う

    View Slide

  15. Androidにおける


    メディアアクセスで


    備えておくべき知識

    View Slide

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


    • 権限不要だが、外部/内部ストレージ
    で外部アプリへの共有可否が変わる
    • 共有可能なメディアファイル


    • MediaStore API経由でアクセス


    • 外部アプリのメディアにアクセスす
    るには READ_EXTERNAL_STORAGE か
    WRITE_EXTERNAL_STORAGE 権限が必要


    • Android
    9
    以前ではすべてのメディア
    に対して権限が必要
    • メディア以外のコンテンツ


    • SAF経由でアクセス


    • 権限不要

    View Slide

  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

    View Slide

  18. • Android
    7
    .
    0
    以降、プライベートディレクトリ周りのアクセス制限が強化


    • targetSdkVersion
    24
    以降、File URIをIntentに⽤いることができなく
    なった


    • FileUriExposedExceptionがthrowされる


    • 回避策


    • FileProvider#getUriForFile によってContent URIに変換する


    • 対象のファイルがAndroidManifestによって共有指定をした

    ディレクトリか外部ストレージにあることが条件
    File URIとContent URIとアプリ間の共有制限
    18

    View Slide

  19. • AndroidManifest上でrequestLegacyExternalStorage をtrueにすること
    でScoped Storageをオプトアウトすることができた


    • targetSdkVersion
    29
    に併せたScoped Storageの対応が間に合わないア
    プリに対しての救済措置


    • アプリがアンインストールされるまでオプトアウトが有効になる


    • targetSdkVersion
    30
    + Android
    1 1
    からScoped Storageの対応は必須と
    なったため、このオプトアウトは無効になるので注意が必要
    requestLegacyExternalStorage
    19

    View Slide

  20. ContentResolverと


    MediaStore APIによる


    メディアアクセスの実装

    View Slide

  21. • 起動後に「Load Images」ボタンをタップするこ
    とで、端末内の画像のロードが⾏われ、グリッド
    表⽰される


    • ロードされたアイテムをタップすることで、画像
    をクロップし、新規に画像を保存する


    • ここでは以下の項⽬について解説する


    • 端末内の画像のロード


    • 編集後の画像の新規保存
    サンプルアプリの概要
    21

    View Slide

  22. ContentResolverによる


    端末内の画像ロードの実装

    View Slide

  23. • ContentResolverからContentProviderへのクエリを発⾏する


    • ContextからContentResolverのインスタンスを取得する


    • Context#getContentResolverメソッド


    • Projectionを指定する


    • Selection + args、SortOrderを指定する


    • ContentResolver#queryメソッドでクエリを発⾏する


    • クエリ結果からデータを取り出す


    • 実際に取得したデータを表⽰する
    ContentResolverによる端末内の画像ロードの実装
    23

    View Slide

  24. • ContentResolverからContentProviderへのクエリを発⾏する


    • ContextからContentResolverのインスタンスを取得する


    • Context#getContentResolverメソッド


    • Projectionを指定する


    • Selection + args、SortOrderを指定する


    • ContentResolver#queryメソッドでクエリを発⾏する


    • クエリ結果からデータを取り出す


    • 実際に取得したデータを表⽰する
    ContentResolverによる端末内の画像ロードの実装
    24

    View Slide

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


    )

    View Slide

  26. • MediaStore.(Images|Video|Audio).Media それぞれに以下のインター
    フェースの実装を⾏うことで定義している


    •android.provider.BaseColumns


    •android.provider.MediaStore.MediaColumns


    • それぞれのMediaStoreが独⾃に持つ値を定義したColumns Interface
    NOTE:Projectionの定義について
    26

    View Slide

  27. MediaStore.Images.MediaにおけるProjectionの例
    27
    カラム名 内容
    _ID
    そのメディアが格納されているContentProvider上のID。

    ContentスキーマURIで利⽤。
    DISPLAY_NAME そのメディアファイルの実際のファイル名に相当
    BUCKET_DISPLAY_NAME そのメディアファイルが属しているバケット(= ディレクトリ)名に相当
    HEIGHT/WIDTH そのメディアファイルが持っている縦幅/横幅
    MIME_TYPE そのメディアファイルが持っているMimeType
    DATE_TAKEN そのメディアファイルが撮影された(画像だとEXIF由来)の⽇時

    View Slide

  28. • ContentResolverからContentProviderへのクエリを発⾏する


    • ContextからContentResolverのインスタンスを取得する


    • Context#getContentResolverメソッド


    • Projectionを指定する


    • Selection + args、SortOrderを指定する


    • ContentResolver#queryメソッドでクエリを発⾏する


    • クエリ結果からデータを取り出す


    • 実際に取得したデータを表⽰する
    ContentResolverによる端末内の画像ロードの実装
    28

    View Slide

  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”

    View Slide

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

    View Slide

  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”

    View Slide

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

    View Slide

  33. • ContentResolverからContentProviderへのクエリを発⾏する


    • ContextからContentResolverのインスタンスを取得する


    • Context#getContentResolverメソッド


    • Projectionを指定する


    • Selection + args、SortOrderを指定する


    • ContentResolver#queryメソッドでクエリを発⾏する


    • クエリ結果からデータを取り出す


    • 実際に取得したデータを表⽰する
    ContentResolverによる端末内の画像ロードの実装
    33

    View Slide

  34. • ⽤意したProjection、Selection、SortOrderを⽤い、
    ContentResolver#queryメソッドをワーカースレッドで実⾏する


    • 第⼀引数はSQL⽂のFROM句に相当するので、対象となるURIを選ぶ
    ContentResolver#queryメソッドでクエリを発⾏する
    34
    val query = contentResolver.query(


    collection, // クエリ対象となるコレクションのURI


    projection,


    selection,


    selectionArgs,


    sortOrder


    )

    View Slide

  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


    }

    View Slide

  36. • ContentResolverからContentProviderへのクエリを発⾏する


    • ContextからContentResolverのインスタンスを取得する


    • Context#getContentResolverメソッド


    • Projectionを指定する


    • Selection + args、SortOrderを指定する


    • ContentResolver#queryメソッドでクエリを発⾏する


    • クエリ結果からデータを取り出す


    • 実際に取得したデータを表⽰する
    ContentResolverによる端末内の画像ロードの実装
    36

    View Slide

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


    }

    View Slide

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


    }
    ⾏ごとにループする際にこのメソッドが毎回呼ばれる


    のを回避するため、あらかじめキャッシュしておく

    View Slide

  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)


    )


    // あとはデータ構造を作って、そのリストを結果として返す(省略)


    }


    }

    View Slide

  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)


    )


    // あとはデータ構造を作って、そのリストを結果として返す(省略)


    }


    }
    キャッシュしておいたカラムインデックスを使って

    実際に各⾏が持っているデータを抽出する

    View Slide

  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を⽣成する

    View Slide

  42. • ContentResolverからContentProviderへのクエリを発⾏する


    • ContextからContentResolverのインスタンスを取得する


    • Context#getContentResolverメソッド


    • Projectionを指定する


    • Selection + args、SortOrderを指定する


    • ContentResolver#queryメソッドでクエリを発⾏する


    • クエリ結果からデータを取り出す


    • 実際に取得したデータを表⽰する
    ContentResolverによる端末内の画像ロードの実装
    42

    View Slide

  43. • Glideのような画像読み込みライブラリを⽤いてURIからロードする


    • Bitmapとして読み込んで表⽰させる


    • 取得したURIをつかってParcelFileDescriptorとして開き、

    BitmapFactory#DecodeFileDescriptorメソッドでデコードする


    • 取得したURIをつかってInputStreamとして開き

    BitmapFactory#decodeStreamメソッドでデコードする
    実際に取得したデータを表⽰する
    43

    View Slide

  44. • ギャラリーのサムネイルのような「⼩さいサイズでしか表⽰しないので
    オリジナルサイズの画像は必要ない」ケース


    • オリジナル画像だとサイズが⼤きすぎてメモリを圧迫する


    • ContentResolver#loadThumbnailメソッドを利⽤することで、サムネイ
    ルとして指定したサイズの画像をBitmapで取得することができる
    NOTE: ギャラリーのサムネイルについて
    44
    val thumbnail: Bitmap = context.contentResolver.loadThumbnail(


    [ContentスキーマURI], Size(640, 480), null


    )

    View Slide

  45. • クエリ結果のCursorインスタンスにある⾏の数だけ回す


    • 対象のファイルが多いほど結果が表⽰されるのは遅くなる


    • Orderをページング⽤に追加指定することで⼀回の読み込み量を減らす


    • eg. [もともとのOrder] limit 100 offset 100


    • targetSdkVersion
    30
    以降 + Android
    1 1
    以降ではlimit句が使⽤不可


    • Selection、Orderと⼀緒にBundleで指定できるオーバーロードが
    API
    26
    からあるので、バージョン分岐させてそちらを使うようにする
    NOTE: クエリのページングについて(1/2)
    45

    View Slide

  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


    )

    View Slide

  47. 編集後の画像の新規保存

    View Slide

  48. • ファイル名等、保存に必要な要素を⽤意する


    • 画像であればファイル名、MimeTypeなど…


    • ファイル保存先のURIを取得する


    • アプリ固有のファイルとして保存するURIを取得する


    • MediaStoreに登録したURIを取得する


    • 外部のアプリに対して共有できるメディアになる


    • 取得したURIにファイルの中⾝を書き込む
    編集後の画像の新規保存の実装
    48

    View Slide

  49. • ファイル名等、保存に必要な要素を⽤意する


    • 画像であればファイル名、MimeTypeなど…


    • ファイル保存先のURIを取得する


    • アプリ固有のファイルとして保存するURIを取得する


    • MediaStoreに登録したURIを取得する


    • 外部のアプリに対して共有できるメディアになる


    • 取得したURIにファイルの中⾝を書き込む
    編集後の画像の新規保存の実装
    49

    View Slide

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

    View Slide

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

    View Slide

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


    }

    View Slide

  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になるのでスキャンが


    失敗したことがわかるようになる

    View Slide

  54. • ファイル名等、保存に必要な要素を⽤意する


    • 画像であればファイル名、MimeTypeなど…


    • ファイル保存先のURIを取得する


    • アプリ固有のファイルとして保存するURIを取得する


    • MediaStoreに登録したURIを取得する


    • 外部のアプリに対して共有できるメディアになる


    • 取得したURIにファイルの中⾝を書き込む
    編集後の画像の新規保存の実装
    54

    View Slide

  55. • ContentResolver#insert メソッドによって、すでにMediaStoreに登録
    されたContentスキーマURIを取得する


    • ContentValuesを⽤意することで、ファイル名やMimeTypeといった

    メディアに必要なデータを⼀緒にMediaStoreに登録する
    MediaStoreに登録する
    55

    View Slide

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

    View Slide

  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


    }

    View Slide

  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

    View Slide

  59. • ファイル名等、保存に必要な要素を⽤意する


    • 画像であればファイル名、MimeTypeなど…


    • ファイル保存先のURIを取得する


    • アプリ固有のストレージを使う


    • アプリ内のみで扱うファイルになる


    • MediaStoreに登録する


    • 外部のアプリに対して共有できるメディアになる


    • 取得したURIにファイルの中⾝を書き込む
    編集後の画像の新規保存の実装
    59

    View Slide

  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)


    }


    }


    }

    View Slide

  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)

    View Slide

  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にすることで他のアプリからの

    参照を回避することができる

    View Slide

  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にして更新すること


    で他のアプリから参照できるようにする

    View Slide

  64. google/modernstorage


    を覗き⾒る

    View Slide

  65. • AndroidのDevRelチームがストレージチームと協⼒して開発している

    ライブラリ群


    • ストレージ周りに抽象化レイヤーを提供し、よりストレージアクセス
    周りの実装を簡単にするのが狙い


    • MediaStore


    • Storage Access Framework


    • 現状のバージョンは1.0.0-alpha
    02
    google/modernstorage
    65

    View Slide

  66. • AndroidのDevRelチームがストレージチームと協⼒して開発している

    ライブラリ群


    • ストレージ周りに抽象化レイヤーを提供し、よりストレージアクセス
    周りの実装を簡単にするのが狙い


    • MediaStore


    • Storage Access Framework


    • 現状のバージョンは1.0.0-alpha
    02
    google/modernstorage
    66

    View Slide

  67. • MediaStoreの扱いを抽象化したレイヤーを提供するライブラリ


    • MediaStoreRepositoryが抽象化レイヤーとなる


    • バージョン毎のURI⽣成などを考慮せずMediaStoreにアクセスできる


    • (現状)できることは以下


    • MediaStoreアクセスに伴うパーミッションチェック


    • MediaStoreに登録されたURIの⽣成


    • MediaStoreへのメディアの追加、変更時のスキャン


    • MediaStoreへの単⼀メディアの問い合わせ
    modernstorage-mediastore
    67

    View Slide

  68. • Scoped Storageに伴う複雑なパーミッション周りのチェックを⾏う

    ヘルパーメソッドが⽤意されている


    •can(Read|Write)OwnEntries()


    • MediaStoreにあるアプリ⾃⾝が作成したメディアの読み書きの

    パーミッションチェック


    •can(Read|Write)SharedEntries()


    • MediaStoreにある外部アプリが作成したメディアの読み書きの

    パーミッションチェック
    MediaStoreアクセスに伴うパーミッションチェック
    68

    View Slide

  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


    )

    View Slide

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

    View Slide

  71. • URIを使ってMediaStoreに登録されたメディアを問い合わせて、ファイ
    ル名やファイルサイズといった情報を取得することができる
    MediaStoreへの単⼀メディアの問い合わせ
    71
    val mediaDetails = mediaStore.getResourceByUri(mediaUri)


    mediaDetails.uri // content://media/external/images/media/123


    mediaDetails.mimeType // image/jpeg


    mediaDetails.path // ファイルの実体のパス

    View Slide

  72. まとめ

    View Slide

  73. • Android
    10
    以降で実装されたScoped Storageによってメディアアクセス
    周りの複雑性が増した


    • ストレージの場所からファイルの利⽤⽬的への権限の基準の変更が発
    ⽣した


    • しかし、メディアアクセスにはMediaStoreとContentResolverを駆
    使する必要があるのは以前と変わらない


    • google/modernstorageは2021年の後半をターゲットに開発を進めてい
    る、と書かれており、いずれはストレージアクセスのスタンダードにな
    りそう
    まとめ
    73

    View Slide

  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

    View Slide