mobilegame-qapermissionsiosandroid

Тестирование runtime-разрешений в мобильном приложении: состояния, отзыв на лету и edge-кейсы

Разрешения (permissions) — одно из самых недооценённых мест в мобильном QA. Их часто тестируют поверхностно: «дал доступ — камера работает». А настоящие баги живут в отказах, отзывах на лету и состояниях, о которых забывают: «Don’t ask again» на Android, limited-доступ к фото на iOS, убийство процесса после смены разрешения в Настройках. Разберём модель состояний, когда и как просить, платформенные особенности и чек-лист, который ловит то, что обычно пропускают.

Модель состояний: их не два, а минимум пять

Главная ошибка — считать, что состояний два (дал / не дал). На самом деле каждое опасное разрешение живёт в нескольких состояниях, и каждое — отдельный тест-кейс:

  • Not determined / не запрашивалось — система ещё ни разу не спрашивала. Это единственное состояние, в котором можно показать системный диалог.
  • Granted / выдано — доступ есть.
  • Denied / отказано — на iOS системный диалог повторно уже не покажется, только переход в Настройки. На Android после двух отказов система сама ставит «не спрашивать».
  • Permanently denied / «Don’t ask again» (Android) — системный диалог больше не появляется, запрос молча «отказывает». Нужно вести пользователя в Настройки руками.
  • Limited / частичный — iOS Photos (выбранные фото), приблизительная вместо точной геолокации, «While Using» вместо «Always».
  • Provisional / ephemeral — тихие пуши без диалога (iOS), временный доступ.

Подробная модель и обработка отказа — в гайде Android по запросу разрешений.

Когда и как просить

  • Не пачкой на старте. Запрос — в момент, когда польза очевидна: нажал «сделать фото» → просим камеру. Пять диалогов на первом экране — антипаттерн и путь к отказам.
  • Pre-permission priming. Свой экран-объяснение ДО системного диалога. На iOS системный диалог показывается один раз: если юзер откажет «на холодную», второго шанса не будет — только Настройки. Поэтому сначала свой экран «зачем это нужно», потом системный.
  • Usage description строки (iOS). NSCameraUsageDescription, NSLocationWhenInUseUsageDescription и т.п. должны быть на месте и осмысленны — без них iOS крашит приложение в момент запроса.

Тест-кейсы: отказ на priming-экране не тратит системный диалог (разрешение остаётся not determined); согласие на priming → показывается системный диалог.

Отказ не должен ломать флоу

На каждый отказ — внятный graceful fallback, а не тупик, краш или пустой экран:

  • Камера отклонена → объяснение + кнопка «Открыть Настройки» или выбор из галереи.
  • Геолокация отклонена → ручной ввод города/адреса.
  • Пуши отклонены → приложение работает, фича помечена как выключенная.
  • Контакты / микрофон / фото — то же: фича деградирует, но не блокирует остальное.

Отдельно проверяем: нет бесконечного цикла запроса (нельзя долбить системный диалог); фича в состоянии «отказано» не выглядит доступной — кнопка не должна вести в никуда.

Отзыв на лету — самый забываемый кейс

На обеих платформах пользователь может отозвать разрешение в системных Настройках, пока приложение в фоне. И вот ключевой момент, который пропускают:

При изменении опасного разрешения через Настройки ОС убивает процесс приложения. При возврате приложение стартует заново (cold/warm start) — и состояние должно восстановиться без краша.

Сценарий: дать разрешение → пользоваться → свернуть → в Настройках отозвать → вернуться в приложение. Ожидание: не краш, фича корректно переключилась в «нет доступа», экран и данные восстановлены. Обратный сценарий: отозвать → выдать снова → вернуться → фича снова доступна без ручного перезапуска. Про сброс и пересоздание процесса — в разделе про обработку отказа.

