In order to meet the needs of monitoring user screenshots and displaying the floating feedback portal, we conducted a simple survey on the user screenshot function on the Android side. Since the Android system does not provide APIs related to screen capture notifications, we need to make use of the relevant features that the system can provide to implement them.
Through study, I saw that there are probably three solutions on the Internet:
1. Use FileObserver to monitor resource changes in a directory
2. Use ContentObserver to monitor changes in image resources
3. Monitor shortcut keys for screenshots (due to the diversity of Android systems customized by manufacturers, plus the difference in shortcut keys and third-party applications, it is basically unreliable to monitor shortcut keys for screenshots, and can be ignored directly)
My implementation here is solved through the second solution. I have tested a variety of models and used them online, and there is no problem for now.
Principle introduction
As we all know, the Android system has a media database. Whether we take a picture with a camera or use a system screenshot, the system will add the detailed information of this picture to the media database and issue a content change notification, so we can Use the Content Observer (ContentObserver) to monitor the changes in the media database. When the database changes, get the last inserted picture data. If the picture meets our specific rules, it will be considered as a screenshot.
So what do we need to do to make sure it is a screenshot?
1. Monitor the resource URI of the screenshot (MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
2. Because we want to read the content of the picture, we need to read the permission of the SD card, android.permission.READ_EXTERNAL_STORAGE and apply dynamically.
3. After obtaining the picture information, judge whether the picture complies with the screenshot rules.
screenshot rules
Most of the rules on the Internet:
1. Time judgment, the generation time of the picture is after the start of monitoring, and within 10 seconds of the current time: the picture generated after the start of monitoring is meaningful, and within 10 seconds means that it was just generated.
2. Judging the path, the image path conforms to include specific keywords: This is the key point, the save path of the screenshot image usually contains screenshot strings such as "screenshot".
/** * Use ContentResolver to monitor changes in photo data */ fun ContentResolver.registerObserver( uri: Uri, observer: (selfChange: Boolean) -> Unit ): ContentObserver { val contentObserver = object : ContentObserver(Handler()) { overridefunonChange(selfChange: Boolean) { observer(selfChange) } } registerContentObserver(uri, true, contentObserver) return contentObserver }
Get screenshot image
After adapting to Android 11, the SQL for querying pictures has changed, so some changes need to be made to the query method, see the code below for details.
/** * Only get ordinary pictures, not Gif */ funqueryImages(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 { valdata = 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 }
/** * Only get ordinary pictures, not Gif (in Android11 machines) * After the targetSdkVersion is adapted to 30, the Sql for querying pictures has changed */ @RequiresApi(Build.VERSION_CODES.O) @WorkerThread funqueryImagesP(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)
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 }
/* * Create the bundle object required by Android11 * */ funcreateSqlQueryBundle( selection: String, selectionArgs: Array<String>, sortOrder: String?, limitCount: Int = 0, offset: Int = 0 ): Bundle? { if (selection == null && selectionArgs == null && sortOrder == null) { returnnull } 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 }
write at the end
So far, our needs have been met. Some inexplicable null pointer problems were found in the crash monitoring background, so some mandatory null judgment and try catch processing were added to the code. There will be time to optimize in the follow-up, and I hope everyone can provide valuable opinions.