diff --git a/bin/ui-trace b/bin/ui-trace index b9e6ac7..63a6cef 100755 --- a/bin/ui-trace +++ b/bin/ui-trace @@ -367,7 +367,8 @@ def main(): rec.add_argument("-i", "--interval", type=int, default=16, help="ms between samples (0 = as fast as possible)") rec.add_argument("--do", action="append", default=[], metavar="ACTION", - help="'wait MS' | 'tap X Y' | 'swipe X1 Y1 X2 Y2 [MS]', repeatable") + help="'wait MS' | 'tap X Y' | 'swipe X1 Y1 X2 Y2 [MS]' | " + "'holddrag X1 Y1 X2 Y2 HOLD_MS MOVE_MS', repeatable") rec.add_argument("-o", "--out", help="where to save the trace") rec.add_argument("-m", "--match", help="show a timeline for these afterwards") rec.add_argument("--field", default="top", diff --git a/share/ui-trace/UiTrace.java b/share/ui-trace/UiTrace.java index 151d4b4..c08544d 100644 --- a/share/ui-trace/UiTrace.java +++ b/share/ui-trace/UiTrace.java @@ -221,6 +221,9 @@ public final class UiTrace { case "swipe" -> swipe(Integer.parseInt(step[1]), Integer.parseInt(step[2]), Integer.parseInt(step[3]), Integer.parseInt(step[4]), step.length > 5 ? Long.parseLong(step[5]) : 300); + case "holddrag" -> holdDrag(Integer.parseInt(step[1]), Integer.parseInt(step[2]), + Integer.parseInt(step[3]), Integer.parseInt(step[4]), + Long.parseLong(step[5]), Long.parseLong(step[6])); default -> throw new IllegalArgumentException("unknown action: " + step[0]); } } @@ -345,4 +348,36 @@ public final class UiTrace { } send(MotionEvent.ACTION_UP, down, x2, y2); } + + /** + * A press held stationary for `holdMs`, then moved to `(x2, y2)` over `moveMs`, then + * released -- one continuous touch, never lifted between the hold and the move. + * + * Neither existing action can produce this. `tap` has no hold at all, and `swipe` + * interpolates motion across its *whole* duration starting at t=0, so a long `swipe` with a + * short first segment is still continuous motion throughout, never a hold followed by a + * drag. That gap matters for anything that decides pan-vs-select the way Android itself + * does -- a stationary press held past `LONG_PRESS` (500ms) starts a selection, which + * further drag then extends -- because neither of the other two actions can reach the + * "starts a selection" branch at all. + * + * The move phase reuses `swipe`'s own fixed 10ms cadence for the same reason: a velocity + * tracker needs a real sequence of points, not two. + */ + private void holdDrag(int x1, int y1, int x2, int y2, long holdMs, long moveMs) { + long down = SystemClock.uptimeMillis(); + send(MotionEvent.ACTION_DOWN, down, x1, y1); + SystemClock.sleep(holdMs); + int steps = (int) Math.max(2, moveMs / 10); + long moveStart = SystemClock.uptimeMillis(); + for (int i = 1; i <= steps; i++) { + float part = (float) i / steps; + long due = moveStart + (long) (moveMs * part); + long slack = due - SystemClock.uptimeMillis(); + if (slack > 0) SystemClock.sleep(slack); + send(MotionEvent.ACTION_MOVE, down, Math.round(x1 + (x2 - x1) * part), + Math.round(y1 + (y2 - y1) * part)); + } + send(MotionEvent.ACTION_UP, down, x2, y2); + } }