dely tech blog

クラシル・TRILLを運営するdely株式会社の開発ブログです

思った以上に大変だったクラシルでの Scoped Storage 対応

f:id:rnitame:20201211113547p:plain

本記事は dely #1 Advent Calendar の 23 日目の記事です。

adventar.org

dely #2もあるのでこちらも見てみてください!

adventar.org

こんにちは、Android エンジニアの tummy です。

昨日はうめもりさんの「Androidも宣言的UI(が当たり前になりそうな)時代に非宣言的UIライブラリでこの先生きのこるには」でした。 絶賛このアーキテクチャで実装中ですが、以前より diff が少なくなったのとどこに何を書けばいいのかが明確になってとても捗ってます。ただ、まだ悩みポイントもあるのでこれから潰していきたいと思ってます 😎

さて、タイトルにある通りクラシルでも先日 Scoped Storage 対応をしました。

(今回は Scoped Storage についての説明は省きます、ドキュメントをぜひ参照してください 💁‍♀️)

developer.android.com

MediaStore 使ったりいい感じに権限つければいいんじゃないの?と軽く考えてたんですが、思ったより大変だったのでやったことを書いていこうと思います!

クラシルでの画像使用部分

クラシルでは以下の 2 箇所で画像を用います。

たべれぽ プロフィール
f:id:rnitame:20201211175341p:plain f:id:rnitame:20201211175402p:plain

カメラで撮影したものをそのまま使用できる他、ギャラリーから選択できるようになっています。 たべれぽのみ、フィルターをかけたりクロップすることもできます。

流れとしては以下の図のとおりになります。

f:id:rnitame:20201214173950p:plain

起きていた事象

切り抜きの画面でファイルが見つけられずに無限にローディング状態になっていました。(お問い合わせしていただいたユーザーさんのおかげです、ありがとうございます!)

またこの状態になると、謎のグレーな画像が端末内に保存されることがわかりました。

ローディングが終わらない 謎のグレーな画像たち
f:id:rnitame:20201211135054p:plain f:id:rnitame:20201211135409p:plain

当初は 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/~}

stackoverflow.com

そのためキャッシュを作り、その 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ではエンジニアを絶賛募集中です。ご興味のある方はぜひこちらのリンクを覗いてみてください。 カルチャーについて説明しているスライドや、過去のブログもこちらから見ることができます。

join-us.dely.jp

delyの開発部では定期的にTechTalkというイベントを開催し、クラシル開発における技術や組織に関する内容を発信しています。
クラシルで使われている技術や、エンジニアがどのような働き方をしているのか少しでも気になった方はぜひお気軽にご参加ください!

bethesun.connpass.com