安卓基础(语义树)

发布于:2025-06-11 ⋅ 阅读:(30) ⋅ 点赞:(0)

进化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; // 可点击节点
        
        @Override
        public 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:优先检查是否有集中式的contentDescription
            UnreadResult 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 {
            // 递归查找所有可能包含完整信息的contentDescription
            return 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 {
            // 检查当前节点的contentDescription
            String desc = node.getContentDescription() != null ? 
                    node.getContentDescription().toString() : "";
            
            if (!desc.trim().isEmpty()) {
                Log.d(TAG, "策略0:检查contentDescription: " + desc);
                
                // 解析contentDescription
                UnreadResult 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)) {
                // 火花数字通常在中间,且前面有ImageView
                result.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; // 可点击节点
        
        @Override
        public 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:优先检查是否有集中式的contentDescription
            UnreadResult 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 {
            // 递归查找所有可能包含完整信息的contentDescription
            return 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 {
            // 检查当前节点的contentDescription
            String desc = node.getContentDescription() != null ? 
                    node.getContentDescription().toString() : "";
            
            if (!desc.trim().isEmpty()) {
                // 解析contentDescription
                UnreadResult 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)) {
                // 火花数字通常在中间,且前面有ImageView
                result.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);
        }
    }

}