Skip to content

Commit 67c6ba6

Browse files
committed
fix(android): clear the composer, not just the key plane, before a scroll swipes
The helper kept the largest `TYPE_INPUT_METHOD` rectangle as the keyboard. A composer bar and its key plane can arrive as separate windows and the key plane is the larger one, so the earlier top edge was discarded and the clipped band still ended inside the composer: the swipe landed on keys the rule exists to keep it off. The read now copies every input method window and unions the ones the swipe's centre line crosses, which is the same line the shared clip rule tests. A candidate strip at the edge of the screen that the swipe can never reach no longer shortens the band either. The selection runs on plain window edges, because `Rect` is a device type whose constructors throw off-device, so the two-window case is a unit test rather than a simulator-only path.
1 parent f63e72f commit 67c6ba6

3 files changed

Lines changed: 155 additions & 8 deletions

File tree

android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/GestureViewportReader.java

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.graphics.Rect;
55
import android.view.accessibility.AccessibilityNodeInfo;
66
import android.view.accessibility.AccessibilityWindowInfo;
7+
import java.util.ArrayList;
78
import java.util.List;
89
import java.util.concurrent.TimeoutException;
910

@@ -29,6 +30,33 @@ static final class Reading {
2930
}
3031
}
3132

33+
/**
34+
* One reported window's edges in screen pixels. Plain fields because {@code Rect} is a device type
35+
* whose constructors throw off-device, and which of several input method windows a swipe strikes is
36+
* arithmetic that has to be testable without one.
37+
*/
38+
static final class WindowEdges {
39+
final int left;
40+
final int top;
41+
final int right;
42+
final int bottom;
43+
44+
WindowEdges(int left, int top, int right, int bottom) {
45+
this.left = left;
46+
this.top = top;
47+
this.right = right;
48+
this.bottom = bottom;
49+
}
50+
51+
static WindowEdges of(Rect rect) {
52+
return new WindowEdges(rect.left, rect.top, rect.right, rect.bottom);
53+
}
54+
55+
Rect toRect() {
56+
return new Rect(left, top, right, bottom);
57+
}
58+
}
59+
3260
@SuppressWarnings("deprecation")
3361
static Reading readReading(UiAutomation automation) {
3462
try {
@@ -45,20 +73,18 @@ static Reading readReading(UiAutomation automation) {
4573
AccessibilityTreeCapture.enableInteractiveWindowRetrieval(automation);
4674
Rect activeBounds = null;
4775
Rect fallbackBounds = null;
48-
Rect inputMethodBounds = null;
76+
List<WindowEdges> inputMethodWindows = new ArrayList<>();
4977
List<AccessibilityWindowInfo> windows = automation.getWindows();
5078
try {
5179
for (AccessibilityWindowInfo window : windows) {
5280
int type = window.getType();
5381
if (type == AccessibilityWindowInfo.TYPE_INPUT_METHOD) {
54-
// Keep the largest IME window: a composer bar and its key plane can be reported as
55-
// separate windows, and the scroll only needs how far down the free surface reaches.
82+
// Copy every input method window. Which of them a swipe has to clear depends on the
83+
// application window, which this loop has not finished reading, so they are collected here
84+
// and resolved once it has.
5685
Rect bounds = new Rect();
5786
window.getBoundsInScreen(bounds);
58-
if (!bounds.isEmpty() && (inputMethodBounds == null || bounds.height() * bounds.width()
59-
> inputMethodBounds.height() * inputMethodBounds.width())) {
60-
inputMethodBounds = bounds;
61-
}
87+
if (!bounds.isEmpty()) inputMethodWindows.add(WindowEdges.of(bounds));
6288
continue;
6389
}
6490
if (type != AccessibilityWindowInfo.TYPE_APPLICATION) continue;
@@ -76,7 +102,40 @@ static Reading readReading(UiAutomation automation) {
76102
window.recycle();
77103
}
78104
}
79-
return new Reading(resolveApplication(automation, activeBounds, fallbackBounds), inputMethodBounds);
105+
Rect application = resolveApplication(automation, activeBounds, fallbackBounds);
106+
WindowEdges struck = struckInputMethod(
107+
inputMethodWindows, application == null ? null : WindowEdges.of(application));
108+
return new Reading(application, struck == null ? null : struck.toRect());
109+
}
110+
111+
/**
112+
* The input method share a swipe has to stay above, or null when none of it is in the way.
113+
*
114+
* <p>A composer bar and its key plane can arrive as separate windows, and the larger rectangle is
115+
* usually the lower key plane: keeping only that leaves the swipe inside the composer reaching
116+
* further up the screen. So this unions the windows the swipe's centre line crosses — the same line
117+
* the shared clip rule tests — and ignores the ones beside it that the swipe cannot reach.
118+
*/
119+
static WindowEdges struckInputMethod(List<WindowEdges> inputMethodWindows, WindowEdges application) {
120+
WindowEdges struck = null;
121+
for (WindowEdges bounds : inputMethodWindows) {
122+
if (application != null) {
123+
double swipeCenterX = application.left + (application.right - application.left) / 2.0;
124+
boolean strikesSwipePath = swipeCenterX >= bounds.left && swipeCenterX < bounds.right;
125+
boolean overlapsWindow = bounds.bottom > application.top && bounds.top < application.bottom;
126+
if (!strikesSwipePath || !overlapsWindow) continue;
127+
}
128+
if (struck == null) {
129+
struck = bounds;
130+
continue;
131+
}
132+
struck = new WindowEdges(
133+
Math.min(struck.left, bounds.left),
134+
Math.min(struck.top, bounds.top),
135+
Math.max(struck.right, bounds.right),
136+
Math.max(struck.bottom, bounds.bottom));
137+
}
138+
return struck;
80139
}
81140

82141
static Rect read(UiAutomation automation) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.callstack.agentdevice.snapshothelper;
2+
3+
import java.util.Arrays;
4+
import java.util.Collections;
5+
import java.util.List;
6+
7+
public final class GestureViewportReaderTest {
8+
private GestureViewportReaderTest() {}
9+
10+
private static final GestureViewportReader.WindowEdges APPLICATION =
11+
new GestureViewportReader.WindowEdges(0, 0, 1080, 2400);
12+
private static final GestureViewportReader.WindowEdges KEY_PLANE =
13+
new GestureViewportReader.WindowEdges(0, 1517, 1080, 2400);
14+
private static final GestureViewportReader.WindowEdges COMPOSER =
15+
new GestureViewportReader.WindowEdges(0, 1400, 1080, 1517);
16+
private static final GestureViewportReader.WindowEdges SIDE_STRIP =
17+
new GestureViewportReader.WindowEdges(900, 1200, 1080, 2400);
18+
19+
static void run() {
20+
assertNoInputMethodOnScreen();
21+
assertComposerAboveItsKeyPlaneKeepsItsEarlierTopEdge();
22+
assertWindowBesideTheSwipePathIsIgnored();
23+
}
24+
25+
private static void assertNoInputMethodOnScreen() {
26+
assertEdges(
27+
GestureViewportReader.struckInputMethod(
28+
Collections.<GestureViewportReader.WindowEdges>emptyList(), APPLICATION),
29+
null,
30+
"no input method window on screen");
31+
}
32+
33+
private static void assertComposerAboveItsKeyPlaneKeepsItsEarlierTopEdge() {
34+
List<GestureViewportReader.WindowEdges> both = Arrays.asList(KEY_PLANE, COMPOSER);
35+
// The key plane is the larger rectangle. Keeping only it would plan a swipe ending inside the
36+
// composer, whose top edge reaches 117px further up the screen.
37+
assertEdges(
38+
GestureViewportReader.struckInputMethod(both, APPLICATION),
39+
new GestureViewportReader.WindowEdges(0, 1400, 1080, 2400),
40+
"composer above its key plane");
41+
assertEdges(
42+
GestureViewportReader.struckInputMethod(Arrays.asList(COMPOSER, KEY_PLANE), APPLICATION),
43+
new GestureViewportReader.WindowEdges(0, 1400, 1080, 2400),
44+
"composer listed after its key plane");
45+
}
46+
47+
private static void assertWindowBesideTheSwipePathIsIgnored() {
48+
// A floating candidate strip at the right edge never crosses the centre line a vertical swipe
49+
// travels, so its higher top edge must not shorten the band.
50+
assertEdges(
51+
GestureViewportReader.struckInputMethod(Arrays.asList(KEY_PLANE, SIDE_STRIP), APPLICATION),
52+
KEY_PLANE,
53+
"input method window beside the swipe path");
54+
assertEdges(
55+
GestureViewportReader.struckInputMethod(
56+
Collections.singletonList(SIDE_STRIP), APPLICATION),
57+
null,
58+
"only an unreachable input method window on screen");
59+
}
60+
61+
private static void assertEdges(
62+
GestureViewportReader.WindowEdges actual,
63+
GestureViewportReader.WindowEdges expected,
64+
String label) {
65+
if (expected == null) {
66+
if (actual != null) {
67+
throw new AssertionError(
68+
"Expected no input method rect for " + label + ", got " + describe(actual));
69+
}
70+
return;
71+
}
72+
if (actual == null) {
73+
throw new AssertionError("Expected " + describe(expected) + " for " + label + ", got none");
74+
}
75+
if (actual.left != expected.left
76+
|| actual.top != expected.top
77+
|| actual.right != expected.right
78+
|| actual.bottom != expected.bottom) {
79+
throw new AssertionError(
80+
"Expected " + describe(expected) + " for " + label + ", got " + describe(actual));
81+
}
82+
}
83+
84+
private static String describe(GestureViewportReader.WindowEdges edges) {
85+
return "[" + edges.left + "," + edges.top + "][" + edges.right + "," + edges.bottom + "]";
86+
}
87+
}

android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/SnapshotHelperTestSuite.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ public static void main(String[] args) throws Exception {
77
PointerEventScheduleTest.run();
88
AccessibilityCaptureStabilizerTest.run();
99
BoundedUiAutomationConnectionTest.run();
10+
GestureViewportReaderTest.run();
1011
}
1112
}

0 commit comments

Comments
 (0)