为什么在Android 8中,当用户授予绘制悬浮窗的权限并返回应用程序时,Settings.canDrawOverlays()方法返回“false”?

32

我正在开发一款家长可用于控制孩子设备的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,直到用户关闭应用程序并重新启动它或仅在最近的应用程序对话框中选择它。我在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

请问,有人能解释一下这种行为吗?我能依赖于操作"android:system_alert_window"mode == 1并假设用户已经授予权限吗?

9个回答

22

我也遇到了同样的问题。我的解决方案是尝试添加一个不可见的覆盖层,如果抛出异常则表示未授予权限。这可能不是最好的解决方案,但它有效。

2020年10月编辑:如评论中所述,当抛出SecurityException时,WindowManager中可能会发生内存泄漏,导致即使使用显式调用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;
    }
}

1
由于某种原因,在Android 8.1上,即使在绘制顶部的设置屏幕中未授予该应用程序此权限,此操作也会返回true。这怎么可能呢? - android developer
1
在 Android 8.1 及以上版本中,此代码将返回 Settings.canDrawOverlays,因为据说 Google 在十月份修复了这个 bug。由于 Android 8.1 的发布时间比那更晚,所以它应该包含这个修复。如果没有,请将 >= 改为 >,然后您就可以继续使用了。 - Ch4t4r
在MI 3s中出现问题,目标版本为26,OS为6.0.1,有什么解决办法吗? - Chintan Khetiya
1
我建议使用这种方法时要谨慎,因为如果“addView”方法抛出异常,“WindowManager”本身会创建一个巨大的内存泄漏。我们的一款应用程序因此遭受了多个ANR(应用程序未响应)问题,而且花费了相当长的时间才找到原因,正是这个答案。您可以在此处查看详细的问题和答案:https://dev59.com/1Lroa4cB1Zd3GeqPiFXw#64334332 - Furkan Yurdakul
1
@FurkanYurdakul 发现很有趣。对于普通应用程序 - 没有循环许可检查 - 这可能是可以的。不过,如果谷歌提供可用的API,那就太棒了。 - Ch4t4r
显示剩余5条评论

7
我发现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中未修复的bug。我认为问题已经解决了,并将您的帖子标记为答案。 - Nikolay
请问您能否分享完整的代码来检查应用程序是否被授予了该权限? - android developer
运行Android P,看起来问题没有得到修复。 - Lennoard Silva
安卓8.1模拟器已修复,但安卓8.0仍未解决。 - hiddeneyes02

7

以下是我的全能解决方案,它结合了其他方案,适用于大多数情况:
第一步使用 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;

}

它将生成一个异常。有没有不生成异常的解决方案? - Patel Jaimin
第一行应为 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context)) return true;,因为旧的 API 已自动授予权限。 - Phocacius
2
我建议小心使用这种方法,因为如果addView方法抛出异常,WindowManager本身会创建一个巨大的内存泄漏。我们的一款应用程序由于此原因遭受了多个ANR的困扰,而且花费了很长时间才找到原因,就是这个答案。您可以在此处查看详细的问题和答案:https://dev59.com/1Lroa4cB1Zd3GeqPiFXw#64334332 - Furkan Yurdakul

6
实际上,在安卓8.0上,只有当您等待5到15秒,并使用Settings.canDrawOverlays(context)方法再次查询权限时,它才会返回true
因此,您需要向用户显示一个包含有关问题的消息的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();
    }
}

它是15秒。 - Amin Pinjari
我已经在Redmi Note 6 Pro、Honor 9N、OnePlus 5T和运行Android 8.0和8.1的模拟器上成功测试过,延迟仅为5秒。 - Vikas Patidar
Doogee S55不到2秒(甚至1秒) - Vlad

4
在我的情况下,我针对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中,如果onOpChangedListener不为空,则应调用stopWatchingMode,类似于上面的onActivityResult

值得注意的是,在当前实现(Android O)中,系统在调用回调之前不会对已注册的侦听器进行去重。注册startWatchingMode(ops, packageName, listener)将导致侦听器被调用匹配操作或匹配包名称,如果两者都匹配,则会被调用2次,因此在上面将包名称设置为null以避免重复调用。多次注册监听器而没有通过stopWatchingMode注销将导致监听器被多次调用 - 这也适用于Activity destroy-create生命周期。

