Недавно я пытался определить способы обнаружения создания скриншотов в Android. Оказывается, официального API для подобного нет. Досадно, но есть обходное решение, позволяющее определить, сделал ли пользователь скриншот во время использования приложения.
Вы могли задаваться вопросом, как такие приложения, как Snapchat и Instagram, могут определять, что был сделан скриншот. Тут мы рассмотрим, как можно это сделать.
Честно говоря, когда я узнал, как это можно сделать, то я не мог поверить, насколько простым было это решение. Суть заключается в том, что когда пользователь использует приложение, мы проверяем изображения на устройстве и определяем, было ли добавлено новое изображение в папку “скриншоты”. И это всё!
Погружаемся в код
За любыми изменениями с изображениями на устройстве мы будем следить при помощи ContentObserver. Он принимает Handler в качестве параметра, и метод onChange запускается каждый раз, когда в указанном нами URI произойдёт изменение.
1 2 3 4 5 |
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) } } |
Далее этот ContentObserver нужно зарегистрировать, метод registerContentObserver принимает три аргумента. Первый — URI, за которым будет наблюдать ContentObserver. Второй notifyForDescendants параметр должен быть true, если мы хотим наблюдать за изменениями на всех URI, которые начинаются с указанного нами. В противном случае, мы будем получать уведомления только тогда, когда происходят изменения, конкретного указанного URI. Последний параметр – это сам ContentObserver, который мы хотим зарегистрировать.
1 2 3 4 5 |
context.contentResolver.registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver ) |
Итак, теперь, когда мы зарегистрировали наш ContentObserver, давайте посмотрим, как можно определять скриншоты. Идея проста. Всякий раз, когда происходит изменение медиафайлов во время использования приложения, мы получаем URI, из которого пробуем получить путь к файлу. Мы можем использовать атрибут DATA из MediaStore.Images API для получения пути к файлу, но этот атрибут был помечен как deprecated в API 29. И, хотя этот атрибут все еще доступен даже в API 30, в документации упоминается, что разработчики не должны учитывать, что файл будет доступен не всегда. В API 29 был предложен новый атрибут RELATIVE_PATH. Таким образом, для устройств с API 29 или выше мы можем использовать RELATIVE_PATH и DISPLAY_NAME, чтобы получить путь к файлу скриншота, а для других устройств мы можем продолжать использовать атрибут DATA.
1 2 3 4 5 |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { queryRelativeDataColumn(uri) } else { queryDataColumn(uri) } |
В следующем фрагменте кода мы запрашиваем атрибут DATA, который мы получили от ContentObserver. Если этот путь содержит строку “screenshot” в любом месте, мы можем предположить, что пользователь сделал снимок экрана.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private fun queryRelativeDataColumn(uri: Uri) { val projection = arrayOf( MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.RELATIVE_PATH ) context.contentResolver.query( uri, projection, null, null, null )?.use { cursor -> val relativePathColumn = cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH) val displayNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) while (cursor.moveToNext()) { val name = cursor.getString(displayNameColumn) val relativePath = cursor.getString(relativePathColumn) if (name.contains("screenshot", true) or relativePath.contains("screenshot", true) ) { // do something } |
В дополнение
Важно! Не забудьте удалить свой ContentObserver, когда пользователь не использует ваше приложение. Это можно сделать в onStop.
1 |
context.contentResolver.unregisterContentObserver(contentObserver) |
И ещё одно важное замечание. Так как мы получаем доступ к медиафайлам устройста, то нам нужны соответствующие разрешения в AndroidManifest.xml:
1 |
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
И да, как вы понимаете, это работает для всех отслеживающих подобное приложений. А это значит, вы можете просто забрать доступ к файлам и приложение уже не сможет определять делаете вы скрины или же нет. Автор оригинала проверил это с Instagram, и приложение не восприняло снимок экрана. А вот Snapchat просто не разрешил использовать приложение:
И да. Это решение будет работать только для устройств, которые хранят скриншоты в папке “Screenshots” или если в имени файла есть строка “screenshot”. Что подходит для обычного Android, но может быть изменено в каких-либо оболочках. Но всегда можно дополнить фильтры для совместимости и с другими устройствами.
Репозиторий GitHub проекта тут!
Пингбэк: Лучшие инструменты для создания скриншотов ваших мобильных приложений