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

相关推荐
robotx2 小时前
安卓线程相关
android
消失的旧时光-19432 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon3 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon3 小时前
VSYNC 信号完整流程2
android
dalancon3 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013844 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android5 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才5 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶6 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙6 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github