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

相关推荐
QING6181 小时前
Jetpack Compose 中 Flow 收集详解 —— 新手指南
android·kotlin·android jetpack
_李小白1 小时前
【Android FrameWork】第二十一天:AudioFlinger
android
向葭奔赴♡1 小时前
Android SharedPreferences实战指南
android·java·开发语言
愤怒的代码1 小时前
一个使用 AI 开发的 Android Launcher
android
北京自在科技1 小时前
Find Hub迎来重大升级,UWB技术实现厘米级精准定位,离线追踪覆盖更广
android·google findhub
悠哉清闲1 小时前
SoundPool
android
鹏多多2 小时前
flutter-使用url_launcher打开链接/应用/短信/邮件和评分跳转等
android·前端·flutter
2501_915921432 小时前
iOS 性能分析工具全景解析,构建从底层诊断到真机监控的多层级性能分析体系
android·ios·小程序·https·uni-app·iphone·webview