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

时间:2017-09-12 10:00:47

标签: android permissions

我有父母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 Studio中使用 Android O 在最新版本的虚拟设备上进行了测试,因为我还没有真正的设备。

我做了一些研究并另外用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"并假设用户已授予权限吗?

8 个答案:

答案 0 :(得分:18)

我遇到了同样的问题。我使用一种尝试添加不可见叠加层的解决方法。如果抛出异常,则不授予权限。它可能不是最好的解决方案,但它确实有效。 我无法告诉你有关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;
    }
}

答案 1 :(得分:6)

我也发现了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版本仍将受到影响。

答案 2 :(得分:6)

这是我的全部解决方案,它是其他解决方案的组合,但适用于大多数情况
首先根据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;

}

答案 3 :(得分:4)

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

因此,您需要做的是向用户展示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();
    }
}

答案 4 :(得分:3)

在我的情况下,我的目标是API级别&lt;在Ch4t4的答案中,奥利奥和隐形叠加方法不起作用,因为它不会抛出异常。

详细说明l0v3上面的答案,并且需要防止用户不止一次切换权限,我使用下面的代码(Android版本检查省略):

在活动/片段中:

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

要在activity / fragment中请求权限:

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,类似于上面的onActivityResult

重要的是要注意,从当前实现(Android O)开始,系统不会在回调之前重复删除已注册的侦听器。注册startWatchingMode(ops, packageName, listener)将导致侦听器被调用以进行匹配操作或匹配包名称,并且如果它们两者匹配,将被调用2次,因此包名称在上面设置为null以避免重复呼叫。同时注册监听器而不通过stopWatchingMode取消注册,将导致多次调用监听器 - 这也适用于Activity destroy-create生命周期。

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

答案 5 :(得分:1)

如您所知,存在一个已知的错误: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);
            }
        }
    }
    

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

希望这对某人有所帮助!

答案 6 :(得分:1)

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

我的解决方案是:

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

答案 7 :(得分:0)

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

window.open