こんにちは。サイバーエージェント メディア事業本部 Ameba事業本部でAndroidアプリエンジニアをしている和田( @e10dokup )です。
10/19-21で開催されたDroidKaigi 2021において、「Now and Future of Media Access / メディアアクセス古今東西」という、Scoped Storageを始めとする昨今のAndroidを取り巻くメディアアクセス手法について解説する内容の登壇をさせていただきました。
内容としては、以下のような構成です。
- 現行のAndroidのローカルストレージとこれまでのアップデートについて
- ContentResolverによるメディアアクセスの手法についての解説
- google/modernstorage によってメディアアクセスがどう変わるのか
また、発表動画、及びスライドもすでに公開されておりますので、合わせてご覧ください。
本記事では、このセッションの書き起こし、及びDroidKaigi 2021で気になったセッションの紹介をしていこうと思います。
本セッションのトピックとゴール
まず、本セッションで扱うのはAndroidアプリにおけるメディアを扱うユースケースのうち、「外部アプリとのメディア共有」にコンテキストを絞っています。具体的には「カメラ等、外部アプリによって生成されたデータを取得すること」であったり、「編集等で生成したデータの外部アプリへの共有」のような処理です。
そこで、本セッションのゴールは以下の2点で定義しました。
- ContentResolverを用いたメディアアクセスについて理解すること
- 後述するgoogle/modernstorageによるメディアアクセス手法の変化を垣間見ること
これらによって、今開発されているアプリ、開発することになるアプリにメディアアクセスを伴う機能があるとき、その実装をより最新に即した状態でスマートに行うことができるようになることを目指します。
Androidで扱われてきたメディアアクセスの手法
先述の通り、トピックを「外部アプリとのメディア共有」に絞ったので、今回扱うのはあくまで「メディアファイルのURIを取得し、それを外部アプリとやり取りが可能にできる状態にすること」になります。
まず、AndroidではIntentを用いることで外部アプリを呼び出し、その結果をonActivityResultで受け取ることができます。つまり、Android初期から実装されている ACTION_GET_CONTENT
やAndroid 4.4以降から追加された ACTION_OPEN_DOCUMENT
といったIntentを利用することでギャラリーアプリやStorage Access FrameworkによるシステムUI経由でファイルのURIを取得することができました。
そしてもう一方で、今回メインで取り扱うContentResolver、MediaStore APIを利用してアプリ内に独自実装する手法もAndroid初期から存在します。Androidはシステムによるスキャンで画像は MediaStore.Images
、動画は MediaStore.Videos
、…といった具合にコレクションされているので、これにContentResolverを用いてアクセスする、というわけです。
実際にMediaStoreのデータが保管されるのはContentProviderという、アプリ間をまたがったデータ共有を行うフレームワークです。ここにSQLのテーブルのような形でメディアファイルの情報がコレクションされていきます。そこに対してContentResolverを用いてアクセスし、クエリや保存、更新のような操作を実現しています。
Androidにおけるメディアアクセスで備えておくべき知識
Content URI/File URIの扱いやAndroid 6で実装されたRuntime Permissions等、様々なバージョンアップでAndroidでのメディアアクセス手法は変化してきましたが、特に大きなトピックはAndroid 10以降で実装された、Scoped Storageでしょう。これによって、ファイルアクセスに対して要求する権限がファイルの置き場によるものからアプリ固有のファイル/メディア/ドキュメントやファイルといったファイルの利用目的によるものに変更されました。
Android 10まででは requestRegacyExternalStorage
をAndroidManifest.xml上でtrueにすることでオプトアウトすることが可能でしたが、Android 11のタイミングでこれが無視され強制的にScoped Storageが適用されるので、今アップデートを控えていたり、新規にメディアアクセスを実装する際には注意が必要です。
ContentResolverとMediaStore APIによるメディアアクセスの実装
ここまで紹介したところで本題として、ContentResolverとMediaStore APIを用いたメディアアクセスの実装例を見ていきます。本セッションでは次に示す2点について解説したので、それぞれについて見ていきましょう。
- 端末内の画像ロード
- 編集後等の画像の新規保存
端末内の画像ロード
流れとしては
- ContentResolverからContentProviderへのクエリを発行する
- ContentResolverのクエリ結果からデータ(ContentスキーマURI)を取り出す
- 実際に取得したデータを表示させる
のような流れです。まずはContentResolverからContentProviderへのクエリを発行する箇所を見ていきましょう。前提として、ContentResolverが発行するクエリはSQL文に相当して考えることができます。それを踏まえて、クエリの発行に必要なProjection、Selection、SortOrderの設定手法を見ていきます。
まず、ProjectionはSQL文におけるSELECTの直後に続くカラム指定に相当します。なので、ここで指定したカラムの値がクエリ結果に乗ることになります。
実際にProjectionが指定する内容は MediaStore.(Images/Videos/...).Media
下に様々なインターフェースの実装として用意されており、ここから参照することになります。
続いてSelectionはArgと組み合わせることでSQL文におけるWHERE句に相当する条件指定が可能です。一方で、SortOrderはSQL文に於けるORDER BY句に相当するクエリ結果の順序指定が可能です。
こちらも、Projectionに渡したものと同様のカラムを参照し、Selectionであれば不等号などを用いた判定式であったり、SortOrderであればASC/DESCのようなその値に関する条件値を指定することになります。
ここまで揃ったタイミングで、ようやくクエリを発行することが可能になります。実際にはContentResolver#queryメソッドでクエリを発行し、このメソッドの引数に先程用意したProjection/Selection/SortOrderを与えていくのですが、第一引数にCollectionというワードが出てきました。CollectionはSQL文に於けるFROM句に相当するもので、クエリ対象となるコレクションのURIを与えます。
今は画像をロードしているので、MediaStore.ImagesのURIを選ぶ必要があります。Android 10未満では MediaStore.Images.Media.EXTERNAL_CONTENT_URL
を参照してURIを直接取得すれば良かったのですが、Android 10以降では MediaStore.Images.Media.getContentUri
メソッドに MediaStore.VOLUME_EXTERNAL
という値を渡すことが推奨されています。
ここまで行えば実際のクエリ結果が得られるので、結果が入っているCursorインスタンスを操作して必要なデータの抽出を行うことになります。Cursorインスタンスを操作する中で、カラムインデックスは共通で扱うことができるため、操作中に何度も呼び出すことを避ける目的でキャッシュをまずは行いましょう。
続いて、キャッシュしたカラムインデックスを用いて必要なデータを抽出していきます。
基本はカラムインデックスを与えてgetString/getIntのようなメソッドを実行することでファイル名、ファイルサイズといったデータを抽出しますが、メディアそのものを指すContentスキーマURIは ContentUris.withApendedId
メソッドを使って生成しましょう。
ここまでやれば、ContentスキーマURIをUIまで持ってくることができるので、実際に画像を表示させることができるようになります。サムネイルとしてのBitmapの取得の方法であったり、多すぎる画像のロードのためのページングロードについてもスライドのNOTEで解説しているので是非スライドや発表動画も参照してみてください。
編集後等の画像の新規保存
メディアは読み込むだけではなく、新規に保存することももちろんユースケースに入ります。ここでは編集後の画像を新しい画像として保存し、ContentResolverがクエリできる形にする手法を紹介しました。具体的な流れとしては、
- ファイル名等の保存に必要な要素を用意する
- ファイル保存先のURIを用意する
- 用意したURIにファイルの中身を書き込む
という流れになります。1に上げた保存に必要な要素の用意については、ファイル名を生成したり、既存のファイルと同じファイルのMimeTypeを用意したりすることなので省略し、2に上げたファイル保存先にURIの生成について解説します。発表内容ではアプリ固有のファイルとしての保存についても紹介しましたが、今回はMediaStoreに登録したURIを取得する手法に絞って解説します。
MediaStoreに登録したURIを発行することで、「スキャンされなくともMediaStoreにそのメディアが存在する」状況を作ることができます。実際にContentResolver#insertメソッドでそのURIを生成することができるのですが、その前段階として、ContentValuesを用意し、そこに先程用意したファイル名等の要素をputしていく操作が必要です。
また、Android 10以降、ContentValuesに追加できるカラムとして IS_PENDING
が追加されました。これを1にすることによって「ファイル操作中は他のアプリに対して表示されない」といったことを行うことが可能になります。操作後は0に戻すことで、他のアプリからのアクセスができる状態にしてあげましょう。
また、URIの生成にも対象となるMediaStoreのコレクションのURIを指定する必要があります。ほとんどクエリと同様の指定なのですが、クエリと違って、今回はAndroid 10以降で MediaStore.VOLUME_EXTERNAL_PRIMARY
を MediaStore.Images.Media.getContentUri
メソッドに渡しています。 MediaStore.VOLUME_EXTERNAL
、 MediaStore.VOLUME_EXTERNAL_PRIMARY
には、それぞれ読み取り専用、読み書き可能なURIとしての使い分けがあるので、一緒くたにしないようにしましょう。
ここまでで保存先のURIが生成できたので、後はOutputStreamなどを使ってファイルの中身を書き込めば、保存は完了です。この時点でMediaStoreに登録されたURIに保存しているので、画像のクエリを再実行すると保存した画像がすぐに表示される、といった状態が実現できるようになります。
google/modernstorage を覗き見る
最後に google/modernstorage を未来としてみていきましょう。これはAndroidのDevRelチームがストレージチームと協力して開発しているライブラリ群です。原状だとMediaStore/Storage Access Frameworkそれぞれに向けて実装が進んでいますが、今回は実装例でも取り上げたMediaStore用の実装であるmodernstorage-mediastoreについて見ていきます。
modernstorage-mediastore
modernstorage-mediastoreはMediaStoreの扱いを抽象化したレイヤーを提供するライブラリで、使用者はライブラリ中にある MediaStoreRepository
を操作することでこれまで説明したScoped Storageに伴うバージョンごとの実装差異といった複雑な部分を考慮せずメディアアクセスを実装することができるようになります。
例えば、ModernStorageに伴うメディアアクセス周りのパーミッションチェックを行うヘルパーメソッドであったり、
今回実装例で説明したようなメディアストアに登録するURIを発行しつつ新規ファイルとして保存するような処理を addMediaFromStream
で1メソッドで実装することができるようになるなど、複数のメディアアクセスにまつわる処理が実装されています。
終わりに
本セッションではAndroidのバージョン遷移に伴うメディアアクセスの変更を見ていきながら必要な知識を抑え、実際のContentResolverとMediaStore APIを用いたメディアアクセスの実装例を解説し、メディアアクセスの未来として開発が進められている google/modernstorage を紹介しました。AndroidにおけるメディアアクセスはOSバージョンアップに伴う変更が大きく行われることが多く、いざアップデートするタイミングで「ここの改修が必要なのでは」と判明することがそこそこ発生するかと思います。しかし、SNSのようなユーザの画像・動画を扱うことがコア機能に扱われるようなサービスではユーザ体験に大きく関わるため、細かくキャッチアップを行い、今後も適宜必要な改修を施し、ユーザに適切なメディアアクセスを提供できるよう、頑張っていきましょう。
余談:DroidKaigi 2021に参加して気になったセッション
今回のDroidKaigi 2021ではスピーカーとして参加しましたが、聞いていて興味深かったセッションをいくつか紹介したいと思います。
持続的なサービス提供のための計測と分析
サービスの機能改善を行うときに必要な効果検証について解説されたセッションです。エンジニアの方面からも施策の改善提案だったり、自発的に施策をすることもあるので、何を以て仮設を立てるのか、どのような計画をたてるのか、これらを踏まえて何を目標とするのか、実際に結果をどう解釈して効果を検証するのか、その結果を受けてどう改善していくのかを事実ベースで実現していく重要性が実例を踏まえてよく感じられるセッションだったと思います。
2021年こそアクセシビリティと向き合おう
Androidアプリにおけるアクセシビリティの実装について解説されたセッションです。僕が開発を担当しているAmebaはブログを「読んでもらう・書いてもらう」サービスなので、アクセシビリティがとても大事な要素となっており、興味深く聞いていました。動画はDroidKaigi 2021中では語られていない部分も含む完全版となっていて、デザインレベルでの方針であったり、ContentDescriptionの定義であったりといった基礎的な部分からJetpack Compose環境下におけるアクセシビリティ実装まで解説されているので、DroidKaigi 2021本会中でお話を聞いたよ、という方にも是非もう一度おすすめしたいセッションだと思います。
【PR】カジュアル面談やってます
末筆になりますが、サイバーエージェントはDroidKaigi 2021に協賛し、ゴールドスポンサーをさせていただきました。スタッフの皆様、DroidKaigi 2021の運営お疲れさまでした!そしてありがとうございました!
DroidKaigi 2021 では、GOLDスポンサーとして株式会社サイバーエージェント様 @ca_developers にご協力いただいています!
At this time, CyberAgent, Inc. @ca_developers supports us as GOLD Sponsor of DroidKaigi 2021.#DroidKaigi
— DroidKaigi (@DroidKaigi) September 28, 2021
また、私が所属しているAmebaを始め、サイバーエージェントには様々なサービスが存在しています。どのプロダクトも絶賛Androidエンジニア採用中でして、弊社社員とのカジュアル面談をやっております。下記ツイートのようなmiroをご用意しており、色々お話ができればと思いますので、是非下記ツイートのHRMOSリンクからご応募いただけますと幸いです!
現在CyberAgentではAndroidエンジニアの積極採用を行っています🙌今回からABEMAやAmebaなど、募集中のプロダクト情報や開発体制などをMiroにて一挙大公開します。この情報を見られるのはカジュアル面談だけ!
少しでも興味を持ってくださった方はぜひご応募ください😊https://t.co/uJZy74TKiW pic.twitter.com/y8XeehCTjm— CyberAgentDevelopers (@ca_developers) October 21, 2021