Skip to content

Commit be0b154

Browse files
EarthXPclaude
andcommitted
fix: detect am start silent failures and fall back to resolved component
am start -p <package> can exit 0 while printing "Error: Activity not started" to stdout. The previous try/catch never entered the fallback because runCmd did not throw. Now we check the output for error indicators via isAmStartError() and proceed to the resolve-activity fallback when the primary launch silently fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 142e109 commit be0b154

2 files changed

Lines changed: 70 additions & 41 deletions

File tree

src/platforms/android/__tests__/index.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import os from 'node:os';
55
import path from 'node:path';
66
import {
77
inferAndroidAppName,
8+
isAmStartError,
89
listAndroidApps,
910
openAndroidApp,
1011
parseAndroidLaunchComponent,
@@ -139,6 +140,23 @@ test('parseAndroidLaunchComponent returns null when no component is present', ()
139140
assert.equal(parseAndroidLaunchComponent(stdout), null);
140141
});
141142

143+
test('isAmStartError detects am start failure in stdout', () => {
144+
assert.equal(
145+
isAmStartError(
146+
'Starting: Intent { ... }\nError: Activity not started, unable to resolve Intent { ... }',
147+
'',
148+
),
149+
true,
150+
);
151+
});
152+
153+
test('isAmStartError returns false for successful am start', () => {
154+
assert.equal(
155+
isAmStartError('Status: ok\nLaunchState: COLD\nActivity: com.example/.MainActivity', ''),
156+
false,
157+
);
158+
});
159+
142160
test('inferAndroidAppName derives readable names from package ids', () => {
143161
assert.equal(inferAndroidAppName('com.android.settings'), 'Settings');
144162
assert.equal(inferAndroidAppName('com.google.android.apps.maps'), 'Maps');
@@ -363,12 +381,13 @@ test('openAndroidApp fallback resolve-activity includes MAIN/LAUNCHER flags', as
363381
' echo "package:com.microsoft.office.outlook"',
364382
' exit 0',
365383
'fi',
366-
'# First am start (with -p) fails to simulate multi-entry app issue',
384+
'# First am start (with -p) outputs error but exits 0 (real Android behavior)',
367385
'if [ "$1" = "shell" ] && [ "$2" = "am" ] && [ "$3" = "start" ]; then',
368386
' for arg in "$@"; do',
369387
' if [ "$arg" = "-p" ]; then',
370-
' echo "Error: Activity not started" >&2',
371-
' exit 1',
388+
' echo "Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.DEFAULT,android.intent.category.LAUNCHER] pkg=com.microsoft.office.outlook }"',
389+
' echo "Error: Activity not started, unable to resolve Intent { act=android.intent.action.MAIN cat=[android.intent.category.DEFAULT,android.intent.category.LAUNCHER] flg=0x10000000 pkg=com.microsoft.office.outlook }"',
390+
' exit 0',
372391
' fi',
373392
' done',
374393
' echo "Status: ok"',

src/platforms/android/index.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -238,46 +238,51 @@ export async function openAndroidApp(
238238
);
239239
return;
240240
}
241-
try {
242-
await runCmd(
243-
'adb',
244-
adbArgs(device, [
245-
'shell',
246-
'am',
247-
'start',
248-
'-W',
249-
'-a',
250-
'android.intent.action.MAIN',
251-
'-c',
252-
'android.intent.category.DEFAULT',
253-
'-c',
254-
'android.intent.category.LAUNCHER',
255-
'-p',
256-
resolved.value,
257-
]),
258-
);
241+
const primaryResult = await runCmd(
242+
'adb',
243+
adbArgs(device, [
244+
'shell',
245+
'am',
246+
'start',
247+
'-W',
248+
'-a',
249+
'android.intent.action.MAIN',
250+
'-c',
251+
'android.intent.category.DEFAULT',
252+
'-c',
253+
'android.intent.category.LAUNCHER',
254+
'-p',
255+
resolved.value,
256+
]),
257+
{ allowFailure: true },
258+
);
259+
if (primaryResult.exitCode === 0 && !isAmStartError(primaryResult.stdout, primaryResult.stderr)) {
259260
return;
260-
} catch (initialError) {
261-
const component = await resolveAndroidLaunchComponent(device, resolved.value);
262-
if (!component) throw initialError;
263-
await runCmd(
264-
'adb',
265-
adbArgs(device, [
266-
'shell',
267-
'am',
268-
'start',
269-
'-W',
270-
'-a',
271-
'android.intent.action.MAIN',
272-
'-c',
273-
'android.intent.category.DEFAULT',
274-
'-c',
275-
'android.intent.category.LAUNCHER',
276-
'-n',
277-
component,
278-
]),
279-
);
280261
}
262+
const component = await resolveAndroidLaunchComponent(device, resolved.value);
263+
if (!component) {
264+
throw new AppError('COMMAND_FAILED', `Failed to launch ${resolved.value}`, {
265+
stdout: primaryResult.stdout,
266+
stderr: primaryResult.stderr,
267+
});
268+
}
269+
await runCmd(
270+
'adb',
271+
adbArgs(device, [
272+
'shell',
273+
'am',
274+
'start',
275+
'-W',
276+
'-a',
277+
'android.intent.action.MAIN',
278+
'-c',
279+
'android.intent.category.DEFAULT',
280+
'-c',
281+
'android.intent.category.LAUNCHER',
282+
'-n',
283+
component,
284+
]),
285+
);
281286
}
282287

283288
async function resolveAndroidLaunchComponent(
@@ -304,6 +309,11 @@ async function resolveAndroidLaunchComponent(
304309
return parseAndroidLaunchComponent(result.stdout);
305310
}
306311

312+
export function isAmStartError(stdout: string, stderr: string): boolean {
313+
const output = `${stdout}\n${stderr}`;
314+
return /Error:.*(?:Activity not started|unable to resolve Intent)/i.test(output);
315+
}
316+
307317
export function parseAndroidLaunchComponent(stdout: string): string | null {
308318
const lines = stdout
309319
.split('\n')

0 commit comments

Comments
 (0)