一.需求背景
首先介绍一下我们的设备,是一台基于android11系统的播放器设备,一般这类设备给到用户手里的时候一定会”禁用鼠标“,”禁用系统通知栏(状态栏)“,”关闭ADB“。这些设置本身没什么问题,只是今天突然收到用户反馈,说设备放电影的过程中弹出个通知栏,而且还一直存在,收不回去了,这就尴尬了。
二.问题分析
很明显,这是android设备接收到蓝牙文件共享时的通知弹窗。于是赶紧咨询用户,发现是用户想传几个音乐到设备中,但是又不知道如何传输,以为通过蓝牙直接连接就可以传音乐了。
咱们首先是为用户服务的,不能说用户操作不当,只能老老实实解决问题。这里面就有点坑了,首先咱们之后设备出厂时其实是可以关闭蓝牙通知的,这个好说,不在此讨论,单说已经弹出的用户如何维护。咱们普通手机上都知道只要把这个通知栏的消息清空一下就可以了,但是设备上是禁用鼠标的,也就是说用户通过常规方法是无法将其隐藏掉的。而且因为我们禁用了状态栏(settings put system status_bar_show 0),所以导致这个通知是无法自动收起的,一直卡在屏幕上面很难受。
三.解决方案
遇到这种情况我第一时间想到了通过shell命令去清除通知,很遗憾,不好使T.T,至于为啥不好使不在这里细说,大家如果能够好使的话,尽量用这种方案去做。
然后马上想到使用模拟点击的方式,去将这个通知向上划走,事实上也能用:
//模拟搓动动作(从x1y1按下,搓动到x2y2抬起)
input swipe x1 y1 x2 y2
只是这种方案仅仅只能把通知隐藏,这条通知实际上还处于未处理的状态,我们关闭设备重新开机后,这条通知又坚挺的弹出了,好烦T.T
马上再想其他办法,目标是清除所有通知信息。通过观察可以发现,我们的通知栏展开后,会有一个按钮叫”全部清除“,那思路就清晰了,我们只要模拟点击这个按钮就可以了。
期间试过辅助服务(AccessibilityService)的方式,在不重装系统的前提下也无法实现模拟点击,不符合需求,pass。
再使用uiautomator的系列方法,想过做一个runtest,结果发现根据资源ID去模拟点击的话,需要uiautomator2,设备rom也不支持,不可能为这种功能去大批量重刷,pass。
最后还是把目光瞄上了模拟点击的shell命令。首先把整个通知栏展开:
service call statusbar 1
然后找到”全部清除“按钮,模拟点击它。做到这突然觉得不对劲,因为我们无法确定有几条通知,这个按钮的Y轴到底在哪里,我们在代码中甚至不知道是否真的有这个按钮,而且不同分辨率的设置下,这个按钮的XY坐标也会变化!
没关系,难不倒我,突然灵光一现想到刚才使用的uiautomator方案,虽然整体方案走不通,但是谁说所有步骤都不灵呢~最终确定了解决方案:
1、展开通知栏
2、通过uiautomator得到当前窗口的UI信息
3、解析步骤2得到的信息,可以确定”全部清除“按钮的实际坐标
4、模拟点击得到的实际坐标
不废话了,直接上最终的解决方案代码:
public static void clearAllNotification(BaseActivity activity) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Utils.customCommand("service call statusbar 1");//展开状态栏
Utils.customCommand("uiautomator dump /storage/emulated/0/ui.xml");//获取当前窗口UI信息,并生成一个临时文件
String xml = FileUtil.readTxtFile("/storage/emulated/0/ui.xml");//读取文件准备解析
Log.e("test_mn_click", xml);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(new StringReader(xml));
int eventType = parser.getEventType();
Map<String, String> currentNodeAttributes = new HashMap<>();
boolean hasFoundTargetButton = false;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
String nodeName = parser.getName();
if ("node".equals(nodeName)) {
currentNodeAttributes.clear();
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attrName = parser.getAttributeName(i);
String attrValue = parser.getAttributeValue(i);
currentNodeAttributes.put(attrName, attrValue);
}
if ("全部清除".equals(currentNodeAttributes.get("text"))) {//找到全部清除按钮
Log.e("test_mn_click", "找到了全部清除按钮,准备点击");
// 找到目标按钮
String boundsStr = currentNodeAttributes.get("bounds");
if (boundsStr != null) {
processBounds(boundsStr);
hasFoundTargetButton = true;
break;
}
}
}
}
eventType = parser.next();
}
if (!hasFoundTargetButton) {
Log.e("test_mn_click", "没有找到目标按钮,准备收起通知栏");
customCommand("service call statusbar 2");//收起通知栏
Log.e("test_mn_click", "收起通知栏完成");
} else {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
activity.showTip2s("清除通知完成");
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private static void processBounds(String boundsStr) {
// 解析 bounds 字符串
String[] boundsArray = boundsStr.replace("][", ",").replace("[", "").replace("]", "").split(",");
if (boundsArray.length == 4) {
int left = Integer.parseInt(boundsArray[0]);
int top = Integer.parseInt(boundsArray[1]);
int right = Integer.parseInt(boundsArray[2]);
int bottom = Integer.parseInt(boundsArray[3]);
// 计算按钮的左边缘和中间值
int leftEdge = left;
int middleX = left + (right - left) / 2;
int middleY = top + (bottom - top) / 2;
Log.e("test_mn_click", "Left edge: " + leftEdge);
Log.e("test_mn_click", "Middle X: " + middleX);
Log.e("test_mn_click", "Middle Y: " + middleY);
customCommand("input tap " + middleX + " " + middleY);
Log.e("test_mn_click", "点击全部清除按钮完成");
}
}
顺便贴一段我们通过uiautomator获取到的当前窗口UI信息的样子:
。。。。
<node index="1" text="全部清除"
resource-id="com.android.systemui:id/dismiss_text" class="android.widget.Button" package="com.android.systemui" content-desc="清除所有通知。" checkable="false"
checked="false" clickable="true" enabled="true" focusable="true"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[1311,710][1431,782]" /></node>
。。。。。