Why in Android O method Settings.canDrawOverlays()

2019-02-03 04:43发布

I have the MDM app for parents to control child's devices and it uses permission SYSTEM_ALERT_WINDOW to display warnings on child's device when forbidden action performed. On devices M+ during installation the app checks the permission using this method:

Settings.canDrawOverlays(getApplicationContext()) 

and if this method return false the app opens system dialog where user can grant the permission:

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

In Android O, when user successfully grant permission and return to the app by pressing back button, method canDrawOverlays() still return false, until user don't close the app and open it again or just choose it in recent apps dialog. I tested it on latest version of virtual device with Android O in Android Studio because I haven't real device.

I did a little research and additionally check the permission with 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);

And so:

  • when the application does not have this permission, the mode is "2" (MODE_ERRORED) (canDrawOverlays() returns false) when the user
  • granted permission and returned to the application, the mode is "1" (MODE_IGNORED) (canDrawOverlays() returns false)
  • and if you now reopen the app, the mode is "0" (MODE_ALLOWED) (canDrawOverlays() returns true)

Please, can anyone explain this behavior to me? Can I rely on mode == 1 of operation "android:system_alert_window" and assume that the user has granted permission?

5条回答
别忘想泡老子
2楼-- · 2019-02-03 04:58

Here is my all in one solution, this is a combination of others but serves well for most situations
First checks using the standard check according to Android Docs
Second Check is Using the AppOpsManager
Third and final check if all else fails is to try to display an overlay, if that fails it definitely aint gonna work ;)

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楼-- · 2019-02-03 05:00

Actually on Android 8.0 it returns true but there is catch, it returns only when you wait for 5 to 15 seconds and again query for overlay permission using Settings.canDrawOverlays(context) method.

So what you need to do is show a ProgressDialog to user with a message explaining about issue and run a CountDownTimer to check for overlay permission using Settings.canDrawOverlays(context) in onTick method.

Please have a look a sample code from a Fragment:

@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楼-- · 2019-02-03 05:00

In my case I was targeting API level < Oreo and the invisible overlay method in Ch4t4's answer does not work as it does not throw an exception.

Elaborating on l0v3's answer above, and the need to guard against the user toggling the permission more than once, I used the below code (Android version checks omitted) :

In the activity / fragment :

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

To request the permission in the 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 */);

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

To guard against your activity being destroyed in the background, inside onCreate:

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

and inside onDestroy, stopWatchingMode should be invoked if onOpChangedListener is not null, similar to onActivityResult above.

It is important to note that, as of the current implementation (Android O), the system will not de-duplicate registered listeners before calling back. Registering startWatchingMode(ops, packageName, listener), will result in the listener being called for either matching operations OR matching package name, and in case both of them matches, will be called 2 times, so the package name is set to null above to avoid the duplicate call. Also registering the listener multiple times, without unregistering by stopWatchingMode, will result in the listener being called multiple times - this applies also across Activity destroy-create lifecycles.

An alternative to the above is to set a delay of around 1 second before calling Settings.canDrawOverlays(context), but the value of delay depends on device and may not be reliable. (Reference: https://issuetracker.google.com/issues/62047810 )

查看更多
相关推荐>>
5楼-- · 2019-02-03 05:04

I've found this problems with checkOp too. In my case I have the flow which allows to redirect to settings only when the permission is not set. And the AppOps is set only when redirecting to settings.

Assuming that AppOps callback is called only when something is changed and there is only one switch, which can be changed. That means, if callback is called, user has to grant the permission.

if (VERSION.SDK_INT >= VERSION_CODES.O &&
                (AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW.equals(op) && 
                     packageName.equals(mContext.getPackageName()))) {
    // proceed to back to your app
}

After the app is restored, checking with canDrawOverlays() starts worked for me. For sure I restart the app and check if permission is granted via standard way.

It's definitely not a perfect solution, but it should work, till we know more about this from Google.

EDIT: I asked google: https://issuetracker.google.com/issues/66072795

EDIT 2: Google fixes this. But it seem that the Android O version will be affected still.

查看更多
虎瘦雄心在
6楼-- · 2019-02-03 05:12

I ran into the same problem. I use a workaround which tries to add an invisible overlay. If an exception is thrown the permission isn't granted. It might not be the best solution, but it works. I can't tell you anything about the AppOps solution, but it looks reliable.

/**
 * 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;
    }
}
查看更多
登录 后发表回答