7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

qnoteAdvent Calendar 2022

Day 5

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

Last updated at Posted at 2022-12-04

はじめに

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

動作環境

Kotlin 1.6.21
AndroidStudio Bumblebee(2021.1.1)
CompileSdkVersion 32

注意点

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

サンプルアプリ

記事内では説明のため省略している箇所がたくさんありますので、実際に試す場合はぜひサンプルアプリを見てみてください。
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生成

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(通知)を設定します。

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を取得します。

mediaProjectionManager = getSystemService(Service.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

2. MediaProjectionのPermission許可のダイアログを表示

startActivityForResult()の第一引数にmediaProjectionManager.createScreenCaptureIntent()を渡して、Permission許可のダイアログを出します。

startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE)

3. Service起動

上記のダイアログでのユーザーの選択をonActivityResult()でキャッチし、許可されていたらServiceを起動します。
このとき、resultCodedataをとっておきます。あとでMediaProjectionを取得するときに必要になります。

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を設定します。

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取得

先ほどとっておいたresultCodedataを使って、MediaProjectionを取得します。

mediaProjection = mediaProjectionManager.getMediaProjection(mResultCode, mData)

6. FloatingViewタップ時にMediaProjectionを使ってスクリーンショット実行

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から画像を表示させ続ける
常に画面の最前面に表示されたままになる View を作る (TYPE_APPLICATION_OVERLAY)
フローティングアプリを作るためのはじめの一歩
Foreground Serviceの基本
ANDROID 5.0 アプリからスクリーンショットを撮影する
画面のスナップショットを撮影する
【kotlin】Androidでスクリーンショットを実装する方法(サンプルプロジェクトあり)

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?