32

I'm developing the MDM app for parents to control children's devices and it uses permission SYSTEM_ALERT_WINDOW to display warnings on device if forbidden action has performed. On devices with SDK 23+ (Android 6.0) during installation the app checks the permission using this method:

Settings.canDrawOverlays(getApplicationContext()) 

and if this method returns 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);

But on device with SDK 26 (Android 8.0), when user has successfully granted permission and returned to the app by pressing back button, method canDrawOverlays() still returns false, until user doesn't close the app and starts it again or just chooses it in the recent apps dialog. I tested it on latest version of virtual device with Android 8 in Android Studio because I didn't have real device.

I've done 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 restart 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?

Nikolay
  • 1,429
  • 1
  • 13
  • 24

8 Answers8

22

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.

Edit October 2020: As mentioned in the comments there might be a memory leak inside the WindowManager when the SecurityException is thrown, causing the workaround view not to be removed (even with explicit calls to removeView). For normal uses this should not be much of a problem, but avoid running too many checks in the same app session (Without testing it I'd assume anything below hundred should be alright).

/**
 * 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;
    }
}
Ch4t4r
  • 1,387
  • 1
  • 11
  • 30
  • Thanks for your answer. I was surprised but on all my test devices: Xiaomi Redmi 4 (Android 6.0), Galaxy J3 (Android 7.0) and Galaxy S8 (Android 7.1), overlays works without any checks and permission requests, only declaration in Manifest. I always used this piece of legacy code as axiom and never test it. I leaved it as is, but just added in code, where I check the permission, one condition for Android O, to avoid looping, and TODO comment to pay attention this place in future. – Nikolay Sep 12 '17 at 14:34
  • 1
    For some reason, on Android 8.1, this returns true even though when in the settings screen of draw-on-top, the app isn't granted this permission. How could it be? – android developer Jan 27 '18 at 16:54
  • 1
    With android 8.1 and above this returns `Settings.canDrawOverlays` because the bug was supposedly fixed by Google in October. As android 8.1 was released later than that it should contain the fix. If it doesn't just change >= to > and you should be good to go. – Ch4t4r Jan 28 '18 at 10:03
  • It should be ```>``` because XiaoMi 8 (MIUI 10.0.2.0 Android 8.1) has the same issue – firemaples Oct 19 '18 at 09:06
  • That's device specific then, the Android issue tracker mentions that this was fixed in 8.1. So for general use this should be correct. – Ch4t4r Oct 19 '18 at 09:11
  • Having an issue in MI 3s, Target version 26, OS 6.0.1, any work around? – Chintan Khetiya Dec 18 '18 at 05:49
  • When I first use the function, error was catch and it returned false, as expected, but I don't know why on back pressed it returned true, even if settings wasn't modified... – Barbara K Jun 26 '19 at 13:45
  • 1
    I would recommend using this approach with caution, since the `WindowManager` itself creates a huge memory leak if the `addView` method throws an exception. One of our apps has suffered from multiple ANRs because of this, and it took long enough to find the reason for it, and it was this answer. You can check the detailed question and answer here: https://stackoverflow.com/questions/61183961/call-to-displaymanagerglobal-getdisplayinfo-causes-app-not-responding-anr/64334332#64334332 – Furkan Yurdakul Oct 13 '20 at 11:29
  • 1
    @FurkanYurdakul interesting find. For normal applications - without a looping permission check - it should probably be fine. It would be awesome though if Google provided working APIs. – Ch4t4r Oct 13 '20 at 15:06
  • This solution might not reliable, on a tested device, even if the user do nothing and the switch is kept off, after return from overlay permission setting page, this method will return true without exception throw. – alijandro Jul 22 '21 at 01:16
7

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.

l0v3
  • 963
  • 7
  • 26
7

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;

}
Thunderstick
  • 1,193
  • 13
  • 12
  • It will generate an exception. Any solution without generating exception ? – Patel Jaimin Nov 27 '18 at 10:49
  • The first line should be `if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context)) return true;` since the permission is automatically granted for older APIs. – Phocacius Jan 07 '19 at 13:16
  • 2
    I would recommend using this approach with caution, since the `WindowManager` itself creates a huge memory leak if the `addView` method throws an exception. One of our apps has suffered from multiple ANRs because of this, and it took long enough to find the reason for it, and it was this answer. You can check the detailed question and answer here: https://stackoverflow.com/questions/61183961/call-to-displaymanagerglobal-getdisplayinfo-causes-app-not-responding-anr/64334332#64334332 – Furkan Yurdakul Oct 13 '20 at 11:29
6

Actually on Android 8.0 it returns true but only when you wait for 5 to 15 seconds and again query for the 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.

Here is sample code:

@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();
    }
}
Vlad
  • 7,997
  • 3
  • 56
  • 43
Vikas Patidar
  • 42,865
  • 22
  • 93
  • 106
4

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 )

headuck
  • 2,763
  • 16
  • 19
  • This only tells you that something has changed there, no? What about really knowing that it got to be granted? For me, `Settings.canDrawOverlays` always returns false. Tested on Android 8.1, Pixel 2. – android developer Jan 27 '18 at 17:17
  • The boolean `canDraw` toggles when there are changes. Since `Settings.canDrawOverlays` works outside of `onActivityResult` (at least in emulator) this is used to initialize the state of the boolean. This works around the bug on Android 8.0. Not sure if there are separate bugs in Pixel 2 / Android 8.1. – headuck Jan 29 '18 at 13:16
  • ok. thanks. I don't get why I have a serious issue on my Pixel 2. Reported about it here: https://issuetracker.google.com/issues/72479203 – android developer Jan 29 '18 at 13:42
3

If you check several times, it will work correctly ...

My Solution is :

        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);
    }
Javad Shirkhani
  • 343
  • 6
  • 11
1

As you know there is a known bug: Settings.canDrawOverlays(context) always returns false on some devices on Android 8 and 8.1. For now, the best answer is by @Ch4t4r with a quick test, but it still has flaws.

  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
    

this results to a problem that if we try to check if the view is actually attached to a window we will get false instantly.


Ok, how we gonna fix this?

So I updated this workaround with the overlay permission quick-test approach and added a little delay to give some time to a view to attach to a window. Because of this we will use a listener with a callback method which will notify us when the test is completed:

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

NOTE: This is not a perfect solution, if you come up with a better solution please let me know. Also, I faced some strange behavior due to postDelayed actions but seems that the reason is in my code rather than in the approach.

Hope this helped someone!

Kirill Karmazin
  • 6,256
  • 2
  • 54
  • 42
0

Kotlin coroutines version based on answer of @Vikas Patidar

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
                }
            }
        }
    }
}
Vlad
  • 7,997
  • 3
  • 56
  • 43