Skip to content

Commit 240f49b

Browse files
committed
refactor(android): read model and OS version in one getprop call
1 parent 9fc1cda commit 240f49b

5 files changed

Lines changed: 70 additions & 41 deletions

File tree

‎packages/platform-android/src/inventory-parsers.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isAndroidEmulatorSerial,
55
normalizeAndroidDeviceName,
66
parseAndroidAvdList,
7+
parseAndroidDeviceDescription,
78
parseAndroidDeviceEntries,
89
parseAndroidEmulatorAvdNameOutput,
910
parseAndroidFeatureListForTv,
@@ -33,3 +34,12 @@ test('Android inventory parsers preserve device, AVD-name, and TV detection sema
3334
assert.equal(parseAndroidTargetFromCharacteristics('phone,tablet'), null);
3435
assert.equal(parseAndroidFeatureListForTv('feature:android.hardware.type.television'), true);
3536
});
37+
38+
test('Android description parser reads the model and OS version lines and skips unset props', () => {
39+
assert.deepEqual(parseAndroidDeviceDescription('sdk_gphone16k_arm64\r\n17\r\n'), {
40+
model: 'sdk_gphone16k_arm64',
41+
osVersion: '17',
42+
});
43+
assert.deepEqual(parseAndroidDeviceDescription('\n16\n'), { osVersion: '16' });
44+
assert.deepEqual(parseAndroidDeviceDescription(''), {});
45+
});

‎packages/platform-android/src/inventory-parsers.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ export function parseAndroidDeviceEntries(rawOutput: string): AndroidDeviceEntry
2727
return devices;
2828
}
2929

