美文网首页
AppOpsManager源码探析及检测悬浮窗权限

AppOpsManager源码探析及检测悬浮窗权限

作者: InnerNight | 来源:发表于2017-03-08 16:53 被阅读2435次

    在开发悬浮窗过程中,我们会遇到的很大一个问题就是权限问题。在6.0引入动态权限之后,权限被分为了一般权限和危险权限。一般权限只要在清单文件中注册可使用,危险权限可以通过动态获取来获得(比如获取联系人)。而有一些权限必须要通过指定Intent才能获得(比如录屏)。但像悬浮窗权限,是属于默认关闭的权限,必须要用户手动打开。
    那如何检测用户是否同意给了悬浮窗权限呢?这里要用到Android中的一个服务叫做AppOpsManager,这个api在4.4之后引入,设计意图是统一管理系统的权限。但是后来一直也不怎么成功,直到6.0引入了动态权限,这套机制的作用更小了。但是这并不影响我们这里用它来判断悬浮窗权限。
    关于AppOpsManager可以参考这篇文章
    checkOpNoThrow()这个方法
    比如我们要检测悬浮窗权限,可以这样用:

    private boolean checkAlertWindowAllowed() {
        AppOpsManager manager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
        return AppOpsManager.MODE_ALLOWED == manager.checkOpNoThrow(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW,
                Binder.getCallingUid(), getPackageName());
    }
    

    这上面使用的checkOpNoThrow()在没有权限时不会像checkOp那样抛出SecurityException,而是返回错误值,所以可以直接判断。
    很简单吧!
    但是!
    但是!
    但是!
    你以为这么简单吗?如果你的app只要支持6.0以上的机型,那么的确是这么简单。但是!如果你要支持其它4.4以上的机型,就会遇到这样一个问题:6.0以下找不到这个权限AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW
    似乎是因为这个权限对应的常量在6.0以上android才单独拿出来,果然坑爹。
    通过查看checkOp的源码我们可以发现,其实这个方法最后会调用一个参数不同的checkOp方法:

    public int checkOp(String op, int uid, String packageName) {
        return checkOp(strOpToOp(op), uid, packageName);
    }
    

    在strOpToOp方法中,我们传入的权限string被转成对应的int值。而在6.0以下机型,也是这个方法抛出的异常:

    public static int strOpToOp(String op) {
        Integer val = sOpStrToOp.get(op);
        if (val == null) {
            throw new IllegalArgumentException("Unknown operation string: " + op);
        }
        return val;
    }
    

    而悬浮窗对应的常量值,我们在源文件里也可以找到,问题是这个常量被hide了:

    /** @hide */
    public static final int OP_SYSTEM_ALERT_WINDOW = 24;
    

    那你说,我直接hardcode 24不可以么?当然可以。但问题是,下面这个方法它也是hide的:

    /**
     * Do a quick check for whether an application might be able to perform an operation.
     * This is <em>not</em> a security check; you must use {@link #noteOp(int, int, String)}
     * or {@link #startOp(int, int, String)} for your actual security checks, which also
     * ensure that the given uid and package name are consistent.  This function can just be
     * used for a quick check to see if an operation has been disabled for the application,
     * as an early reject of some work.  This does not modify the time stamp or other data
     * about the operation.
     * @param op The operation to check.  One of the OP_* constants.
     * @param uid The user id of the application attempting to perform the operation.
     * @param packageName The name of the application attempting to perform the operation.
     * @return Returns {@link #MODE_ALLOWED} if the operation is allowed, or
     * {@link #MODE_IGNORED} if it is not allowed and should be silently ignored (without
     * causing the app to crash).
     * @throws SecurityException If the app has been configured to crash on this op.
     * @hide
     */
    public int checkOp(int op, int uid, String packageName) {
        try {
            int mode = mService.checkOperation(op, uid, packageName);
            if (mode == MODE_ERRORED) {
                throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
            }
            return mode;
        } catch (RemoteException e) {
        }
        return MODE_IGNORED;
    }
    

    所以只好使用最后的杀招:反射。代码如下:

    private boolean isAlertWindowAllowed() {
        try {
            Object object = getSystemService(Context.APP_OPS_SERVICE);
            if (object == null) {
                return false;
            }
            Class localClass = object.getClass();
            Class[] arrayOfClass = new Class[3];
            arrayOfClass[0] = Integer.TYPE;
            arrayOfClass[1] = Integer.TYPE;
            arrayOfClass[2] = String.class;
            Method method = localClass.getMethod("checkOp", arrayOfClass);
    
            if (method == null) {
                return false;
            }
            Object[] arrayOfObject = new Object[3];
            arrayOfObject[0] = Integer.valueOf(24);
            arrayOfObject[1] = Integer.valueOf(Binder.getCallingUid());
            arrayOfObject[2] = getPackageName();
            int m = ((Integer) method.invoke(object, arrayOfObject)).intValue();
            return m == AppOpsManager.MODE_ALLOWED;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    

    附上一些参考和拓展文章:
    AppOpsManager介绍
    Android无需权限显示悬浮窗

    相关文章

      网友评论

          本文标题:AppOpsManager源码探析及检测悬浮窗权限

          本文链接:https://www.haomeiwen.com/subject/zmemgttx.html