На Android разрешение объявляют в манифесте и запрашивают самостоятельно в рантайме; в iOS общего вызова для запроса нет: система спрашивает при первом обращении к API и завершает приложение, если в Info.plist нет purpose string.
Android: dangerous-разрешению нужна запись <uses-permission> и запрос через ActivityResultContracts.RequestPermission в выбранный вами момент
iOS: у каждого фреймворка свой вызов (AVCaptureDevice.requestAccess, CLLocationManager.requestWhenInUseAuthorization), а текст NS…UsageDescription показывается в алерте дословно
Обе платформы: пользователь может отозвать разрешение в Настройках в любой момент, поэтому статус проверяют прямо перед использованием фичи, а не один раз при запуске
AndroidMiddle
Разрешения и приватность
Runtime-разрешения, purpose strings, ATT, геолокация, фото, privacy manifest, согласия, аудит SDK
Закрыто вопросов
0 / 19
- 0Повторить
- 2Часто спрашивают
Вопросы
С Android 11 пользователь может выдать одноразовое разрешение, которое отзывается вскоре после ухода приложения с экрана, а с Android 12 запрос геолокации может вернуться только с грубой точностью. Оба исхода нормальные, код должен их обрабатывать, а не считать ошибками.
Одноразовое: предлагается для геолокации, камеры и микрофона; разрешение живёт, пока приложение на экране, и ещё немного после, поэтому checkSelfPermission вызывают заново при каждом запуске фичи
Приблизительное: пользователь может выдать только ACCESS_COARSE_LOCATION с точностью до нескольких квадратных километров; фича должна продолжать работать в урезанном режиме
Просите оба сразу: на Android 12+ запрос одного ACCESS_FINE_LOCATION игнорируется, поэтому fine и coarse запрашивают одним вызовом
Спрашивайте в момент, когда пользователь запускает фичу, которой нужно разрешение, и сначала показывайте своё короткое объяснение. Системный диалог можно показать один-два раза, поэтому запрос до того, как пользователь увидел пользу, его просто сжигает.
Экран-пояснение: что делает фича и какие данные использует, плюс кнопка Не сейчас, которая системный диалог не открывает; до настоящего запроса доходят только нажавшие «Продолжить»
Не имитируйте системный диалог: фальшивые «Разрешить/Запретить», которые не открывают настоящий запрос, вводят в заблуждение и грозят отклонением в сторе
Продумайте путь отказа: фича деградирует (ручной ввод адреса, приглашение по ссылке), а не упирается в тупик
registerForActivityResult вызывается внутри обработчика клика, когда фрагмент уже создан, поэтому бросается IllegalStateException: «Fragments must call registerForActivityResult() before they are created». Регистрируйте один раз как свойство, а из клика вызывайте только launch.
Зачем это правило: результат может прийти после пересоздания активити или смерти процесса; новый экземпляр получит его, только если зарегистрирует тот же колбэк в том же порядке во время создания
Вариант с активити: та же ошибка падает с «LifecycleOwners must call register before they are STARTED»; в Compose это за вас делает rememberLauncherForActivityResult
Проверьте и манифест: без <uses-permission android:name="android.permission.CAMERA"/> лаунчер сразу возвращает false, и диалог не появляется
Обработайте отказ: колбэк игнорирует false, поэтому у пользователя, сказавшего «нет», кнопка молча ничего не делает
По одному этому вызову это никак не определить: он возвращает true только после отказа, который ещё можно пересмотреть, и false как до первого запроса, так и после окончательного отказа. Записывайте результат собственного запроса: отказ при false у rationale означает «заблокировано, ведите в Настройки».
Ни разу не спрашивали: false и флага блокировки нет: просто запрашивайте
Один отказ: true: покажите объяснение и спросите снова
Заблокировано: колбэк получил отказ, а rationale равен false: на Android 11+ два отказа делают его окончательным, и лаунчер возвращает false без диалога; в два отказа засчитываются только явные нажатия «Отклонить»
Путь назад: только Settings.ACTION_APPLICATION_DETAILS_SETTINGS; пользователь уходит из приложения, поэтому состояние пересчитывают в onResume
Разрешения пропадают и сами: auto-reset отзывает рантайм-разрешения у приложений, которыми не пользовались несколько месяцев (Android 11, бэкпорт на 6.0+ через Play services), поэтому проверяйте в точке вызова каждый раз
Многие: когда системный UI действует по явному выбору пользователя, приложение получает только результат (одно фото, один контакт, одну SMS) и широкое разрешение ему не нужно. Это значит: нет диалога, не нужно продумывать отказ, проще ревью в Play.
Фото и файлы: Photo Picker и OpenDocument вместо READ_MEDIA_*
Сделать снимок: TakePicture отдаёт работу приложению камеры; но если в смёрженном манифесте объявлен CAMERA и он не выдан, ACTION_IMAGE_CAPTURE бросает SecurityException
Контакты: PickContact даёт доступ на чтение к одному выбранному контакту, без READ_CONTACTS
Звонки: ACTION_DIAL открывает звонилку с подставленным номером; CALL_PHONE нужен только для ACTION_CALL
SMS-коды: SMS Retriever или SMS User Consent API вместо READ_SMS, который Play разрешает в основном SMS-приложениям по умолчанию
Устройства рядом: CompanionDeviceManager показывает системный выбор устройства и сопрягает без BLUETOOTH_SCAN и геолокации; если сканируете сами, BLUETOOTH_SCAN с usesPermissionFlags="neverForLocation" убирает требование геолокации, но остаётся рантайм-разрешением
Photo Picker представляет собой системный UI, который возвращает только выбранные пользователем файлы, вообще без разрешения. Частичный доступ (Android 14) позволяет ответить на запрос READ_MEDIA_* вариантом «только выбранные фото». Для аватара или пары вложений пикер служит выбором по умолчанию.
Нет разрешения, нет и отказа: ActivityResultContracts.PickVisualMedia и PickMultipleVisualMedia; там, где пикера нет, androidx откатывается на ACTION_OPEN_DOCUMENT
Политика Play: для READ_MEDIA_IMAGES и READ_MEDIA_VIDEO нужна декларация, и их одобряют, только если широкий доступ к галерее составляет основную функцию приложения, например галерея или бэкап
Частичный доступ: объявите READ_MEDIA_VISUAL_USER_SELECTED и запрашивайте его вместе с READ_MEDIA_IMAGES; если пользователь выбрал часть фото, выдаётся только первое, и приложение должно показать это состояние, а не ошибку
Без этой декларации: система даёт временный доступ к выбранным файлам, и он заканчивается, когда приложение уходит в фон
Изменить выбор: добавить фото можно только новым запросом, поэтому дайте пользователю для этого кнопку
Приложение сохраняет content://-URI из пикера, но не право на его чтение. Фотопикер выдаёт временный доступ, который заканчивается вместе с процессом приложения, поэтому после перезапуска openInputStream бросает SecurityException.
Почему сначала всё работает: право на чтение привязано к живому процессу и действует, пока приложение не остановится или устройство не перезагрузится; строка в SharedPreferences живёт дольше, поэтому баг проявляется только после холодного старта, смерти процесса или перезагрузки
Исправление: takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) в колбэке просит систему сохранить доступ после перезапусков и перезагрузок; это работает для URI из пикера и для ACTION_OPEN_DOCUMENT, который androidx использует как запасной вариант
Очистка: число сохранённых доступов на приложение ограничено, поэтому, когда пользователь выбирает новую аватарку, для старого URI вызывают releasePersistableUriPermission
Часто проще: сразу после выбора скопировать картинку в filesDir или загрузить на сервер и хранить свой путь: пользователь всё равно может удалить или отредактировать оригинал в галерее, а доступ не нужен вовсе
Как поймать: выберите фото, выполните adb shell am force-stop <package>, откройте снова; аватарка должна загрузиться или показать заглушку
Запрашивайте точную и приблизительную геолокацию, пока приложение на экране, и ведите поездку в foreground-сервисе с типом location; фоновая геолокация обычно не нужна. ACCESS_BACKGROUND_LOCATION нужен только для геолокации без видимого сервиса, например для геофенсинга.
Шаг 1: запросите ACCESS_FINE_LOCATION и ACCESS_COARSE_LOCATION вместе и примите coarse как урезанный режим
Шаг 2, на время доставки: запустите сервис с foregroundServiceType="location", разрешением FOREGROUND_SERVICE_LOCATION (Android 14) и постоянным уведомлением; из фона его запустить нельзя
Шаг 3, только если действительно нужно: ACCESS_BACKGROUND_LOCATION отдельным, более поздним запросом; вместе с foreground-разрешениями на Android 11+ весь запрос игнорируется, а сам по себе он открывает страницу настроек приложения, где пользователь выбирает «Разрешить в любом режиме»
Ревью: для фоновой геолокации в Play нужна декларация с демо-видео и заметное раскрытие (prominent disclosure) в приложении; приложения часто отклоняют, потому что хватило бы foreground-сервиса
Аналог в iOS: «При использовании» плюс фоновый режим location покрывает активную поездку; «Всегда» нужен только для геофенсинга и significant-change monitoring
Видимость пакетов (package visibility): с Android 11 приложение видит лишь отфильтрованный список установленных приложений, поэтому resolveActivity возвращает null для невидимого. Список приложений раскрывает многое о человеке, потому это приватные данные.
Самое простое исправление: вызывайте startActivity сразу и ловите ActivityNotFoundException; запуск неявного интента видимости не требует
Если проверить заранее всё же нужно: объявите интент в <queries> в манифесте: <intent> с ACTION_VIEW и <data android:scheme="geo"/> или <package> для конкретного приложения
**QUERY_ALL_PACKAGES: видит всё, но Play разрешает его, только если список приложений составляет основную функцию (лаунчеры, антивирусы, управление устройством), и с декларацией
Видны всегда**: ваше собственное приложение, установщик, приложения, которые запускают ваше, биндятся к нему или используют ваш content provider, и часть системных пакетов
Зелёная точка появляется, пока приложение использует камеру или микрофон, а пользователь может выключить любой из сенсоров для всего устройства. При выключенном переключателе отказа нет: приходят чёрные кадры или тишина, поэтому код, ожидающий данных, может зависнуть.
Индикатор привязан к сессии: не отвязанный use case CameraX или не освобождённый AudioRecord держат точку включённой, и пользователи воспринимают это как слежку; отвязывайте и освобождайте сразу после завершения фичи
Переключатели: разрешение по-прежнему числится выданным; при старте захвата система сама показывает диалог разблокировки, а SensorPrivacyManager.supportsSensorToggle() подскажет, есть ли переключатель на устройстве
Закладывайте тишину в дизайн: ставьте таймаут на ожидание первого кадра или буфера и подсказывайте про переключатель в быстрых настройках
Privacy dashboard: в Настройках видно, какие приложения и когда использовали геолокацию, камеру и микрофон, так что любой необъяснимый доступ видят все пользователи
iOS: та же идея: оранжевая точка для микрофона и зелёная для камеры, горят, пока приложение реально записывает звук или видео
Манифесты библиотек мёржатся в ваш, поэтому любая зависимость может добавить <uses-permission>, которого никто в команде не писал. Смёрженный манифест показывает разрешение, а отчёт manifest merger называет библиотеку, из которой оно пришло.
Найти: вкладка Merged Manifest в Android Studio, build/outputs/logs/manifest-merger-release-report.txt или aapt2 dump permissions на собранном APK (именно его видит Play)
Убрать: tools:node="remove" в вашем манифесте. Это меняет поведение: SDK может бросить SecurityException или молча деградировать, поэтому проверьте код, который его использовал
Сначала спросите вендора: у многих есть облегчённый артефакт или флаг конфигурации, который убирает разрешение
**AD_ID: ещё один частый случай: его добавляют рекламные и аналитические SDK; если рекламы нет, уберите его и заполните форму Data safety соответственно
Предотвратить следующий раз**: сравнивайте смёрженные разрешения с allow-list в репозитории на CI и включите dependency locking, чтобы транзитивное обновление приходило как диф на ревью
Это публичная декларация того, какие данные приложение собирает и передаёт, зачем и как защищает; она покрывает и данные ваших SDK: Play считает их трафик вашим. Если форма расходится с тем, что приложение реально отправляет, это ведёт к санкциям по политике, а не просто к отклонённому обновлению.
Collected (сбор): любые пользовательские данные, отправленные с устройства; данные, обрабатываемые только на устройстве, сбором не считаются, а эфемерная обработка или сквозное шифрование могут давать исключение
Shared (передача): данные, отправленные третьей стороне; передача сервис-провайдеру, работающему от вашего имени, по требованию закона или по действию самого пользователя считается исключением
Также декларируются: шифрование при передаче, возможность запросить удаление данных и соответствие политике Families
SDK: аналитический SDK добавляет взаимодействия с приложением и идентификаторы устройства, крэш-репортер добавляет диагностику, рекламный SDK добавляет рекламный идентификатор и обычно передачу; опирайтесь на гайды вендоров по Data safety и Google Play SDK Index
Поля, о которых забывают: сырой поисковый запрос в аналитическом событии считается пользовательским контентом, email в ключе крэш-отчёта делает данные о падениях персональными; отправляйте характеристику значения (длину, количество, псевдонимный ID), а не само значение
Аналог в iOS: privacy label в App Store, с дополнительной категорией «используется для отслеживания»
ATT представляет собой диалог iOS, обязательный перед трекингом: объединением своих данных с данными других компаний для рекламы или передачей брокерам данных. После отказа IDFA читается нулями, а атрибуция переходит на агрегированные отложенные postback-и SKAdNetwork или AdAttributionKit.
Не трекинг: ваша собственная first-party аналитика, данные, которые не покидают устройство, передача только для борьбы с фродом
Обходных путей нет: подменять IDFA фингерпринтом (IP, модель устройства, настройки) запрещено независимо от согласия пользователя
Спрашивайте, когда приложение активно: запрос из didFinishLaunching молча отбрасывается, а если пользователь выключил «Разрешить приложениям запрашивать трекинг», вызов сразу возвращает .denied
Аналог в Android: диалога нет; для чтения рекламного идентификатора нужно разрешение AD_ID (target 33+), а пользователь, удаливший ID, отдаёт нули
Для маркетинга: планируйте цифры на уровне кампаний, с задержкой, плюс собственные first-party сигналы, потому что большинство пользователей отказывает
PrivacyInfo.xcprivacy представляет собой property list, где приложение и каждый SDK декларируют трекинг, собираемые данные и причину для каждого вызываемого API из списка «required reason». ITMS-91053: сборка использует такой API (например, UserDefaults или временные метки файлов) без одобренного кода причины.
Четыре ключа: NSPrivacyTracking, NSPrivacyTrackingDomains, NSPrivacyCollectedDataTypes, NSPrivacyAccessedAPITypes
Пробел в SDK становится вашим реджектом: SDK из списка Apple «часто используемых» обязаны поставлять собственный подписанный манифест (ITMS-91061, если его нет); исправляется обычно обновлением версии
Домены трекинга проверяются в рантайме: если пользователь не разрешил трекинг, соединения с перечисленными доменами не проходят, поэтому никогда не указывайте там свой API-хост
Это не privacy label App Store: Generate Privacy Report в Xcode сводит все манифесты, чтобы помочь заполнить label, но заполняете его всё равно вручную
В Android такого файла нет: эту роль играет форма Data safety, но без проверки при сборке
Из автоинициализации: Firebase и многие SDK стартуют через ContentProvider или инициализатор androidx.startup, который выполняется до Application.onCreate, поэтому они начинают собирать данные раньше, чем ваш флаг вообще прочитан. Выключатель должен быть в манифесте.
Отключите автосбор: установите firebase_analytics_collection_enabled и firebase_crashlytics_collection_enabled в false и включайте в рантайме, когда пользователь ответит
**Не firebase_analytics_collection_deactivated: он выключает аналитику навсегда, и рантайм-сеттер её уже не вернёт
Consent Mode v2**: по умолчанию выставьте в манифесте все типы согласия в denied (google_analytics_default_allow_analytics_storage и рекламные ключи), затем вызывайте setConsent с ANALYTICS_STORAGE, AD_STORAGE, AD_USER_DATA, AD_PERSONALIZATION
Реклама в ЕЭЗ и Великобритании: нужна сертифицированная Google CMP, например UMP SDK; она записывает ключи IABTCF_ в SharedPreferences, которые рекламные SDK читают при старте, поэтому CMP должна отработать до инициализации рекламы
Другие SDK: уберите их запись из InitializationProvider через tools:node="remove" и инициализируйте их сами после согласия
Позднее «нет»: остановите сбор, удалите события из очереди, вызовите resetAnalyticsData() и сохраните решение с временной меткой и версией показанного текста
Детская аудитория выключает почти всю стороннюю телеметрию: нет рекламного ID и персонализации, только SDK, сертифицированные для семейных приложений, и проверяемое согласие родителей до сбора данных. Под эти правила приложение попадает через декларацию целевой аудитории в консоли стора.
Сторы
Google Play: целевая аудитория, включающая детей, переводит приложение под политику Families: нужна политика конфиденциальности, реклама только от самосертифицированных для Families SDK, и рекламный идентификатор для детей не отправляется
App Store: категория Kids запрещает стороннюю рекламу и аналитику за узкими исключениями, а ссылки за пределы приложения и покупки прячутся за родительский гейт
Закон
COPPA (США): проверяемое согласие родителей для детей до 13 лет; поправки 2025 года добавили отдельное согласие на передачу данных третьим сторонам, например для таргетированной рекламы, с обязательным соблюдением с апреля 2026 года
GDPR, статья 8: возраст цифрового согласия от 13 до 16 лет в зависимости от страны ЕС
Сигналы возраста: Declared Age Range API от Apple и Age Signals API от Google Play возвращают возрастной диапазон без сбора даты рождения приложением; толчком стали законы штатов США о сторах приложений
В приложении
Один переключатель до старта любого SDK (при выключенном автосборе в манифесте): пометка child-directed на каждом рекламном запросе, персонализация выключена, аналитика сведена к неидентифицирующим счётчикам
Нейтральный возрастной гейт: поле даты, которое спрашивают один раз без повторной попытки; храните возрастную группу, а не дату рождения, и по умолчанию включайте ограниченный режим, если возраст неизвестен
Оба стора требуют, чтобы удаление начиналось внутри приложения, а строка в базе оказывается самой простой частью: сервер рассылает удаление по всем системам и вендорам, у которых есть данные пользователя, устройство очищается, а бэкапы вычищаются по заявленному сроку хранения.
Правила сторов
Apple 5.1.1(v): если в приложении можно создать аккаунт, его должно быть можно и удалить в приложении; ссылка на email поддержки означает реджект. Токены Sign in with Apple нужно также отозвать через REST API Apple
Google Play: путь в приложении плюс веб-ссылка, работающая без приложения, указанная в форме Data safety; можно также дать удалить часть данных без закрытия аккаунта
Сервер
Надёжная задача, а не один запрос: разошлите удаление в хранилище, логи, API удаления у вендоров аналитики и крэшей, саппорт-систему, CRM, объектное хранилище и поисковый индекс, с ретраями и алертом на зависшую задачу
Tombstone: оставьте минимальную метку «удалён», чтобы восстановление бэкапа не вернуло аккаунт
Хранение по закону: счета и записи о фроде может понадобиться сохранить; задокументируйте, что хранится и почему
Устройство
Очистите локальное состояние: базу, кеши, DataStore, ключи Keystore и пуш-токен через FirebaseMessaging.deleteToken(), иначе удалённый пользователь продолжит получать уведомления
Что сказать пользователю
Удалено из рабочих систем сейчас, вычищено из бэкапов в течение заявленного числа дней, плюс список того, что хранится по закону
Три вещи: инвентарь данных, который ревьюят как код, запись трафика со свежей установки на реальном устройстве и проверку в CI, которая падает, если приложение обращается к хосту, которого нет в инвентаре.
Инвентарь
Одна строка на поле: источник, хост назначения, цель, срок хранения, тип в Data safety и категория в privacy label App Store; обе формы заполняются по нему, и он меняется в том же PR, что и payload
Запись трафика
Свежая установка, все разрешения отклонены, согласие не дано, затем второй прогон с согласием, потому что спорным обычно оказывается именно трафик за согласием
На Android нужен debuggable аудит-вариант, у которого network security config доверяет пользовательским CA, а certificate pinning выключен, иначе прокси вроде mitmproxy увидит только TLS
Подождите несколько минут: некоторые SDK отправляют первую пачку с задержкой
В iOS отчёт App Privacy Report в Настройках фиксирует домены и сенсоры, которые использовало каждое приложение: это доказательство от ОС, а не от вас
В коде
Data access auditing (AppOpsManager.setOnOpNotedCallback, Android 11) сообщает о каждом обращении к геолокации, контактам или камере со стектрейсом, а attribution tags показывают, какая фича его сделала
Гейт в CI
Прогоните smoke-тест через записывающий прокси и проверьте, что посещённые хосты образуют подмножество allow-list, тогда утверждение в форме становится тестом