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

在开发悬浮窗过程中,我们会遇到的很大一个问题就是权限问题。在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 not 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源码探析及检测悬浮窗权限)