【Android】写真選択ツールを使おう【ポリシー対応】

## Google Playの写真と動画の権限に関するポリシー

Androidアプリ開発者の皆様、[Google Playの写真と動画の権限に関するポリシーの詳細](https://support.google.com/googleplay/android-developer/answer/14115180?sjid=13742031292945085590-AP#zippy=%2C%E5%86%99%E7%9C%9F%E3%82%84%E5%8B%95%E7%94%BB%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%B8%80%E5%BA%A6%E3%81%A0%E3%81%91%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%81%BE%E3%81%9F%E3%81%AF%E9%A0%BB%E7%B9%81%E3%81%AB%E3%81%AF%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%AA%E3%81%84%E3%81%A8%E3%81%AF%E3%81%A9%E3%81%86%E3%81%84%E3%81%86%E6%84%8F%E5%91%B3%E3%81%A7%E3%81%99%E3%81%8B%2Cread-media-images-%E6%A8%A9%E9%99%90%E3%82%84-read-media-video-%E6%A8%A9%E9%99%90%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%AE%E3%81%AF%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%AA%E5%A0%B4%E5%90%88%E3%81%A7%E3%81%99%E3%81%8B)の対応は進んでいますか？

写真や動画を頻繁に利用しないアプリでは、 `READ_MEDIA_IMAGES`と `READ_MEDIA_VIDEO`をアプリから削除する必要があります。

> 写真や動画のファイルを一度だけ使用する、または頻繁には使用しないとはどういう意味ですか？
>>写真や動画のファイルを一度だけ使用する、または頻繁には使用しないとは、たとえば、プロフィール写真のアップロード、プレイリスト用の画像のアップロード、銀行取引を目的とした小切手の写真のアップロードなどが該当します。
>>
>>「頻繁には使用しない」とは、アプリのコア機能として写真や動画のユースケースがないということです。アプリのユースケースが写真や動画のファイルを一度だけ使用するものの場合、または頻繁には使用しないものの場合は、READ_MEDIA_IMAGES 権限や READ_MEDIA_VIDEO 権限を使用できません。代わりに、ユーザーのプライバシーを保護するため、システムの選択ツールを利用することを推奨します。
>>
>>頻繁には使用しない、または一度だけ使用するユースケースには、ソーシャル、コミュニケーション、写真 / 動画エディタのカテゴリに属するアプリや、幅広いアクセスへのニーズを実証できるアプリ（例: 写真エディタ、ユーザー作成コンテンツ プラットフォーム、画像検索機能、QR コードスキャナ）などがあります。

> READ_MEDIA_IMAGES 権限や READ_MEDIA_VIDEO 権限にアクセスできるのは、どのような場合ですか？
>> コア機能として写真や動画への幅広いアクセスが必要となるアプリでは、上記の権限を使用できます。こうした権限を使用できるアプリとしては、写真や動画を管理するアプリやギャラリー アプリが一般的です。

このポリシーへの対応を絶賛進行中なので、備忘録的に書き残そうと思います。

## 写真選択ツール
`READ_MEDIA_IMAGES` 権限や `READ_MEDIA_VIDEO` 権限を削除しつつ、端末のストレージの写真や動画にアクセスするにはどうしたらいいの？と思うかもしれませんが、もちろん代替案は用意されています。

ポリシーの詳細にも書かれていますが、Androidでは[写真選択ツール](https://developer.android.com/training/data-storage/shared/photopicker?hl=ja)を使うことができます。

これを使うことで、アプリはピッカーで選択した写真・動画のみにアクセスが許可されるようになります。
早速使ってみましょう。

### 1. ライブラリの追加・更新
写真選択ツールを使うには`androidx.activity`のバージョン1.7.0以上が必要です。まずは追加・更新しましょう。

```kotlin:bulid.gradle
dependencies {
    implementation("androidx.activity:activity-ktx:1.7.0")
}
```

### 2. ActivityResultLauncherを定義
写真選択ツールを使うためには、ActivityやFragmentでActivityResultLauncherを定義します。具体的には`registerForActivityResult`を使います。

`registerForActivityResult`に引数には、用途によって異なるコントラクトを与えます。1つの写真・動画を取得する場合は`PickVisualMedia()`を、複数の写真・動画を取得する場合は`PickMultipleVisualMedia()`を引数にします。

Activity の状態が STARTED となる前に定義しないとクラッシュするので注意！

```kotlin:Activity
// 1つの写真・動画を取得する場合
private val pickMedia = registerForActivityResult(PickVisualMedia()) { uri ->
        if (uri != null) {
            // 写真や動画を受け取った後の処理を記述
        }
    }
```
``` kotlin:Activity
// 複数の写真・動画を取得する場合
private val pickMedia = registerForActivityResult(PickMultipleVisualMedia()) { uris ->
        if (uris.isNotEmpty()) {
            // 写真や動画を受け取った後の処理を記述
        }
    }
```
```kotlin:Activity
// 選択可能な写真・動画の数を制限することもできる
// 例えば5個に制限したい場合はPickMultipleVisualMediaの引数に5を与えれば良い
private val pickMedia = registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
        if (uris.isNotEmpty() {
            // 写真や動画を受け取った後の処理を記述
        }
    }
```

### 3. 写真選択ツールを起動
写真や動画を選択させたい場所でLauncherを起動させましょう。`launch`の引数には`PickVisualMediaRequest`を与えますが、取得したいコンテンツの種別（写真 or 動画 or 両方）に応じて、`PickVisualMediaRequest`の引数を指定します。具体的には

- 写真のみ -> PickVisualMedia.ImageOnly
- 動画のみ -> PickVisualMedia.VideoOnly
- 写真と動画の両方 -> PickVisualMedia.ImageAndVideo

のように使い分けます。

```kotlin:Activity
// 写真のみ
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
// 動画のみ
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))
// 写真と動画の両方
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))
```

これで写真選択ツールが起動して写真や動画を選択することができます。この方法だと`READ_MEDIA_IMAGES`と `READ_MEDIA_VIDEO`の権限が必要ないのでこれらの権限を削除できます。やったね！

## 置き換え完了...... ?
以上の方法で簡単に置き換えられる......と良かったんですが、そう簡単にはいかないこともあります。

写真選択ツールで写真や動画を選択した後に返ってくる値はUriなので、既存の実装が写真や動画の選択後の処理をUriで進めているのであればそのまま扱うことができますが、pathを渡して実行しているような場合はUriからpathを取得する必要があります。

`getPath()`などでpathを取得しようとすると、取得したpathでは後続の処理が適切に実行されないこともあると思います。

```kotlin
private val pickerMedia =
    registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
        uri?.let {
            Log.d("testComment", "path = ${uri.path}")
        }
    }
```

```
//　こんな感じで返ってくる
path = /picker/0/com.google.android.apps.photos.cloudpicker/media/c38e2756-bf62-4f99-8e80-71f706728be7-1_all_1333
```

既存の処理を書き換えてもいいですが、結構手間になると思います。
そんなときは`contentResolver`を使って`MediaStore`の`DATA`を参照しましょう。

```kotlin: Activity
private val pickerMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
    uri?.let {
        val path = getPathFromUri(uri)
        Log.d("testComment", "uri = ${path}")
    }
}
    
 private fun getPathFromUri(uri: Uri): String {
    val projection = arrayOf(MediaStore.MediaColumns.DATA)
    val cursor = contentResolver.query(uri, projection, null, null, null)
    var path = ""
        
    cursor?.apply {
        if (moveToFirst()) {
            path = getString(getColumnIndexOrThrow(MediaStore.MediaColumns.DATA))
        }
    }
    cursor?.close()
    return path
}
```

```
// しっかり拡張子もついてくる
path = /sdcard/.transforms/synthetic/picker/0/com.google.android.apps.photos.cloudpicker/media/c38e2756-bf62-4f99-8e80-71f706728be7-1_all_1333.jpg
```

動画の長さ（時間）が欲しい場合は、`MediaStore.MediaColumns.DURATION`を使うと取得することができます。

```kotlin: Activity
private val pickerMedia = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
    uri?.let {
        val duration = getMovieDuration(uri)
        Log.d("testComment", "duration = ${getMovieDuration(uri)}")
    }
}
    
private fun getMovieDuration(uri: Uri): Long {
    val projection = arrayOf(MediaStore.MediaColumns.DURATION)
    val cursor = contentResolver.query(uri, projection, null, null, null)
    var duration = 0L
        
  cursor?.apply {
        if (moveToFirst()) {
            duration = getLong(getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION))
        }
    }
    cursor?.close()
    return duration
}
```

```
// 動画の長さがミリ秒で返ってくる
duration = 4592
```

その他`DISPLAY_NAME`などの情報を取ってくることができるので、必要に応じて指定すれば、既存の処理を大きく変えずともポリシー対応ができると思います。