最近做项目遇到小米手机比较人(zhuang)性(bi)化的悬浮窗权限,当在小米手机上安装完应用后默认是关闭这个权限的,需要用户手动到应用详情页打开该权限。

重(keng)要(die)的是使用这个权限开关系统window后, 小米手机不给任何提示就是不给弹窗。一开始以为是自己代码逻辑写错了,半天才反应过来,小米还有个这个权限,当天6.0以上安卓系统也需要这个权限,但是会有log提示的。

这么麻烦的操作怎么可能让用户自己去找应用详情然后开启操作呢?本文将实现一键开启小米悬浮窗权限!

分析问题

想要实现自动调整到改应用的详情页的权限管理页面,就要知道权限管理页的类名及包名,我们又没有小米rom的源码,怎么才能知道指定页面的相关信息呢?

查看权限页面类名

这个方法应该有很多中,但是我只验证了一种:想到了 adb shell dumpsys activity

usb链接电脑后,手动打开应用的详情页面里的权限管理页面:

类名信息: com.miui.securitycenter/com.miui.permcenter.permissions.AppPermissionsEditorActivity

构造跳转Intent

知道到了要跳转的activity,我们直接构造Intent 是否可以直接跳过去?

答案肯定是不行的. Intent 需要构造参数,来区分指定app的权限管理页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 经测试V5版本是有区别的
* @param context
*/
public void openMiuiPermissionActivity(Context context) {
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
if ("V5".equals(getProperty())) {
PackageInfo pInfo = null;
try {
pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
Log.e("canking", "error");
}
intent.setClassName("com.miui.securitycenter", "com.miui.securitycenter.permission.AppPermissionsEditor");
intent.putExtra("extra_package_uid", pInfo.applicationInfo.uid);
} else {
intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
intent.putExtra("extra_pkgname", context.getPackageName());
}
if (isActivityAvailable(context, intent)) {
if (context instanceof Activity) {
Activity a = (Activity) context;
a.startActivityForResult(intent, 2);
}
} else {
Log.e("canking", "Intent is not available!");
}
}

测试适配rom

经测试V5版本和后续版本是后区别的, 分别需要app ID和pkgname. 为了区分V5版本,我们需要得到小米rom的版本名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static String getProperty() {
String property = "null";
if (!"Xiaomi".equals(Build.MANUFACTURER)) {
return property;
}
try {
Class<?> spClazz = Class.forName("android.os.SystemProperties");
Method method = spClazz.getDeclaredMethod("get", String.class, String.class);
property = (String) method.invoke(spClazz, "ro.miui.ui.version.name", null);
} catch (Exception e) {
e.printStackTrace();
}
return property;
}

该反射方法来自网络,经验证是有效的.

这样我们就跳转到了指定应用的权限管理页面.

实现一键打开

标题已经写了,我们的目标是用户一键开启,入口做到一键就能开启小米rom悬浮窗权限呢? 可以利用安卓辅助功能自动帮用户跳转, 自动点击打开权限,完成操作后返回.

这里写了个BaseAccessibilityService 通用的操作方法封装在这里.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* Created by changxing on 16-6-2.
*/
public class BaseAccessService extends AccessibilityService {
@Override
protected void onServiceConnected() {
....super.onServiceConnected();
}
@Override
public void onInterrupt() {
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
protected boolean clickByText(AccessibilityNodeInfo nodeInfo, String str) {
....if (null != nodeInfo) {
.... List<AccessibilityNodeInfo> list = nodeInfo.findAccessibilityNodeInfosByText(str);
.... if (null != list && list.size() > 0) {
.... AccessibilityNodeInfo node = list.get(list.size() - 1);
.... if (node.isClickable()) {
.... return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
.... } else {
.... AccessibilityNodeInfo parentNode = node;
.... for (int i = 0; i < 5; i++) {
.... if (null != parentNode) {
.... parentNode = parentNode.getParent();
.... if (null != parentNode && parentNode.isClickable()) {
.... return parentNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
.... }
.... }
.... }
.... }
.... }
....}
....return false;
}
protected AccessibilityNodeInfo findOpenButton(AccessibilityNodeInfo node) {
....if (node == null)
.... return null;
....//非layout元素
....if (node.getChildCount() == 0) {
.... if ("android.widget.Button".equals(node.getClassName())) {
.... return node;
.... } else
.... return null;
....}
....//layout元素,遍历找button
....for (int i = 0; i < node.getChildCount(); i++) {
.... AccessibilityNodeInfo button = findOpenButton(node.getChild(i));
.... if (button != null)
.... return button;
....}
....return null;
}
}

继承这个class ,重写onAccessibilityEvent ,在该方法内处理具体逻辑:

到这里只监听TYPE_WINDOW_STATE_CHANGED类型就行了.通过控件的TEXT来实现找到需要点击的控件.
这里可以Dump View hierarchy工具来查看我们想要的控件具体信息.

Dump View hierarchy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int eventType = event.getEventType();
XLogger.v("eventType:" + eventType);
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
String clazzName = event.getClassName().toString();
AccessibilityNodeInfo nodeInfo = event.getSource();
XLogger.i( "悬浮窗:" + clazzName);
if (clazzName.equals("com.miui.permcenter.permissions.AppPermissionsEditorActivity")) {
if (end) {
clickByText(nodeInfo, "XiaomiPJ");
} else {
boolean access = clickByText(nodeInfo, "显示悬浮窗");
XLogger.i("access" + access);
}
}
if (clazzName.equals("miui.app.AlertDialog")) {
end = clickByText(nodeInfo, "允许");
XLogger.i( "getClick:" + end);
}
}
}

到这里就可以实现一键开启小米rom悬浮窗权限了

但是一键开启前我们需要判断,该权限是否已经开启:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 判断MIUI的悬浮窗权限
* @param context
* @return
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean isMiuiFloatWindowOpAllowed(Context context) {
final int version = Build.VERSION.SDK_INT;
if (version >= 19) {
return checkOp(context, 24); // AppOpsManager.OP_SYSTEM_ALERT_WINDOW
} else {
if ((context.getApplicationInfo().flags & 1 << 27) == 1 << 27) {
return true;
} else {
return false;
}
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean checkOp(Context context, int op) {
final int version = Build.VERSION.SDK_INT;
if (version >= 19) {
AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
try {
Class<?> spClazz = Class.forName(manager.getClass().getName());
Method method = manager.getClass().getDeclaredMethod("checkOp", int.class, int.class, String.class);
int property = (Integer) method.invoke(manager, op,
Binder.getCallingUid(), context.getPackageName());
XLogger.e(AppOpsManager.MODE_ALLOWED + " invoke " + property);
if (AppOpsManager.MODE_ALLOWED == property) {
return true;
} else {
return false;
}
} catch (Exception e) {
XLogger.e(e.getMessage());
}
} else {
XLogger.e("Below API 19 cannot invoke!");
}
return false;
}

api>=19需要用反射来活取系统相关配置信息,应该也适用于魅族手机,为验证.

这里我们就实现了一键开启小米Rom悬浮窗权限,并且实现了判断是否已经开启了该权限状态.
本Demo相关源码地址: https://github.com/CankingApp/XiaomiPJ