本記事は dely #1 Advent Calendar の 23 日目の記事です。
dely #2もあるのでこちらも見てみてください!
こんにちは、Android エンジニアの tummy です。
昨日はうめもりさんの「Androidも宣言的UI(が当たり前になりそうな)時代に非宣言的UIライブラリでこの先生きのこるには」でした。 絶賛このアーキテクチャで実装中ですが、以前より diff が少なくなったのとどこに何を書けばいいのかが明確になってとても捗ってます。ただ、まだ悩みポイントもあるのでこれから潰していきたいと思ってます 😎
さて、タイトルにある通りクラシルでも先日 Scoped Storage 対応をしました。
(今回は Scoped Storage についての説明は省きます、ドキュメントをぜひ参照してください 💁♀️)
MediaStore 使ったりいい感じに権限つければいいんじゃないの?と軽く考えてたんですが、思ったより大変だったのでやったことを書いていこうと思います!
クラシルでの画像使用部分
クラシルでは以下の 2 箇所で画像を用います。
たべれぽ | プロフィール |
---|---|
カメラで撮影したものをそのまま使用できる他、ギャラリーから選択できるようになっています。 たべれぽのみ、フィルターをかけたりクロップすることもできます。
流れとしては以下の図のとおりになります。
起きていた事象
切り抜きの画面でファイルが見つけられずに無限にローディング状態になっていました。(お問い合わせしていただいたユーザーさんのおかげです、ありがとうございます!)
またこの状態になると、謎のグレーな画像が端末内に保存されることがわかりました。
ローディングが終わらない | 謎のグレーな画像たち |
---|---|
当初は Intent の渡し方が悪かったのかな?と思ったのですが、よくよく調べていくと Scoped Storage に対応しないとだめだということがわかり、着手しました。
行った対応
カメラかギャラリーか判定見直し
ギャラリーを起動する際のコードを以下のようにしました。
val intentGallery = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) intentGallery.type = "image/jpeg"
こうするとギャラリーの Intent の情報に data がセットされるので、これがあるかないかで判定ができるようになります。 今までは変数として保持している Uri があるかないかで判断していたので、このロジックを変更しました。
FileProvider の活用
Uri.fromFile()
を使うと共有の制限にひっかかるのと、権限をアプリ内で使うパスに渡すために FileProvider を使います。
qiita.com
<?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external_files" path="." /> <cache-path name="cache" path="." /> </paths>
private fun getUriFromFile(context: Context, file: File): Uri { return FileProvider.getUriForFile( context, BuildConfig.APPLICATION_ID + ".provider", file ) }
キャッシュを作ってそれの Uri を使い回す
Google Photos 経由で画像を選ぶと Uri が以下のような形式になり、Google Photos の ContentsProvider に対して権限をもらえないためエラーになります。
content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F19018/ORIGINAL/NONE/~}
そのためキャッシュを作り、その Uri を使い回すことで対応しました。
OkHttp の RequestBody に content スキームの Uri をのせるためのクラスを作る
普通のファイルであれば MultipartBuilder などを使って渡せましたが、content スキームの場合は渡せないのでひと手間加える必要がありました。
そのため以下のような RequestBody を拡張したクラスを作り、渡すようにしました。
class ImageRequestBody( private val context: Context, private val uri: Uri ) : RequestBody() { override fun contentType(): MediaType? = MediaType.parse("image/jpeg") override fun writeTo(sink: BufferedSink) { val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r") ?: return Okio.source(FileInputStream(parcelFileDescriptor.fileDescriptor)).use { sink.writeAll(it) } parcelFileDescriptor.close() } }
初期対応で足りなかったところ
ドキュメントを開く Intent も投げる
画像の Intent のみだと、Galaxy などでデフォルトで入っているギャラリーアプリが認識されないことがわかりました。そのためドキュメントの Intent を使い、type を画像にしぼって開くことで認識してくれるようになったので、この対応を行いました。
val intentDocument = Intent(Intent.ACTION_OPEN_DOCUMENT) intentDocument.type = "image/*" intentDocument.addCategory(Intent.CATEGORY_OPENABLE)
できてないこと
カメラで撮った際に画像を端末内に反映させていたのですが、その処理を消したのでできてません。冒頭に少し触れた、実装中のリアーキテクチャでできるといいな…と思っています。
さいごに
delyではエンジニアを絶賛募集中です。ご興味のある方はぜひこちらのリンクを覗いてみてください。 カルチャーについて説明しているスライドや、過去のブログもこちらから見ることができます。
delyの開発部では定期的にTechTalkというイベントを開催し、クラシル開発における技術や組織に関する内容を発信しています。
クラシルで使われている技術や、エンジニアがどのような働き方をしているのか少しでも気になった方はぜひお気軽にご参加ください!