Skip to content

Commit d8657ed

Browse files
committed
fix: correct unified gesture helper behavior
1 parent 6f221ce commit d8657ed

12 files changed

Lines changed: 126 additions & 22 deletions

File tree

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,7 @@ private static PointerPair pointerPairAt(GestureSpec spec, double t) {
409409
endRadius = spec.radius * spec.scale;
410410
}
411411
double radius = startRadius + (endRadius - startRadius) * t;
412-
return new PointerPair(
413-
new float[] {(float) (spec.x - radius), (float) (spec.x + radius)},
414-
new float[] {(float) spec.y, (float) spec.y});
412+
return twoPointerPairAt(spec.x, spec.y, radius, 0.0d);
415413
}
416414
double centerX = spec.x;
417415
double centerY = spec.y;
@@ -427,16 +425,15 @@ private static PointerPair pointerPairAt(GestureSpec spec, double t) {
427425
}
428426
radius = startRadius + (endRadius - startRadius) * t;
429427
}
430-
double angle = Math.toRadians(-90 + spec.degrees * t);
428+
return twoPointerPairAt(centerX, centerY, radius, spec.degrees * t);
429+
}
430+
431+
private static PointerPair twoPointerPairAt(
432+
double centerX, double centerY, double radius, double rotationDegrees) {
433+
float[][] coordinates = TwoPointerGeometry.pointsAt(
434+
centerX, centerY, radius, rotationDegrees);
431435
return new PointerPair(
432-
new float[] {
433-
(float) (centerX + Math.cos(angle) * radius),
434-
(float) (centerX - Math.cos(angle) * radius)
435-
},
436-
new float[] {
437-
(float) (centerY + Math.sin(angle) * radius),
438-
(float) (centerY - Math.sin(angle) * radius)
439-
});
436+
coordinates[0], coordinates[1]);
440437
}
441438