30+
/** Reads the model and OS version lines printed by the inventory description probe, in order. */
31+
export function parseAndroidDeviceDescription(rawOutput: string): {
32+
model?: string;
33+
osVersion?: string;
34+
} {
35+
const [model, osVersion] = rawOutput.split(/\r?\n/).map((line) => line.trim());
36+
return {
37+
...(model ? { model } : {}),
38+
...(osVersion ? { osVersion } : {}),
39+
};
40+
}
41+
3042
export function parseAndroidAvdList(rawOutput: string): string[] {
3143
return rawOutput
3244
.split('\n')

‎packages/platform-android/src/inventory.test.ts‎

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ const scope: PlatformRequestScope = {
1717
const RUNNING_EMULATOR_PROBES: ReadonlyArray<readonly [string, string]> = [
1818
['ro.boot.qemu.avd_name', 'Pixel_9_Pro_XL\n'],
1919
['sys.boot_completed', '1\n'],
20-
['ro.product.model', 'sdk_gphone16k_arm64\n'],
21-
['ro.build.version.release', '17\n'],
20+
['ro.product.model', 'sdk_gphone16k_arm64\n17\n'],
2221
['ro.build.characteristics', 'phone\n'],
2322
['has-feature', 'false\n'],
2423
['pm\0list\0features', ''],
@@ -70,30 +69,39 @@ test.each([
7069
},
7170
);
7271

73-
test('Android inventory leaves model and OS version unset when the getprop probe fails', async () => {
74-
const host = createHost(async (request) => {
75-
const key = request.args.join('\0');
76-
if (key === 'devices\0-l')
77-
return result('List of devices attached\nR5CT1 device model:SM_S921B\n');
78-
if (key.includes('ro.product.model') || key.includes('ro.build.version.release')) {
79-
return result('', 'error: closed', 1);
80-
}
81-
if (key.includes('sys.boot_completed')) return result('1\n');
82-
if (key.includes('ro.build.characteristics')) return result('phone\n');
83-
if (key.includes('has-feature')) return result('false\n');
84-
if (key.includes('pm\0list\0features')) return result('');
85-
throw new Error(`Unexpected command: ${request.executable} ${request.args.join(' ')}`);
86-
});
72+
test.each([
73+
['exits non-zero', async () => result('', 'error: closed', 1)],
74+
[
75+
'times out',
76+
async () => {
77+
throw new AppError('COMMAND_FAILED', 'adb timed out after 10000ms', { timeoutMs: 10_000 });
78+
},
79+
],
80+
] as const)(
81+
'Android inventory leaves model and OS version unset when the description probe %s',
82+
async (_case, describeProbe) => {
83+
const host = createHost(async (request) => {
84+
const key = request.args.join('\0');
85+
if (key === 'devices\0-l') {
86+
return result('List of devices attached\nR5CT1 device model:SM_S921B\n');
87+
}
88+
if (key.includes('ro.product.model')) return await describeProbe();
89+
const probe = RUNNING_EMULATOR_PROBES.find(([fragment]) => key.includes(fragment));
90+
if (probe) return result(probe[1]);
91+
throw new Error(`Unexpected command: ${request.executable} ${request.args.join(' ')}`);
92+
});
8793

88-
const [device] = await createAndroidInventory(host).discover(
89-
{ androidAvdSelection: 'running-only' },
90-
scope,
91-
);
94+
const [device] = await createAndroidInventory(host).discover(
95+
{ androidAvdSelection: 'running-only' },
96+
scope,
97+
);
9298

93-
assert.equal(device?.name, 'SM S921B');
94-
assert.equal(device?.model, undefined);
95-
assert.equal(device?.osVersion, undefined);
96-
});
99+
assert.equal(device?.name, 'SM S921B');
100+
assert.equal(device?.booted, true);
101+
assert.equal(device?.model, undefined);
102+
assert.equal(device?.osVersion, undefined);
103+
},
104+
);
97105

98106
test('Android inventory fails closed when adb is unavailable', async () => {
99107
const host = createHost(async () => result(''), { adb: undefined });

‎packages/platform-android/src/inventory.ts‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'node:path';
22
import type { DeviceInfo } from '@agent-device/kernel/device';
3-
import { deviceShellArgv, type ShellWord } from '@agent-device/kernel/device-shell';
3+
import { deviceShellArgv, shellFragment, type ShellWord } from '@agent-device/kernel/device-shell';
44
import { AppError, asAppError } from '@agent-device/kernel/errors';
55
import type {
66
DeviceInventoryHostFor,
@@ -16,6 +16,7 @@ import {
1616
isAndroidEmulatorSerial,
1717
normalizeAndroidDeviceName,
1818
parseAndroidAvdList,
19+
parseAndroidDeviceDescription,
1920
parseAndroidDeviceEntries,
2021
parseAndroidEmulatorAvdNameOutput,
2122
parseAndroidFeatureListForTv,
@@ -24,6 +25,9 @@ import {
2425
} from './inventory-parsers.ts';
2526

2627
const PROBE_TIMEOUT_MS = 10_000;
28+
const DEVICE_DESCRIPTION_PROBE = shellFragment(
29+
'getprop ro.product.model; getprop ro.build.version.release',
30+
);
2731
const TV_FEATURES = [
2832
'android.software.leanback',
2933
'android.software.leanback_only',
@@ -119,37 +123,34 @@ async function probeRunningDevice(
119123
context: AndroidInventoryContext,
120124
entry: AndroidDeviceEntry,
121125
): Promise<DeviceInfo> {
122-
const [name, booted, target, model, osVersion] = await Promise.all([
126+
const [name, booted, target, description] = await Promise.all([
123127
resolveDeviceName(context, entry),
124128
isBooted(context, entry.serial),
125129
resolveTarget(context, entry.serial),
126-
readDeviceProp(context, entry.serial, 'ro.product.model'),
127-
readDeviceProp(context, entry.serial, 'ro.build.version.release'),
130+
readDeviceDescription(context, entry.serial),
128131
]);
129132
return {
130133
platform: 'android',
131134
id: entry.serial,
132135
name,
133136
kind: isAndroidEmulatorSerial(entry.serial) ? 'emulator' : 'device',
134137
target,
135-
...(model ? { model } : {}),
136-
...(osVersion ? { osVersion } : {}),
138+
...description,
137139
booted,
138140
};
139141
}
140142

141-
/** Best-effort `getprop` read for presentation fields; a failed probe leaves the field unset. */
142-
async function readDeviceProp(
143+
/** Best-effort model and OS version read in one shell call; a failed probe leaves both unset. */
144+
async function readDeviceDescription(
143145
context: AndroidInventoryContext,
144146
serial: string,
145-
prop: string,
146-
): Promise<string | undefined> {
147+
): Promise<Pick<DeviceInfo, 'model' | 'osVersion'>> {
147148
try {
148-
const result = await runAdbShell(context, serial, ['getprop', prop]);
149-
return (result.exitCode === 0 && result.stdout.trim()) || undefined;
149+
const result = await runAdbShell(context, serial, [DEVICE_DESCRIPTION_PROBE]);
150+
return result.exitCode === 0 ? parseAndroidDeviceDescription(result.stdout) : {};
150151
} catch (error) {
151152
if (context.scope.signal.aborted) throw error;
152-
return undefined;
153+
return {};
153154
}
154155
}
155156

‎src/managed-device-reachability.test.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,8 @@ test.skipIf(process.platform === 'win32')(
102102
String.raw` ? "1\n"`,
103103
' : args.includes("ro.build.characteristics")',
104104
String.raw` ? "phone\n"`,
105-
' : args.includes("ro.product.model")',
106-
String.raw` ? "sdk_gphone64_arm64\n"`,
107-
' : args.includes("ro.build.version.release")',
108-
String.raw` ? "16\n"`,
105+
' : args.some((arg) => arg.includes("ro.product.model"))',
106+
String.raw` ? "sdk_gphone64_arm64\n16\n"`,
109107
' : args.includes("has-feature") || args.includes("pm")',
110108
String.raw` ? "false\n"`,
111109
' : JSON.stringify({args, port: process.env.ANDROID_ADB_SERVER_PORT ?? null});',

0 commit comments

Comments
 (0)