Skip to content

Commit c8fd3ef

Browse files
committed
fix: stabilize Android transform injection
1 parent ac17e38 commit c8fd3ef

3 files changed

Lines changed: 102 additions & 37 deletions

File tree

android-multitouch-helper/src/main/java/com/callstack/agentdevice/multitouchhelper/MultiTouchInstrumentation.java

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public final class MultiTouchInstrumentation extends Instrumentation {
1818
private static final int MAX_RADIUS = 1200;
1919
private static final int MIN_DURATION_MS = 16;
2020
private static final int MAX_DURATION_MS = 10_000;
21+
private static final int MOVE_FRAME_INTERVAL_MS = 32;
22+
private static final int MAX_MOVE_FRAMES = 60;
23+
private static final boolean WAIT_FOR_INPUT_DISPATCH = false;
2124
private Bundle arguments;
2225

2326
@Override
@@ -91,57 +94,83 @@ private int injectGesture(GestureSpec spec) {
9194
long eventTime = downTime;
9295
PointerPair start = pointerPairAt(spec, 0);
9396
PointerPair end = pointerPairAt(spec, 1);
97+
PointerPair activePointers = start.firstOnly();
9498
int count = 0;
9599

96-
inject(
97-
automation,
98-
motionEvent(downTime, eventTime, MotionEvent.ACTION_DOWN, start.firstOnly()));
99-
count += 1;
100-
eventTime += 8;
101-
inject(
102-
automation,
103-
motionEvent(
104-
downTime,
105-
eventTime,
106-
MotionEvent.ACTION_POINTER_DOWN | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
107-
start));
108-
count += 1;
100+
try {
101+
inject(
102+
automation,
103+
motionEvent(downTime, eventTime, MotionEvent.ACTION_DOWN, activePointers));
104+
count += 1;
105+
eventTime += 8;
106+
inject(
107+
automation,
108+
motionEvent(
109+
downTime,
110+
eventTime,
111+
MotionEvent.ACTION_POINTER_DOWN | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
112+
start));
113+
count += 1;
114+
activePointers = start;
115+
116+
int frameCount =
117+
Math.max(
118+
3,
119+
Math.min(
120+
MAX_MOVE_FRAMES,
121+
Math.round(spec.durationMs / (float) MOVE_FRAME_INTERVAL_MS)));
122+
for (int index = 1; index < frameCount; index += 1) {
123+
double t = (double) index / (double) frameCount;
124+
PointerPair frame = pointerPairAt(spec, t);
125+
eventTime = downTime + Math.round(spec.durationMs * t);
126+
inject(automation, motionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE, frame));
127+
count += 1;
128+
activePointers = frame;
129+
}
109130

110-
int frameCount = Math.max(3, Math.round(spec.durationMs / 16.0f));
111-
for (int index = 1; index < frameCount; index += 1) {
112-
double t = (double) index / (double) frameCount;
113-
PointerPair frame = pointerPairAt(spec, t);
114-
eventTime = downTime + Math.round(spec.durationMs * t);
115-
inject(automation, motionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE, frame));
131+
eventTime = downTime + spec.durationMs;
132+
inject(
133+
automation,
134+
motionEvent(
135+
downTime,
136+
eventTime,
137+
MotionEvent.ACTION_POINTER_UP | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
138+
end));
139+
count += 1;
140+
activePointers = end.firstOnly();
141+
inject(
142+
automation,
143+
motionEvent(downTime, eventTime + 8, MotionEvent.ACTION_UP, activePointers));
116144
count += 1;
145+
SystemClock.sleep(32);
146+
return count;
147+
} catch (RuntimeException error) {
148+
if (count > 0) {
149+
injectCancel(automation, downTime, eventTime + 16, activePointers);
150+
}
151+
throw error;
117152
}
118-
119-
eventTime = downTime + spec.durationMs;
120-
inject(
121-
automation,
122-
motionEvent(
123-
downTime,
124-
eventTime,
125-
MotionEvent.ACTION_POINTER_UP | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
126-
end));
127-
count += 1;
128-
inject(
129-
automation,
130-
motionEvent(downTime, eventTime + 8, MotionEvent.ACTION_UP, end.firstOnly()));
131-
count += 1;
132-
return count;
133153
}
134154

