为什么 Android 8 中的 Settings.canDrawOverlays() 方法在用户授予绘制叠加的权限并返回到应用程序时返回"false"?



我正在为父母开发MDM应用程序来控制孩子的设备,如果执行了禁止的操作,它使用权限SYSTEM_ALERT_WINDOW在设备上显示警告。 在装有 SDK 23+ (Android 6.0) 的设备上,应用在安装过程中会使用此方法检查权限:

Settings.canDrawOverlays(getApplicationContext()) 

如果此方法返回false则应用程序将打开系统对话框,用户可以在其中授予权限:

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

但在装有 SDK 26 (Android 8.0) 的设备上,当用户成功授予权限并通过按返回按钮返回应用时,方法canDrawOverlays()仍会返回false,直到用户不关闭应用并重新启动它,或者只是在最近的应用对话框中选择它。我在AndroidStudio中使用Android 8在最新版本的虚拟设备上对其进行了测试,因为我没有真正的设备。

我做了一些研究,并另外检查了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)

拜托,谁能向我解释这种行为?是否可以依赖操作"android:system_alert_window"mode == 1并假定用户已授予权限?

我遇到了同样的问题。我使用了一种尝试添加不可见覆盖的解决方法。如果引发异常,则不会授予权限。这可能不是最好的解决方案,但它有效。 我无法告诉您有关AppOps解决方案的任何信息,但它看起来很可靠。

编辑 2020 年 10 月:如评论中所述,抛出SecurityExceptionWindowManager内可能存在内存泄漏,导致解决方法视图无法删除(即使显式调用removeView)。对于正常使用,这应该不是什么大问题,但避免在同一应用程序会话中运行太多检查(如果不进行测试,我认为低于一百的任何内容都应该没问题)。

/**
* 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;
}
}

我也发现了 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()开始对我有用。我确定重新启动应用程序并检查是否通过标准方式授予权限。

这绝对不是一个完美的解决方案,但它应该有效,直到我们从谷歌那里了解更多。

编辑:我问谷歌:https://issuetracker.google.com/issues/66072795

编辑2:谷歌修复了这个问题。但似乎Android O版本仍然会受到影响。

这是我的多合一解决方案,这是其他解决方案的组合,但在大多数情况下都很好
根据 Android 文档使用标准检查进行首次检查
第二个检查是使用 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;
}

实际上在Android 8.0上,它会返回true但仅当您等待5到15秒并使用Settings.canDrawOverlays(context)方法再次查询权限时。

因此,您需要做的是向用户显示一个ProgressDialog,其中包含一条解释问题的消息,并运行CountDownTimer以使用onTick方法中的Settings.canDrawOverlays(context)检查覆盖权限。

下面是示例代码:

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

就我而言,我的目标是 Oreo

为了防止您的活动在后台被破坏,在onCreate内部

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

onDestroy中,如果onOpChangedListener不为 null,则应调用stopWatchingMode,类似于上面的onActivityResult

需要注意的是,从当前实现(Android O)开始,系统不会在回调之前删除重复的注册侦听器。 注册startWatchingMode(ops, packageName, listener)将导致侦听器被调用以进行匹配操作或匹配包名称,如果它们都匹配,将被调用 2 次,因此包名称在上面设置为 null 以避免重复调用。 此外,多次注册侦听器,而不由stopWatchingMode取消注册,将导致侦听器被多次调用 - 这也适用于活动销毁-创建生命周期。

上述方法的替代方案是在调用Settings.canDrawOverlays(context)之前设置大约 1 秒的延迟,但延迟的值取决于设备并且可能不可靠。(参考资料:https://issuetracker.google.com/issues/62047810)

如果您检查几次,它将正常工作...

我的解决方案是:

if (!Settings.canDrawOverlays(this)) {
switchMaterialDraw.setChecked(false);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (Settings.canDrawOverlays(HomeActivity.this)){
switchMaterialDraw.setChecked(true);
}
}
}, 500);
switchMaterialDraw.setChecked(false);
} else {
switchMaterialDraw.setChecked(true);
}

如您所知,有一个已知的错误:Settings.canDrawOverlays(context)总是在Android 8和8.1的某些设备上返回false。 目前,最好的答案是通过快速测试@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. 它假设如果我们尝试在没有覆盖权限的情况下向窗口添加视图,系统将引发异常,但在某些设备上并非如此(许多中国制造商,例如小米),因此我们不能完全依赖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. 当用户应该启用权限时调用它,我们需要检查他是否直接启用了它(例如onActivityResult()):

    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);
    }
    }
    }
    

注意:这不是一个完美的解决方案,如果您想出更好的解决方案,请告诉我。此外,由于 postDelay 操作,我遇到了一些奇怪的行为,但似乎原因出在我的代码中而不是在方法中。

希望这对某人有所帮助!

基于@Vikas Patidar 答案的 Kotlin 协程版本

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// see https://stackoverflow.com/questions/46173460/why-in-android-8-method-settings-candrawoverlays-returns-false-when-user-has
if (requestCode == <your_code> && !PermsChecker.hasOverlay(applicationContext)) {
lifecycleScope.launch {
for (i in 1..150) {
delay(100)
if (PermsChecker.hasOverlay(applicationContext)) {
// todo update smth
break
}
}
}
}
}

最新更新