前言
很多应用在当你截屏的时候能够感知到,并提示你是否要发送截屏等等。
Andorid 官方 API 没有检测截屏的能力,但有一些变通的办法能够检测用户在使用应用时是否进行了截屏操作。本文将探讨如何实现这一功能。
思路非常简单:用户使用 App 期间,监听用户设备中的图片,查看 “截屏” 文件夹中是否新增了图片
实现方式
我们定义一个 ContentObserver
观察用户设备中图片变化。它以一个 Handler 作为参数,每当我们指定的 URI(统一资源标识符)发生变化时,onChange 方法就会被触发。
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
}
}
接下来需要注册 ContentObserver
。
registerContentObserver
方法需要三个参数。
- 第一个是 ContentObserver 要观察的 URI。
- 第二个参数 notifyForDescendants,如果我们希望观察以指定 URI 开头的所有 URI 的变化,该参数应设为 true;如果我们只希望在指定的 URI 本身发生变化时收到通知,该参数可以设为 false。
- 最后一个参数是我们要注册的 ContentObserver。
context.contentResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true,
contentObserver
)
注册后,来看看如何检测截屏。思路很简单,当用户使用应用时,只要媒体文件发生变化,我们就获取 URI,然后尝试从该 URI 中提取文件路径。我们可以使用 MediaStore.Images
API 的 DATA
属性来获取文件路径,但该属性在 API 29 中已被弃用。尽管在 API 30 中该属性仍然存在,但文档中提到开发者不应假定文件始终可用。在 API 29 中,引入了一个名为 RELATIVE_PATH
的新属性。因此,对于 API 29 及更高版本的设备,我们可以使用 RELATIVE_PATH
和 DISPLAY_NAME
来获取截屏文件的路径;对于其他设备,我们可以继续使用 DATA
属性。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryRelativeDataColumn(uri)
} else {
queryDataColumn(uri)
}
在下面的代码片段中,我们使用 ContentObserver
返回给我们的 URI 来查询 DATA
属性。如果该路径中包含 “screenshot” 字符串,我们就可以判定用户进行了截屏操作。
private fun queryDataColumn(uri: Uri) {
val projection = arrayOf(
MediaStore.Images.Media.DATA
)
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
while (cursor.moveToNext()) {
val path = cursor.getString(dataColumn)
if (path.contains("screenshot", true)) {
// do something
}
}
}
}
同样,也可以像下面的代码片段那样,使用 DISPLAY_NAME
和 RELATIVE_PATH
来实现相同的功能。
private fun queryDataColumn(uri: Uri) {
val projection = arrayOf(
MediaStore.Images.Media.DATA
)
context.contentResolver.query(
uri,
projection,
null,
null,
null
)?.use { cursor ->
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
while (cursor.moveToNext()) {
val path = cursor.getString(dataColumn)
if (path.contains("screenshot", true)) {
// do something
}
}
}
}
当用户不再使用你的应用时,别忘了注销 ContentObserver
。可以在 onStop
方法中进行注销。
context.contentResolver.unregisterContentObserver(contentObserver)
由于要访问用户设备的媒体文件,所以需要在 AndroidManifest.xml 中添加相应权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
大部分应用的截屏监听都是这么做的,如果知道某个应用在会跟踪你的截屏操作,可以撤销该权限,它就无法正常工作了,例如 Instagram 和 Snapchat 都是是这么做的。只不过如果关闭此权限 Snapchat 会直接不让你启动应用。
这个解决方案仅适用于将截屏存储在 “screenshot” 文件夹中,或者文件名包含 “screenshot” 字符串的设备。在原生安卓设备上,截屏会保存在 “screeshot” 文件夹中,但如果设备厂商更改了默认存储位置,就无法检测到这些截屏了。不过,如果你遇到这种情况,可以针对新目录添加另一个检查条件, ContentObserver
会返回截屏的文件路径。