Commit 1c662a14bf3b509170b1cb4de3d05380b22e78e5

Authored by yibo
1 parent 2a044a3d

feat: A11y chat list 滚动查找(方案 A + C 兜底)

问题:目标会话被大量群消息挤出可见 chat list → FSM CHAT_LIST 状态
findChatListRow 找不到 → 直接退搜索 → 搜索 UI 时序脆弱经常卡死。

方案 A(主):CHAT_LIST 找不到目标时,dispatchGesture 上滑列表
(540,1500→540,600)逐屏 scan,会话一定在列表里只是被挤下去。
- chatListScrolls 计数,上限 MAX_CHATLIST_SCROLLS=6
- 每条消息在 onReceive 重置
- 滚动不计入 transitionCount(用自己的上限防死循环)

方案 C(兜底):滚到上限还找不到才 performSearch 退搜索。

新增 scrollChatListDown(onCompleted) — 合成上滑手势。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
wechat_plain/scripts/lsposed-wechat-sendmsg/app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java
@@ -54,6 +54,8 @@ public class HermesAccessibilityService extends AccessibilityService { @@ -54,6 +54,8 @@ public class HermesAccessibilityService extends AccessibilityService {
54 private String pendingContent = null; 54 private String pendingContent = null;
55 private String pendingQuoteText = null; // 要引用的消息片段(substring 匹配 bkl text),null=不引用 55 private String pendingQuoteText = null; // 要引用的消息片段(substring 匹配 bkl text),null=不引用
56 private String pendingAtSender = null; // 要 @ 的群成员显示名,null=不 @ 56 private String pendingAtSender = null; // 要 @ 的群成员显示名,null=不 @
  57 + private int chatListScrolls = 0; // chat list 滚动查找次数(每条消息重置)
  58 + private static final int MAX_CHATLIST_SCROLLS = 6; // 滚动上限,超了才退搜索
57 59
58 @Override 60 @Override
59 protected void onServiceConnected() { 61 protected void onServiceConnected() {
@@ -108,6 +110,7 @@ public class HermesAccessibilityService extends AccessibilityService { @@ -108,6 +110,7 @@ public class HermesAccessibilityService extends AccessibilityService {
108 pendingContent = content; 110 pendingContent = content;
109 pendingQuoteText = (quoteText != null && !quoteText.isEmpty()) ? quoteText : null; 111 pendingQuoteText = (quoteText != null && !quoteText.isEmpty()) ? quoteText : null;
110 pendingAtSender = (atSender != null && !atSender.isEmpty()) ? atSender : null; 112 pendingAtSender = (atSender != null && !atSender.isEmpty()) ? atSender : null;
  113 + chatListScrolls = 0;
111 114
112 // 拉起微信主界面(如果不在则会启动;如果在则保留当前 fragment 状态) 115 // 拉起微信主界面(如果不在则会启动;如果在则保留当前 fragment 状态)
113 Intent launch = getPackageManager().getLaunchIntentForPackage("com.tencent.mm"); 116 Intent launch = getPackageManager().getLaunchIntentForPackage("com.tencent.mm");
@@ -210,9 +213,19 @@ public class HermesAccessibilityService extends AccessibilityService { @@ -210,9 +213,19 @@ public class HermesAccessibilityService extends AccessibilityService {
210 row.recycle(); 213 row.recycle();
211 root.recycle(); 214 root.recycle();
212 mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 2200); 215 mainHandler.postDelayed(() -> driveStateMachine(transitionCount + 1), 2200);
  216 + } else if (chatListScrolls < MAX_CHATLIST_SCROLLS) {
  217 + // A 方案:目标不在可见列表 → 滚动列表逐屏找(会话一定在列表里,只是被挤下去)
  218 + chatListScrolls++;
  219 + Log.i(TAG, "FSM: " + pendingToUserDisplayName + " not visible, scrolling chat list ("
  220 + + chatListScrolls + "/" + MAX_CHATLIST_SCROLLS + ")");
  221 + root.recycle();
  222 + // 滚动不计入 transitionCount(用 chatListScrolls 自己的上限防死循环)
  223 + scrollChatListDown(() -> mainHandler.postDelayed(
  224 + () -> driveStateMachine(transitionCount), 700));
213 } else { 225 } else {
  226 + // C 方案:滚到底还找不到 → 退回搜索
214 Log.w(TAG, "FSM: " + pendingToUserDisplayName 227 Log.w(TAG, "FSM: " + pendingToUserDisplayName
215 - + " not in visible chat list, falling back to search"); 228 + + " not found after " + chatListScrolls + " scrolls, falling back to search");
216 performSearch(root); 229 performSearch(root);
217 // performSearch owns root from here 230 // performSearch owns root from here
218 } 231 }
@@ -502,6 +515,22 @@ public class HermesAccessibilityService extends AccessibilityService { @@ -502,6 +515,22 @@ public class HermesAccessibilityService extends AccessibilityService {
502 } 515 }
503 } 516 }
504 517
  518 + /** 上滑会话列表(露出下面更多会话),用合成手势。完成后回调。 */
  519 + private void scrollChatListDown(Runnable onCompleted) {
  520 + Path path = new Path();
  521 + // 从屏幕中下部往上滑(露出列表下方的会话);坐标按竖屏估算,避开顶部搜索栏和底部 nav
  522 + path.moveTo(540, 1500);
  523 + path.lineTo(540, 600);
  524 + GestureDescription gesture = new GestureDescription.Builder()
  525 + .addStroke(new GestureDescription.StrokeDescription(path, 0, 300))
  526 + .build();
  527 + boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() {
  528 + @Override public void onCompleted(GestureDescription g) { if (onCompleted != null) onCompleted.run(); }
  529 + @Override public void onCancelled(GestureDescription g) { if (onCompleted != null) onCompleted.run(); }
  530 + }, mainHandler);
  531 + if (!dispatched && onCompleted != null) onCompleted.run();
  532 + }
  533 +
505 /** 在长按菜单中点击「引用」 — 搜索所有可见窗口(菜单可能在 popup window 里) */ 534 /** 在长按菜单中点击「引用」 — 搜索所有可见窗口(菜单可能在 popup window 里) */
506 private void clickQuoteMenuItem() { 535 private void clickQuoteMenuItem() {
507 AccessibilityNodeInfo menuItem = findClickableByTextAllWindows(MENU_QUOTE_TEXT); 536 AccessibilityNodeInfo menuItem = findClickableByTextAllWindows(MENU_QUOTE_TEXT);