Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,9 @@ from the device profile's tag: `android-tv` is `tv`, `android-wear` is
`watch`, `android-automotive` and its variants are `auto`, `android-desktop`
is `desktop`, and a profile with no tag is `phone`. A custom profile
found only in `devices.xml` takes its class from the tag Android Studio saved
for it, and is `phone` when it saved none. A profile Simlock ships carries its own class. A model whose tooling reports nothing usable has
for it, and is `phone` when it saved none. When a custom name is one an SDK or
Simlock-shipped profile already answers to, the class is that profile's, since a
lease of that name creates its device. A profile Simlock ships carries its own class. A model whose tooling reports nothing usable has
no entry, and is still listed and leasable by name.

`classDefaults` says, for a class, which model Simlock would create for it on
Expand Down
6 changes: 3 additions & 3 deletions src/drivers/android/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ started, then `avdmanager delete avd`, frees the port.
`listManaged`: `<root>/*.avd` + `adb devices` + `ro.boot.qemu.avd_name` per serial; only root members count.

Profiles — library (`profiles/`), SDK (`avdmanager list device`), devices.xml
(`$ANDROID_USER_HOME/devices.xml`, else `.android/devices.xml` under `ANDROID_PREFS_ROOT`,
`ANDROID_SDK_HOME` or home). First case-insensitive name match wins. A devices.xml name the
library shadows → advisory `device-profile-hidden`. SDK class from `Tag`: none `phone`,
(`$ANDROID_USER_HOME/devices.xml`, else `.android/devices.xml` under `ANDROID_PREFS_ROOT`, `ANDROID_SDK_HOME`
or home). First case-insensitive name match wins and classes the model by the profile its name resolves to. A
devices.xml name the library shadows → advisory `device-profile-hidden`. SDK class from `Tag`: none `phone`,
`android-tv` `tv`, `android-wear` `watch`, `android-automotive*` `auto`, `android-desktop` `desktop`.

Fit — `fits(profile, image)`: API range, image kind (tag), Play Store, tablet. The catalog's
Expand Down
71 changes: 71 additions & 0 deletions src/drivers/android/device-profile-source.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";

import type { DeviceClass } from "../../core/index.js";
import { MemoryFilesystem, ScriptedProcessRunner } from "../../ports/index.js";
import {
BuiltinDeviceProfileSource,
Expand All @@ -9,6 +10,7 @@ import {
UserDeviceProfileSource,
type DeviceProfileSourceDiagnostic,
} from "./device-profile-source.js";
import { LibraryDeviceProfileSource } from "./profiles/index.js";

const avdmanager = "/android-sdk/cmdline-tools/latest/bin/avdmanager";
const pixelDevices = `Available devices:\nid: 0 or "pixel_8"\n Name: Pixel 8\n OEM : Google\n`;
Expand Down Expand Up @@ -869,6 +871,75 @@ describe("device classes", () => {
expect(classes).toEqual({ "My Custom Phone": "phone", "Pixel 8": "phone" });
});

it("classes a listed model by the profile its name resolves to, not the one it was listed from", async () => {
// why: the three devices.xml profiles are named after profile ids the library or the SDK
// ships -- `tv_1080p` and `wearos_large_round` resolve to the library's TV and Wear OS
// profiles, `tv_720p` to an SDK-only one -- so a lease of each name creates that TV or
// Wear OS device; the class the catalog lists must follow the profile the name resolves
// to, not the untagged devices.xml entry the name was listed from.
const collidingDevicesXml = `<d:devices xmlns:d="http://schemas.android.com/sdk/devices/7">
<d:device><d:name>tv_1080p</d:name><d:hardware><d:ram unit="MiB">2048</d:ram></d:hardware></d:device>
<d:device><d:name>tv_720p</d:name></d:device>
<d:device><d:name>wearos_large_round</d:name></d:device>
<d:device><d:name>My Custom Phone</d:name></d:device>
</d:devices>`;
const sdkTelevision =
'id: 58 or "tv_1080p"\n Name: Television (1080p)\n OEM : Google\n Tag : android-tv\n';
const sdk720 =
'id: 60 or "tv_720p"\n Name: Television (720p)\n OEM : Google\n Tag : android-tv\n';
const sdkWear =
'id: 61 or "wearos_large_round"\n Name: Wear OS Large Round\n OEM : Google\n Tag : android-wear\n';
const sdkOutput =
`Available devices:\n${sdkTelevision}---------\n${sdk720}---------\n${sdkWear}---------\n` +
`id: 43 or "pixel_8"\n Name: Pixel 8\n OEM : Google\n---------\n`;
// why: `avdmanager list device` is read once for the catalog and once more for each resolve
// of a name the library lacks (`Television (720p)`, `tv_720p`, `My Custom Phone`, and the
// `tv_720p` resolve below).
const runner = new ScriptedProcessRunner(
Array.from({ length: 5 }, () => processResult(sdkOutput)),
);
const registry = new DeviceProfileRegistry([
new LibraryDeviceProfileSource(),
new BuiltinDeviceProfileSource(avdmanager, runner),
new UserDeviceProfileSource(
devicesXmlPath,
await filesystemWithDevicesXml(collidingDevicesXml),
),
]);

const catalog = await registry.catalog();
const expected: Record<string, DeviceClass | undefined> = {};
for (const model of catalog.models) {
const resolved = await registry.resolve(model);
expected[model] =
resolved.kind === "properties"
? resolved.imageKind === "android-tv"
? "tv"
: resolved.imageKind === "android-wear"
? "watch"
: resolved.imageKind === undefined
? "phone"
: undefined
: resolved.deviceClass;
}
expect(catalog.modelClasses).toEqual(expected);
await expect(registry.resolve("tv_1080p")).resolves.toMatchObject({
deviceClass: "tv",
kind: "library",
name: "Television (1080p)",
});
await expect(registry.resolve("tv_720p")).resolves.toMatchObject({
avdmanagerId: "tv_720p",
deviceClass: "tv",
kind: "builtin",
});
await expect(registry.resolve("wearos_large_round")).resolves.toMatchObject({
deviceClass: "watch",
kind: "library",
name: "Wear OS Large Round",
});
});

it("lists no class for a profile whose tag is outside the table, and keeps the model", async () => {
const output = entry("odd", "Odd Device", "android-toaster") + entry("pixel_8", "Pixel 8");
const registry = new DeviceProfileRegistry([
Expand Down
13 changes: 10 additions & 3 deletions src/drivers/android/device-profile-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,18 @@ export class DeviceProfileRegistry {
const profile = matchProfile(profiles, model);
return profile !== undefined && isCustom(profile);
});
// why: a lease of a name creates the profile that name resolves to, so its class in
// the catalog must come from that profile, not the one it happened to be listed from.
const modelClasses: Record<string, DeviceClass> = {};
for (const profile of listed.values()) {
for (const model of models) {
const profile = matchProfile(profiles, model);
const deviceClass =
profile.kind === "properties" ? classOfTag(profile.imageKind) : profile.deviceClass;
if (deviceClass !== undefined) modelClasses[profile.name] = deviceClass;
profile === undefined
? undefined
: profile.kind === "properties"
? classOfTag(profile.imageKind)
: profile.deviceClass;
if (deviceClass !== undefined) modelClasses[model] = deviceClass;
}
const modelProfiles = new Map<string, DeviceProfile>();
for (const model of models) {
Expand Down
Loading