Android 监听系统截屏操作

在Android App中监听系统截屏功能,没有系统标准的监听器或者api可以调用,需要自己实现。针对这个需求,目前大部分实现方案是监听系统的媒体数据库。

原理: 每当产生一张新图片,系统都会把这张图片的详细信息加入到媒体数据库,并发出内容改变通知。

实现: 利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合特定的规则,则认为用户截屏了。
监听两个Uri:

// 内部存储空间的 content:// 格式Uri:
MediaStore.Images.Media.INTERNAL_CONTENT_URI
// 主要外部存储空间 content:// 格式Uri:
MediaStore.Images.Media.EXTERNAL_CONTENT_URI

权限: 开始监听媒体数据库变化之前,需要先获取权限READ_EXTERNAL_STORAGE

因为需要存储权限,所有可能存在相关的法务风险,慎用

具体实现

1、定义内容观察者

class MediaContentObserver constructor(
    handler: Handler?,
    private val mContentUri: Uri,
    private val contentResolver: ContentResolver,
    onScreenShotListener: OnScreenShotListener?
    ) : ContentObserver(handler)
    {

        private var lastData: String? = null

        /** * 截屏依据中的路径判断关键字  */
        private val keys = arrayOf(
            "screenshot", "screen_shot", "screen-shot", "screen shot",
            "screencapture", "screen_capture", "screen-capture", "screen capture",
            "screencap", "screen_cap", "screen-cap", "screen cap"
        )

        private val oldAPi = arrayOf(
            MediaStore.Images.ImageColumns.DATA,
            MediaStore.Images.ImageColumns.DATE_TAKEN,
            MediaStore.Images.ImageColumns.DATE_ADDED,
        )

        @RequiresApi(Build.VERSION_CODES.Q)
        private val newAPi = arrayOf(
            MediaStore.Images.ImageColumns.RELATIVE_PATH,
            MediaStore.Images.ImageColumns.DATE_TAKEN,
            MediaStore.Images.ImageColumns.DATE_ADDED,
        )

        private val shotCallBack = Runnable {
            val path = lastData
            if (path != null && path.isNotEmpty()) {
                onScreenShotListener?.onShot(path)
            }
        }

        override fun onChange(selfChange: Boolean) {
            super.onChange(selfChange)
            handleMediaContentChange(mContentUri)
        }

        private fun handleMediaContentChange(contentUri: Uri) {
            var cursor: Cursor? = null
            try {
                val limitedCallLogUri = contentUri.buildUpon()
                    .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY, "1").build()

                // 数据改变时查询数据库中最后加入的一条数据
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P){
                    cursor = contentResolver.query(
                        limitedCallLogUri,
                        newAPi,
                        null,
                        null,
                        MediaStore.Images.ImageColumns.DATE_ADDED + " desc"
                    )
                }else{
                    cursor = contentResolver.query(
                        limitedCallLogUri,
                        oldAPi,
                        null,
                        null,
                        MediaStore.Images.ImageColumns.DATE_ADDED + " desc"
                    )
                }

                if (cursor == null || !cursor.moveToFirst()) {
                    return
                }

                val dataIndex: Int = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P){
                    cursor.getColumnIndex(MediaStore.Images.ImageColumns.RELATIVE_PATH)
                }else{
                    cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)
                }

                val dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)
                val dateAddIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED)

                if (dataIndex >= 0){
                    // 获取行数据
                    val data = cursor.getString(dataIndex)
                    val dateTaken = cursor.getLong(dateTakenIndex)
                    val dateAdded = cursor.getLong(dateAddIndex)
                    if (TextUtil.isNotEmptyNotNull(data)) {
                        if (TextUtils.equals(lastData, data)) {
                            //更改资源文件名也会触发,并且传递过来的是之前的截屏文件,所以只对分钟以内的有效
                            if (System.currentTimeMillis() - dateTaken < 3 * 3600) {
                                UiThreadHandler.removeCallbacks(shotCallBack)
                                UiThreadHandler.postDelayed(shotCallBack, 500)
                            }
                        } else if (dateTaken == 0L || dateTaken == dateAdded * 1000) {
                            UiThreadHandler.removeCallbacks(shotCallBack)
                        } else if (checkScreenShot(data)) {
                            UiThreadHandler.removeCallbacks(shotCallBack)
                            lastData = data
                            UiThreadHandler.postDelayed(shotCallBack, 500)
                        }
                    }
                }
            } catch (e: Exception) {
                LogService.getInstance().log2sd(e.toString())
            } finally {
                if (cursor != null && !cursor.isClosed) {
                    cursor.close()
                }
            }
        }

        /**
         * 根据包含关键字判断是否是截屏
         */
        private fun checkScreenShot(data: String): Boolean {
            val lowerData = data.lowercase(Locale.getDefault())
            for (keyWork in keys) {
                if (lowerData.contains(keyWork)) {
                    return true
                }
            }
            return false
        }
    }

    interface OnScreenShotListener {
        fun onShot(data: String)
    }

2、截屏观察控制类

class ScreenShotManager private constructor(){

    /**
     * 内部存储器内容观察者
     */
    private var mInternalObserver: ContentObserver? = null

    /**
     * 外部存储器内容观察者
     */
    private var mExternalObserver: ContentObserver? = null

    private var mResolver: ContentResolver? = null

    /** * 已回调过的路径  */
    private val mHasCallbackPaths: MutableList<String> = ArrayList()

    companion object{
        val instance: ScreenShotManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            ScreenShotManager() }
    }

    fun startListen(){
        // 初始化
        mResolver = DriverApplication.getInstance().contentResolver

        val onScreenShotListener = object : OnScreenShotListener {
            override fun onShot(data: String) {
                if (!mHasCallbackPaths.contains(data)){
                    mHasCallbackPaths.add(data)
                }
            }
        }
        mInternalObserver = MediaContentObserver(
            UiThreadHandler.getsUiHandler(),
            MediaStore.Images.Media.INTERNAL_CONTENT_URI,
            mResolver!!,
            onScreenShotListener
        )

        mExternalObserver = MediaContentObserver(
            UiThreadHandler.getsUiHandler(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            mResolver!!,
            onScreenShotListener
        )

        // TODO 需要判断存储权限
        //Android Q(10) ContentObserver 不回调 onChange
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            // 添加监听
            mResolver?.registerContentObserver(
                MediaStore.Images.Media.INTERNAL_CONTENT_URI,
                true,
                mInternalObserver as MediaContentObserver
            )
            mResolver?.registerContentObserver(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                true,
                mExternalObserver!!
            )

        }else{
            // 添加监听
            mResolver?.registerContentObserver(
                MediaStore.Images.Media.INTERNAL_CONTENT_URI,
                false,
                mInternalObserver as MediaContentObserver
            )
            mResolver?.registerContentObserver(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                false,
                mExternalObserver!!
            )
        }
    }

    fun stopListen(){
        mResolver?.unregisterContentObserver(mInternalObserver!!)
        mResolver?.unregisterContentObserver(mExternalObserver!!)

        mInternalObserver = null
        mExternalObserver = null
        mHasCallbackPaths.clear()
    }
}

Android Q(10) ContentObserver 不回调 onChange,在Android Q版本上调用注册媒体数据库监听的方法registerContentObserver时传入 notifyForDescendants参数值需要改为 true,Android Q之前的版本仍然传入 false。
如果值为false,则只要指定的URI或路径层次结构中URI的祖先之一发生变化,就会通知观察者。 如果为true,则每当路径层次结构中URI的后代发生更改时,也会通知观察者。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值