安卓基础(语义树)

进化1

package com.example.demotest.unread;import android.accessibilityservice.AccessibilityService;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.List;/*** 无障碍树打印器 - 打印界面视图树和TalkBack语义树*/
public class UnreadMessageAnalyzer {private static final String TAG = "UnreadAnalysis";private AccessibilityService accessibilityService;private int screenWidth;private int screenHeight;public UnreadMessageAnalyzer(AccessibilityService service) {this.accessibilityService = service;DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();this.screenWidth = metrics.widthPixels;this.screenHeight = metrics.heightPixels;}/*** 打印界面视图树和语义树*/public void printAccessibilityTrees() {Log.d(TAG, "\n=== 开始打印无障碍树 ===");AccessibilityNodeInfo rootNode = accessibilityService.getRootInActiveWindow();if (rootNode == null) {Log.e(TAG, "无法获取当前窗口信息");return;}try {// 打印界面视图树Log.d(TAG, "\n【界面视图树】");printUIViewTree(rootNode, 0);// 打印语义树Log.d(TAG, "\n【TalkBack语义树】");printSemanticTree(rootNode, 0);} catch (Exception e) {Log.e(TAG, "打印无障碍树时出错: " + e.getMessage(), e);} finally {rootNode.recycle();}Log.d(TAG, "\n=== 无障碍树打印完成 ===");}/*** 打印UI视图树*/private void printUIViewTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString());// 递归打印子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printUIViewTree(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 打印语义树*/private void printSemanticTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;try {// 只处理对TalkBack有意义的节点if (!shouldFocusNode(node)) {// 继续检查子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printSemanticTree(child, depth);}}return;}String indent = "  ".repeat(depth);// 构建语义节点描述StringBuilder semanticInfo = new StringBuilder();semanticInfo.append(indent).append("├─ ");// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {semanticInfo.append("\"").append(node.getText()).append("\"");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {semanticInfo.append(" ");}semanticInfo.append("(").append(node.getContentDescription()).append(")");}// 如果既没有文本也没有描述,显示类名if ((node.getText() == null || node.getText().toString().trim().isEmpty()) &&(node.getContentDescription() == null || node.getContentDescription().toString().trim().isEmpty())) {String className = node.getClassName() != null ? node.getClassName().toString() : "unknown";semanticInfo.append("[").append(className).append("]");}// 添加操作信息和无障碍属性List<String> actions = new ArrayList<>();if (node.isClickable()) actions.add("可点击");if (node.isLongClickable()) actions.add("可长按");if (node.isCheckable()) actions.add(node.isChecked() ? "已选中" : "可选择");if (node.isScrollable()) actions.add("可滚动");if (node.isFocusable()) actions.add("可聚焦");if (node.isAccessibilityFocused()) actions.add("当前焦点");if (node.isSelected()) actions.add("已选择");if (!node.isEnabled()) actions.add("已禁用");// 添加角色信息 (getRoleDescription在较新版本才可用,这里暂时跳过)// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {//     if (node.getRoleDescription() != null && !node.getRoleDescription().toString().trim().isEmpty()) {//         actions.add("角色:" + node.getRoleDescription());//     }// }// 添加状态信息 (getError方法在API 21以上可用)if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {try {CharSequence error = node.getError();if (error != null && !error.toString().trim().isEmpty()) {actions.add("错误:" + error);}} catch (Exception e) {// 忽略getError方法调用异常}}if (!actions.isEmpty()) {semanticInfo.append(" [").append(String.join(", ", actions)).append("]");}// 打印语义节点信息Log.d(TAG, semanticInfo.toString());// 继续处理子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printSemanticTree(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, "打印语义节点时出错: " + e.getMessage());}}/*** 判断节点是否应该获得TalkBack焦点* 基于Android无障碍服务的焦点规则进行完整判断*/private boolean shouldFocusNode(AccessibilityNodeInfo node) {if (node == null) return false;try {// 1. 基本可见性检查if (!node.isVisibleToUser()) {return false;}// 2. 检查节点是否启用(禁用的节点可能仍需要语音反馈)// 注意:即使isEnabled()为false,某些情况下仍可能需要TalkBack焦点// 3. 检查是否明确设置为可获得无障碍焦点if (node.isFocusable() || node.isAccessibilityFocused()) {return true;}// 4. 检查是否有有意义的文本或描述内容if ((node.getText() != null && !node.getText().toString().trim().isEmpty()) ||(node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty())) {return true;}// 5. 检查是否是可操作的交互元素if (node.isClickable() || node.isLongClickable() || node.isCheckable() || node.isScrollable()) {return true;}// 6. 检查特定的重要UI组件类型String className = node.getClassName() != null ? node.getClassName().toString() : "";if (isImportantUIComponent(className)) {return true;}// 7. 检查是否是容器类型但有重要语义信息if (isSemanticContainer(node)) {return true;}// 8. 检查是否有无障碍操作可执行 (getActionList在API 21以上可用)if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {try {if (node.getActionList() != null && !node.getActionList().isEmpty()) {// 过滤掉一些通用的无意义操作boolean hasmeaningfulAction = false;for (AccessibilityNodeInfo.AccessibilityAction action : node.getActionList()) {if (!isCommonAction(action.getId())) {hasmeaningfulAction = true;break;}}if (hasmeaningfulAction) {return true;}}} catch (Exception e) {// 忽略getActionList方法调用异常}}return false;} catch (Exception e) {Log.w(TAG, "判断节点焦点时出错: " + e.getMessage());return false;}}/*** 判断是否为重要的UI组件类型*/private boolean isImportantUIComponent(String className) {if (className == null || className.isEmpty()) return false;// 重要的UI组件类型return className.contains("Button") ||className.contains("EditText") ||className.contains("TextView") ||className.contains("ImageView") ||className.contains("CheckBox") ||className.contains("RadioButton") ||className.contains("Switch") ||className.contains("ToggleButton") ||className.contains("SeekBar") ||className.contains("ProgressBar") ||className.contains("Spinner") ||className.contains("TabHost") ||className.contains("WebView") ||className.contains("VideoView");}/*** 判断是否为有语义意义的容器*/private boolean isSemanticContainer(AccessibilityNodeInfo node) {try {String className = node.getClassName() != null ? node.getClassName().toString() : "";// 检查是否是具有语义的容器类型boolean isSemanticContainerType = className.contains("RecyclerView") ||className.contains("ListView") ||className.contains("GridView") ||className.contains("ViewPager") ||className.contains("TabLayout") ||className.contains("NavigationView") ||className.contains("ActionBar") ||className.contains("Toolbar");if (!isSemanticContainerType) return false;// 容器如果有内容描述或者是空的(需要告知用户为空),则应该获得焦点return (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) ||node.getChildCount() == 0;} catch (Exception e) {return false;}}/*** 判断是否为常见的无意义操作*/private boolean isCommonAction(int actionId) {return actionId == AccessibilityNodeInfo.ACTION_FOCUS ||actionId == AccessibilityNodeInfo.ACTION_CLEAR_FOCUS ||actionId == AccessibilityNodeInfo.ACTION_SELECT ||actionId == AccessibilityNodeInfo.ACTION_CLEAR_SELECTION ||actionId == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS ||actionId == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;}
} 

进化2

package com.example.demotest.unread;import android.accessibilityservice.AccessibilityService;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.List;/*** 无障碍树打印器 - 打印界面视图树和TalkBack语义树*/
public class UnreadMessageAnalyzer {private static final String TAG = "UnreadAnalysis";private AccessibilityService accessibilityService;private int screenWidth;private int screenHeight;public UnreadMessageAnalyzer(AccessibilityService service) {this.accessibilityService = service;DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();this.screenWidth = metrics.widthPixels;this.screenHeight = metrics.heightPixels;}/*** 打印界面视图树和语义树*/public void printAccessibilityTrees() {Log.d(TAG, "\n=== 开始打印无障碍树 ===");AccessibilityNodeInfo rootNode = accessibilityService.getRootInActiveWindow();if (rootNode == null) {Log.e(TAG, "无法获取当前窗口信息");return;}try {// 打印界面视图树Log.d(TAG, "\n【界面视图树】");printUIViewTree(rootNode, 0);// 打印语义树Log.d(TAG, "\n【TalkBack语义树】");printSemanticTree(rootNode, 0);} catch (Exception e) {Log.e(TAG, "打印无障碍树时出错: " + e.getMessage(), e);} finally {rootNode.recycle();}Log.d(TAG, "\n=== 无障碍树打印完成 ===");}/*** 打印UI视图树*/private void printUIViewTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString());// 递归打印子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printUIViewTree(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 打印语义树*/private void printSemanticTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;try {// 检查是否是聊天项目父级节点if (shouldFocusNode(node)) {// 找到聊天项目父级,打印父级信息printChatItemParent(node, depth);// 打印该父级下的所有子节点printAllChildren(node, depth + 1);// 处理完这个聊天项目后,不再继续处理其子节点return;}// 如果不是聊天项目父级,继续检查子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printSemanticTree(child, depth);}}} catch (Exception e) {Log.w(TAG, "打印语义节点时出错: " + e.getMessage());}}/*** 打印聊天项目父级节点信息*/private void printChatItemParent(AccessibilityNodeInfo node, int depth) {String indent = "  ".repeat(depth);// 构建父级节点描述StringBuilder parentInfo = new StringBuilder();parentInfo.append(indent).append("├─ ");// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {parentInfo.append("\"").append(node.getText()).append("\"");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {parentInfo.append(" ");}parentInfo.append("(").append(node.getContentDescription()).append(")");}// 如果既没有文本也没有描述,显示类名if ((node.getText() == null || node.getText().toString().trim().isEmpty()) &&(node.getContentDescription() == null || node.getContentDescription().toString().trim().isEmpty())) {String className = node.getClassName() != null ? node.getClassName().toString() : "unknown";parentInfo.append("[").append(className).append("]");}// 添加操作信息List<String> actions = new ArrayList<>();if (node.isClickable()) actions.add("可点击");if (node.isLongClickable()) actions.add("可长按");if (node.isCheckable()) actions.add(node.isChecked() ? "已选中" : "可选择");if (node.isScrollable()) actions.add("可滚动");if (node.isFocusable()) actions.add("可聚焦");if (node.isAccessibilityFocused()) actions.add("当前焦点");if (node.isSelected()) actions.add("已选择");if (!node.isEnabled()) actions.add("已禁用");if (!actions.isEmpty()) {parentInfo.append(" [").append(String.join(", ", actions)).append("]");}// 打印父级节点信息Log.d(TAG, parentInfo.toString());}/*** 打印节点下的所有子节点(递归显示)*/private void printAllChildren(AccessibilityNodeInfo parentNode, int depth) {try {for (int i = 0; i < parentNode.getChildCount(); i++) {AccessibilityNodeInfo child = parentNode.getChild(i);if (child != null && child.isVisibleToUser()) {printChildNode(child, depth);// 递归打印子节点的子节点printAllChildren(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, "打印所有子节点时出错: " + e.getMessage());}}/*** 打印单个子节点信息*/private void printChildNode(AccessibilityNodeInfo node, int depth) {String indent = "  ".repeat(depth);StringBuilder childInfo = new StringBuilder();childInfo.append(indent).append("├─ ");// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {childInfo.append("\"").append(node.getText()).append("\"");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {childInfo.append(" ");}childInfo.append("(").append(node.getContentDescription()).append(")");}// 如果既没有文本也没有描述,显示类名if ((node.getText() == null || node.getText().toString().trim().isEmpty()) &&(node.getContentDescription() == null || node.getContentDescription().toString().trim().isEmpty())) {String className = node.getClassName() != null ? node.getClassName().toString() : "unknown";childInfo.append("[").append(className).append("]");}// 打印子节点信息Log.d(TAG, childInfo.toString());}/*** 判断节点是否应该获得TalkBack焦点* 专门识别聊天项目父级:可点击可聚焦且子节点包含时间信息的节点*/private boolean shouldFocusNode(AccessibilityNodeInfo node) {if (node == null) return false;try {// 1. 基本可见性检查if (!node.isVisibleToUser()) {return false;}// 2. 必须是可交互的元素(聊天项目父级特征)if (!node.isClickable() && !node.isLongClickable()) {return false;}// 3. 必须是可聚焦的(聊天项目父级特征)if (!node.isFocusable()) {return false;}// 4. 检查子节点是否包含时间信息(关键过滤条件)if (hasTimeInDirectChildren(node)) {return true;}return false;} catch (Exception e) {Log.w(TAG, "判断节点焦点时出错: " + e.getMessage());return false;}}/*** 检查节点的直接子节点是否包含时间信息* 用于识别聊天项目父级节点*/private boolean hasTimeInDirectChildren(AccessibilityNodeInfo parentNode) {try {for (int i = 0; i < parentNode.getChildCount(); i++) {AccessibilityNodeInfo child = parentNode.getChild(i);if (child != null && child.isVisibleToUser()) {// 检查直接子节点的文本内容String childText = getNodeAllText(child);if (isTimePattern(childText)) {return true;}// 也检查子节点的子节点(递归一层)if (hasTimeInChildren(child)) {return true;}}}} catch (Exception e) {Log.w(TAG, "检查直接子节点时间信息时出错: " + e.getMessage());}return false;}/*** 获取节点的所有文本内容*/private String getNodeAllText(AccessibilityNodeInfo node) {StringBuilder allText = new StringBuilder();if (node.getText() != null) {allText.append(node.getText().toString()).append(" ");}if (node.getContentDescription() != null) {allText.append(node.getContentDescription().toString()).append(" ");}return allText.toString();}/*** 递归检查子节点是否包含时间信息*/private boolean hasTimeInChildren(AccessibilityNodeInfo parentNode) {try {for (int i = 0; i < parentNode.getChildCount(); i++) {AccessibilityNodeInfo child = parentNode.getChild(i);if (child != null && child.isVisibleToUser()) {String childText = getNodeAllText(child);if (isTimePattern(childText)) {return true;}// 递归检查更深层的子节点if (hasTimeInChildren(child)) {return true;}}}} catch (Exception e) {Log.w(TAG, "检查子节点时间信息时出错: " + e.getMessage());}return false;}/*** 判断文本是否包含时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) {return false;}String lowerText = text.toLowerCase().trim();// 检查常见的时间模式return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.matches(".*\\d+:\\d+.*") ||        // HH:MM 格式lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||  // MM月DD日 格式lowerText.matches(".*\\d{4}-\\d{1,2}-\\d{1,2}.*") || // YYYY-MM-DD 格式lowerText.contains("刚刚") ||lowerText.contains("今天") ||lowerText.contains("周一") ||lowerText.contains("周二") ||lowerText.contains("周三") ||lowerText.contains("周四") ||lowerText.contains("周五") ||lowerText.contains("周六") ||lowerText.contains("周日") ||lowerText.contains("星期");}/*** 判断是否为重要的UI组件类型*/private boolean isImportantUIComponent(String className) {if (className == null || className.isEmpty()) return false;// 重要的UI组件类型return className.contains("Button") ||className.contains("EditText") ||className.contains("TextView") ||className.contains("ImageView") ||className.contains("CheckBox") ||className.contains("RadioButton") ||className.contains("Switch") ||className.contains("ToggleButton") ||className.contains("SeekBar") ||className.contains("ProgressBar") ||className.contains("Spinner") ||className.contains("TabHost") ||className.contains("WebView") ||className.contains("VideoView");}/*** 判断是否为有语义意义的容器*/private boolean isSemanticContainer(AccessibilityNodeInfo node) {try {String className = node.getClassName() != null ? node.getClassName().toString() : "";// 检查是否是具有语义的容器类型boolean isSemanticContainerType = className.contains("RecyclerView") ||className.contains("ListView") ||className.contains("GridView") ||className.contains("ViewPager") ||className.contains("TabLayout") ||className.contains("NavigationView") ||className.contains("ActionBar") ||className.contains("Toolbar");if (!isSemanticContainerType) return false;// 容器如果有内容描述或者是空的(需要告知用户为空),则应该获得焦点return (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) ||node.getChildCount() == 0;} catch (Exception e) {return false;}}/*** 判断是否为常见的无意义操作*/private boolean isCommonAction(int actionId) {return actionId == AccessibilityNodeInfo.ACTION_FOCUS ||actionId == AccessibilityNodeInfo.ACTION_CLEAR_FOCUS ||actionId == AccessibilityNodeInfo.ACTION_SELECT ||actionId == AccessibilityNodeInfo.ACTION_CLEAR_SELECTION ||actionId == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS ||actionId == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;}
} 

时间回溯

package com.example.demotest.unread;import android.accessibilityservice.AccessibilityService;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;/*** 无障碍树打印器 - 打印界面视图树*/
public class UnreadMessageAnalyzer {private static final String TAG = "UnreadAnalysis";private AccessibilityService accessibilityService;private int screenWidth;private int screenHeight;public UnreadMessageAnalyzer(AccessibilityService service) {this.accessibilityService = service;DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();this.screenWidth = metrics.widthPixels;this.screenHeight = metrics.heightPixels;}/*** 打印界面视图树和处理过的视图树*/public void printAccessibilityTrees() {Log.d(TAG, "\n=== 开始打印无障碍树 ===");AccessibilityNodeInfo rootNode = accessibilityService.getRootInActiveWindow();if (rootNode == null) {Log.e(TAG, "无法获取当前窗口信息");return;}try {// 打印完整的界面视图树Log.d(TAG, "\n【界面视图树】");printUIViewTree(rootNode, 0);// 打印处理过的视图树(聊天项目)Log.d(TAG, "\n【处理过的视图树(聊天项目)】");printProcessedViewTree(rootNode);} catch (Exception e) {Log.e(TAG, "打印无障碍树时出错: " + e.getMessage(), e);} finally {rootNode.recycle();}Log.d(TAG, "\n=== 无障碍树打印完成 ===");}/*** 打印UI视图树*/private void printUIViewTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString());// 递归打印子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printUIViewTree(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 打印处理过的视图树 - 基于时间节点回溯的聊天项目*/private void printProcessedViewTree(AccessibilityNodeInfo rootNode) {try {// 第一步:收集所有时间节点List<AccessibilityNodeInfo> timeNodes = new ArrayList<>();collectTimeNodes(rootNode, timeNodes);Log.d(TAG, "找到 " + timeNodes.size() + " 个时间节点");// 第二步:对每个时间节点进行回溯,找到可点击父级Set<AccessibilityNodeInfo> processedParents = new HashSet<>();for (AccessibilityNodeInfo timeNode : timeNodes) {AccessibilityNodeInfo clickableParent = findNearestClickableParent(timeNode);if (clickableParent != null && !processedParents.contains(clickableParent)) {// 打印找到的聊天项目printChatItem(clickableParent, 0);// 标记为已处理,避免重复processedParents.add(clickableParent);Log.d(TAG, ""); // 空行分隔不同的聊天项目}}if (processedParents.isEmpty()) {Log.d(TAG, "未找到符合条件的聊天项目");}} catch (Exception e) {Log.w(TAG, "打印处理过的视图树时出错: " + e.getMessage());}}/*** 收集所有包含时间信息的节点*/private void collectTimeNodes(AccessibilityNodeInfo node, List<AccessibilityNodeInfo> timeNodes) {if (node == null || !node.isVisibleToUser()) return;try {// 检查当前节点是否包含时间信息String nodeText = getNodeText(node);if (isTimePattern(nodeText)) {timeNodes.add(node);Log.d(TAG, "发现时间节点: " + nodeText.trim());}// 递归检查所有子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {collectTimeNodes(child, timeNodes);}}} catch (Exception e) {Log.w(TAG, "收集时间节点时出错: " + e.getMessage());}}/*** 向上回溯找到最近的可点击父级*/private AccessibilityNodeInfo findNearestClickableParent(AccessibilityNodeInfo timeNode) {if (timeNode == null) return null;try {AccessibilityNodeInfo current = timeNode;// 向上遍历父级节点,找到第一个可点击的父级while (current != null) {AccessibilityNodeInfo parent = current.getParent();if (parent == null) break;// 检查父级是否满足可点击条件if (isClickableParent(parent)) {Log.d(TAG, "找到可点击父级: " + parent.getClassName());return parent;}current = parent;}return null;} catch (Exception e) {Log.w(TAG, "查找可点击父级时出错: " + e.getMessage());return null;}}/*** 检查节点是否满足可点击父级条件*/private boolean isClickableParent(AccessibilityNodeInfo node) {if (node == null || !node.isVisibleToUser()) return false;// 满足条件:// 1. {clickable, long-clickable} 或 {clickable, long-clickable, visible}// 2. {clickable, visible}return node.isClickable() && (node.isLongClickable() || node.isVisibleToUser());}/*** 打印聊天项目(可点击父级及其所有子节点)*/private void printChatItem(AccessibilityNodeInfo parentNode, int depth) {if (parentNode == null) return;try {// 打印父级节点信息printNodeInfo(parentNode, depth);// 递归打印所有子节点printAllChildNodes(parentNode, depth + 1);} catch (Exception e) {Log.w(TAG, "打印聊天项目时出错: " + e.getMessage());}}/*** 递归打印所有子节点*/private void printAllChildNodes(AccessibilityNodeInfo parentNode, int depth) {try {for (int i = 0; i < parentNode.getChildCount(); i++) {AccessibilityNodeInfo child = parentNode.getChild(i);if (child != null && child.isVisibleToUser()) {// 打印子节点信息printNodeInfo(child, depth);// 递归打印子节点的子节点printAllChildNodes(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, "打印子节点时出错: " + e.getMessage());}}/*** 打印节点信息(统一格式)*/private void printNodeInfo(AccessibilityNodeInfo node, int depth) {String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString());} catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 获取节点的文本内容*/private String getNodeText(AccessibilityNodeInfo node) {StringBuilder text = new StringBuilder();if (node.getText() != null) {text.append(node.getText().toString()).append(" ");}if (node.getContentDescription() != null) {text.append(node.getContentDescription().toString()).append(" ");}return text.toString();}/*** 判断文本是否包含时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) {return false;}String lowerText = text.toLowerCase().trim();// 检查常见的时间模式return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.contains("今天") ||lowerText.contains("刚刚") ||lowerText.matches(".*\\d+:\\d+.*") ||                    // HH:MM 格式lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||          // MM月DD日 格式lowerText.matches(".*\\d{4}/\\d{1,2}/\\d{1,2}.*") ||     // YYYY/MM/DD 格式lowerText.matches(".*\\d{4}-\\d{1,2}-\\d{1,2}.*") ||     // YYYY-MM-DD 格式lowerText.contains("周一") ||lowerText.contains("周二") ||lowerText.contains("周三") ||lowerText.contains("周四") ||lowerText.contains("周五") ||lowerText.contains("周六") ||lowerText.contains("周日") ||lowerText.contains("星期一") ||lowerText.contains("星期二") ||lowerText.contains("星期三") ||lowerText.contains("星期四") ||lowerText.contains("星期五") ||lowerText.contains("星期六") ||lowerText.contains("星期日");}} 

最新版本

package com.example.demotest.unread;import android.accessibilityservice.AccessibilityService;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;/*** 无障碍树打印器 - 打印界面视图树*/
public class UnreadMessageAnalyzer {private static final String TAG = "UnreadAnalysis";private AccessibilityService accessibilityService;private int screenWidth;private int screenHeight;public UnreadMessageAnalyzer(AccessibilityService service) {this.accessibilityService = service;DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();this.screenWidth = metrics.widthPixels;this.screenHeight = metrics.heightPixels;}/*** 打印界面视图树和处理过的视图树*/public void printAccessibilityTrees() {Log.d(TAG, "\n=== 开始打印无障碍树 ===");AccessibilityNodeInfo rootNode = accessibilityService.getRootInActiveWindow();if (rootNode == null) {Log.e(TAG, "无法获取当前窗口信息");return;}try {// 打印完整的界面视图树Log.d(TAG, "\n【界面视图树】");printUIViewTree(rootNode, 0);// 打印处理过的视图树(聊天项目)Log.d(TAG, "\n【处理过的视图树(聊天项目)】");printProcessedViewTree(rootNode);} catch (Exception e) {Log.e(TAG, "打印无障碍树时出错: " + e.getMessage(), e);} finally {rootNode.recycle();}Log.d(TAG, "\n=== 无障碍树打印完成 ===");}/*** 打印UI视图树*/private void printUIViewTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString());// 递归打印子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printUIViewTree(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 打印处理过的视图树 - 基于时间节点回溯的聊天项目*/private void printProcessedViewTree(AccessibilityNodeInfo rootNode) {try {// 第一步:收集所有时间节点List<AccessibilityNodeInfo> timeNodes = new ArrayList<>();collectTimeNodes(rootNode, timeNodes);Log.d(TAG, "找到 " + timeNodes.size() + " 个时间节点");// 第二步:对每个时间节点进行回溯,找到可点击父级Set<AccessibilityNodeInfo> processedParents = new HashSet<>();for (AccessibilityNodeInfo timeNode : timeNodes) {AccessibilityNodeInfo clickableParent = findNearestClickableParent(timeNode);if (clickableParent != null && !processedParents.contains(clickableParent)) {// 打印找到的聊天项目printChatItem(clickableParent, 0);// 标记为已处理,避免重复processedParents.add(clickableParent);Log.d(TAG, ""); // 空行分隔不同的聊天项目}}if (processedParents.isEmpty()) {Log.d(TAG, "未找到符合条件的聊天项目");}} catch (Exception e) {Log.w(TAG, "打印处理过的视图树时出错: " + e.getMessage());}}/*** 收集所有包含时间信息的节点*/private void collectTimeNodes(AccessibilityNodeInfo node, List<AccessibilityNodeInfo> timeNodes) {if (node == null || !node.isVisibleToUser()) return;try {// 检查当前节点是否包含时间信息String nodeText = getNodeText(node);if (isTimePattern(nodeText)) {timeNodes.add(node);Log.d(TAG, "发现时间节点: " + nodeText.trim());}// 递归检查所有子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {collectTimeNodes(child, timeNodes);}}} catch (Exception e) {Log.w(TAG, "收集时间节点时出错: " + e.getMessage());}}/*** 向上回溯找到最近的可点击父级*/private AccessibilityNodeInfo findNearestClickableParent(AccessibilityNodeInfo timeNode) {if (timeNode == null) return null;try {AccessibilityNodeInfo current = timeNode;// 向上遍历父级节点,找到第一个可点击的父级while (current != null) {AccessibilityNodeInfo parent = current.getParent();if (parent == null) break;// 检查父级是否满足可点击条件if (isClickableParent(parent)) {Log.d(TAG, "找到可点击父级: " + parent.getClassName());return parent;}current = parent;}return null;} catch (Exception e) {Log.w(TAG, "查找可点击父级时出错: " + e.getMessage());return null;}}/*** 检查节点是否满足可点击父级条件*/private boolean isClickableParent(AccessibilityNodeInfo node) {if (node == null || !node.isVisibleToUser()) return false;// 满足条件:// 1. {clickable, long-clickable} 或 {clickable, long-clickable, visible}// 2. {clickable, visible}return node.isClickable() && (node.isLongClickable() || node.isVisibleToUser());}/*** 打印聊天项目(可点击父级及其所有子节点)*/private void printChatItem(AccessibilityNodeInfo parentNode, int depth) {if (parentNode == null) return;try {// 打印父级节点信息printNodeInfo(parentNode, depth);// 递归打印所有子节点printAllChildNodes(parentNode, depth + 1);} catch (Exception e) {Log.w(TAG, "打印聊天项目时出错: " + e.getMessage());}}/*** 递归打印所有子节点*/private void printAllChildNodes(AccessibilityNodeInfo parentNode, int depth) {try {for (int i = 0; i < parentNode.getChildCount(); i++) {AccessibilityNodeInfo child = parentNode.getChild(i);if (child != null && child.isVisibleToUser()) {// 打印子节点信息printNodeInfo(child, depth);// 递归打印子节点的子节点printAllChildNodes(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, "打印子节点时出错: " + e.getMessage());}}/*** 打印节点信息(统一格式)*/private void printNodeInfo(AccessibilityNodeInfo node, int depth) {String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString()); } catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 获取节点的文本内容*/private String getNodeText(AccessibilityNodeInfo node) {StringBuilder text = new StringBuilder();if (node.getText() != null) {text.append(node.getText().toString()).append(" ");}if (node.getContentDescription() != null) {text.append(node.getContentDescription().toString()).append(" ");}return text.toString();}/*** 判断文本是否包含时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) {return false;}String lowerText = text.toLowerCase().trim();// 检查常见的时间模式return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.contains("今天") ||lowerText.contains("刚刚") ||lowerText.matches(".*\\d+:\\d+.*") ||                    // HH:MM 格式lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||          // MM月DD日 格式lowerText.matches(".*\\d{4}/\\d{1,2}/\\d{1,2}.*") ||     // YYYY/MM/DD 格式lowerText.matches(".*\\d{4}-\\d{1,2}-\\d{1,2}.*") ||     // YYYY-MM-DD 格式lowerText.contains("周一") ||lowerText.contains("周二") ||lowerText.contains("周三") ||lowerText.contains("周四") ||lowerText.contains("周五") ||lowerText.contains("周六") ||lowerText.contains("周日") ||lowerText.contains("星期一") ||lowerText.contains("星期二") ||lowerText.contains("星期三") ||lowerText.contains("星期四") ||lowerText.contains("星期五") ||lowerText.contains("星期六") ||lowerText.contains("星期日");}} 

最终版本

UnreadMessageAnalyzer.java   

package com.example.demotest.unread;import android.accessibilityservice.AccessibilityService;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;/*** 无障碍树打印器 - 打印界面视图树*/
public class UnreadMessageAnalyzer {private static final String TAG = "UnreadAnalysis";private AccessibilityService accessibilityService;private int screenWidth;private int screenHeight;public UnreadMessageAnalyzer(AccessibilityService service) {this.accessibilityService = service;DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();this.screenWidth = metrics.widthPixels;this.screenHeight = metrics.heightPixels;}/*** 打印界面视图树和处理过的视图树*/public void printAccessibilityTrees() {Log.d(TAG, "\n=== 开始打印无障碍树 ===");AccessibilityNodeInfo rootNode = accessibilityService.getRootInActiveWindow();if (rootNode == null) {Log.e(TAG, "无法获取当前窗口信息");return;}try {// 打印完整的界面视图树Log.d(TAG, "\n【界面视图树】");printUIViewTree(rootNode, 0);// 打印处理过的视图树(聊天项目)Log.d(TAG, "\n【处理过的视图树(聊天项目)】");printProcessedViewTree(rootNode);} catch (Exception e) {Log.e(TAG, "打印无障碍树时出错: " + e.getMessage(), e);} finally {rootNode.recycle();}Log.d(TAG, "\n=== 无障碍树打印完成 ===");}/*** 打印UI视图树*/private void printUIViewTree(AccessibilityNodeInfo node, int depth) {if (node == null) return;String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString());// 递归打印子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {printUIViewTree(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 打印处理过的视图树 - 基于时间节点回溯的聊天项目*/private void printProcessedViewTree(AccessibilityNodeInfo rootNode) {try {// 第一步:收集所有时间节点List<AccessibilityNodeInfo> timeNodes = new ArrayList<>();collectTimeNodes(rootNode, timeNodes);Log.d(TAG, "找到 " + timeNodes.size() + " 个时间节点");// 第二步:对每个时间节点进行回溯,找到可点击父级Set<AccessibilityNodeInfo> processedParents = new HashSet<>();List<AccessibilityNodeInfo> chatItems = new ArrayList<>();for (AccessibilityNodeInfo timeNode : timeNodes) {AccessibilityNodeInfo clickableParent = findNearestClickableParent(timeNode);if (clickableParent != null && !processedParents.contains(clickableParent)) {// 打印找到的聊天项目printChatItem(clickableParent, 0);// 添加到聊天项目列表中chatItems.add(clickableParent);// 标记为已处理,避免重复processedParents.add(clickableParent);Log.d(TAG, ""); // 空行分隔不同的聊天项目}}if (processedParents.isEmpty()) {Log.d(TAG, "未找到符合条件的聊天项目");} else {// 使用未读消息检测器分析所有聊天项analyzeUnreadMessages(chatItems);}} catch (Exception e) {Log.w(TAG, "打印处理过的视图树时出错: " + e.getMessage());}}/*** 收集所有包含时间信息的节点*/private void collectTimeNodes(AccessibilityNodeInfo node, List<AccessibilityNodeInfo> timeNodes) {if (node == null || !node.isVisibleToUser()) return;try {// 检查当前节点是否包含时间信息String nodeText = getNodeText(node);if (isTimePattern(nodeText)) {timeNodes.add(node);Log.d(TAG, "发现时间节点: " + nodeText.trim());}// 递归检查所有子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {collectTimeNodes(child, timeNodes);}}} catch (Exception e) {Log.w(TAG, "收集时间节点时出错: " + e.getMessage());}}/*** 向上回溯找到最近的可点击父级*/private AccessibilityNodeInfo findNearestClickableParent(AccessibilityNodeInfo timeNode) {if (timeNode == null) return null;try {AccessibilityNodeInfo current = timeNode;// 向上遍历父级节点,找到第一个可点击的父级while (current != null) {AccessibilityNodeInfo parent = current.getParent();if (parent == null) break;// 检查父级是否满足可点击条件if (isClickableParent(parent)) {Log.d(TAG, "找到可点击父级: " + parent.getClassName());return parent;}current = parent;}return null;} catch (Exception e) {Log.w(TAG, "查找可点击父级时出错: " + e.getMessage());return null;}}/*** 检查节点是否满足可点击父级条件*/private boolean isClickableParent(AccessibilityNodeInfo node) {if (node == null || !node.isVisibleToUser()) return false;// 满足条件:// 1. {clickable, long-clickable} 或 {clickable, long-clickable, visible}// 2. {clickable, visible}return node.isClickable() && (node.isLongClickable() || node.isVisibleToUser());}/*** 打印聊天项目(可点击父级及其所有子节点)*/private void printChatItem(AccessibilityNodeInfo parentNode, int depth) {if (parentNode == null) return;try {// 打印父级节点信息printNodeInfo(parentNode, depth);// 递归打印所有子节点printAllChildNodes(parentNode, depth + 1);} catch (Exception e) {Log.w(TAG, "打印聊天项目时出错: " + e.getMessage());}}/*** 递归打印所有子节点*/private void printAllChildNodes(AccessibilityNodeInfo parentNode, int depth) {try {for (int i = 0; i < parentNode.getChildCount(); i++) {AccessibilityNodeInfo child = parentNode.getChild(i);if (child != null && child.isVisibleToUser()) {// 打印子节点信息printNodeInfo(child, depth);// 递归打印子节点的子节点printAllChildNodes(child, depth + 1);}}} catch (Exception e) {Log.w(TAG, "打印子节点时出错: " + e.getMessage());}}/*** 打印节点信息(统一格式)*/private void printNodeInfo(AccessibilityNodeInfo node, int depth) {String indent = "  ".repeat(depth);try {// 获取节点基本信息String className = node.getClassName() != null ? node.getClassName().toString() : "null";Rect bounds = new Rect();node.getBoundsInScreen(bounds);// 构建节点描述StringBuilder nodeInfo = new StringBuilder();nodeInfo.append(indent).append("├─ ").append(className);// 添加文本内容if (node.getText() != null && !node.getText().toString().trim().isEmpty()) {nodeInfo.append(" [text=\"").append(node.getText()).append("\"]");}// 添加内容描述if (node.getContentDescription() != null && !node.getContentDescription().toString().trim().isEmpty()) {nodeInfo.append(" [desc=\"").append(node.getContentDescription()).append("\"]");}// 添加边界信息nodeInfo.append(" [").append(bounds.toString()).append("]");// 添加关键属性List<String> attributes = new ArrayList<>();if (node.isClickable()) attributes.add("clickable");if (node.isLongClickable()) attributes.add("long-clickable");if (node.isCheckable()) attributes.add(node.isChecked() ? "checked" : "checkable");if (node.isScrollable()) attributes.add("scrollable");if (node.isVisibleToUser()) attributes.add("visible");if (!attributes.isEmpty()) {nodeInfo.append(" {").append(String.join(", ", attributes)).append("}");}// 打印节点信息Log.d(TAG, nodeInfo.toString()); } catch (Exception e) {Log.w(TAG, indent + "├─ [打印节点出错: " + e.getMessage() + "]");}}/*** 获取节点的文本内容*/private String getNodeText(AccessibilityNodeInfo node) {StringBuilder text = new StringBuilder();if (node.getText() != null) {text.append(node.getText().toString()).append(" ");}if (node.getContentDescription() != null) {text.append(node.getContentDescription().toString()).append(" ");}return text.toString();}/*** 判断文本是否包含时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) {return false;}String lowerText = text.toLowerCase().trim();// 检查常见的时间模式return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.contains("今天") ||lowerText.contains("刚刚") ||lowerText.matches(".*\\d+:\\d+.*") ||                    // HH:MM 格式lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||          // MM月DD日 格式lowerText.matches(".*\\d{4}/\\d{1,2}/\\d{1,2}.*") ||     // YYYY/MM/DD 格式lowerText.matches(".*\\d{4}-\\d{1,2}-\\d{1,2}.*") ||     // YYYY-MM-DD 格式lowerText.contains("周一") ||lowerText.contains("周二") ||lowerText.contains("周三") ||lowerText.contains("周四") ||lowerText.contains("周五") ||lowerText.contains("周六") ||lowerText.contains("周日") ||lowerText.contains("星期一") ||lowerText.contains("星期二") ||lowerText.contains("星期三") ||lowerText.contains("星期四") ||lowerText.contains("星期五") ||lowerText.contains("星期六") ||lowerText.contains("星期日");}/*** 分析聊天项的未读消息*/private void analyzeUnreadMessages(List<AccessibilityNodeInfo> chatItems) {Log.d(TAG, "\n🔍 ===== 开始未读消息分析 =====");try {// 创建未读消息检测器UnreadMessageDetector detector = new UnreadMessageDetector(screenWidth);// 检测所有聊天项的未读消息List<UnreadMessageDetector.UnreadResult> unreadResults = detector.detectMultipleUnreadMessages(chatItems);// 输出分析结果if (unreadResults.isEmpty()) {Log.d(TAG, "🟢 当前页面没有发现未读消息");} else {Log.d(TAG, "🔴 发现 " + unreadResults.size() + " 个有未读消息的聊天项:");for (int i = 0; i < unreadResults.size(); i++) {UnreadMessageDetector.UnreadResult result = unreadResults.get(i);Log.d(TAG, String.format("\n📱 第%d个未读消息:", i + 1));Log.d(TAG, "   👤 昵称: " + (result.nickname != null ? result.nickname : "未知"));Log.d(TAG, "   💬 消息: " + (result.lastMessage != null ? result.lastMessage : "无"));Log.d(TAG, "   ⏰ 时间: " + (result.time != null ? result.time : "未知"));Log.d(TAG, "   🔴 未读标识: " + result.unreadCount);Log.d(TAG, "   📍 点击坐标: " + result.clickBounds.toString());Log.d(TAG, "   📱 坐标中心: (" + result.clickBounds.centerX() + ", " + result.clickBounds.centerY() + ")");}// 输出可直接使用的坐标列表Log.d(TAG, "\n📍 未读消息用户点击坐标汇总:");for (int i = 0; i < unreadResults.size(); i++) {UnreadMessageDetector.UnreadResult result = unreadResults.get(i);Log.d(TAG, String.format("用户%d [%s] 未读标识[%s] → 点击坐标(%d, %d)", i + 1, result.nickname != null ? result.nickname : "未知用户",result.unreadCount,result.clickBounds.centerX(), result.clickBounds.centerY()));}}} catch (Exception e) {Log.e(TAG, "分析未读消息时出错: " + e.getMessage(), e);}Log.d(TAG, "🔍 ===== 未读消息分析完成 =====\n");}} 

UnreadMessageDetector.java 

package com.example.demotest.unread;import android.graphics.Rect;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;/*** 未读消息检测器* 分析聊天项的布局结构并检测未读消息*/
public class UnreadMessageDetector {private static final String TAG = "UnreadDetector";// 屏幕宽度,用于计算相对位置private int screenWidth;/*** 文本元素信息*/private static class TextElement {String text;String description;Rect bounds;AccessibilityNodeInfo node;TextElement(String text, String description, Rect bounds, AccessibilityNodeInfo node) {this.text = text;this.description = description;this.bounds = bounds;this.node = node;}/*** 获取有效文本内容*/String getEffectiveText() {if (text != null && !text.trim().isEmpty()) {return text.trim();}if (description != null && !description.trim().isEmpty()) {return description.trim();}return "";}/*** 获取X坐标中心点*/int getCenterX() {return bounds.left + (bounds.width() / 2);}/*** 获取Y坐标中心点*/int getCenterY() {return bounds.top + (bounds.height() / 2);}}/*** 未读消息结果*/public static class UnreadResult {public String nickname;        // 昵称public String lastMessage;     // 最后消息public String time;           // 时间public String unreadCount;    // 未读数public Rect clickBounds;      // 可点击区域坐标public AccessibilityNodeInfo clickableNode; // 可点击节点@Overridepublic String toString() {return String.format("未读消息 - 昵称:%s, 消息:%s, 时间:%s, 未读标识:%s, 坐标:%s", nickname, lastMessage, time, unreadCount, clickBounds.toString());}}public UnreadMessageDetector(int screenWidth) {this.screenWidth = screenWidth;}/*** 检测聊天项是否有未读消息*/public UnreadResult detectUnreadMessage(AccessibilityNodeInfo chatItemNode) {try {Log.d(TAG, "\n=== 开始检测未读消息 ===");// 策略0:优先检查是否有集中式的contentDescriptionUnreadResult contentDescResult = detectFromContentDescription(chatItemNode);if (contentDescResult != null) {Log.d(TAG, "🔴 策略0成功:从contentDescription检测到未读消息");return contentDescResult;}// 收集所有文本元素List<TextElement> textElements = new ArrayList<>();collectTextElements(chatItemNode, textElements);if (textElements.isEmpty()) {Log.d(TAG, "未找到任何文本元素");return null;}Log.d(TAG, "收集到 " + textElements.size() + " 个文本元素");// 按Y坐标分层LayerAnalysis layerAnalysis = analyzeLayersByY(textElements);// 分析第一层元素(昵称、时间、火花)FirstLayerElements firstLayer = analyzeFirstLayer(layerAnalysis.firstLayerElements, textElements);// 分析第二层元素(内容、未读数)SecondLayerElements secondLayer = analyzeSecondLayer(layerAnalysis.secondLayerElements);// 四种策略检测未读消息(1-3为原有策略)String unreadIndicator = detectUnreadIndicator(firstLayer, secondLayer, textElements);// 检测是否有未读消息if (unreadIndicator != null && !unreadIndicator.isEmpty()) {UnreadResult result = new UnreadResult();result.nickname = firstLayer.nickname;result.lastMessage = secondLayer.content;result.time = firstLayer.time;result.unreadCount = unreadIndicator;result.clickableNode = chatItemNode;// 获取点击坐标Rect bounds = new Rect();chatItemNode.getBoundsInScreen(bounds);result.clickBounds = bounds;Log.d(TAG, "🔴 发现未读消息: " + result.toString());return result;} else {Log.d(TAG, "该聊天项无未读消息");return null;}} catch (Exception e) {Log.e(TAG, "检测未读消息时出错: " + e.getMessage(), e);return null;}}/*** 策略0:从集中式contentDescription检测未读消息* 适用于所有信息都集中在一个contentDescription中的情况* 格式示例:"VSCode技术交流群, ,有164条未读,[有新文件]树木上的林: [图片]这个打不开谁能帮我下载一下里面的东西,15:12"*/private UnreadResult detectFromContentDescription(AccessibilityNodeInfo chatItemNode) {try {// 递归查找所有可能包含完整信息的contentDescriptionreturn findContentDescriptionInTree(chatItemNode);} catch (Exception e) {Log.w(TAG, "策略0:解析contentDescription出错: " + e.getMessage());return null;}}/*** 在节点树中递归查找包含完整聊天信息的contentDescription*/private UnreadResult findContentDescriptionInTree(AccessibilityNodeInfo node) {if (node == null) return null;try {// 检查当前节点的contentDescriptionString desc = node.getContentDescription() != null ? node.getContentDescription().toString() : "";if (!desc.trim().isEmpty()) {Log.d(TAG, "策略0:检查contentDescription: " + desc);// 解析contentDescriptionUnreadResult result = parseContentDescription(desc, node);if (result != null) {return result;}}// 递归检查子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {UnreadResult childResult = findContentDescriptionInTree(child);if (childResult != null) {return childResult;}}}} catch (Exception e) {Log.w(TAG, "策略0:遍历节点树出错: " + e.getMessage());}return null;}/*** 解析集中式contentDescription字符串* 支持的格式:* 1. "昵称, ,有X条未读,消息内容,时间"* 2. "昵称, ,有X条未读,消息内容"* 3. "昵称,消息内容,有X条未读,时间"*/private UnreadResult parseContentDescription(String desc, AccessibilityNodeInfo node) {if (desc == null || desc.trim().isEmpty()) return null;String trimmedDesc = desc.trim();Log.d(TAG, "策略0:解析描述字符串: " + trimmedDesc);// 检查是否包含未读标识if (!containsUnreadIndicator(trimmedDesc)) {Log.d(TAG, "策略0:描述字符串不包含未读标识");return null;}try {// 按逗号分割描述字符串String[] parts = trimmedDesc.split(",");if (parts.length < 3) {Log.d(TAG, "策略0:描述字符串格式不符合预期,部分数量: " + parts.length);return null;}// 清理每个部分的空白字符for (int i = 0; i < parts.length; i++) {parts[i] = parts[i].trim();}Log.d(TAG, "策略0:分割后的部分数量: " + parts.length);for (int i = 0; i < parts.length; i++) {Log.d(TAG, String.format("策略0:部分[%d]: \"%s\"", i, parts[i]));}// 解析各个部分UnreadResult result = new UnreadResult();result.clickableNode = node;// 获取点击坐标Rect bounds = new Rect();node.getBoundsInScreen(bounds);result.clickBounds = bounds;// 提取信息extractInfoFromParts(parts, result);// 验证解析结果if (isValidUnreadResult(result)) {Log.d(TAG, String.format("策略0:解析成功 - 昵称:%s, 未读:%s, 消息:%s, 时间:%s", result.nickname, result.unreadCount, result.lastMessage, result.time));return result;} else {Log.d(TAG, "策略0:解析结果验证失败");return null;}} catch (Exception e) {Log.w(TAG, "策略0:解析描述字符串出错: " + e.getMessage());return null;}}/*** 检查描述字符串是否包含未读标识*/private boolean containsUnreadIndicator(String desc) {String lowerDesc = desc.toLowerCase();return lowerDesc.contains("未读") || lowerDesc.contains("unread") ||lowerDesc.matches(".*有\\d+条.*");}/*** 从分割的部分中提取信息*/private void extractInfoFromParts(String[] parts, UnreadResult result) {// 通常第一个部分是昵称(排除空字符串)for (int i = 0; i < parts.length; i++) {if (!parts[i].isEmpty() && result.nickname == null) {result.nickname = parts[i];Log.d(TAG, "策略0:提取昵称: " + result.nickname);break;}}// 查找未读数信息for (String part : parts) {if (part.contains("未读") || part.contains("unread")) {result.unreadCount = extractUnreadCount(part);Log.d(TAG, "策略0:提取未读数: " + result.unreadCount);break;}}// 查找时间信息(通常是最后一个非空部分,且符合时间格式)for (int i = parts.length - 1; i >= 0; i--) {if (!parts[i].isEmpty() && isTimePattern(parts[i])) {result.time = parts[i];Log.d(TAG, "策略0:提取时间: " + result.time);break;}}// 查找消息内容(排除昵称、未读数、时间后的其他内容)StringBuilder messageBuilder = new StringBuilder();for (String part : parts) {if (!part.isEmpty() && !part.equals(result.nickname) && !part.contains("未读") && !part.contains("unread") &&!isTimePattern(part)) {if (messageBuilder.length() > 0) {messageBuilder.append(",");}messageBuilder.append(part);}}if (messageBuilder.length() > 0) {result.lastMessage = messageBuilder.toString();Log.d(TAG, "策略0:提取消息内容: " + result.lastMessage);}}/*** 从未读标识字符串中提取具体的未读数*/private String extractUnreadCount(String unreadText) {if (unreadText == null) return null;// 匹配 "有X条未读" 格式java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("有(\\d+)条");java.util.regex.Matcher matcher = pattern.matcher(unreadText);if (matcher.find()) {return matcher.group(1);}// 匹配其他数字格式pattern = java.util.regex.Pattern.compile("(\\d+)");matcher = pattern.matcher(unreadText);if (matcher.find()) {return matcher.group(1);}// 如果没有具体数字,返回原始文本return unreadText;}/*** 验证解析结果是否有效*/private boolean isValidUnreadResult(UnreadResult result) {return result != null &&result.nickname != null && !result.nickname.trim().isEmpty() &&result.unreadCount != null && !result.unreadCount.trim().isEmpty();}/*** 收集所有文本元素*/private void collectTextElements(AccessibilityNodeInfo node, List<TextElement> elements) {if (node == null || !node.isVisibleToUser()) return;try {// 检查当前节点是否有文本内容String text = node.getText() != null ? node.getText().toString() : "";String desc = node.getContentDescription() != null ? node.getContentDescription().toString() : "";if (!text.trim().isEmpty() || !desc.trim().isEmpty()) {Rect bounds = new Rect();node.getBoundsInScreen(bounds);TextElement element = new TextElement(text, desc, bounds, node);elements.add(element);Log.d(TAG, String.format("文本元素: \"%s\" [%s]", element.getEffectiveText(), bounds.toString()));}// 递归处理子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {collectTextElements(child, elements);}}} catch (Exception e) {Log.w(TAG, "收集文本元素时出错: " + e.getMessage());}}/*** 层级分析结果*/private static class LayerAnalysis {List<TextElement> firstLayerElements = new ArrayList<>();List<TextElement> secondLayerElements = new ArrayList<>();int layerThreshold; // Y坐标分层阈值}/*** 按Y坐标分层分析*/private LayerAnalysis analyzeLayersByY(List<TextElement> elements) {LayerAnalysis analysis = new LayerAnalysis();if (elements.isEmpty()) return analysis;// 找到最小和最大Y坐标int minY = Integer.MAX_VALUE;int maxY = Integer.MIN_VALUE;for (TextElement element : elements) {int centerY = element.getCenterY();minY = Math.min(minY, centerY);maxY = Math.max(maxY, centerY);}// 计算分层阈值(约在中间位置)analysis.layerThreshold = minY + (maxY - minY) / 2;Log.d(TAG, String.format("Y坐标范围: %d - %d, 分层阈值: %d", minY, maxY, analysis.layerThreshold));// 分层分配元素for (TextElement element : elements) {if (element.getCenterY() <= analysis.layerThreshold) {analysis.firstLayerElements.add(element);Log.d(TAG, String.format("第一层: \"%s\" Y=%d", element.getEffectiveText(), element.getCenterY()));} else {analysis.secondLayerElements.add(element);Log.d(TAG, String.format("第二层: \"%s\" Y=%d", element.getEffectiveText(), element.getCenterY()));}}return analysis;}/*** 第一层元素分析结果*/private static class FirstLayerElements {String nickname;    // 昵称String time;        // 时间String sparkCount;  // 火花数字TextElement nicknameElement; // 昵称元素(用于检测左侧未读数)}/*** 分析第一层元素(昵称、时间、火花)*/private FirstLayerElements analyzeFirstLayer(List<TextElement> elements, List<TextElement> allElements) {FirstLayerElements result = new FirstLayerElements();if (elements.isEmpty()) return result;// 按X坐标排序elements.sort((a, b) -> Integer.compare(a.getCenterX(), b.getCenterX()));// 找到内容区域中Y坐标最小的元素作为昵称TextElement nicknameElement = null;int minY = Integer.MAX_VALUE;for (TextElement element : elements) {String text = element.getEffectiveText();int relativeX = element.getCenterX() * 100 / screenWidth; // 转换为相对位置百分比Log.d(TAG, String.format("第一层元素分析: \"%s\" X位置=%d%% Y位置=%d", text, relativeX, element.getCenterY()));if (isTimePattern(text)) {// 时间通常在右侧result.time = text;Log.d(TAG, "识别为时间: " + text);} else if (isSparkNumber(text, element)) {// 火花数字通常在中间,且前面有ImageViewresult.sparkCount = text;Log.d(TAG, "识别为火花数字: " + text);} else if (relativeX >= 30) {// 昵称应该在内容区域中(X >= 30%),在此区域中找Y坐标最小的int elementY = element.getCenterY();if (elementY < minY) {minY = elementY;nicknameElement = element;result.nickname = text;}}}if (nicknameElement != null) {Log.d(TAG, String.format("识别昵称: \"%s\" Y坐标: %d", result.nickname, nicknameElement.getCenterY()));result.nicknameElement = nicknameElement;}return result;}/*** 第二层元素分析结果*/private static class SecondLayerElements {String content;     // 消息内容String unreadCount; // 未读数}/*** 分析第二层元素(内容、未读数)*/private SecondLayerElements analyzeSecondLayer(List<TextElement> elements) {SecondLayerElements result = new SecondLayerElements();if (elements.isEmpty()) return result;// 按X坐标排序elements.sort((a, b) -> Integer.compare(a.getCenterX(), b.getCenterX()));for (TextElement element : elements) {String text = element.getEffectiveText();int relativeX = element.getCenterX() * 100 / screenWidth; // 转换为相对位置百分比Log.d(TAG, String.format("第二层元素分析: \"%s\" X位置=%d%%", text, relativeX));if (isUnreadNumber(text, relativeX)) {// 未读数:纯数字 + 在右侧位置result.unreadCount = text;Log.d(TAG, "✅ 识别为未读数: " + text);} else if (relativeX < 80) {// 消息内容通常在左侧或中间if (result.content == null || result.content.isEmpty()) {result.content = text;} else {result.content += " " + text; // 拼接多个内容元素}Log.d(TAG, "识别为消息内容: " + text);}}return result;}/*** 判断是否为时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) return false;String lowerText = text.toLowerCase().trim();return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.contains("今天") ||lowerText.contains("刚刚") ||lowerText.matches(".*\\d+:\\d+.*") ||lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||lowerText.contains("周一") || lowerText.contains("周二") ||lowerText.contains("周三") || lowerText.contains("周四") ||lowerText.contains("周五") || lowerText.contains("周六") ||lowerText.contains("周日") || lowerText.contains("星期一") ||lowerText.contains("星期二") || lowerText.contains("星期三") ||lowerText.contains("星期四") || lowerText.contains("星期五") ||lowerText.contains("星期六") || lowerText.contains("星期日");}/*** 判断是否为火花数字* 特征:数字 + 前后有空格 + 可能有前置ImageView*/private boolean isSparkNumber(String text, TextElement element) {if (text == null || text.trim().isEmpty()) return false;// 检查是否为纯数字(可能有空格)String trimmed = text.trim();if (!Pattern.matches("\\d+", trimmed)) return false;// 检查是否有前后空格(火花数字的特征)if (text.startsWith(" ") || text.endsWith(" ")) {Log.d(TAG, "疑似火花数字(有空格): \"" + text + "\"");return true;}// 检查X坐标是否在中间区域(30%-70%)int relativeX = element.getCenterX() * 100 / screenWidth;if (relativeX >= 30 && relativeX <= 70) {Log.d(TAG, "疑似火花数字(中间位置): \"" + text + "\" X=" + relativeX + "%");return true;}return false;}/*** 判断是否为未读标识(数字或文字)* 特征:数字未读数 或 文字未读标识 + 在右侧位置*/private boolean isUnreadNumber(String text, int relativeX) {if (text == null || text.trim().isEmpty()) return false;String trimmed = text.trim();// 必须在右侧位置(75%以后,稍微放宽一点)if (relativeX < 75) return false;// 检查是否为数字类型的未读数if (isNumericUnread(trimmed, text)) {return true;}// 检查是否为文字类型的未读标识if (isTextUnread(trimmed)) {return true;}return false;}/*** 判断是否为数字类型的未读数*/private boolean isNumericUnread(String trimmed, String originalText) {// 必须是纯数字if (!Pattern.matches("\\d+", trimmed)) return false;// 未读数通常是1-999的范围try {int number = Integer.parseInt(trimmed);if (number < 1 || number > 999) return false;} catch (NumberFormatException e) {return false;}// 不应该有前后空格(区别于火花数字)if (originalText.startsWith(" ") || originalText.endsWith(" ")) return false;return true;}/*** 判断是否为文字类型的未读标识*/private boolean isTextUnread(String text) {if (text == null || text.trim().isEmpty()) return false;String lowerText = text.toLowerCase().trim();// 中文未读标识if (lowerText.equals("未读") || lowerText.equals("新消息") || lowerText.equals("新") || lowerText.equals("未读消息")) {return true;}// 英文未读标识if (lowerText.equals("unread") || lowerText.equals("new") || lowerText.equals("!") || lowerText.equals("new message") ||lowerText.equals("message") || lowerText.equals("msg")) {return true;}// 其他可能的标识if (lowerText.equals("●") || lowerText.equals("•") || lowerText.equals("🔴") || lowerText.equals("红点")) {return true;}return false;}/*** 分散元素策略检测未读消息(策略1-3)* 策略1:右侧区域未读数:在消息内容右边的数字/文本标识* 策略2:昵称左侧未读数:在昵称左边的数字角标* 策略3:文本形式未读标识:如"未读"、"new"等文字* 注:策略0(集中式contentDescription)已在主检测方法中优先执行*/private String detectUnreadIndicator(FirstLayerElements firstLayer, SecondLayerElements secondLayer, List<TextElement> allElements) {Log.d(TAG, "\n=== 开始分散元素策略检测未读消息(策略1-3)===");// 策略1:传统的右侧区域未读数if (secondLayer.unreadCount != null && !secondLayer.unreadCount.isEmpty()) {Log.d(TAG, "策略1成功:右侧区域未读数 = " + secondLayer.unreadCount);return secondLayer.unreadCount;}// 策略2:昵称左侧未读数(头像右上角)String nicknameLeftUnread = detectNicknameLeftUnread(firstLayer, allElements);if (nicknameLeftUnread != null && !nicknameLeftUnread.isEmpty()) {Log.d(TAG, "策略2成功:昵称左侧未读数 = " + nicknameLeftUnread);return nicknameLeftUnread;}// 策略3:文本形式未读标识String textUnreadIndicator = detectTextUnreadIndicator(allElements);if (textUnreadIndicator != null && !textUnreadIndicator.isEmpty()) {Log.d(TAG, "策略3成功:文本形式未读标识 = " + textUnreadIndicator);return textUnreadIndicator;}Log.d(TAG, "分散元素策略(1-3)均未检测到未读消息");return null;}/*** 策略2:检测昵称左侧的未读数(头像右上角)*/private String detectNicknameLeftUnread(FirstLayerElements firstLayer, List<TextElement> allElements) {if (firstLayer.nicknameElement == null) {Log.d(TAG, "策略2:无昵称元素,跳过");return null;}TextElement nicknameElement = firstLayer.nicknameElement;int nicknameX = nicknameElement.getCenterX();int nicknameY = nicknameElement.getCenterY();Log.d(TAG, String.format("策略2:昵称位置 X=%d Y=%d,搜索左侧数字", nicknameX, nicknameY));for (TextElement element : allElements) {String text = element.getEffectiveText();// 检查是否在昵称左侧if (element.getCenterX() >= nicknameX) continue;// 检查Y坐标是否相近(±50像素内)int deltaY = Math.abs(element.getCenterY() - nicknameY);if (deltaY > 50) continue;// 检查是否为纯数字if (text != null && text.trim().matches("\\d+")) {String trimmed = text.trim();try {int number = Integer.parseInt(trimmed);if (number >= 1 && number <= 999) {Log.d(TAG, String.format("策略2:找到昵称左侧未读数 \"%s\" X=%d Y=%d", text, element.getCenterX(), element.getCenterY()));return trimmed;}} catch (NumberFormatException e) {// 忽略}}}Log.d(TAG, "策略2:未找到昵称左侧未读数");return null;}/*** 策略3:检测文本形式的未读标识*/private String detectTextUnreadIndicator(List<TextElement> allElements) {Log.d(TAG, "策略3:搜索文本形式未读标识");for (TextElement element : allElements) {String text = element.getEffectiveText();if (text == null || text.trim().isEmpty()) continue;String trimmed = text.trim().toLowerCase();// 检查各种文本形式的未读标识if (trimmed.equals("未读") || trimmed.equals("新消息") || trimmed.equals("新") || trimmed.equals("未读消息") ||trimmed.equals("unread") || trimmed.equals("new") || trimmed.equals("new message") || trimmed.equals("message") ||trimmed.equals("●") || trimmed.equals("•") || trimmed.equals("🔴")) {// 确保在右侧位置(避免误判)int relativeX = element.getCenterX() * 100 / screenWidth;if (relativeX >= 70) {Log.d(TAG, String.format("策略3:找到文本未读标识 \"%s\" X位置=%d%%", text, relativeX));return text.trim();}}}Log.d(TAG, "策略3:未找到文本形式未读标识");return null;}/*** 检测多个聊天项的未读消息*/public List<UnreadResult> detectMultipleUnreadMessages(List<AccessibilityNodeInfo> chatItems) {List<UnreadResult> results = new ArrayList<>();Log.d(TAG, "\n🔍 开始批量检测未读消息,共 " + chatItems.size() + " 个聊天项");for (int i = 0; i < chatItems.size(); i++) {AccessibilityNodeInfo chatItem = chatItems.get(i);Log.d(TAG, "\n--- 检测第 " + (i + 1) + " 个聊天项 ---");UnreadResult result = detectUnreadMessage(chatItem);if (result != null) {results.add(result);}}Log.d(TAG, "\n📊 检测完成,发现 " + results.size() + " 个有未读消息的聊天项");// 输出所有未读消息的用户坐标if (!results.isEmpty()) {Log.d(TAG, "\n📍 有未读消息的用户坐标列表:");for (int i = 0; i < results.size(); i++) {UnreadResult result = results.get(i);Log.d(TAG, String.format("%d. %s - 点击坐标: %s", i + 1, result.nickname, result.clickBounds.toString()));}}return results;}
} 

去除杂质log的简化版本

UnreadMessageDetector.java 

package com.example.demotest.unread;import android.graphics.Rect;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;/*** 未读消息检测器* 分析聊天项的布局结构并检测未读消息*/
public class UnreadMessageDetector {private static final String TAG = "UnreadDetector";// 屏幕宽度,用于计算相对位置private int screenWidth;/*** 文本元素信息*/private static class TextElement {String text;String description;Rect bounds;AccessibilityNodeInfo node;TextElement(String text, String description, Rect bounds, AccessibilityNodeInfo node) {this.text = text;this.description = description;this.bounds = bounds;this.node = node;}/*** 获取有效文本内容*/String getEffectiveText() {if (text != null && !text.trim().isEmpty()) {return text.trim();}if (description != null && !description.trim().isEmpty()) {return description.trim();}return "";}/*** 获取X坐标中心点*/int getCenterX() {return bounds.left + (bounds.width() / 2);}/*** 获取Y坐标中心点*/int getCenterY() {return bounds.top + (bounds.height() / 2);}}/*** 未读消息结果*/public static class UnreadResult {public String nickname;        // 昵称public String lastMessage;     // 最后消息public String time;           // 时间public String unreadCount;    // 未读数public Rect clickBounds;      // 可点击区域坐标public AccessibilityNodeInfo clickableNode; // 可点击节点@Overridepublic String toString() {return String.format("未读消息 - 昵称:%s, 消息:%s, 时间:%s, 未读标识:%s, 坐标:%s", nickname, lastMessage, time, unreadCount, clickBounds.toString());}}public UnreadMessageDetector(int screenWidth) {this.screenWidth = screenWidth;}/*** 检测聊天项是否有未读消息*/public UnreadResult detectUnreadMessage(AccessibilityNodeInfo chatItemNode) {try {// 策略0:优先检查是否有集中式的contentDescriptionUnreadResult contentDescResult = detectFromContentDescription(chatItemNode);if (contentDescResult != null) {return contentDescResult;}// 收集所有文本元素List<TextElement> textElements = new ArrayList<>();collectTextElements(chatItemNode, textElements);if (textElements.isEmpty()) {return null;}// 按Y坐标分层LayerAnalysis layerAnalysis = analyzeLayersByY(textElements);// 分析第一层元素(昵称、时间、火花)FirstLayerElements firstLayer = analyzeFirstLayer(layerAnalysis.firstLayerElements, textElements);// 分析第二层元素(内容、未读数)SecondLayerElements secondLayer = analyzeSecondLayer(layerAnalysis.secondLayerElements);// 四种策略检测未读消息(1-3为原有策略)String unreadIndicator = detectUnreadIndicator(firstLayer, secondLayer, textElements);// 检测是否有未读消息if (unreadIndicator != null && !unreadIndicator.isEmpty()) {UnreadResult result = new UnreadResult();result.nickname = firstLayer.nickname;result.lastMessage = secondLayer.content;result.time = firstLayer.time;result.unreadCount = unreadIndicator;result.clickableNode = chatItemNode;// 获取点击坐标Rect bounds = new Rect();chatItemNode.getBoundsInScreen(bounds);result.clickBounds = bounds;return result;} else {return null;}} catch (Exception e) {Log.e(TAG, "检测未读消息时出错: " + e.getMessage(), e);return null;}}/*** 策略0:从集中式contentDescription检测未读消息* 适用于所有信息都集中在一个contentDescription中的情况* 格式示例:"VSCode技术交流群, ,有164条未读,[有新文件]树木上的林: [图片]这个打不开谁能帮我下载一下里面的东西,15:12"*/private UnreadResult detectFromContentDescription(AccessibilityNodeInfo chatItemNode) {try {// 递归查找所有可能包含完整信息的contentDescriptionreturn findContentDescriptionInTree(chatItemNode);} catch (Exception e) {Log.w(TAG, "策略0:解析contentDescription出错: " + e.getMessage());return null;}}/*** 在节点树中递归查找包含完整聊天信息的contentDescription*/private UnreadResult findContentDescriptionInTree(AccessibilityNodeInfo node) {if (node == null) return null;try {// 检查当前节点的contentDescriptionString desc = node.getContentDescription() != null ? node.getContentDescription().toString() : "";if (!desc.trim().isEmpty()) {// 解析contentDescriptionUnreadResult result = parseContentDescription(desc, node);if (result != null) {return result;}}// 递归检查子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {UnreadResult childResult = findContentDescriptionInTree(child);if (childResult != null) {return childResult;}}}} catch (Exception e) {// 静默处理错误}return null;}/*** 解析集中式contentDescription字符串* 支持的格式:* 1. "昵称, ,有X条未读,消息内容,时间"* 2. "昵称, ,有X条未读,消息内容"* 3. "昵称,消息内容,有X条未读,时间"*/private UnreadResult parseContentDescription(String desc, AccessibilityNodeInfo node) {if (desc == null || desc.trim().isEmpty()) return null;String trimmedDesc = desc.trim();// 检查是否包含未读标识if (!containsUnreadIndicator(trimmedDesc)) {return null;}try {// 按逗号分割描述字符串String[] parts = trimmedDesc.split(",");if (parts.length < 3) {return null;}// 清理每个部分的空白字符for (int i = 0; i < parts.length; i++) {parts[i] = parts[i].trim();}// 解析各个部分UnreadResult result = new UnreadResult();result.clickableNode = node;// 获取点击坐标Rect bounds = new Rect();node.getBoundsInScreen(bounds);result.clickBounds = bounds;// 提取信息extractInfoFromParts(parts, result);// 验证解析结果if (isValidUnreadResult(result)) {return result;} else {return null;}} catch (Exception e) {return null;}}/*** 检查描述字符串是否包含未读标识*/private boolean containsUnreadIndicator(String desc) {String lowerDesc = desc.toLowerCase();return lowerDesc.contains("未读") || lowerDesc.contains("unread") ||lowerDesc.matches(".*有\\d+条.*");}/*** 从分割的部分中提取信息*/private void extractInfoFromParts(String[] parts, UnreadResult result) {// 通常第一个部分是昵称(排除空字符串)for (int i = 0; i < parts.length; i++) {if (!parts[i].isEmpty() && result.nickname == null) {result.nickname = parts[i];break;}}// 查找未读数信息for (String part : parts) {if (part.contains("未读") || part.contains("unread")) {result.unreadCount = extractUnreadCount(part);break;}}// 查找时间信息(通常是最后一个非空部分,且符合时间格式)for (int i = parts.length - 1; i >= 0; i--) {if (!parts[i].isEmpty() && isTimePattern(parts[i])) {result.time = parts[i];break;}}// 查找消息内容(排除昵称、未读数、时间后的其他内容)StringBuilder messageBuilder = new StringBuilder();for (String part : parts) {if (!part.isEmpty() && !part.equals(result.nickname) && !part.contains("未读") && !part.contains("unread") &&!isTimePattern(part)) {if (messageBuilder.length() > 0) {messageBuilder.append(",");}messageBuilder.append(part);}}if (messageBuilder.length() > 0) {result.lastMessage = messageBuilder.toString();}}/*** 从未读标识字符串中提取具体的未读数*/private String extractUnreadCount(String unreadText) {if (unreadText == null) return null;// 匹配 "有X条未读" 格式java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("有(\\d+)条");java.util.regex.Matcher matcher = pattern.matcher(unreadText);if (matcher.find()) {return matcher.group(1);}// 匹配其他数字格式pattern = java.util.regex.Pattern.compile("(\\d+)");matcher = pattern.matcher(unreadText);if (matcher.find()) {return matcher.group(1);}// 如果没有具体数字,返回原始文本return unreadText;}/*** 验证解析结果是否有效*/private boolean isValidUnreadResult(UnreadResult result) {return result != null &&result.nickname != null && !result.nickname.trim().isEmpty() &&result.unreadCount != null && !result.unreadCount.trim().isEmpty();}/*** 收集所有文本元素*/private void collectTextElements(AccessibilityNodeInfo node, List<TextElement> elements) {if (node == null || !node.isVisibleToUser()) return;try {// 检查当前节点是否有文本内容String text = node.getText() != null ? node.getText().toString() : "";String desc = node.getContentDescription() != null ? node.getContentDescription().toString() : "";if (!text.trim().isEmpty() || !desc.trim().isEmpty()) {Rect bounds = new Rect();node.getBoundsInScreen(bounds);TextElement element = new TextElement(text, desc, bounds, node);elements.add(element);}// 递归处理子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {collectTextElements(child, elements);}}} catch (Exception e) {// 静默处理错误}}/*** 层级分析结果*/private static class LayerAnalysis {List<TextElement> firstLayerElements = new ArrayList<>();List<TextElement> secondLayerElements = new ArrayList<>();int layerThreshold; // Y坐标分层阈值}/*** 按Y坐标分层分析*/private LayerAnalysis analyzeLayersByY(List<TextElement> elements) {LayerAnalysis analysis = new LayerAnalysis();if (elements.isEmpty()) return analysis;// 找到最小和最大Y坐标int minY = Integer.MAX_VALUE;int maxY = Integer.MIN_VALUE;for (TextElement element : elements) {int centerY = element.getCenterY();minY = Math.min(minY, centerY);maxY = Math.max(maxY, centerY);}// 计算分层阈值(约在中间位置)analysis.layerThreshold = minY + (maxY - minY) / 2;// 分层分配元素for (TextElement element : elements) {if (element.getCenterY() <= analysis.layerThreshold) {analysis.firstLayerElements.add(element);} else {analysis.secondLayerElements.add(element);}}return analysis;}/*** 第一层元素分析结果*/private static class FirstLayerElements {String nickname;    // 昵称String time;        // 时间String sparkCount;  // 火花数字TextElement nicknameElement; // 昵称元素(用于检测左侧未读数)}/*** 分析第一层元素(昵称、时间、火花)*/private FirstLayerElements analyzeFirstLayer(List<TextElement> elements, List<TextElement> allElements) {FirstLayerElements result = new FirstLayerElements();if (elements.isEmpty()) return result;// 按X坐标排序elements.sort((a, b) -> Integer.compare(a.getCenterX(), b.getCenterX()));// 找到内容区域中Y坐标最小的元素作为昵称TextElement nicknameElement = null;int minY = Integer.MAX_VALUE;for (TextElement element : elements) {String text = element.getEffectiveText();int relativeX = element.getCenterX() * 100 / screenWidth; // 转换为相对位置百分比if (isTimePattern(text)) {// 时间通常在右侧result.time = text;} else if (isSparkNumber(text, element)) {// 火花数字通常在中间,且前面有ImageViewresult.sparkCount = text;} else if (relativeX >= 30) {// 昵称应该在内容区域中(X >= 30%),在此区域中找Y坐标最小的int elementY = element.getCenterY();if (elementY < minY) {minY = elementY;nicknameElement = element;result.nickname = text;}}}if (nicknameElement != null) {result.nicknameElement = nicknameElement;}return result;}/*** 第二层元素分析结果*/private static class SecondLayerElements {String content;     // 消息内容String unreadCount; // 未读数}/*** 分析第二层元素(内容、未读数)*/private SecondLayerElements analyzeSecondLayer(List<TextElement> elements) {SecondLayerElements result = new SecondLayerElements();if (elements.isEmpty()) return result;// 按X坐标排序elements.sort((a, b) -> Integer.compare(a.getCenterX(), b.getCenterX()));for (TextElement element : elements) {String text = element.getEffectiveText();int relativeX = element.getCenterX() * 100 / screenWidth; // 转换为相对位置百分比if (isUnreadNumber(text, relativeX)) {// 未读数:纯数字 + 在右侧位置result.unreadCount = text;} else if (relativeX < 80) {// 消息内容通常在左侧或中间if (result.content == null || result.content.isEmpty()) {result.content = text;} else {result.content += " " + text; // 拼接多个内容元素}}}return result;}/*** 判断是否为时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) return false;String lowerText = text.toLowerCase().trim();return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.contains("今天") ||lowerText.contains("刚刚") ||lowerText.matches(".*\\d+:\\d+.*") ||lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||lowerText.contains("周一") || lowerText.contains("周二") ||lowerText.contains("周三") || lowerText.contains("周四") ||lowerText.contains("周五") || lowerText.contains("周六") ||lowerText.contains("周日") || lowerText.contains("星期一") ||lowerText.contains("星期二") || lowerText.contains("星期三") ||lowerText.contains("星期四") || lowerText.contains("星期五") ||lowerText.contains("星期六") || lowerText.contains("星期日");}/*** 判断是否为火花数字* 特征:数字 + 前后有空格 + 可能有前置ImageView*/private boolean isSparkNumber(String text, TextElement element) {if (text == null || text.trim().isEmpty()) return false;// 检查是否为纯数字(可能有空格)String trimmed = text.trim();if (!Pattern.matches("\\d+", trimmed)) return false;// 检查是否有前后空格(火花数字的特征)if (text.startsWith(" ") || text.endsWith(" ")) {return true;}// 检查X坐标是否在中间区域(30%-70%)int relativeX = element.getCenterX() * 100 / screenWidth;if (relativeX >= 30 && relativeX <= 70) {return true;}return false;}/*** 判断是否为未读标识(数字或文字)* 特征:数字未读数 或 文字未读标识 + 在右侧位置*/private boolean isUnreadNumber(String text, int relativeX) {if (text == null || text.trim().isEmpty()) return false;String trimmed = text.trim();// 必须在右侧位置(75%以后,稍微放宽一点)if (relativeX < 75) return false;// 检查是否为数字类型的未读数if (isNumericUnread(trimmed, text)) {return true;}// 检查是否为文字类型的未读标识if (isTextUnread(trimmed)) {return true;}return false;}/*** 判断是否为数字类型的未读数*/private boolean isNumericUnread(String trimmed, String originalText) {// 必须是纯数字if (!Pattern.matches("\\d+", trimmed)) return false;// 未读数通常是1-999的范围try {int number = Integer.parseInt(trimmed);if (number < 1 || number > 999) return false;} catch (NumberFormatException e) {return false;}// 不应该有前后空格(区别于火花数字)if (originalText.startsWith(" ") || originalText.endsWith(" ")) return false;return true;}/*** 判断是否为文字类型的未读标识*/private boolean isTextUnread(String text) {if (text == null || text.trim().isEmpty()) return false;String lowerText = text.toLowerCase().trim();// 中文未读标识if (lowerText.equals("未读") || lowerText.equals("新消息") || lowerText.equals("新") || lowerText.equals("未读消息")) {return true;}// 英文未读标识if (lowerText.equals("unread") || lowerText.equals("new") || lowerText.equals("!") || lowerText.equals("new message") ||lowerText.equals("message") || lowerText.equals("msg")) {return true;}// 其他可能的标识if (lowerText.equals("●") || lowerText.equals("•") || lowerText.equals("🔴") || lowerText.equals("红点")) {return true;}return false;}/*** 分散元素策略检测未读消息(策略1-3)* 策略1:右侧区域未读数:在消息内容右边的数字/文本标识* 策略2:昵称左侧未读数:在昵称左边的数字角标* 策略3:文本形式未读标识:如"未读"、"new"等文字* 注:策略0(集中式contentDescription)已在主检测方法中优先执行*/private String detectUnreadIndicator(FirstLayerElements firstLayer, SecondLayerElements secondLayer, List<TextElement> allElements) {// 策略1:传统的右侧区域未读数if (secondLayer.unreadCount != null && !secondLayer.unreadCount.isEmpty()) {return secondLayer.unreadCount;}// 策略2:昵称左侧未读数(头像右上角)String nicknameLeftUnread = detectNicknameLeftUnread(firstLayer, allElements);if (nicknameLeftUnread != null && !nicknameLeftUnread.isEmpty()) {return nicknameLeftUnread;}// 策略3:文本形式未读标识String textUnreadIndicator = detectTextUnreadIndicator(allElements);if (textUnreadIndicator != null && !textUnreadIndicator.isEmpty()) {return textUnreadIndicator;}return null;}/*** 策略2:检测昵称左侧的未读数(头像右上角)*/private String detectNicknameLeftUnread(FirstLayerElements firstLayer, List<TextElement> allElements) {if (firstLayer.nicknameElement == null) {return null;}TextElement nicknameElement = firstLayer.nicknameElement;int nicknameX = nicknameElement.getCenterX();int nicknameY = nicknameElement.getCenterY();for (TextElement element : allElements) {String text = element.getEffectiveText();// 检查是否在昵称左侧if (element.getCenterX() >= nicknameX) continue;// 检查Y坐标是否相近(±50像素内)int deltaY = Math.abs(element.getCenterY() - nicknameY);if (deltaY > 50) continue;// 检查是否为纯数字if (text != null && text.trim().matches("\\d+")) {String trimmed = text.trim();try {int number = Integer.parseInt(trimmed);if (number >= 1 && number <= 999) {return trimmed;}} catch (NumberFormatException e) {// 忽略}}}return null;}/*** 策略3:检测文本形式的未读标识*/private String detectTextUnreadIndicator(List<TextElement> allElements) {for (TextElement element : allElements) {String text = element.getEffectiveText();if (text == null || text.trim().isEmpty()) continue;String trimmed = text.trim().toLowerCase();// 检查各种文本形式的未读标识if (trimmed.equals("未读") || trimmed.equals("新消息") || trimmed.equals("新") || trimmed.equals("未读消息") ||trimmed.equals("unread") || trimmed.equals("new") || trimmed.equals("new message") || trimmed.equals("message") ||trimmed.equals("●") || trimmed.equals("•") || trimmed.equals("🔴")) {// 确保在右侧位置(避免误判)int relativeX = element.getCenterX() * 100 / screenWidth;if (relativeX >= 70) {return text.trim();}}}return null;}/*** 检测多个聊天项的未读消息*/public List<UnreadResult> detectMultipleUnreadMessages(List<AccessibilityNodeInfo> chatItems) {List<UnreadResult> results = new ArrayList<>();for (int i = 0; i < chatItems.size(); i++) {AccessibilityNodeInfo chatItem = chatItems.get(i);UnreadResult result = detectUnreadMessage(chatItem);if (result != null) {results.add(result);}}return results;}
} 

UnreadMessageAnalyzer.java 

package com.example.demotest.unread;import android.accessibilityservice.AccessibilityService;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;/*** 无障碍树打印器 - 打印界面视图树*/
public class UnreadMessageAnalyzer {private static final String TAG = "UnreadAnalysis";private AccessibilityService accessibilityService;private int screenWidth;private int screenHeight;public UnreadMessageAnalyzer(AccessibilityService service) {this.accessibilityService = service;DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();this.screenWidth = metrics.widthPixels;this.screenHeight = metrics.heightPixels;}/*** 打印界面视图树和处理过的视图树*/public void printAccessibilityTrees() {AccessibilityNodeInfo rootNode = accessibilityService.getRootInActiveWindow();if (rootNode == null) {Log.e(TAG, "无法获取当前窗口信息");return;}try {// 打印处理过的视图树(聊天项目)printProcessedViewTree(rootNode);} catch (Exception e) {Log.e(TAG, "打印无障碍树时出错: " + e.getMessage(), e);} finally {rootNode.recycle();}}/*** 打印处理过的视图树 - 基于时间节点回溯的聊天项目*/private void printProcessedViewTree(AccessibilityNodeInfo rootNode) {try {// 第一步:收集所有时间节点List<AccessibilityNodeInfo> timeNodes = new ArrayList<>();collectTimeNodes(rootNode, timeNodes);// 第二步:对每个时间节点进行回溯,找到可点击父级Set<AccessibilityNodeInfo> processedParents = new HashSet<>();List<AccessibilityNodeInfo> chatItems = new ArrayList<>();for (AccessibilityNodeInfo timeNode : timeNodes) {AccessibilityNodeInfo clickableParent = findNearestClickableParent(timeNode);if (clickableParent != null && !processedParents.contains(clickableParent)) {// 添加到聊天项目列表中chatItems.add(clickableParent);// 标记为已处理,避免重复processedParents.add(clickableParent);}}if (!processedParents.isEmpty()) {// 使用未读消息检测器分析所有聊天项analyzeUnreadMessages(chatItems);}} catch (Exception e) {Log.w(TAG, "打印处理过的视图树时出错: " + e.getMessage());}}/*** 收集所有包含时间信息的节点*/private void collectTimeNodes(AccessibilityNodeInfo node, List<AccessibilityNodeInfo> timeNodes) {if (node == null || !node.isVisibleToUser()) return;try {// 检查当前节点是否包含时间信息String nodeText = getNodeText(node);if (isTimePattern(nodeText)) {timeNodes.add(node);}// 递归检查所有子节点for (int i = 0; i < node.getChildCount(); i++) {AccessibilityNodeInfo child = node.getChild(i);if (child != null) {collectTimeNodes(child, timeNodes);}}} catch (Exception e) {// 静默处理错误}}/*** 向上回溯找到最近的可点击父级*/private AccessibilityNodeInfo findNearestClickableParent(AccessibilityNodeInfo timeNode) {if (timeNode == null) return null;try {AccessibilityNodeInfo current = timeNode;// 向上遍历父级节点,找到第一个可点击的父级while (current != null) {AccessibilityNodeInfo parent = current.getParent();if (parent == null) break;// 检查父级是否满足可点击条件if (isClickableParent(parent)) {return parent;}current = parent;}return null;} catch (Exception e) {Log.w(TAG, "查找可点击父级时出错: " + e.getMessage());return null;}}/*** 检查节点是否满足可点击父级条件*/private boolean isClickableParent(AccessibilityNodeInfo node) {if (node == null || !node.isVisibleToUser()) return false;// 满足条件:// 1. {clickable, long-clickable} 或 {clickable, long-clickable, visible}// 2. {clickable, visible}return node.isClickable() && (node.isLongClickable() || node.isVisibleToUser());}/*** 获取节点的文本内容*/private String getNodeText(AccessibilityNodeInfo node) {StringBuilder text = new StringBuilder();if (node.getText() != null) {text.append(node.getText().toString()).append(" ");}if (node.getContentDescription() != null) {text.append(node.getContentDescription().toString()).append(" ");}return text.toString();}/*** 判断文本是否包含时间模式*/private boolean isTimePattern(String text) {if (text == null || text.trim().isEmpty()) {return false;}String lowerText = text.toLowerCase().trim();// 检查常见的时间模式return lowerText.contains("分钟前") ||lowerText.contains("小时前") ||lowerText.contains("天前") ||lowerText.contains("昨天") ||lowerText.contains("前天") ||lowerText.contains("今天") ||lowerText.contains("刚刚") ||lowerText.matches(".*\\d+:\\d+.*") ||                    // HH:MM 格式lowerText.matches(".*\\d{1,2}月\\d{1,2}日.*") ||          // MM月DD日 格式lowerText.matches(".*\\d{4}/\\d{1,2}/\\d{1,2}.*") ||     // YYYY/MM/DD 格式lowerText.matches(".*\\d{4}-\\d{1,2}-\\d{1,2}.*") ||     // YYYY-MM-DD 格式lowerText.contains("周一") ||lowerText.contains("周二") ||lowerText.contains("周三") ||lowerText.contains("周四") ||lowerText.contains("周五") ||lowerText.contains("周六") ||lowerText.contains("周日") ||lowerText.contains("星期一") ||lowerText.contains("星期二") ||lowerText.contains("星期三") ||lowerText.contains("星期四") ||lowerText.contains("星期五") ||lowerText.contains("星期六") ||lowerText.contains("星期日");}/*** 分析聊天项的未读消息*/private void analyzeUnreadMessages(List<AccessibilityNodeInfo> chatItems) {try {// 创建未读消息检测器UnreadMessageDetector detector = new UnreadMessageDetector(screenWidth);// 检测所有聊天项的未读消息List<UnreadMessageDetector.UnreadResult> unreadResults = detector.detectMultipleUnreadMessages(chatItems);// 输出分析结果if (unreadResults.isEmpty()) {Log.d(TAG, "🟢 当前页面没有发现未读消息");} else {Log.d(TAG, "🔴 发现 " + unreadResults.size() + " 个有未读消息的聊天项:");for (int i = 0; i < unreadResults.size(); i++) {UnreadMessageDetector.UnreadResult result = unreadResults.get(i);Log.d(TAG, String.format("📱 第%d个未读消息:", i + 1));Log.d(TAG, "    👤 昵称: " + (result.nickname != null ? result.nickname : "未知"));Log.d(TAG, "    💬 消息: " + (result.lastMessage != null ? result.lastMessage : "无"));Log.d(TAG, "    ⏰ 时间: " + (result.time != null ? result.time : "未知"));Log.d(TAG, "    🔴 未读标识: " + result.unreadCount);Log.d(TAG, "    📍 点击坐标: " + result.clickBounds.toString());Log.d(TAG, "    📱 坐标中心: (" + result.clickBounds.centerX() + ", " + result.clickBounds.centerY() + ")");}// 输出可直接使用的坐标列表Log.d(TAG, "📍 未读消息用户点击坐标汇总:");for (int i = 0; i < unreadResults.size(); i++) {UnreadMessageDetector.UnreadResult result = unreadResults.get(i);Log.d(TAG, String.format("用户%d [%s] 未读标识[%s] → 点击坐标(%d, %d)", i + 1, result.nickname != null ? result.nickname : "未知用户",result.unreadCount,result.clickBounds.centerX(), result.clickBounds.centerY()));}}} catch (Exception e) {Log.e(TAG, "分析未读消息时出错: " + e.getMessage(), e);}}} 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/84353.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux基础开发工具——vim工具

文章目录 vim工具什么是vimvim的多模式和使用vim的基础模式vim的三种基础模式三种模式的初步了解 常用模式的详细讲解插入模式命令模式模式转化光标的移动文本的编辑 底行模式替换模式视图模式总结 使用vim的小技巧vim的配置(了解) vim工具 本文章仍然是继续讲解Linux系统下的…

C++_核心编程_多态案例二-制作饮品

#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为&#xff1a;煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例&#xff0c;提供抽象制作饮品基类&#xff0c;提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…

AcWing--数据结构1

用数组来模拟链表。这种实现链表的方式也叫静态链表。 1.单链表 写邻接表&#xff1a;存储图和树 我们定义&#xff1a;e[N]用来表示某个点的值是多少&#xff1b;ne[N]用来表示某个点的next指针是多少 e和ne是用下标关联起来的 如&#xff1a;head->3->5->7->…

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…

多模态分类案例实现

以下是基于飞桨平台实现的多模态分类详细案例&#xff0c;结合图像和文本信息进行分类任务。案例包含数据处理、模型构建、训练和评估的完整流程&#xff0c;并提供详细注释&#xff1a; 一、多模态分类案例实现 import os import json import numpy as np from PIL import I…

Express框架:Node.js的轻量级Web应用利器

Hi,我是布兰妮甜 !在当今快速发展的Web开发领域,Node.js已成为构建高性能、可扩展网络应用的重要基石。而在这片肥沃的生态系统中,Express框架犹如一座经久不衰的灯塔,指引着无数开发者高效构建Web应用的方向。本文章在为读者提供一份全面而深入的Express框架指南。无论您…

K-Means颜色变卦和渐变色

一、理论深度提升&#xff1a;补充算法细节与数学基础 1. K-Means 算法核心公式&#xff08;增强专业性&#xff09; 在 “原理步骤” 中加入数学表达式&#xff0c;说明聚类目标&#xff1a; K-Means 的目标是最小化簇内平方和&#xff08;Within-Cluster Sum of Squares, W…

深入解析C#表达式求值:优先级、结合性与括号的魔法

—— 为什么2/6*4不等于1/12&#xff1f; &#x1f50d; 一、表达式求值顺序为何重要&#xff1f; 表达式如精密仪器&#xff0c;子表达式求值顺序直接决定结果。例如&#xff1a; int result 3 * 5 2;若先算乘法&#xff1a;(3*5)2 17 ✅若先算加法&#xff1a;3*(52)21…

Docker 离线安装指南

参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性&#xff0c;不同版本的Docker对内核版本有不同要求。例如&#xff0c;Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本&#xff0c;Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…

Spring——Spring相关类原理与实战

摘要 本文深入探讨了 Spring 框架中 InitializingBean 接口的原理与实战应用&#xff0c;该接口是 Spring 提供的一个生命周期接口&#xff0c;用于在 Bean 属性注入完成后执行初始化逻辑。文章详细介绍了接口定义、作用、典型使用场景&#xff0c;并与其他相关概念如 PostCon…

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…

冯诺依曼架构是什么?

冯诺依曼架构是什么&#xff1f; 冯诺依曼架构&#xff08;Von Neumann Architecture&#xff09;是现代计算机的基础设计框架&#xff0c;由数学家约翰冯诺依曼&#xff08;John von Neumann&#xff09;及其团队在1945年提出。其核心思想是通过统一存储程序与数据&#xff0…

【持续更新】linux网络编程试题

问题1 请简要说明TCP/IP协议栈的四层结构&#xff0c;并分别举出每一层出现的典型协议或应用。 答案 应用层&#xff1a;ping,telnet,dns 传输层&#xff1a;tcp,udp 网络层&#xff1a;ip,icmp 数据链路层&#xff1a;arp,rarp 问题2 下列协议或应用分别属于TCP/IP协议…

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…

【JVM】- 内存结构

引言 JVM&#xff1a;Java Virtual Machine 定义&#xff1a;Java虚拟机&#xff0c;Java二进制字节码的运行环境好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收的功能数组下标越界检查&#xff08;会抛异常&#xff0c;不会覆盖到其他代码…

React 基础入门笔记

一、JSX语法规则 1. 定义虚拟DOM时&#xff0c;不要写引号 2.标签中混入JS表达式时要用 {} &#xff08;1&#xff09;.JS表达式与JS语句&#xff08;代码&#xff09;的区别 &#xff08;2&#xff09;.使用案例 3.样式的类名指定不要用class&#xff0c;要用className 4.内…

Linux链表操作全解析

Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表&#xff1f;1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…

SQL进阶之旅 Day 19:统计信息与优化器提示

【SQL进阶之旅 Day 19】统计信息与优化器提示 文章简述 在数据库性能调优中&#xff0c;统计信息和优化器提示是两个至关重要的工具。统计信息帮助数据库优化器评估查询成本并选择最佳执行计划&#xff0c;而优化器提示则允许开发人员对优化器的行为进行微调。本文深入探讨了…

安宝特方案丨船舶智造AR+AI+作业标准化管理系统解决方案(维保)

船舶维保管理现状&#xff1a;设备维保主要由维修人员负责&#xff0c;根据设备运行状况和维护计划进行定期保养和故障维修。维修人员凭借经验判断设备故障原因&#xff0c;制定维修方案。 一、痛点与需求 1 Arbigtec 人工经验限制维修效率&#xff1a; 复杂设备故障的诊断和…