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 = lastDataif (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? = nulltry {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 = dataUiThreadHandler.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? = nullprivate 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().contentResolverval 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 不回调 onChangeif (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 = nullmExternalObserver = nullmHasCallbackPaths.clear()}
}

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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部