Как запросить runtime‑разрешения в Android (Java) и корректно обработать отказ

Чтобы запросить runtime‑разрешение в Android на Java, проверяйте ContextCompat.checkSelfPermission, запускайте запрос через ActivityResultContracts.RequestPermission (ActivityResultLauncher) и при отказе используйте shouldShowRequestPermissionRationale(), чтобы либо показать объяснение и повторить запрос, либо отправить пользователя в настройки.

Оглавление

Проверка и запрос одного разрешения

  1. Сначала проверьте состояние разрешения:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    // нужно запросить
} else {
    startCamera();
}
  1. Инициализируйте ActivityResultLauncher в onCreate (регистрировать нужно до первого использования):
private ActivityResultLauncher<String> requestPermissionLauncher;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...
    requestPermissionLauncher = registerForActivityResult(
        new ActivityResultContracts.RequestPermission(),
        isGranted -> {
            if (isGranted) {
                startCamera();
            } else {
                handlePermissionDenied();
            }
        });
}
  1. Запуск запроса:
private void requestCameraPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
        requestPermissionLauncher.launch(Manifest.permission.CAMERA);
    } else {
        startCamera();
    }
}

Всегда проверяйте разрешение перед запросом — лишние запросы раздражают пользователя и снижают конверсию.

Обработка отказа и "Don't ask again"

После отказа важно понять, был ли выбран "Не спрашивать снова". Для этого используйте shouldShowRequestPermissionRationale():

private void handlePermissionDenied() {
    if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
        // Пользователь выбрал "Не спрашивать снова" или система не будет показывать диалог
        showAppSettingsDialog();
    } else {
        // Показываем объяснение и предлагаем повторить запрос
        showPermissionRationaleDialog();
    }
}

private void showPermissionRationaleDialog() {
    new AlertDialog.Builder(this)
        .setMessage("Камера нужна для сканирования QR-кодов. Разрешите доступ?")
        .setPositiveButton("OK", (d, w) -> requestCameraPermission())
        .setNegativeButton("Отмена", null)
        .show();
}

private void showAppSettingsDialog() {
    new AlertDialog.Builder(this)
        .setMessage("Разрешение отключено. Включите доступ в настройках приложения.")
        .setPositiveButton("Настройки", (d, w) -> {
            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            intent.setData(Uri.parse("package:" + getPackageName()));
            startActivity(intent);
        })
        .setNegativeButton("Отмена", null)
        .show();
}

Практический совет: в объяснении указывайте конкретную пользу (что произойдет, если дать разрешение), а не общие фразы.

На Android 11+ доступны одноразовые (one-time) разрешения — доступ может быть сброшен после закрытия приложения. Учтите это в UX и проверяйте разрешения каждый запуск.

Множественные разрешения

Для групповых запросов используйте RequestMultiplePermissions:

private ActivityResultLauncher<String[]> multiplePermissionsLauncher;

@Override
protected void onCreate(Bundle savedInstanceState) {
    multiplePermissionsLauncher = registerForActivityResult(
        new ActivityResultContracts.RequestMultiplePermissions(),
        results -> {
            Boolean cam = results.get(Manifest.permission.CAMERA);
            Boolean mic = results.get(Manifest.permission.RECORD_AUDIO);
            if (Boolean.TRUE.equals(cam) && Boolean.TRUE.equals(mic)) {
                startVideoRecording();
            } else {
                handleMultiplePermissionDenied(results);
            }
        });
}

private void requestCameraAndMic() {
    multiplePermissionsLauncher.launch(new String[]{
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    });
}

Обрабатывайте по отдельности: возможно, нужно объяснить необходимость именно того разрешения, которое отсутствует.

Рекомендации по UX и тестированию

  • Запрашивайте разрешение в контексте действия (перед камерой — при попытке открыть камеру), не при старте приложения.
  • Первое объяснение: короткое и конкретное (что делает функция, зачем нужно разрешение).
  • После отказа — не надо бесконечно предлагать. Если пользователь отказал несколько раз, предложите альтернативу или перевод в настройки.
  • Тестируйте сценарии: первый запрос, отказ + повторный запрос, отказ с "Don't ask again", одноразовое разрешение, ревокирование через adb.

Тестирование через adb:

  • revoke: adb shell pm revoke com.yourapp android.permission.CAMERA
  • grant: adb shell pm grant com.yourapp android.permission.CAMERA

Частые ошибки

  • Не указали permission в AndroidManifest.xml:
  • Запрос до инициализации UI/лаунчера — регистрируйте лаунчеры в onCreate, но просите разрешение когда UI готов.
  • Игнорирование shouldShowRequestPermissionRationale — без объяснения пользователи чаще отказывают.
  • Непроверка состояния после возврата из настроек — пользователь мог не дать разрешение.

FAQ

  • Нужно ли запрашивать обычные (normal) разрешения?
    Нет, обычные разрешения выдаются автоматически при установке.

  • Чем заменить deprecated onRequestPermissionsResult?
    Рекомендуется использовать ActivityResultContracts.RequestPermission / RequestMultiplePermissions и ActivityResultLauncher.

  • Как понять, что пользователь выбрал "Не спрашивать снова"?
    shouldShowRequestPermissionRationale(...) вернёт false после окончательного отказа (плюс при первом показе тоже false — учитывайте логику вызовов).

  • Если пользователь дал одноразовое разрешение, нужно ли повторять запрос?
    Да — одноразовое разрешение может быть отменено после закрытия приложения, поэтому проверяйте и запрашивайте заново при следующем запуске/действии.

Если нужно, могу прислать пример для Jetpack Compose или переводить логику на Kotlin.