赞
踩
在Android App中监听系统截屏功能,没有系统标准的监听器或者api可以调用,需要自己实现。针对这个需求,目前大部分实现方案是监听系统的媒体数据库。
原理: 每当产生一张新图片,系统都会把这张图片的详细信息加入到媒体数据库,并发出内容改变通知。
实现: 利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合特定的规则,则认为用户截屏了。
监听两个Uri:
// 内部存储空间的 content:// 格式Uri:
MediaStore.Images.Media.INTERNAL_CONTENT_URI
// 主要外部存储空间 content:// 格式Uri:
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
权限: 开始监听媒体数据库变化之前,需要先获取权限READ_EXTERNAL_STORAGE
因为需要存储权限,所有可能存在相关的法务风险,慎用
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) }
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的后代发生更改时,也会通知观察者。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。