442439
private static PlannedPointerPath[] readPlannedPointers(
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.callstack.agentdevice.multitouchhelper;
2+
3+
final class TwoPointerGeometry {
4+
private static final double INITIAL_ANGLE_DEGREES = -90.0d;
5+
6+
private TwoPointerGeometry() {}
7+
8+
static float[][] pointsAt(
9+
double centerX, double centerY, double radius, double rotationDegrees) {
10+
double angle = Math.toRadians(INITIAL_ANGLE_DEGREES + rotationDegrees);
11+
return new float[][] {
12+
{
13+
(float) (centerX + Math.cos(angle) * radius),
14+
(float) (centerX - Math.cos(angle) * radius)
15+
},
16+
{
17+
(float) (centerY + Math.sin(angle) * radius),
18+
(float) (centerY - Math.sin(angle) * radius)
19+
}
20+
};
21+
}
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.callstack.agentdevice.multitouchhelper;
2+
3+
public final class TwoPointerGeometryTest {
4+
private TwoPointerGeometryTest() {}
5+
6+
public static void main(String[] args) {
7+
float[][] points = TwoPointerGeometry.pointsAt(300.0d, 400.0d, 60.0d, 0.0d);
8+
9+
assertNear(300.0f, points[0][0], "first x");
10+
assertNear(300.0f, points[0][1], "second x");
11+
assertNear(340.0f, points[1][0], "first y");
12+
assertNear(460.0f, points[1][1], "second y");
13+
}
14+
15+
private static void assertNear(float expected, float actual, String label) {
16+
if (Math.abs(expected - actual) > 0.001f) {
17+
throw new AssertionError(label + ": expected " + expected + ", got " + actual);
18+
}
19+
}
20+
}

scripts/build-android-multitouch-helper.sh

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,31 @@ VERSION_CODE="$(
4747

4848
BUILD_DIR="$HELPER_DIR/build"
4949
CLASSES_DIR="$BUILD_DIR/classes"
50+
TEST_CLASSES_DIR="$BUILD_DIR/test-classes"
5051
DEX_DIR="$BUILD_DIR/dex"
5152
KEYSTORE="$PROJECT_DIR/android-snapshot-helper/debug.keystore"
5253
UNSIGNED_APK="$BUILD_DIR/helper-unsigned.apk"
5354
ALIGNED_APK="$BUILD_DIR/helper-aligned.apk"
5455
APK_PATH="$OUTPUT_DIR/$APK_BASENAME"
5556

5657
rm -rf "$BUILD_DIR"
57-
mkdir -p "$CLASSES_DIR" "$DEX_DIR" "$OUTPUT_DIR"
58+
mkdir -p "$CLASSES_DIR" "$TEST_CLASSES_DIR" "$DEX_DIR" "$OUTPUT_DIR"
5859

5960
javac \
6061
--release 11 \
6162
-classpath "$ANDROID_JAR" \
6263
-d "$CLASSES_DIR" \
6364
$(find "$HELPER_DIR/src/main/java" -name '*.java' | sort)
6465

66+
javac \
67+
--release 11 \
68+
-classpath "$CLASSES_DIR" \
69+
-d "$TEST_CLASSES_DIR" \
70+
$(find "$HELPER_DIR/src/test/java" -name '*.java' | sort)
71+
72+
java -classpath "$CLASSES_DIR:$TEST_CLASSES_DIR" \
73+
com.callstack.agentdevice.multitouchhelper.TwoPointerGeometryTest
74+
6575
"$BUILD_TOOLS_DIR/d8" \
6676
--min-api "$MIN_SDK" \
6777
--classpath "$ANDROID_JAR" \

src/core/__tests__/capabilities.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ test('device capability matrix stays consistent across shared command groups', (
153153
],
154154
},
155155
{
156-
commands: ['swipe'],
156+
commands: ['gesture', 'swipe'],
157157
checks: [
158158
{ device: iosSimulator, expected: true, label: 'on iOS sim' },
159159
{ device: iosDevice, expected: true, label: 'on iOS device' },
@@ -232,6 +232,7 @@ test('macOS supports the Apple runner interaction core but excludes mobile-only
232232
'scroll',
233233
'snapshot',
234234
'swipe',
235+
'gesture',
235236
'trigger-app-event',
236237
'type',
237238
'wait',
@@ -382,6 +383,7 @@ test('web supports only the initial browser interaction slice', () => {
382383
'boot',
383384
'clipboard',
384385
'diff',
386+
'gesture',
385387
'home',
386388
'install',
387389
'install-from-source',

src/core/__tests__/gesture-capabilities.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ test('TV, spatial, watch, desktop, Linux, and web gesture policy stays explicit'
8080
const web = device({ platform: 'web', kind: 'device', target: 'desktop' });
8181

8282
assertSupported(oneFingerPan, androidTv);
83-
assertUnsupported(twoFingerPan, androidTv, /tvOS/);
83+
assertUnsupported(twoFingerPan, androidTv, /Android TV/);
84+
assert.throws(
85+
() => requireGestureSupported(twoFingerPan, androidTv),
86+
(error: unknown) =>
87+
error instanceof AppError &&
88+
/Android TV has no touch input/.test(String(error.details?.hint)),
89+
);
8490
assertUnsupported(twoFingerPan, tvOs, /tvOS/);
8591
assertUnsupported(twoFingerPan, visionOs, /visionOS/i);
8692
assertUnsupported(oneFingerPan, watchOs, /watchos/);

src/core/capabilities.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ function requireMultiTouchGestureSupported(input: GestureSemanticInput, device:
165165
if (device.target !== 'tv') return;
166166
throw unsupportedGesture(
167167
input,
168-
`gesture ${input.intent} is not supported on tvOS`,
169-
'tvOS has no touch input — this gesture is supported on Android and the iOS simulator only.',
168+
`gesture ${input.intent} is not supported on Android TV`,
169+
'Android TV has no touch input — this gesture is supported on Android phones, tablets, and the iOS simulator only.',
170170
);
171171
}
172172
if (device.platform !== 'apple') {

src/core/command-descriptor/__tests__/parity.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ const NO_CAPABILITY_PUBLIC_COMMANDS = new Set<string>([
4747
PUBLIC_COMMANDS.devices,
4848
PUBLIC_COMMANDS.doctor,
4949
PUBLIC_COMMANDS.events,
50-
PUBLIC_COMMANDS.gesture,
5150
PUBLIC_COMMANDS.prepare,
5251
PUBLIC_COMMANDS.replay,
5352
PUBLIC_COMMANDS.test,
@@ -285,6 +284,7 @@ test('capability-checked command list is built from descriptor capabilities', ()
285284

286285
assert.deepEqual(listCapabilityCheckedCommandNames(), expected);
287286
assert.ok(expectedNames.has(PUBLIC_COMMANDS.snapshot), 'snapshot remains capability-checked');
287+
assert.ok(expectedNames.has(PUBLIC_COMMANDS.gesture), 'gesture remains capability-checked');
288288
assert.equal(
289289
expectedNames.has(PUBLIC_COMMANDS.capabilities),
290290
false,

src/core/command-descriptor/registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ export const RAW_COMMAND_DESCRIPTORS = [
772772
...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}),
773773
catalog: { group: 'public' },
774774
daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true },
775+
capability: ALL_DEVICE_COMMAND_CAPABILITY,
775776
timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
776777
batchable: true,
777778
},

src/daemon/handlers/__tests__/session-capabilities.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,16 @@ test('capabilities reports supported commands for the selected session device',
3434
kind: 'emulator',
3535
});
3636
expect(response.data?.availableCommands).toEqual(
37-
expect.arrayContaining(['open', 'screenshot', 'snapshot', 'press', 'fill', 'network', 'perf']),
37+
expect.arrayContaining([
38+
'open',
39+
'screenshot',
40+
'snapshot',
41+
'press',
42+
'fill',
43+
'network',
44+
'perf',
45+
PUBLIC_COMMANDS.gesture,
46+
]),
3847
);
3948
expect(response.data?.availableCommands).not.toContain(PUBLIC_COMMANDS.capabilities);
4049
expect(response.data?.availableCommands).not.toContain(PUBLIC_COMMANDS.devices);

0 commit comments

Comments
 (0)