当前位置:   article > 正文

Android 监听系统截屏操作_android 截屏监听

android 截屏监听

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

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

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

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

权限: 开始监听媒体数据库变化之前,需要先获取权限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)
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127

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()
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/418774
推荐阅读
相关标签
  

闽ICP备14008679号