在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的后代发生更改时,也会通知观察者。