PATH, включённая «Отладка по USB», библиотека OCR на выбор (ML Kit Text Recognition или Tesseract — актуальную версию и координаты зависимости сверьте с документацией выбранной библиотеки)Итог: плавающая кнопка поверх любых приложений, по нажатию — снимок экрана, OCR и вывод распознанного текста в лог и в собственный оверлей.

mediaProjection).adb devices — в списке должно быть ваше устройство в статусе device.com.example.screenocr — он понадобится в командах appops и logcat.Чек-лист перед стартом:
adb devices показывает устройствоПример: если adb devices печатает пустой список, включите «Отладка по USB» в «Настройки → О системе → Номер сборки (7 тапов) → Для разработчиков». Если эмулятора нет, поднимите его по инструкции из гайда Как запускать Android-приложения и игры на ПК: эмуляторы, Waydroid и облачные сервисы.
Схема пайплайна, которую будем собирать:
1. Плавающая кнопка (TYPE_APPLICATION_OVERLAY) — ожидаемый результат: кружок висит поверх рабочего стола и других приложений.
2. Запрос захвата экрана (MediaProjectionManager.createScreenCaptureIntent()) — ожидаемый результат: системный диалог «Начать запись экрана?».
3. VirtualDisplay + ImageReader — ожидаемый результат: в лог приходит размер кадра.
4. OCR по Bitmap — ожидаемый результат: в лог приходит распознанная строка.
Что сделать: объявите разрешения и foreground service в app/src/main/AndroidManifest.xml, затем добавьте кнопку через WindowManager.
Зачем: окно TYPE_APPLICATION_OVERLAY — единственный штатный способ рисовать поверх чужих приложений без root. Разрешение SYSTEM_ALERT_WINDOW выдаётся пользователем вручную и без него addView падает.
Как проверить: после запуска приложения кнопка видна поверх рабочего стола, а appops показывает allow.
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<service
android:name=".CaptureService"
android:exported="false"
android:foregroundServiceType="mediaProjection"/>
private fun showFloatingButton() {
if (!Settings.canDrawOverlays(this)) {
startActivity(
Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName"))
)
return
}
val button = Button(this).apply { text = "OCR" }
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 24
y = 300
}
getSystemService(WINDOW_SERVICE).let {
(it as WindowManager).addView(button, params)
}
}
Пример: adb shell appops get com.example.screenocr SYSTEM_ALERT_WINDOW → SYSTEM_ALERT_WINDOW: allow. Если вывод SYSTEM_ALERT_WINDOW: deny — разрешение не выдано, кнопки на экране не будет.
Почему это работает: WindowManager рисует view в отдельном окне, которое принадлежит вашему процессу, но отображается в общем слое поверх остальных приложений.