135155
private static void inject(UiAutomation automation, MotionEvent event) {
136156
try {
137-
if (!automation.injectInputEvent(event, true)) {
157+
if (!automation.injectInputEvent(event, WAIT_FOR_INPUT_DISPATCH)) {
138158
throw new IllegalStateException("injectInputEvent returned false");
139159
}
140160
} finally {
141161
event.recycle();
142162
}
143163
}
144164

165+
private static void injectCancel(
166+
UiAutomation automation, long downTime, long eventTime, PointerPair pair) {
167+
try {
168+
inject(automation, motionEvent(downTime, eventTime, MotionEvent.ACTION_CANCEL, pair));
169+
} catch (RuntimeException ignored) {
170+
// Best-effort cleanup; preserve the original injection failure.
171+
}
172+
}
173+
145174
private static MotionEvent motionEvent(long downTime, long eventTime, int action, PointerPair pair) {
146175
MotionEvent.PointerProperties[] properties =
147176
new MotionEvent.PointerProperties[pair.pointerCount];

src/platforms/android/__tests__/multitouch-helper.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@ test('parseAndroidMultiTouchHelperOutput returns final instrumentation gesture m
5959

6060
test('runAndroidMultiTouchHelperGesture encodes protocol payload for instrumentation', async () => {
6161
let capturedArgs: string[] | undefined;
62+
let capturedOptions: Parameters<AndroidAdbExecutor>[1];
6263
const result = await runAndroidMultiTouchHelperGesture({
63-
adb: async (args) => {
64+
adb: async (args, options) => {
6465
capturedArgs = args;
66+
capturedOptions = options;
6567
return {
6668
exitCode: 0,
6769
stdout: [resultRecord({ ok: 'true', kind: 'rotate' }), 'INSTRUMENTATION_CODE: 0'].join(
@@ -96,6 +98,34 @@ test('runAndroidMultiTouchHelperGesture encodes protocol payload for instrumenta
9698
durationMs: 250,
9799
});
98100
assert.equal(capturedArgs.at(-1), manifest.instrumentationRunner);
101+
assert.equal(capturedOptions?.timeoutMs, 90_000);
102+
});
103+
104+
test('runAndroidMultiTouchHelperGesture preserves helper failure messages', async () => {
105+
await assert.rejects(
106+
() =>
107+
runAndroidMultiTouchHelperGesture({
108+
adb: async () => ({
109+
exitCode: 1,
110+
stdout: [
111+
resultRecord({
112+
ok: 'false',
113+
errorType: 'java.lang.IllegalStateException',
114+
message: 'injectInputEvent returned false',
115+
}),
116+
'INSTRUMENTATION_CODE: 1',
117+
].join('\n'),
118+
stderr: '',
119+
}),
120+
request: { kind: 'pinch', x: 100, y: 200, scale: 1.5, radius: 120, durationMs: 250 },
121+
packageName: manifest.packageName,
122+
instrumentationRunner: manifest.instrumentationRunner,
123+
}),
124+
{
125+
code: 'COMMAND_FAILED',
126+
message: 'injectInputEvent returned false',
127+
},
128+
);
99129
});
100130

101131
test('pinchAndroid, rotateGestureAndroid, and transformGestureAndroid prefer provider-native touch injection', async () => {

src/platforms/android/multitouch-helper.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const ANDROID_MULTITOUCH_HELPER_RUNNER =
2222
'com.callstack.agentdevice.multitouchhelper/.MultiTouchInstrumentation';
2323
const ANDROID_MULTITOUCH_HELPER_PROTOCOL = 'android-multitouch-helper-v1';
2424
const ANDROID_MULTITOUCH_HELPER_INSTALL_TIMEOUT_MS = 30_000;
25-
const ANDROID_MULTITOUCH_HELPER_GESTURE_TIMEOUT_MS = 15_000;
25+
const ANDROID_MULTITOUCH_HELPER_GESTURE_TIMEOUT_MS = 90_000;
2626
const ANDROID_MULTITOUCH_HELPER_DEFAULT_DURATION_MS = 300;
2727
const ANDROID_MULTITOUCH_HELPER_DEFAULT_RADIUS = 160;
2828
const ANDROID_MULTITOUCH_HELPER_ROTATE_MAX_DEGREES_PER_FRAME = 3;
@@ -309,6 +309,12 @@ export async function runAndroidMultiTouchHelperGesture(options: {
309309
try {
310310
output = parseAndroidMultiTouchHelperOutput(`${result.stdout}\n${result.stderr}`);
311311
} catch (error) {
312+
if (
313+
error instanceof AppError &&
314+
error.message !== 'Android multi-touch helper did not return a final result'
315+
) {
316+
throw error;
317+
}
312318
throw new AppError(
313319
'COMMAND_FAILED',
314320
result.exitCode === 0

0 commit comments

Comments
 (0)