Repository navigation
跳过按钮点击升级为手势(完善 #90)+ 息屏/解锁后的热启动广告处理 - #110
Conversation
…ives performAction(ACTION_CLICK) returning true only means the view handled the event; some ad SDKs ignore accessibility clicks, and a node that is not laid out yet (bounds 0,0,0,0, seen with Zhihu on a real device) reports success without any effect. PR #90 proposed replacing every ACTION_CLICK with a coordinate gesture, which handles those apps but taps the origin for unlaid-out nodes, can hit whatever replaced the ad when it closed a moment earlier, and drops the onlyClick rule flag. This keeps ACTION_CLICK (node, then its parent) as the first attempt, falls back to a gesture immediately when ACTION_CLICK is rejected, and otherwise escalates to a gesture only when the same widget is still on screen at least 500 ms later. A widget is attempted at most twice per skip-ad process. Widgets are identified by class, view id and bounds so that a changing countdown label ("跳过 5" -> "跳过 4") does not reset the attempt count. Gestures are only dispatched at a non-empty, on-screen, visible spot. The onlyClick flag of widget rules still forces a gesture. Adds Robolectric tests for the escalation, the immediate fallback, the parent click, invalid coordinates, onlyClick and attempt reset; 47 unit tests in total.
Apps often show a splash ad when they come back to the foreground after
the screen was locked. The existing hook only reacted to USER_PRESENT,
restarted the skip-ad process for whatever package was last recorded,
even a whitelisted app or the launcher, and then waited passively for
events.
- detect the warm start from the accessibility events: a system window
shown while the keyguard is locked (or the screen is off) marks the
current target app as covered, and the next window of that app
restarts the skip-ad process. On OPPO devices the system skips
USER_PRESENT/SCREEN_ON deliveries to third-party apps ("oplus skip sp"),
so this path does not depend on broadcasts at all
- the broadcast receiver stays as an additional trigger and now also
listens for SCREEN_ON (devices without a lock screen never send
USER_PRESENT)
- both paths only act when the foreground app is one we handle, and
scan its window right away and once more a second later instead of
waiting for further events
Adds Robolectric tests for the unlock broadcast, SCREEN_ON, unhandled
apps, a foreign active window, the event-based detection and its
negatives (notification shade, the app's own dialogs); 55 unit tests.
- a gesture is only dispatched after AccessibilityNodeInfo.refresh() succeeds and the refreshed node still belongs to a target package, still matches its rule, and is a visible spot on screen. A queued snapshot of a button whose ad has already closed is therefore never tapped at its old coordinates - widget rules stay active until the matched widget has had its final attempt, so a button that only matches by id can still be escalated to a gesture at the next sighting - click attempts are counted only when ACTION_CLICK or a gesture was actually issued; a gesture refused by the checks (e.g. the widget is not visible yet) leaves the attempt budget untouched - the pending warm start is kept while the device is still locked: a same-package window during the lock screen, or SCREEN_ON with the keyguard up, no longer starts a process that expires before the user unlocks Adds regression tests for each case; 61 unit tests in total.
|
已按审查意见修复,见提交
单元测试共 61 个,全部通过。 |
startSkipAdProcess() clears the per-process click attempts. A click whose performAction() was still blocking on the worker when the user left and re-entered the app then recorded itself into the fresh table, so the same button in the new ad was either held back by the retry interval or escalated straight to a gesture. Every startSkipAdProcess() now bumps a generation counter. A traversal stops as soon as the generation changes, and a click only records its attempt when it completes under the generation it started in. Adds two controlled-concurrency tests; 63 unit tests in total.
|
已修复,见提交 按建议引入流程代数 回归测试(受控并发,2 条): |
The generation check before recording a click attempt was still a check-then-act: the worker could pass the check, get descheduled, and write into the table that startSkipAdProcess() had cleared in between. Click state now lives in a ClickSession object. startSkipAdProcess() installs a fresh session; a traversal captures the session it started under and passes it to every click, so an attempt is recorded in that session no matter when the click completes. A click that finishes after the user re-entered the app lands in the discarded old session and can never hold back or escalate the first click of the new ad. The stale traversal itself still stops as soon as the session changes. Test asserts the record ends up in the old session and the new one stays empty; 63 unit tests in total.
|
已修复,见提交 按建议改为每轮流程持有独立状态对象: 回归测试更新为直接断言:旧点击返回后记录落在旧 session(1 条),新 session 为空(0 条)。单元测试共 63 个,全部通过。 |
The final attempt of a widget rule cleared the shared setTargetedWidgets field. When that attempt completed after the user had re-entered the app, it switched off the widget rules of the new process, which reuses the same rule set. The flag now lives in the ClickSession the scan started under and only that session's rules are considered done; scheduling reads the rules through activeWidgetRules(), which hides them once the current session is finished. Test blocks the old process inside its final gesture, starts a new process and checks the rules stay active; 64 unit tests in total.
|
已修复,见提交 "控件规则已完成"状态也放进 回归测试:旧流程的第二次(最终)手势在发出前阻塞,期间切出再进入应用启动新流程(复用同一规则 Set),旧手势返回后断言新流程的控件规则仍然有效,且新广告里的按钮仍被规则处理。单元测试共 64 个,全部通过。 |
本 PR 包含两部分改动,均在真机(OPPO PHY110 / Android 16)上验证过。
一、点击方式:ACTION_CLICK 无效时升级为坐标手势(完善 #90)
#90 提出把所有
performAction(ACTION_CLICK)换成坐标手势点击,动机成立:performAction返回 true 只表示 View 处理了事件,部分广告 SDK 会忽略无障碍点击;真机上也观察到知乎首次命中时节点尚未布局(Position=[0,0,0,0]),performAction返回 true 但无效。但 #90 的做法有回归风险:坐标为 [0,0,0,0] 时会点到屏幕左上角;广告恰好已关闭时手势会落在下一屏,最坏点进广告落地页;同时删掉了控件规则里用户可配置的
onlyClick语义。本 PR 在保留performAction优点的前提下解决同一问题,可替代 #90。performAction(ACTION_CLICK)(节点不可点则试父容器)。performAction明确返回 false → 立刻退回坐标手势(原有行为)。performAction返回 true 但 ≥500ms 后同一按钮仍出现在扫描结果中 → 视为假成功,第二次升级为坐标手势;每个按钮每轮最多 2 次尝试。isVisibleToUser(),不满足则不点击。onlyClick=true仍直接手势点击。二、息屏 / 解锁后的热启动广告
原逻辑只响应
USER_PRESENT广播,对最近记录的包名无条件重启流程(哪怕是白名单应用或桌面),然后被动等事件。真机排查发现 OPPO 系统对第三方应用直接跳过USER_PRESENT/SCREEN_ON的投递(广播历史里原因为oplus skip sp),该路径在这类机型上完全收不到通知。KeyguardManager.isKeyguardLocked()或屏幕不可交互,则标记当前目标应用被锁屏覆盖;之后该应用窗口回到前台即视为热启动,重启跳过流程。不依赖任何广播;下拉通知栏、音量面板(未锁屏)和应用自身的对话框不会触发。SCREEN_ON(无锁屏的设备不会发USER_PRESENT)。验证
onlyClick/新一轮重置;解锁广播/SCREEN_ON/非目标应用/锁屏仍是活动窗口;事件识别热启动及其反例(通知栏、自身对话框、非目标应用)。🤖 Generated with Claude Code