【Android】Viewのスクリーンショットを撮影してギャラリーアプリで見れるようにする

# 背景
画面内に表示されている特定の情報を画像として保存し、後から見返せるようにしてユーザー利便性を向上させたいという要件がありました。(例えば、ゲームアプリのユーザー情報だったり、乗換案内アプリの検索結果だったり。)

この記事ではこの要件の実現方法について、下記のポイントを解説いたします。

- スクリーンショットの撮影方法
- ギャラリーへの反映方法

# コード解説
全てのソースコードはGithubで公開しています。
クローンして手元で動かしてみると処理の流れがイメージしやすいと思います。
https://github.com/umechanhika/playground/tree/master/android/AddScreenshotToGallery

今回のポイントとなるコードは、スクリーンショットの撮影から保存までを行なっている[ScreenshotUtil.kt](https://github.com/umechanhika/playground/blob/master/android/AddScreenshotToGallery/app/src/main/java/com/example/addscreenshottogallery/ui/util/ScreenshotUtil.kt)です。

```kotlin:ScreenshotUtil.kt
package com.example.addscreenshottogallery.ui.util

import android.content.ContentResolver
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.view.View
import androidx.core.view.drawToBitmap
import java.io.File
import java.io.FileOutputStream

object ScreenshotUtil {
  private const val DIRECTORY_NAME = "AddScreenshotToGallery"
  private const val FILE_EXTENSION = ".jpg"

  fun take(
    contentResolver: ContentResolver,
    targetView: View,
    fileName: String,
    completion: (Bitmap, Uri) -> Unit
  ) {
    // 対象のViewをBitmapに書き出します、スクリーンショットの撮影処理はこれだけです
    val bitmap = targetView.drawToBitmap()
    targetView.draw(Canvas(bitmap))

    // ギャラリーへの反映時に必要な情報を格納している値です
    // AndroidQ以降とそれより前で設定可能なKeyが異なるため、
    // ここでは共通部分のみ生成してif文内でそれぞれに必要な値を追加しています
    val contentValues = createContentValues(fileName)
    val uri: Uri

    if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) {
      contentValues.apply {
        // 画像の保存先を指定します（AndroidQより前と指定方法が異なります）
        put(
          MediaStore.Images.Media.RELATIVE_PATH,
          "${Environment.DIRECTORY_PICTURES}/$DIRECTORY_NAME"
        )
        put(MediaStore.MediaColumns.IS_PENDING, 1)
      }

      contentResolver.run {
        uri = insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
          ?: return

        // AndroidQ以降では画像の書き出し前にギャラリーへの登録(正確に言うとMediaStoreへの登録)を
        // 済ませてから該当Uriに画像を書き出します（処理の順番がAndroidQより前と異なります）
        openOutputStream(uri).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }

        contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
        update(uri, contentValues, null, null)
      }
    } else {
      // AndroidQより前の処理では一部メソッドがDeprecatedになっていますが、
      // Q以降の処理に互換性がないためSDK_INTによって処理を分岐させています

      // スクリーンショット画像書き出し用のディレクトリ・ファイルを準備
      val directory = File(
        // ファイルの書き出し先はいくつか候補がありますが、
        // アプリを削除してもファイルが消えない、外部アプリからアクセス可能という理由から、
        // getExternalStoragePublicDirectoryを選択しています
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
        DIRECTORY_NAME
      )
      if (!directory.exists()) directory.mkdirs()
      val file = File(directory, "$fileName$FILE_EXTENSION")

      // Bitmapをファイルに書き出します
      FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }

      // 書き出した画像をギャラリーに反映させています
      contentResolver.run {
        contentValues.put(MediaStore.Images.Media.DATA, file.absolutePath)
        uri = insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return
      }
    }

    completion(bitmap, uri)
  }

  /**
   * [Build.VERSION_CODES.Q]以上とそれより前で設定できる値が違うため、ここでは共通部分のみ設定
   */
  private fun createContentValues(name: String) = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "$name$FILE_EXTENSION")
    put(MediaStore.Images.Media.TITLE, name)
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1_000)
  }
}
```

# GIF
![screen_record.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136368/1714aa61-a869-0d6d-8d15-3da87dc2843d.gif)

# 参考リンク
AndroidQからのストレージアクセスに関する変更点や、MediaStoreの保存処理の変更点についての情報を記載してくださっています
https://medium.com/@star_zero/android-q%E3%81%AEscoped-storage%E3%81%AB%E3%82%88%E3%82%8B%E5%A4%89%E6%9B%B4-afe41cde9f35
https://medium.com/@mag0716/scoped-storage-%E8%AA%BF%E6%9F%BB%E3%83%A1%E3%83%A2-8a13c70d3ec8

Androidのストレージに関する公式ドキュメントです（日本語ページでは一部情報が足りていない場合があるため、言語設定は英語で閲覧することをおすすめします）
https://developer.android.com/training/data-storage

AndroidQ以降とそれより前のバージョンでの実装の違いについて記載されています
https://stackoverflow.com/questions/56904485/how-to-save-an-image-in-android-q-using-mediastore