Android截屏汇报问题

背景

在摇一摇实现截屏问题汇报的过程中,截屏有很多中方案,而通过MediaProjection方案是最通用的,于是对相关功能进行封装,提供给业务侧进行使用。

实现

截屏授权

截屏需要弹出系统授权弹框,用单独的Activity进行启动,以后每次使用可唤起此Activity即可。

复制代码
class HScreenCaptureActivity : ComponentActivity() {

    var projectionManager: MediaProjectionManager? = null
    val screenCaptureLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                //启动前台服务
                startService(Intent(this, HScreenCaptureService::class.java).apply {
                    putExtra("resultCode", result.resultCode)
                    putExtra("data", result.data)
                })
                finish()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        //启动授权
        projectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager?
        projectionManager?.createScreenCaptureIntent()?.let { screenCaptureLauncher.launch(it) }
    }

    override fun onDestroy() {
        sendBroadcast(Intent(H_BROADCAST_ACTION_SCREEN_CAPTURE))
        super.onDestroy()
    }

    /**
     * 清理缓存
     */
    fun cleanCache() {
        val fileParent = File(filesDir.absolutePath, H_FILE_CAPTURE_PARENT)
        if (fileParent.isDirectory && fileParent.exists()) {
            val fileList = fileParent.listFiles { file -> file.name.endsWith(".png", true) }
            fileList?.forEachIndexed { index, file ->
                file.delete()
            }
        }
    }
}

截屏服务

截屏必须有前台服务和通知栏提醒。在服务中依赖MediaProjection进行屏幕的虚拟映射和截图。在接收到截图后保存图片到本地目录提供后后续业务逻辑使用。

复制代码
const val H_SCREEN_CAPTURE_CHANNEL_ID = "h_screen_capture"
const val H_FILE_CAPTURE_PARENT = "capture"

const val H_BROADCAST_ACTION_SCREEN_CAPTURE = "H_BROADCAST_ACTION_SCREEN_CAPTURE"
const val H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT = "H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT"

class HScreenCaptureService : Service() {

    private var notificationManager: NotificationManager? = null
    private var mProjectionManager: MediaProjectionManager? = null
    private var mMediaProjection: MediaProjection? = null
    private var mVirtualDisplay: VirtualDisplay? = null
    private val VIRTUAL_DISPLAY_NAME: String = "ScreenCapture"
    private var mImageReader: ImageReader? = null

    private val H_CAPTURE_NOTIFICATION_ID by lazy { Random.nextInt() }

    private val mediaProjectionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val fileParent = File(filesDir.absolutePath, H_FILE_CAPTURE_PARENT)
            if (!fileParent.exists()) {
                fileParent.mkdir()
            }
            val localFile = File(
                fileParent.absolutePath, "${System.currentTimeMillis()}.png"
            )
            saveScreenCaptureMat(localFile)

            sendBroadcast(Intent(H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT).apply {
                putExtra("image", localFile.absolutePath)
            })
            mMediaProjection?.stop()
            stopForeground(STOP_FOREGROUND_REMOVE)
            stopSelf()
        }
    }

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
        val filter = IntentFilter(H_BROADCAST_ACTION_SCREEN_CAPTURE)
        registerReceiver(mediaProjectionReceiver, filter, RECEIVER_EXPORTED)
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(mediaProjectionReceiver)
    }

    private fun createNotificationChannel() {
        val channel = NotificationChannel(
            H_SCREEN_CAPTURE_CHANNEL_ID, "屏幕捕获服务", NotificationManager.IMPORTANCE_DEFAULT
        )
        notificationManager = getSystemService(NotificationManager::class.java)
        notificationManager?.createNotificationChannel(channel)
    }

    private fun startForegroundService() {
        val notification = NotificationCompat.Builder(this, H_SCREEN_CAPTURE_CHANNEL_ID)
            .setContentTitle("屏幕捕获中").setContentText("应用正在捕获屏幕,您可以在此处查看信息")
            .setSmallIcon(R.drawable.h_icon_checked_icon)
            .setPriority(NotificationCompat.PRIORITY_HIGH).setOngoing(true)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC).build()
        startForeground(H_CAPTURE_NOTIFICATION_ID, notification)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        //FIXME 此处需要兼容低版本
        val resultCode: Int? = intent?.getIntExtra("resultCode", Activity.RESULT_CANCELED)
        val data: Intent = intent?.getParcelableExtra("data", Intent::class.java) as Intent

        startForegroundService()

        mProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager?
        mMediaProjection = mProjectionManager?.getMediaProjection(resultCode ?: 0, data)
        setupVirtualDisplay()

        return START_STICKY
    }

    private fun setupVirtualDisplay() {
        val displayMetrics = resources.displayMetrics
        mImageReader = ImageReader.newInstance(
            displayMetrics.widthPixels, displayMetrics.heightPixels, PixelFormat.RGBA_8888, 2
        )
        mMediaProjection?.registerCallback(object : MediaProjection.Callback() {}, null)
        mVirtualDisplay = mMediaProjection?.createVirtualDisplay(
            VIRTUAL_DISPLAY_NAME,
            displayMetrics.widthPixels,
            displayMetrics.heightPixels,
            displayMetrics.densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            mImageReader?.surface,
            null,
            null
        )
    }

    fun saveScreenCaptureMat(localFile: File) {
        try {
            val image = mImageReader?.acquireLatestImage()
            if (image != null) {
                val planes = image.planes
                val buffer = planes[0].buffer
                val pixelStride = planes[0].pixelStride
                val rowStride = planes[0].rowStride
                val rowPadding = rowStride - pixelStride * (mImageReader?.width ?: 0)

                val bitmap: Bitmap = Bitmap.createBitmap(
                    (mImageReader?.width ?: 0) + rowPadding / pixelStride,
                    mImageReader?.height ?: 0,
                    Bitmap.Config.ARGB_8888
                )
                bitmap.copyPixelsFromBuffer(buffer)

                //写文件
                val os = FileOutputStream(localFile)
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
                os.flush()
                bitmap.recycle()
            }
            image?.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }
}

截屏处理服务记得在清单文件进行声明,特别主要一定要添加android:foregroundServiceType="mediaProjection"。

复制代码
<service
    android:name="com.zhb.devkit.service.HScreenCaptureService"
    android:exported="true"
    android:foregroundServiceType="mediaProjection" />

截屏业务处理

在需要接收的地方进行广播注册和业务处理。

复制代码
registerReceiver(
    object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val localFile: String? = intent?.getStringExtra("image")
            startActivity(Intent(this@MainActivity, FeedbackActivity::class.java).apply {
                putExtra(Constants.PARAMS_KEY_DATA, localFile)
            })
        }
    }, IntentFilter(H_BROADCAST_ACTION_SCREEN_CAPTURE_RESULT), RECEIVER_EXPORTED
)

评价

整体效果可以达到预期,使用顺滑无BUG。在测试过程中,发现对低版本设备不兼容,后续会进行低版本机型的适配。

相关推荐
阿巴斯甜21 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android