Почему в методе Android O Settings.canDrawOverlays() возвращает "false", когда пользователь разрешил накладывать наложения и возвращается в мое приложение?

У меня есть приложение MDM для родителей для управления дочерними устройствами, и оно использует разрешение SYSTEM_ALERT_WINDOW для отображения предупреждений на дочернем устройстве при запрещенных действиях. На устройствах M + во время установки приложение проверяет разрешение с помощью этого метода:

Settings.canDrawOverlays(getApplicationContext()) 

и если этот метод возвращает false, приложение открывает системный диалог, в котором пользователь может предоставить разрешение:

Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE);

В Android O, когда пользователь успешно предоставит разрешение и вернется в приложение, нажав кнопку "Назад", метод canDrawOverlays() все еще возвращает false, пока пользователь не закроет приложение и не откроет его снова или просто выберите его в последнем диалоговом окне приложений. Я тестировал его на последней версии виртуального устройства с Android O в Android Studio, потому что у меня нет реального устройства.

Я провел небольшое исследование и дополнительно проверил разрешение с помощью AppOpsManager:

AppOpsManager appOpsMgr = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
int mode = appOpsMgr.checkOpNoThrow("android:system_alert_window", android.os.Process.myUid(), getPackageName());
Log.d(TAG, "android:system_alert_window: mode=" + mode);

И так:

  • когда приложение не имеет этого разрешения, режим "2" (MODE_ERRORED) (canDrawOverlays() возвращает false), когда пользователь
  • предоставил разрешение и вернулся в приложение, режим "1" (MODE_IGNORED) (canDrawOverlays() возвращает false)
  • и если вы сейчас снова открываете приложение, режим "0" (MODE_ALLOWED) (canDrawOverlays() возвращает true)

Пожалуйста, может ли кто-нибудь объяснить это поведение мне? Можно ли полагаться на mode == 1 операции "android:system_alert_window" и предположить, что пользователь предоставил разрешение?

Ответ 1

Я тоже нашел проблемы с checkOp. В моем случае у меня есть поток, который позволяет перенаправить настройки только тогда, когда разрешение не установлено. И AppOps устанавливается только при перенаправлении настроек.

Предполагая, что обратный вызов AppOps вызывается только тогда, когда что-то изменяется, и есть только один переключатель, который можно изменить. Это означает, что, если вызывается обратный вызов, пользователь должен предоставить разрешение.

if (VERSION.SDK_INT >= VERSION_CODES.O &&
                (AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW.equals(op) && 
                     packageName.equals(mContext.getPackageName()))) {
    // proceed to back to your app
}

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

Это определенно не идеальное решение, но оно должно работать, пока мы не узнаем об этом больше от Google.

EDIT: Я спросил google: https://issuetracker.google.com/issues/66072795

ИЗМЕНИТЬ 2: Google исправляет это. Но похоже, что версия Android O будет затронута.

Ответ 2

Я столкнулся с той же проблемой. Я использую обходное решение, которое пытается добавить невидимый оверлей. Если выбрано исключение, разрешение не предоставляется. Это может быть не лучшее решение, но оно работает. Я ничего не могу сказать о решении AppOps, но он выглядит надежным.

/**
 * Workaround for Android O
 */
public static boolean canDrawOverlays(Context context) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true;
    else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
        return Settings.canDrawOverlays(context);
    } else {
        if (Settings.canDrawOverlays(context)) return true;
        try {
            WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            if (mgr == null) return false; //getSystemService might return null
            View viewToAdd = new View(context);
            WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
                    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
            viewToAdd.setLayoutParams(params);
            mgr.addView(viewToAdd, params);
            mgr.removeView(viewToAdd);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

Ответ 3

Вот мое все в одном решении, это сочетание других, но хорошо подходит для большинства ситуаций
Первые проверки с использованием стандартной проверки в соответствии с Android Docs
Вторая проверка - использование AppOpsManager
Третья и последняя проверка, если все остальное терпит неудачу, состоит в том, чтобы попытаться показать наложение, если это терпит неудачу, это определенно не будет работать;)

static boolean canDrawOverlays(Context context) {

    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M && Settings.canDrawOverlays(context)) return true;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {//USING APP OPS MANAGER
        AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        if (manager != null) {
            try {
                int result = manager.checkOp(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, Binder.getCallingUid(), context.getPackageName());
                return result == AppOpsManager.MODE_ALLOWED;
            } catch (Exception ignore) {
            }
        }
    }

    try {//IF This Fails, we definitely can't do it
        WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        if (mgr == null) return false; //getSystemService might return null
        View viewToAdd = new View(context);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
        viewToAdd.setLayoutParams(params);
        mgr.addView(viewToAdd, params);
        mgr.removeView(viewToAdd);
        return true;
    } catch (Exception ignore) {
    }
    return false;

}

