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。在测试过程中,发现对低版本设备不兼容,后续会进行低版本机型的适配。

相关推荐
佛系打工仔4 小时前
绘制K线第二章:背景网格绘制
android·前端·架构
my_power5207 小时前
车载安卓面试题汇总
android
csj507 小时前
安卓基础之《(15)—内容提供者(1)在应用之间共享数据》
android
yeziyfx8 小时前
kotlin中 ?:的用法
android·开发语言·kotlin
2501_915918419 小时前
只有 Flutter IPA 文件,通过多工具组合完成有效混淆与保护
android·flutter·ios·小程序·uni-app·iphone·webview
robotx9 小时前
AOSP 设置-提示音和振动 添加一个带有开关(Switch)的设置项
android
青莲8439 小时前
RecyclerView 完全指南
android·前端·面试
青莲8439 小时前
Android WebView 混合开发完整指南
android·前端·面试
龙之叶10 小时前
【Android Monkey源码解析三】- 运行解析
android
KevinWang_11 小时前
Android 的 assets 资源和 raw 资源有什么区别?
android