写在开头
为满足监听用户截屏并展示悬浮反馈入口的需求,对Android端的用户截屏功能进行了简单的调研。由于Android系统并没有提供截屏通知相关的API,所有需要我们自己利用系统能提供的相关特性变通实现。
通过学习,看到网上大概了提供了三种解决方案:
- 利用FileObserver监听某个目录中资源变化情况
- 利用ContentObserver监听图片资源的变化
- 监听截屏快捷按键 ( 由于厂商自定义Android系统的多样性,再加上快捷键的不同以及第三方应用,监听截屏快捷键这事基本不靠谱,可以直接忽略 )
这里我的实现是通过第二种方案解决,具体为什么不用1,3两种,下面的博客给了很好的解释,我不在累赘。
Android 截屏监听:如何实现截图分享功能? 但是实现方式略微不同,欢迎大家一起学习指正。
ScreenShotMaster 先给github地址,对于截屏思路有了解的同学可以直接看gitgub示例。
原理介绍
大家都知道,Android系统有一个媒体数据库,不管我们是相机拍摄的照片还是使用系统截屏截取的图片,系统都会把这张图片的详细信息加入到这个媒体数据库,并发出内容改变通知,所以我们可以利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合我们特定的规则,则认为被截屏了。
那么我们需要怎么做才能确定是截图呢?
- 监听截图的资源URI(MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
- 因为我们要读取图片内容,所以我们需要读取SD卡的权限,android.permission.READ_EXTERNAL_STORAGE并动态申请。
- 获得图片信息后判断图片是否符合截图规则。
截图规则
网上绝大多数规则:
- 时间判断,图片的生成时间在开始监听之后, 并与当前时间相隔10秒内:开始监听后生成的图片才有意义,相隔10秒内说明是刚刚生成的。
- 路径判断,图片路径符合包含特定的关键词:这一点是关键,截屏图片的保存路径通常包含“screenshot”等截图string。
实现主要步骤(具体请移步Github)
###注册图片监听者
fun registerContentObserver() {
if (contentObserver == null) {
contentObserver =
ScreenShotApplication.applicationContext.contentResolver.registerObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
) {
//监听到截图后
_dataChanged.value = true
}
}
}
/**
* 利用ContentResolver监听照片数据的变化
*/
fun ContentResolver.registerObserver(
uri: Uri,
observer: (selfChange: Boolean) -> Unit
): ContentObserver {
val contentObserver = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
observer(selfChange)
}
}
registerContentObserver(uri, true, contentObserver)
return contentObserver
}
获取截图图片
因为适配Android11后,查询图片的SQL发生了变化,所以需要针对查询的方式有了一些改变
/*
* 获取截图图片
*/
fun getScreentShotImage(bucketId: String? = null) {
Thread {
try {
var data: ScreentShotInfo? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//11 高版本获取图片信息
data = queryImagesP(bucketId)
} else {
//低版本获取图片信息
data = queryImages(bucketId)
}
val imagePath = data.path?.toLowerCase()
screenShoot.forEach {
if (imagePath?.contains(it)!! && (System.currentTimeMillis() / 1000 - data.addTime < 2)) {
_screentShotInfoData.postValue(data)
return@forEach
}
}
} catch (e: Exception) {
}
}.start()
}
/**
* 只获取普通图片,不获取Gif
*/
fun queryImages(bucketId: String?): ScreentShotInfo {
val screentShotInfo = ScreentShotInfo()
val uri = MediaStore.Files.getContentUri("external")
val sortOrder = MediaStore.Images.Media._ID + " DESC limit 1 "
var selection = (MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) +
" AND " + MediaStore.Images.Media.MIME_TYPE + "=?" +
" or " + MediaStore.Images.Media.MIME_TYPE + "=?"
try {
val data = ScreenShotApplication.applicationContext.contentResolver.query(
uri,
ScreenShotProjection,
selection,
imageType,
sortOrder
)
if (data == null) {
return screentShotInfo
}
if (data.moveToFirst()) {
//查询数据
val imageId: String =
data.getString(data.getColumnIndexOrThrow(ScreenShotProjection[0]))
val imagePath: String =
data.getString(data.getColumnIndexOrThrow(ScreenShotProjection[1]))
val imageSize: Long =
data.getLong(data.getColumnIndexOrThrow(ScreenShotProjection[2]))
val imageWidth: Int =
data.getInt(data.getColumnIndexOrThrow(ScreenShotProjection[3]))
val imageHeight: Int =
data.getInt(data.getColumnIndexOrThrow(ScreenShotProjection[4]))
val imageMimeType: String =
data.getString(data.getColumnIndexOrThrow(ScreenShotProjection[5]))
val imageAddTime: Long =
data.getLong(data.getColumnIndexOrThrow(ScreenShotProjection[6]))
screentShotInfo.path = imagePath
screentShotInfo.addTime = imageAddTime
}
} catch (e: Exception) {
e.printStackTrace()
}
return screentShotInfo
}
/**
* 只获取普通图片,不获取Gif(在Android11的机器中)
* 在targetSdkVersion适配到30后 查询图片的Sql发生了变化
*/
@RequiresApi(Build.VERSION_CODES.O)
@WorkerThread
fun queryImagesP(bucketId: String?): ScreentShotInfo {
val screentShotInfo = ScreentShotInfo()
val uri = MediaStore.Files.getContentUri("external")
val sortOrder = MediaStore.Files.FileColumns._ID + " DESC"
var selection = (MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) +
" AND " + MediaStore.Images.Media.MIME_TYPE + "=?" +
" or " + MediaStore.Images.Media.MIME_TYPE + "=?"
val bundle = createSqlQueryBundle(selection, imageType, sortOrder, 1)
try {
val data = ScreenShotApplication.applicationContext.contentResolver.query(
uri,
ScreenShotProjection,
bundle,
null
)
if (data == null) {
return screentShotInfo
}
if (data.moveToFirst()) {
//查询数据
val imageId: String =
data.getString(data.getColumnIndexOrThrow(ScreenShotProjection[0]))
val imagePath: String =
data.getString(data.getColumnIndexOrThrow(ScreenShotProjection[1]))
val imageSize: Long =
data.getLong(data.getColumnIndexOrThrow(ScreenShotProjection[2]))
val imageWidth: Int =
data.getInt(data.getColumnIndexOrThrow(ScreenShotProjection[3]))
val imageHeight: Int =
data.getInt(data.getColumnIndexOrThrow(ScreenShotProjection[4]))
val imageMimeType: String =
data.getString(data.getColumnIndexOrThrow(ScreenShotProjection[5]))
val imageAddTime: Long =
data.getLong(data.getColumnIndexOrThrow(ScreenShotProjection[6]))
screentShotInfo.path = imagePath
screentShotInfo.addTime = imageAddTime
}
} catch (e: Exception) {
e.printStackTrace()
}
return screentShotInfo
}
/*
* 创建Android11 所需要的bundle对象
* */
fun createSqlQueryBundle(
selection: String,
selectionArgs: Array<String>,
sortOrder: String?, limitCount: Int = 0, offset: Int = 0
): Bundle? {
if (selection == null && selectionArgs == null && sortOrder == null) {
return null
}
val queryArgs = Bundle()
if (selection != null) {
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
}
if (selectionArgs != null) {
queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs)
}
if (sortOrder != null) {
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, sortOrder)
}
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_LIMIT, "$limitCount offset $offset")
return queryArgs
}
写在结尾
到此为止,实现了我们的需求。因为我们的市场是在海外,在bug检测后台发现了一些莫名其妙的空指针问题,所以代码中加了一些强制判空和try catch处理。后续有时间在优化,也希望大家可以提出宝贵的意见。
感谢
在开发和解决bug的过程中,提供帮助和解决思路的网站。
https://zhuanlan.zhihu.com/p/37011146