Перейти к содержимому
Все записи

Android Photo Picker API в 2026 году: прикрепить фото без доступа ко всей галерее

Практическое руководство по Photo Picker API в Android — одиночный и множественный выбор, фильтрация по MIME, бэкпорт для версий до Android 13 и почему это лучше READ_MEDIA_IMAGES.

MFKAPPS 4 мин чтения

Просить у пользователя разрешение видеть всю его фототеку только для того, чтобы он мог прикрепить фото чека к одной трате, — значит просить намного больше, чем на самом деле нужно функции. READ_MEDIA_IMAGES даёт приложению постоянный доступ к каждой фотографии на устройстве — снимкам с отпуска, скриншотам чужих переписок, всему подряд — ради функции, которой на деле нужно было только одно изображение, выбранное пользователем. Именно этот разрыв и должен был устранить Photo Picker в Android.

Я добавил его в Granyn, чтобы пользователь мог прикрепить фото чека к записанной трате. Ниже — как этот API устроен на самом деле и почему он полностью убрал запрос разрешения.

Старый способ и почему он проблематичен

До появления Photo Picker «дать пользователю выбрать фото» означало один из двух путей: запросить READ_MEDIA_IMAGES (или READ_EXTERNAL_STORAGE до Android 13) и самостоятельно опрашивать MediaStore, либо запускать ACTION_GET_CONTENT и надеяться, что системный выбор документов будет вести себя одинаково на разных прошивках производителей. Путь через разрешение работает, но у него есть цена:

  • Запрос разрешения во время выполнения для функции, которую легко отклонить по привычке, — это незаметно убивает саму функцию для части пользователей.
  • Поток декларирования «чувствительных разрешений» в Play Console — широкий доступ к медиа привлекает больше внимания при проверке.
  • Постоянный доступ, который приходится оправдывать в политике конфиденциальности, даже если приложение реально касается только одной фотографии, выбранной пользователем.

Ничего из этого не даёт пользователю ничего взамен. Функции нужен лишь доступ на чтение к одному изображению, один раз.

Как на самом деле работает Photo Picker

Photo Picker — это интерфейс, принадлежащий системе: он работает в отдельном доверенном процессе, а изображение, на которое нажал пользователь, передаётся приложению в виде URI content://. Приложение никогда не получает разрешение на доступ к галерее — оно получает ровно этот один URI, ограниченный этой фотографией, на всё время, пока он нужен.

Для одиночного выбора настройка вообще не требует разрешения в манифесте:

class AddExpenseFragment : Fragment() {

    private val pickImage = registerForActivityResult(
        ActivityResultContracts.PickVisualMedia(),
    ) { uri: Uri? ->
        if (uri != null) attachReceiptPhoto(uri)
    }

    private fun launchPicker() {
        pickImage.launch(
            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly),
        )
    }
}

ActivityResultContracts.PickVisualMedia.ImageOnly ограничивает выбор изображениями; VideoOnly и SingleMimeType("image/png") покрывают остальные типичные случаи. Никакой записи <uses-permission>, никакого запроса во время выполнения, никакого диалога с обоснованием, текст для которого нужно писать.

Выбор нескольких фото

Обычно чек — это одно фото, но у того же contract есть вариант с множественным выбором, с ограничением, которое вы задаёте сами:

private val pickImages = registerForActivityResult(
    ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5),
) { uris: List<Uri> ->
    uris.forEach { attachReceiptPhoto(it) }
}

Системный интерфейс сам применяет это ограничение — он блокирует дальнейшие нажатия, как только пользователь достигает лимита, так что вашему коду не нужно потом отклонять слишком длинный список.

Полученный URI не постоянен

Вот деталь, на которой многие спотыкаются: URI content://, переданный в ваш ActivityResultCallback, гарантированно читаем только на протяжении этого вызова. Если сохранить строку URI в базе данных и попытаться открыть её через несколько дней, может вылететь SecurityException — в отличие от постоянного URI Storage Access Framework, это разрешение не переживает сессию выбора фото.

Решение — сразу же скопировать нужные байты в файл, принадлежащий вашему приложению:

private fun attachReceiptPhoto(sourceUri: Uri) {
    val destFile = File(requireContext().filesDir, "receipts/${UUID.randomUUID()}.jpg")
    destFile.parentFile?.mkdirs()

    requireContext().contentResolver.openInputStream(sourceUri)?.use { input ->
        destFile.outputStream().use { output -> input.copyTo(output) }
    }

    viewModel.setReceiptPath(destFile.absolutePath)
}

Выполняйте копирование не в главном потоке — это файловый ввод-вывод, — но сделайте это до того, как завершится колбэк результата Activity выбора фото, а не на следующем экране. После копирования байтов временное разрешение content:// уже не важно: у приложения теперь есть собственный приватный файл, который можно читать сколько угодно долго.

Бэкпорт для версий до Android 13

Photo Picker появился как системная функция в Android 13, но благодаря Google Play services бэкпортирован вплоть до API 21+ — тот же вызов ActivityResultContracts.PickVisualMedia автоматически разрешается в этот бэкпорт на старых устройствах с установленными Play services. Есть один момент, который стоит проверить явно, если ваш минимальный набор функций действительно требует поведения нативного выбора (например, интеграции облачных медиа-провайдеров для конкретного приложения):

val isPhotoPickerAvailable = ActivityResultContracts.PickVisualMedia
    .isPhotoPickerAvailable(requireContext())

На практике для простого сценария «прикрепить одно фото» ветвление по этому флагу не нужно — contract сам корректно деградирует. Это важно, когда вы решаете, показывать ли подсказки интерфейса, специфичные для выбора фото, которые имеют смысл только в нативной версии.

Что оказалось действительно важным

Выигрыш здесь не в новом жесте и не в более симпатичном интерфейсе выбора — а в том, что модель разрешений наконец соответствует тому, что функция реально делает. Пользователь, прикрепляющий фото одного чека, никогда не должен был давать постоянный доступ ко всей своей фотоплёнке, и пока не появился Photo Picker, у Android не было чистого способа этого избежать. Замена READ_MEDIA_IMAGES на PickVisualMedia убрала одно разрешение из манифеста, один запрос во время выполнения и один абзац из моей политики конфиденциальности — а функция работает для пользователя ровно так же, просто больше не просит лишнего.

// По теме

Ещё из журнала

MFKAPPS 5 мин чтения

Биометрическая аутентификация в Android в 2026 году: BiometricPrompt, Keystore и блокировка local-first приложения

Практическое руководство по androidx.biometric в Android — BiometricPrompt, ключи на основе CryptoObject, резервный вариант с учётными данными устройства и ошибки, из-за которых проверка отпечатка не защищает ничего.

#android #engineering #kotlin
MFKAPPS 5 мин чтения

Health Connect в Android в 2026 году: как читать и писать данные, не отказываясь от local-first

Практическое руководство по API Health Connect в Android в 2026 году — разрешения, фоновое чтение и почему local-first-приложениям стоит относиться к нему как к опциональному источнику, а не как к истине.

#android #engineering #kotlin
MFKAPPS 4 мин чтения

androidx.startup в 2026 году: порядок инициализации библиотек без стопки ContentProvider'ов

Практическое руководство по библиотеке App Startup для Android — объединение initializer'ов в один ContentProvider, объявление зависимостей и случаи ленивой инициализации, которые она не заменяет.

#android #engineering #kotlin