Что сделать: по нажатию кнопки запросите согласие на захват, передайте resultCode и Intent в foreground service, создайте VirtualDisplay с ImageReader и отдайте полученный Bitmap в OCR.
Зачем: MediaProjection отдаёт видеопоток экрана, а ImageReader превращает один кадр в Bitmap — это и есть «скриншот» без хранения файла на диске.
Как проверить: в adb logcat -s CAPTURE:D OCR:D появляются размер кадра и распознанный текст.
| Компонент | Роль | Как проверить |
|---|---|---|
| `WindowManager` + `TYPE_APPLICATION_OVERLAY` | плавающая кнопка | `adb shell appops get <пакет> SYSTEM_ALERT_WINDOW` |
| `MediaProjectionManager` | запрос доступа к экрану | системный диалог захвата |
| `ImageReader` + `VirtualDisplay` | кадры экрана | лог с тегом `CAPTURE` |
| OCR-библиотека | текст из `Bitmap` | лог с тегом `OCR` |
// В Activity: получаем согласие пользователя
val mgr = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
startActivityForResult(mgr.createScreenCaptureIntent(), REQ_CAPTURE)
// В CaptureService: стартуем foreground и поднимаем захват
startForeground(1, buildNotification(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
val projection = mgr.getMediaProjection(resultCode, data)
val dm = resources.displayMetrics
val reader = ImageReader.newInstance(
dm.widthPixels, dm.heightPixels, PixelFormat.RGBA_8888, 2)
projection.createVirtualDisplay(
"ocr-capture",
dm.widthPixels, dm.heightPixels, dm.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
reader.surface, null, null)
reader.setOnImageAvailableListener({ r ->
val image = r.acquireLatestImage() ?: return@setOnImageAvailableListener
val bitmap = image.toBitmap()
image.close()
runOcr(bitmap)
}, handler)
Конвертация кадра с учётом rowStride (иначе картинка «съезжает» по диагонали):
fun Image.toBitmap(): Bitmap {
val plane = planes[0]
val rowPadding = plane.rowStride - plane.pixelStride * width
val raw = Bitmap.createBitmap(
width + rowPadding / plane.pixelStride, height, Bitmap.Config.ARGB_8888)
raw.copyPixelsFromBuffer(plane.buffer)
return Bitmap.createBitmap(raw, 0, 0, width, height)
}
private fun runOcr(bitmap: Bitmap) {
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
recognizer.process(InputImage.fromBitmap(bitmap, 0))
.addOnSuccessListener { Log.d("OCR", it.text) }
.addOnFailureListener { Log.e("OCR", "fail", it) }
}
Пример: на экране открыт текст «Оплата прошла успешно». В логе: D/OCR: Оплата прошла успешно. Плавающую кнопку перед снимком скройте (button.visibility = View.GONE) или исключите из кадра — иначе она попадёт в распознанный текст.
Почему это работает: VirtualDisplay дублирует содержимое дисплея в Surface, который принадлежит ImageReader; библиотека OCR принимает Bitmap и возвращает блок текста.

Что сделать: установите сборку, выдайте разрешения, нажмите кнопку и снимите лог.
Зачем: так вы отделяете проблему разрешений от проблемы OCR — по тегам видно, на каком шаге пайплайн остановился.
Как проверить: в логе есть и CAPTURE, и OCR; на экране — распознанный текст в вашем оверлее.
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb logcat -c
adb logcat -s CAPTURE:D OCR:D
Ожидаемый вывод:
D/CAPTURE: frame 1080x2400
D/OCR: Оплата прошла успешно
Финальный чек-лист успеха:
dumpsys есть запущенный сервис: adb shell dumpsys activity services com.example.screenocr | grep -i mediaprojectionOCR и непустым текстомVirtualDisplayДальше распознанный текст можно отправить в модель — локальный запуск описан в гайде Как запустить ИИ-модель локально на Android: приложения и настройка, а если нужен Linux-инструментарий рядом — в Как запустить Linux на Android без root: Termux, PRoot и ИИ-инструменты.
Проблема. Кнопка не появляется, приложение падает при запуске
android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@8f3a1c -- permission denied for window type 2038SYSTEM_ALERT_WINDOW (тип окна 2038 — это TYPE_APPLICATION_OVERLAY); хеш объекта в тексте у вас будет другим.adb shell appops set com.example.screenocr SYSTEM_ALERT_WINDOW allow и перезапустите приложение. Проверка: adb shell appops get com.example.screenocr SYSTEM_ALERT_WINDOW → allow.Проблема. Захват экрана падает сразу после согласия пользователя
java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTIONgetMediaProjection() вызван не из foreground service или сервис объявлен без android:foregroundServiceType="mediaProjection".CaptureService, добавьте атрибут в манифест и вызывайте startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION) до создания проекции. Проверка: adb shell dumpsys activity services com.example.screenocr | grep -i mediaprojection показывает ваш сервис.Проблема. Кадр получен, но текст пустой
MlKitException: Waiting for the text recognition model to be downloaded. Please wait. (префикс класса зависит от версии библиотеки — сверьтесь с документацией)FLAG_SECURE — там система отдаёт чёрный кадр.Вы собрали оверлей из трёх частей: плавающая кнопка на TYPE_APPLICATION_OVERLAY, захват кадра через MediaProjection + ImageReader и OCR по Bitmap с выводом в лог. Следующий шаг — отправить распознанный текст в модель и вернуть ответ в то же плавающее окно, не забывая скрывать кнопку перед снимком.
Нужен ли root для ИИ-оверлея с распознаванием экрана?
Нет. SYSTEM_ALERT_WINDOW и MediaProjection — штатные механизмы Android, пользователь подтверждает их вручную. Root не требуется ни для кнопки, ни для снимка экрана.
Почему OCR возвращает пустой текст на некоторых приложениях?
Скорее всего, приложение ставит FLAG_SECURE (защита от скриншотов) — система отдаёт чёрный кадр. Проверьте на обычном приложении с текстом: если там текст распознаётся, дело именно в защите окна.
Можно ли обойтись без MediaProjection и читать экран через AccessibilityService?
Можно как альтернативу, но это другой источник данных и другие ограничения по доступным элементам интерфейса; конкретный набор возможностей сверьте с документацией по AccessibilityService для вашей версии Android.
Куда девать распознанный текст дальше?
В лог — для отладки, в собственный оверлей — для пользователя, в модель — для ответа по содержимому экрана. Для локального запуска модели на телефоне см. гайд Как запустить ИИ-модель локально на Android: приложения и настройка.