Bootstrap

android11设备清除所有通知(非手机,root设备)

一.需求背景

        首先介绍一下我们的设备,是一台基于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>
。。。。。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;