Ответ 4

На самом деле на Android 8.0 он возвращает true но только когда вы ждете от 5 до 15 секунд и снова запрашиваете разрешение с помощью метода Settings.canDrawOverlays(context).

Поэтому вам нужно показать пользователю ProgressDialog с сообщением, объясняющим проблему, и запустить CountDownTimer для проверки разрешения наложения, используя Settings.canDrawOverlays(context) в методе onTick.

Вот пример кода:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == 100 && !Settings.canDrawOverlays(getActivity())) {
        //TODO show non cancellable dialog
        new CountDownTimer(15000, 1000) {
            @Override
            public void onTick(long millisUntilFinished) {
                if (Settings.canDrawOverlays(getActivity())) {
                    this.cancel(); // cancel the timer
                    // Overlay permission granted
                    //TODO dismiss dialog and continue
                }
            }

            @Override
            public void onFinish() {
                //TODO dismiss dialog
                if (Settings.canDrawOverlays(getActivity())) {
                    //TODO Overlay permission granted
                } else {
                    //TODO user may have denied it.
                }
            }
        }.start();
    }
}

Ответ 5

В моем случае я нацелился на уровень API < Oreo и невидимый метод наложения в ответе Ch4t4 не работают, поскольку он не генерирует исключение.

Разрабатывая ответ на l0v3 выше, и необходимость защищать пользователя от переключения разрешения более одного раза, я использовал приведенный ниже код (проверка версий Android исключена):

В активности/фрагменте:

Context context; /* get the context */
boolean canDraw;
private AppOpsManager.OnOpChangedListener onOpChangedListener = null;        

Запросить разрешение в действии/фрагменте:

AppOpsManager opsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
canDraw = Settings.canDrawOverlays(context);
onOpChangedListener = new AppOpsManager.OnOpChangedListener() {

    @Override
    public void onOpChanged(String op, String packageName) {
        PackageManager packageManager = context.getPackageManager();
        String myPackageName = context.getPackageName();
        if (myPackageName.equals(packageName) &&
            AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW.equals(op)) {
            canDraw = !canDraw;
        }
    }
};
opsManager.startWatchingMode(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW,
           null, onOpChangedListener);
startActivityForResult(intent, 1 /* REQUEST CODE */);

И внутри onActivityResult

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 1) {
        if (onOpChangedListener != null) {
            AppOpsManager opsManager = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
            opsManager.stopWatchingMode(onOpChangedListener);
            onOpChangedListener = null;
        }
        // The draw overlay permission status can be retrieved from canDraw
        log.info("canDrawOverlay = {}", canDraw);
    }    
}

Для защиты от вашей активности, которая уничтожается в фоновом режиме, внутри onCreate:

if (savedInstanceState != null) {
    canDraw = Settings.canDrawOverlays(context);
}

и внутри onDestroy, stopWatchingMode следует вызывать, если onOpChangedListener не равно null, аналогично onActivityResult выше.

Важно отметить, что с текущей версии (Android O) система не будет дедуплицировать зарегистрированных слушателей, прежде чем перезвонить. Регистрация startWatchingMode(ops, packageName, listener) приведет к тому, что слушатель будет вызван либо для соответствующих операций, либо для соответствия имени пакета, и в случае, если оба они совпадают, вызывается 2 раза, поэтому для имени пакета установлено значение null выше, чтобы избежать дублирования вызова. Кроме того, регистрация слушателя несколько раз, без отмены регистрации с помощью stopWatchingMode, приведет к тому, что слушатель будет вызываться несколько раз - это также относится к жизненным циклам Activity destroy-create.

