Почему в методе 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 с быстрым тестом, но у него все еще есть недостатки.
-
Предполагается, что если текущая версия 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
-
Предполагается, что если мы попытаемся добавить вид в окно без разрешения наложения, система выдаст исключение, но на некоторых устройствах это не так (многие китайские производители, например, Xiaomi), поэтому мы не можем полностью полагаться на try-catch
-
И последнее, когда мы добавляем представление в окно и в следующей строке кода удаляем его - представление не будет добавлено. На самом деле это займет некоторое время, поэтому эти строки не сильно помогут:
mgr.addView(viewToAdd, params);
mgr.removeView(viewToAdd); // the view is removed even before it was added
это приводит к проблеме, заключающейся в том, что если мы попытаемся проверить, действительно ли представление прикреплено к окну, мы немедленно получим false
.
Хорошо, как мы это исправим?
Поэтому я обновил этот обходной путь с помощью подхода быстрого тестирования разрешений наложения и добавил небольшую задержку, чтобы дать время представлению, которое нужно прикрепить к окну. Из-за этого мы будем использовать слушателя с методом обратного вызова, который уведомит нас о завершении теста:
-
Создайте простой интерфейс обратного вызова:
interface OverlayCheckedListener {
void onOverlayPermissionChecked(boolean isOverlayPermissionOK);
}
-
Вызывайте его, когда пользователь должен разрешить разрешение, и нам нужно проверить, активировал ли он его или нет:
private void checkOverlayAndInitUi() {
showProgressBar();
canDrawOverlaysAfterUserWasAskedToEnableIt(this, new OverlayCheckedListener() {
@Override
public void onOverlayPermissionChecked(boolean isOverlayPermissionOK) {
hideProgressBar();
initUi(isOverlayPermissionOK);
}
});
}
-
Сам метод. Магическое число 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, но кажется, что причина в моем коде, а не в подходе.
Надеюсь, это помогло кому-то!