在开发悬浮窗过程中,我们会遇到的很大一个问题就是权限问题。在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无需权限显示悬浮窗
网友评论