web-dev-qa-db-fra.com

Pourquoi dans Android O Settings.canDrawOverlays () renvoie "false" lorsque l'utilisateur a accordé l'autorisation de dessiner des superpositions et revient à mon application?

J'ai l'application MDM pour que les parents contrôlent les appareils de l'enfant et elle utilise la permission SYSTEM_ALERT_WINDOW Pour afficher des avertissements sur l'appareil de l'enfant lorsqu'une action interdite est effectuée. Sur les appareils M + pendant l'installation, l'application vérifie l'autorisation à l'aide de cette méthode:

Settings.canDrawOverlays(getApplicationContext()) 

et si cette méthode renvoie false l'application ouvre une boîte de dialogue système où l'utilisateur peut accorder l'autorisation:

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

Dans Android O, lorsque l'utilisateur accorde avec succès l'autorisation et revient à l'application en appuyant sur le bouton de retour, la méthode canDrawOverlays() renvoie toujours false, jusqu'à ce que l'utilisateur ne ferme pas le et ouvrez-la à nouveau ou choisissez-la simplement dans la boîte de dialogue des applications récentes. Je l'ai testé sur la dernière version de l'appareil virtuel avec Android O dans Android Studio parce que je n'ai pas de véritable appareil.

J'ai fait quelques recherches et vérifié en plus l'autorisation avec 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);

Et donc:

  • lorsque l'application ne dispose pas de cette autorisation, le mode est "2" (MODE_ERRORED) (canDrawOverlays() renvoie false) lorsque l'utilisateur
  • accordé l'autorisation et renvoyé à l'application, le mode est "1" (MODE_IGNORED) (canDrawOverlays() renvoie false)
  • et si vous rouvrez maintenant l'application, le mode est "0" (MODE_ALLOWED) (canDrawOverlays() retourne true)

S'il vous plaît, quelqu'un peut-il m'expliquer ce comportement? Puis-je compter sur mode == 1 D'opération "Android:system_alert_window" Et supposer que l'utilisateur a accordé l'autorisation?

22
Nikolay

J'ai également trouvé ces problèmes avec checkOp. Dans mon cas, j'ai le flux qui permet de rediriger vers les paramètres uniquement lorsque l'autorisation n'est pas définie. Et l'AppOps n'est défini que lors de la redirection vers les paramètres.

En supposant que le rappel AppOps est appelé uniquement lorsque quelque chose est modifié et qu'il n'y a qu'un seul commutateur, qui peut être modifié. Cela signifie que si un rappel est appelé, l'utilisateur doit accorder l'autorisation.

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

Une fois l'application restaurée, la vérification avec canDrawOverlays() démarre a fonctionné pour moi. Pour sûr, je redémarre l'application et vérifie si l'autorisation est accordée par voie standard.

Ce n'est certainement pas une solution parfaite, mais cela devrait fonctionner, jusqu'à ce que nous en sachions plus sur Google.

EDIT: J'ai demandé à google: https://issuetracker.google.com/issues/66072795

EDIT 2: Google corrige cela. Mais il semble que la version Android O sera toujours affectée.

6
l0v3

J'ai rencontré le même problème. J'utilise une solution de contournement qui essaie d'ajouter une superposition invisible. Si une exception est levée, l'autorisation n'est pas accordée. Ce n'est peut-être pas la meilleure solution, mais cela fonctionne. Je ne peux rien vous dire sur la solution AppOps, mais elle semble fiable.

/**
 * 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;
    }
}
18
Ch4t4r

Voici ma solution tout-en-un, c'est une combinaison d'autres, mais sert bien dans la plupart des situations
Premiers contrôles en utilisant le contrôle standard selon Android Docs
La deuxième vérification utilise AppOpsManager
Troisième et dernière vérification si tout le reste échoue est d'essayer d'afficher une superposition, si cela échoue, cela ne fonctionnera certainement pas;)

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;

}
6
Thunderstick

En fait, sur Android 8.0, il renvoie true mais uniquement lorsque vous attendez 5 à 15 secondes et demandez à nouveau l'autorisation en utilisant la méthode Settings.canDrawOverlays(context).

Donc, ce que vous devez faire est d'afficher un ProgressDialog à l'utilisateur avec un message expliquant le problème et d'exécuter un CountDownTimer pour vérifier l'autorisation de superposition en utilisant Settings.canDrawOverlays(context) dans onTick méthode.

Voici un exemple de code:

@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
Vikas Patidar

Dans mon cas, je visais le niveau API <Oreo et la méthode de superposition invisible dans la réponse de Ch4t4 ne fonctionne pas car elle ne lève pas d'exception.

En développant la réponse de l0v3 ci-dessus et la nécessité de se prémunir contre le fait que l'utilisateur bascule l'autorisation plus d'une fois, j'ai utilisé le code ci-dessous (les vérifications de version Android sont omises):

Dans l'activité/le fragment:

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

Pour demander l'autorisation dans l'activité/le 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 */);