Альтернативой вышесказанному является установка задержки около 1 секунды перед вызовом Settings.canDrawOverlays(context), но значение задержки зависит от устройства и может быть ненадежным. (Ссылка: https://issuetracker.google.com/issues/62047810)

Ответ 6

Как вы знаете, существует известная ошибка: Settings.canDrawOverlays(context) всегда возвращает false на некоторых устройствах на Android 8 и 8.1. На данный момент лучший ответ - @Ch4t4r с быстрым тестом, но у него все еще есть недостатки.

  1. Предполагается, что если текущая версия Android - 8. 1+, мы можем положиться на метод Settings.canDrawOverlays(context) но на самом деле мы не можем, как указано в нескольких комментариях. На 8.1 на некоторых устройствах мы все еще можем получить false даже если разрешение только что было получено.

    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1)
        return Settings.canDrawOverlays(context); //Wrong
    
  2. Предполагается, что если мы попытаемся добавить вид в окно без разрешения наложения, система выдаст исключение, но на некоторых устройствах это не так (многие китайские производители, например, Xiaomi), поэтому мы не можем полностью полагаться на try-catch

  3. И последнее, когда мы добавляем представление в окно и в следующей строке кода удаляем его - представление не будет добавлено. На самом деле это займет некоторое время, поэтому эти строки не сильно помогут:

    mgr.addView(viewToAdd, params);
    mgr.removeView(viewToAdd); // the view is removed even before it was added
    

это приводит к проблеме, заключающейся в том, что если мы попытаемся проверить, действительно ли представление прикреплено к окну, мы немедленно получим false.


Хорошо, как мы это исправим?

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

  1. Создайте простой интерфейс обратного вызова:

    interface OverlayCheckedListener {
        void onOverlayPermissionChecked(boolean isOverlayPermissionOK);
    }
    
  2. Вызывайте его, когда пользователь должен разрешить разрешение, и нам нужно проверить, активировал ли он его или нет:

    private void checkOverlayAndInitUi() {
        showProgressBar();
        canDrawOverlaysAfterUserWasAskedToEnableIt(this, new OverlayCheckedListener() {
            @Override
            public void onOverlayPermissionChecked(boolean isOverlayPermissionOK) {
                hideProgressBar();
                initUi(isOverlayPermissionOK);
            }
        });
    }
    
  3. Сам метод. Магическое число 500 - сколько миллисекунд мы задерживаем, прежде чем спрашивать представление, прикреплено ли оно к окну.

    public static void canDrawOverlaysAfterUserWasAskedToEnableIt(Context context, final OverlayCheckedListener listener) {
    if(context == null || listener == null)
        return;
    
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        listener.onOverlayPermissionChecked(true);
        return;
    } else {
        if (Settings.canDrawOverlays(context)) {
            listener.onOverlayPermissionChecked(true);
            return;
        }
        try {
            final WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            if (mgr == null) {
                listener.onOverlayPermissionChecked(false);
                return; //getSystemService might return null
            }
            final View viewToAdd = new View(context);
    
    
            WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
                    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
            viewToAdd.setLayoutParams(params);
    
    
            mgr.addView(viewToAdd, params);
    
            Handler handler = new Handler();
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (listener != null && viewToAdd != null && mgr != null) {
                        listener.onOverlayPermissionChecked(viewToAdd.isAttachedToWindow());
                        mgr.removeView(viewToAdd);
                    }
                }
            }, 500);
    
            } catch (Exception e) {
                listener.onOverlayPermissionChecked(false);
            }
        }
    }
    

ПРИМЕЧАНИЕ: это не идеальное решение, если вы придумали лучшее решение, пожалуйста, дайте мне знать. Кроме того, я столкнулся с некоторым странным поведением из-за действий postDelayed, но кажется, что причина в моем коде, а не в подходе.

Надеюсь, это помогло кому-то!