以上的另一种选择是在调用Settings.canDrawOverlays(context)之前设置大约1秒的延迟,但延迟值取决于设备,可能不可靠。(参考:https://issuetracker.google.com/issues/62047810


这只是告诉你那里发生了一些变化,对吧?那么真正知道它是否被授予了呢?对我来说,“Settings.canDrawOverlays”始终返回false。在Android 8.1,Pixel 2上进行了测试。 - android developer
当有更改时,布尔值 canDraw 切换。由于 Settings.canDrawOverlaysonActivityResult 之外工作(至少在模拟器中),因此用于初始化布尔值的状态。这可以解决 Android 8.0 上的错误。不确定 Pixel 2 / Android 8.1 中是否存在单独的错误。 - headuck
好的,谢谢。我不明白为什么我的Pixel 2出现了严重问题。在这里报告了它:https://issuetracker.google.com/issues/72479203 - android developer

3
如果您检查多次,它将正常工作...
我的解决方案是:
        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);
    }

1

正如您所知,存在一个已知的错误:在Android 8和8.1上,Settings.canDrawOverlays(context)总是返回false,但仅限于某些设备。 目前,最好的答案是由@Ch4t4r提供的快速测试,但仍存在缺陷。

  1. It assumes that if the current Android version is 8.1+ we can rely on the method Settings.canDrawOverlays(context) but actually, we can't, as per multiple comments. On 8.1 on some devices, we still can get false even if the permission was just received.

    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1)
        return Settings.canDrawOverlays(context); //Wrong
    
  2. It assumes that if we try to add a view to a window without the overlay permission the system will throw an exception, but on some devices it's not the case (a lot of Chinese manufacturers, for example Xiaomi), so we can't fully rely on try-catch

  3. And the last thing, when we add a view to a window and on the next line of code we remove it - the view won't be added. It takes some time to actually add it, so these lines won't help much:

    mgr.addView(viewToAdd, params);
    mgr.removeView(viewToAdd); // the view is removed even before it was added
    
这导致了一个问题,如果我们尝试检查视图是否实际附加到窗口,我们会立即得到 false 的结果。


好的,我们该如何修复这个问题?

所以我更新了这个解决方法,采用了覆盖权限快速测试方法,并添加了一些延迟时间,以便让视图附加到窗口上。 由于这样,我们将使用一个带有回调方法的监听器,当测试完成时,它将通知我们:

  1. Create a simple callback interface:

    interface OverlayCheckedListener {
        void onOverlayPermissionChecked(boolean isOverlayPermissionOK);
    }
    
  2. Call it when the user supposed to enable the permission and we need to check whether he acutally enabled it or not (for exmaple in onActivityResult()):

    private void checkOverlayAndInitUi() {
        showProgressBar();
        canDrawOverlaysAfterUserWasAskedToEnableIt(this, new OverlayCheckedListener() {
            @Override
            public void onOverlayPermissionChecked(boolean isOverlayPermissionOK) {
                hideProgressBar();
                initUi(isOverlayPermissionOK);
            }
        });
    }
    
  3. The method itself. The magic number 500 - how many milliseconds we delay before asking a view if it is attached to a window.

    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操作,我遇到了一些奇怪的行为,但似乎原因在我的代码而不是方法上。

希望这能帮助到某个人!


0
  1. 如果我们无法信任系统API的返回值,我们可以尝试检测是否具有draw_overlay权限。
  2. 无论用户是否授予该权限,我们都可以尝试添加一个悬浮窗口。根据我们的经验,不是所有手机型号都会在没有draw_overlay权限时抛出异常。
  3. 在添加视图时,可以添加一个属性FLAG_WATCH_OUTSIDE_TOUCH,并在布局外部添加点击事件监听。
  4. 如果成功添加了悬浮窗口,您将能够在视图内部或外部获取点击事件,从而确认已授予draw_overlay权限。

0
基于 @Vikas Patidar 的答案的 Kotlin 协程版本
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    // see https://dev59.com/NVYO5IYBdhLWcg3wN--f
    if (requestCode == <your_code> && !PermsChecker.hasOverlay(applicationContext)) {
        lifecycleScope.launch {
            for (i in 1..150) {
                delay(100)
                if (PermsChecker.hasOverlay(applicationContext)) {
                    // todo update smth
                    break
                }
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接