Особые и «опасные» разрешения

  • Background location. «Always» на iOS, ACCESS_BACKGROUND_LOCATION на Android 10+ — запрашивается отдельно и обычно после foreground-доступа; стор ревьюит это строго. См. гайд Android по геолокации и CoreLocation authorization.
  • Точная vs приблизительная гео. Android 12+ (ACCESS_FINE/COARSE) и precise-toggle на iOS: юзер может дать только приблизительную — фича обязана работать или осознанно деградировать. См. approximate location.
  • POST_NOTIFICATIONS (Android 13+). Пуши теперь тоже требуют runtime-разрешения, как на iOS, — старый код этого не ждёт.
  • Special permissions (Android). Overlay, all-files-access, exact alarms, notification listener идут через отдельный системный экран, а не обычный диалог. См. special permissions.

iOS-специфика

  • ATT (App Tracking Transparency). Отдельный prompt для IDFA; показывается один раз; при отказе нельзя слать tracking-идентификатор. См. ATT и App Store privacy.
  • Limited Photos. Юзер выбрал N фото — приложение видит только их. Должно дать «выбрать ещё» и не падать на остальных. См. enhanced privacy in Photos.
  • Provisional notifications. Тихая доставка без диалога — проверить, что уведомления приходят в Центр уведомлений.

Android-специфика

  • «Don’t ask again» / permanently denied. После второго отказа shouldShowRequestPermissionRationale → false, системный диалог больше не появится. Единственный путь — Настройки. Тест: не должно быть «мёртвой» кнопки, которая молча ничего не делает.
  • Auto-reset. Если приложением не пользуются месяцами, Android сбрасывает разрешения — при следующем запуске состояние снова not determined.
  • Battery / hibernation. Пересекается с фоновыми ограничениями OEM — фоновая гео/пуши могут не работать даже при выданном разрешении.

Приватность по факту: отказ ≠ данные не уходят

Отказ в UI не гарантирует, что данные не отправляются. Это надо проверять по трафику: после отказа от ATT/гео/аналитики SDK реально не должен слать идентификатор или координаты.

  • Перехват HTTPS (Proxyman/Charles): granted → поле в запросе есть; denied → поля нет или оно обнулено.
  • Особенно критично для рекламных и трекинг-SDK и для compliance (GDPR/ATT).
  • Consent-флоу (например, UMP у AdMob) должен реально влиять на то, что уходит в сеть.

Инструменты

  • Android: adb shell pm grant/revoke <pkg> <perm>, adb shell pm reset-permissions, appops для special-разрешений.
  • iOS (симулятор): xcrun simctl privacy <device> grant|revoke|reset <service> <bundleId>.
  • Proxyman / Charles: подтверждение фактической отправки данных после отказа.
  • Матричный прогон: каждое разрешение × каждое состояние × foreground/отзыв-на-лету.

Чек-лист тестирования разрешений

  • Запрос разрешения — в осмысленный момент, а не пачкой на старте
  • Есть pre-permission priming-экран; его отказ не тратит единственный системный диалог
  • iOS: usage description строки на месте и осмысленны (иначе краш при запросе)
  • Каждое разрешение протестировано в 5 состояниях: not determined / granted / denied / permanently denied / limited
  • Отказ даёт graceful fallback, а не краш, тупик или мёртвую кнопку
  • После permanently denied системный диалог не показывается повторно — ведём в Настройки
  • Отзыв разрешения в Настройках на лету → процесс убит → возврат в приложение без краша, состояние восстановлено
  • Повторная выдача → фича снова работает без ручного перезапуска
  • Точная/приблизительная гео и While Using/Always обрабатываются корректно
  • Background location и special permissions (overlay, exact alarm, POST_NOTIFICATIONS) — отдельные сценарии
  • iOS: ATT, limited Photos, provisional push проверены отдельно
  • По трафику (Proxyman) подтверждено: после отказа SDK реально не шлёт данные

Итог. Разрешения — это не «дал / не дал», а матрица состояний × прерываний × платформенных особенностей. Самые дорогие баги: краш при возврате после отзыва, мёртвая кнопка после permanently denied и SDK, который шлёт данные несмотря на отказ. Прогоните каждое разрешение по чек-листу — и закроете то, что обычно всплывает только в отзывах в сторе.

Источники: Android — Request runtime permissions, Android — Approximate location, Apple — App Tracking Transparency, Apple — Requesting location authorization, Apple — Enhanced privacy in Photos.