Androidでアプリ外のスクリーンショットを撮る

# はじめに
Androidアプリで他のアプリやホーム画面などのスクリーンショットを撮りたい事案が発生したので、やり方を記しておきます。

# 動作環境
Kotlin 1.6.21
AndroidStudio Bumblebee(2021.1.1)
CompileSdkVersion 32

# 注意点
設定アプリでは画面上にViewを出せないので、この記事の方法ではスクリーンショットを撮れません。

# サンプルアプリ
記事内では説明のため省略している箇所がたくさんありますので、実際に試す場合はぜひサンプルアプリを見てみてください。
[ScreenshotOutsideApp](https://github.com/mizu9855/ScreenshotOutsideApp)

# 実装
## Permission設定
AndroidManifestに以下のPermissionを追加しておきます。
```AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xxx.yyy.zzz">
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
</manifest>
```
## アプリを閉じてもViewを表示できるようにする
アプリを閉じてもスクリーンショット用のボタンを表示しておくために、`Service`を使用して常に表示されるFloatingViewをつくります。

### 1. View生成
```kotlin
private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private val view = ImageView(context).apply {
    setImageResource(R.drawable.ic_baseline_photo_camera_24)
    setBackgroundResource(R.drawable.shape_circle_filled)
    val padding = (16 * resources.displayMetrics.density).toInt()
    setPadding(padding, padding, padding, padding)
}
private val params = WindowManager.LayoutParams(
    WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
    PixelFormat.TRANSLUCENT
)
```
`TYPE_APPLICATION_OVERLAY`を使用することで、どのActivityよりも上に表示されますが、ステータスバーやIMEなどの重要なシステムウィンドウよりは下位に表示されます。

### 2. Notification生成
バックグラウンドでServiceが起動していることをユーザーに知らせるため、Notification（通知）を設定します。
```kotlin
val id = "floating_view_channel"
val name = "Floating View"
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
val notificationManager = getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)

val activityIntent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    PendingIntent.getActivity(this, 0, activityIntent, PendingIntent.FLAG_MUTABLE)
} else {
    PendingIntent.getActivity(
        this,
        0,
        activityIntent,
        PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
    )
}
val notification = Notification.Builder(this, id)
    .setContentIntent(pendingIntent)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setContentTitle(FloatingViewService::class.simpleName)
    .setContentText("スクリーンショット用ボタン表示中")
    .build()
```
### 3. Service起動
ActivityからServiceを起動しますが、起動前にOverlayのPermissionが許可されているかを確認します。
許可されていなかった場合、設定画面に飛ばすようにしています。
```MainActivity.kt
private var launcher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) {
    startFloatingViewService()
}

private fun startFloatingViewService() {
    if (Settings.canDrawOverlays(this)) {
        val serviceIntent = Intent(this, FloatingViewService::class.java)
            .setAction(FloatingViewService.ACTION_START)
        startForegroundService(serviceIntent)
    } else {
        val intent = Intent(
            Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
            Uri.parse("package:$packageName")
        )
        startActivity(intent)
        launcher.launch(intent)
    }
}
```
起動できたら、Serviceの`onStartCommand()`で先ほど設定したNotificationをスタートさせ、FloatingViewを表示します。
```FloatingViewService.kt
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    intent?.let {
        startForeground(notificationId, notification)
        windowManager.addView(view, params)
    }
    return Service.START_STICKY
}
```

## スクリーンショット実行
### 1. MediaProjectionManager取得
`Service.MEDIA_PROJECTION_SERVICE`を使ってMediaProjectionManagerを取得します。
```kotlin
mediaProjectionManager = getSystemService(Service.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
```

### 2. MediaProjectionのPermission許可のダイアログを表示
`startActivityForResult()`の第一引数に`mediaProjectionManager.createScreenCaptureIntent()`を渡して、Permission許可のダイアログを出します。
```kotlin
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE)
```

### 3. Service起動
上記のダイアログでのユーザーの選択を`onActivityResult()`でキャッチし、許可されていたらServiceを起動します。
このとき、`resultCode`と`data`をとっておきます。あとでMediaProjectionを取得するときに必要になります。
```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CAPTURE) {
        mResultCode = resultCode
        mData = data

        if (resultCode == Activity.RESULT_OK) {
            val captureServiceIntent = Intent(this, CaptureService::class.java)
                .setAction(CaptureService.ACTION_START)
            startService(captureServiceIntent)

        }
    }
}
```

### 4. Notification生成
FloatingViewと同じようにNotificationを設定します。
```kotlin
val id = "capture_service_channel"
val name = "Capture Service"
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT)
val notificationManager = getSystemService(Service.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)

val activityIntent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    PendingIntent.getActivity(this, 0, activityIntent, PendingIntent.FLAG_MUTABLE)
} else {
    PendingIntent.getActivity(
        this,
        0,
        activityIntent,
        PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
    )
}
val notification = Notification.Builder(this, id)
    .setContentIntent(pendingIntent)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setContentTitle(CaptureService::class.simpleName)
    .setContentText("スクリーンショットサービス起動中")
    .build()

startForeground(notificationId, notification)
```

### 5. MediaProjection取得
先ほどとっておいた`resultCode`と`data`を使って、MediaProjectionを取得します。
```kotlin
mediaProjection = mediaProjectionManager.getMediaProjection(mResultCode, mData)
```

### 6. FloatingViewタップ時にMediaProjectionを使ってスクリーンショット実行
```kotlin
mediaProjection?.let { mediaProjection ->
    capture.run(mediaProjection) { bitmap ->
        // サンプルアプリでは端末に保存するところまで実装してあります
        saveBitmap(bitmap)
        capture.stop()
    }
}
```

```Capture.kt
class Capture(private val context: Context) : ImageReader.OnImageAvailableListener {

    private var display: VirtualDisplay? = null
    private var onCaptureListener: ((Bitmap) -> Unit)? = null

    override fun onImageAvailable(reader: ImageReader) {
        if (display != null) {
            onCaptureListener?.invoke(captureImage(reader))
        }
    }

    fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) {
        this.onCaptureListener = onCaptureListener
        if (display == null) {
            display = createDisplay(mediaProjection)
        }
    }

    fun stop() {
        display?.release()
        display = null
        onCaptureListener = null
    }

    private fun createDisplay(mediaProjection: MediaProjection): VirtualDisplay {
        context.resources.displayMetrics.run {
            val maxImages = 2
            val reader = ImageReader.newInstance(
                widthPixels, heightPixels, PixelFormat.RGBA_8888, maxImages
            )
            reader.setOnImageAvailableListener(this@Capture, null)
            return mediaProjection.createVirtualDisplay(
                "Capture Display", widthPixels, heightPixels, densityDpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                reader.surface, null, null
            )
        }
    }

    private fun captureImage(reader: ImageReader): Bitmap {
        val image = reader.acquireLatestImage()
        context.resources.displayMetrics.let { displayMatrics ->
            image.planes[0].run {
                // PixelFormat.RGBA_8888がエラーになる場合があるが、ビルドできる
                val bitmap = Bitmap.createBitmap(
                    rowStride / pixelStride, displayMatrics.heightPixels, Bitmap.Config.ARGB_8888
                )
                bitmap.copyPixelsFromBuffer(buffer)
                image.close()
                return bitmap
            }
        }
    }
}
```

# おわりに
参考になるページはありましたが過去のものが多く苦労しました。。
誰かの助けになれば幸いです！

# 参考ページ
[[Android] WindowManagerを使ってServiceから画像を表示させ続ける](https://akira-watson.com/android/windowmanager.html)
[常に画面の最前面に表示されたままになる View を作る (TYPE_APPLICATION_OVERLAY)](https://maku77.github.io/android/ui/always-top.html)
[フローティングアプリを作るためのはじめの一歩](https://nosix.hatenablog.com/entry/2016/09/16/175238)
[Foreground Serviceの基本](https://qiita.com/naoi/items/03e76d10948fe0d45597)
[ANDROID 5.0 アプリからスクリーンショットを撮影する](https://techbooster.org/android/application/17026/)
[画面のスナップショットを撮影する](https://nosix.hatenablog.com/entry/2016/09/18/180128)
[【kotlin】Androidでスクリーンショットを実装する方法（サンプルプロジェクトあり）](https://3jigen.net/2018/05/post-720/)