Et à l'intérieur 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);
    }    
}

Pour éviter que votre activité ne soit détruite en arrière-plan, à l'intérieur de onCreate:

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

et à l'intérieur de onDestroy, stopWatchingMode doit être invoqué si onOpChangedListener n'est pas nul, semblable à onActivityResult ci-dessus.

Il est important de noter que, depuis l'implémentation actuelle (Android O), le système ne supprimera pas la duplication des écouteurs enregistrés avant de rappeler. L'enregistrement de startWatchingMode(ops, packageName, listener) entraînera l'appel de l'écouteur pour les opérations de correspondance OR nom de package correspondant, et dans le cas où les deux correspondent, sera appelé 2 fois, donc le le nom du package est défini sur null ci-dessus pour éviter l'appel en double. L'enregistrement de l'écouteur plusieurs fois, sans désinscription par stopWatchingMode, entraînera l'appel de l'écouteur plusieurs fois - cela s'applique également aux cycles de vie Activity destroy-create.

Une alternative à ce qui précède consiste à définir un délai d'environ 1 seconde avant d'appeler Settings.canDrawOverlays(context), mais la valeur du délai dépend de l'appareil et peut ne pas être fiable. (Référence: https://issuetracker.google.com/issues/6204781 )

3
headuck

Comme vous le savez, il existe un bug connu: Settings.canDrawOverlays(context) renvoie toujours false sur certains périphériques sur Android 8 et 8.1. Pour l'instant, la meilleure réponse est par @ Ch4t4r avec un test rapide, mais il a toujours des défauts.

  1. Il suppose que si la version Android version Android est 8.1+, nous pouvons compter sur la méthode Settings.canDrawOverlays(context) mais en fait, nous ne pouvons pas, selon plusieurs commentaires. Sur 8.1 sur certains périphériques, nous pouvons toujours obtenir false même si l'autorisation vient d'être reçue.

    if (Android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1)
        return Settings.canDrawOverlays(context); //Wrong
    
  2. Il suppose que si nous essayons d'ajouter une vue à une fenêtre sans l'autorisation de superposition, le système lèvera une exception, mais sur certains appareils, ce n'est pas le cas (beaucoup de fabricants chinois, par exemple Xiaomi), donc nous ne pouvons pas complètement compter sur try-catch

  3. Et la dernière chose, lorsque nous ajoutons une vue à une fenêtre et sur la ligne de code suivante, nous la supprimons - la vue ne sera pas ajoutée. Il faut un certain temps pour l'ajouter réellement, donc ces lignes n'aideront pas beaucoup:

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

cela entraîne un problème: si nous essayons de vérifier si la vue est réellement attachée à une fenêtre, nous obtiendrons false instantanément.


Ok, comment on va arranger ça?

J'ai donc mis à jour cette solution de contournement avec l'approche de test rapide des autorisations de superposition et ajouté un peu de retard pour donner du temps à une vue pour la joindre à une fenêtre. Pour cette raison, nous utiliserons un écouteur avec une méthode de rappel qui nous avertira lorsque le test sera terminé:

  1. Créez une interface de rappel simple:

    interface OverlayCheckedListener {
        void onOverlayPermissionChecked(boolean isOverlayPermissionOK);
    }
    
  2. Appelez-le lorsque l'utilisateur est censé activer l'autorisation et nous devons vérifier s'il l'a effectivement activée ou non (par exemple, dans onActivityResult()):

    private void checkOverlayAndInitUi() {
        showProgressBar();
        canDrawOverlaysAfterUserWasAskedToEnableIt(this, new OverlayCheckedListener() {
            @Override
            public void onOverlayPermissionChecked(boolean isOverlayPermissionOK) {
                hideProgressBar();
                initUi(isOverlayPermissionOK);
            }
        });
    }
    
  3. La méthode elle-même. Le nombre magique 500 - combien de millisecondes nous retardons avant de demander une vue si elle est attachée à une fenêtre.

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

REMARQUE: Ce n'est pas une solution parfaite, si vous trouvez une meilleure solution, faites-le moi savoir. De plus, j'ai fait face à un comportement étrange en raison d'actions post-retardées, mais il semble que la raison soit dans mon code plutôt que dans l'approche.

J'espère que cela a aidé quelqu'un!

1
Kirill Karmazin