为什么Android 8中的Settings.canDrawOverlays()方法在用户赠款绘制叠加的权限并返回应用时返回“false”?

vsaztqbk  于 2023-08-01  发布在  Android
关注(0)|答案(9)|浏览(114)

我正在为父母开发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中使用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);


因此:

  • 当应用程序没有此权限时,当用户
  • 授予权限并返回到应用程序时,模式为“1”(MODE_IGNORED)(canDrawOverlays()返回false
  • 如果现在重新启动应用程序,则模式为“0”(MODE_ALLOWED)(canDrawOverlays()返回true

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

neekobn8

neekobn81#

我也遇到了同样的问题。我使用了一个变通方法,尝试添加一个不可见的覆盖。如果抛出异常,则不授予权限。这可能不是最好的解决方案,但它有效。我不能告诉你关于AppOps解决方案的任何事情,但它看起来很可靠。

  • 编辑2020年10月 *:正如注解中提到的,当抛出SecurityException时,WindowManager内部可能存在内存泄漏,导致无法删除解决方法视图(即使显式调用removeView)。对于正常使用来说,这应该不是什么大问题,但要避免在同一个应用程序会话中运行太多检查(如果没有测试,我认为低于100的任何检查都应该是可以的)。
/**
 * 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;
    }
}

字符串

nom7f22z

nom7f22z2#

我也发现了CheckOp的这个问题。在我的情况下,我有一个流程,允许只在权限未设置时重定向到设置。并且仅在重定向到设置时设置AppOps。
假设AppOps callback仅在发生更改时调用,并且只有一个switch可以更改。这意味着,如果调用回调,用户必须授予权限。

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:**Google修复了此问题。不过,Android O版本似乎仍将受到影响。

l0oc07j2

l0oc07j23#

这里是我的所有在一个解决方案,这是一个其他的组合,但服务以及为大多数情况
首先根据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;

}

字符串

gijlo24d

gijlo24d4#

实际上,在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();
    }
}

字符串

3phpmpom

3phpmpom5#

在我的例子中,我的目标是API级别< Oreo,Ch 4 t4的答案中的不可见覆盖方法不起作用,因为它没有抛出异常。
为了详细说明l0 v3上面的答案,以及防止用户多次切换权限的需要,我使用了下面的代码(省略了Android版本检查):
在活动/片段中:

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

字符串
在Activity /片段中请求权限:

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销毁-创建生命周期。
另一种方法是在调用Settings.canDrawOverlays(context)之前设置大约1秒的延迟,但延迟值取决于设备,可能不可靠。(参考:https://issuetracker.google.com/issues/62047810

ltskdhd1

ltskdhd16#

如果你检查几次,它就能正常工作...

我的解决方案是:

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

字符串

cvxl0en2

cvxl0en27#

如你所知,有一个已知的bug: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

字符串
1.它假设如果我们尝试在没有覆盖权限的情况下向窗口添加视图,系统将抛出异常,但在某些设备上情况并非如此(许多中国制造商,例如小米),所以我们不能完全依赖try-catch
1.最后一件事,当我们添加一个视图到一个窗口,并在下一行代码中删除它-视图不会被添加。实际添加它需要一些时间,所以这些行不会有太大帮助:

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


这导致了一个问题,即如果我们试图检查视图是否实际上附加到窗口,我们将立即得到false

好吧,我们怎么解决这个问题?

因此,我使用覆盖权限快速测试方法更新了这个解决方案,并添加了一点延迟,给予有时间将视图附加到窗口。因此,我们将使用一个带有回调方法的监听器,当测试完成时,它会通知我们:
1.创建一个简单的回调接口:

interface OverlayCheckedListener {
    void onOverlayPermissionChecked(boolean isOverlayPermissionOK);
}


1.当用户应该启用权限时调用它,我们需要检查他是否实际启用了它(例如在onActivityResult()中):

private void checkOverlayAndInitUi() {
    showProgressBar();
    canDrawOverlaysAfterUserWasAskedToEnableIt(this, new OverlayCheckedListener() {
        @Override
        public void onOverlayPermissionChecked(boolean isOverlayPermissionOK) {
            hideProgressBar();
            initUi(isOverlayPermissionOK);
        }
    });
}


1.方法本身。神奇的数字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操作,但似乎原因是在我的代码中,而不是在方法中。

希望这对某人有帮助!

eblbsuwk

eblbsuwk8#

基于@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
                }
            }
        }
    }
}

字符串

slwdgvem

slwdgvem9#

如何申请许可是互联网上足够的信息。但是我还没有找到如何请求用户的权限来启用Settings.canDrawOverlays(context)选项。

相关问题