diff --git a/libraries/android-bin/src/am.ts b/libraries/android-bin/src/am.ts new file mode 100644 index 00000000..450b21f9 --- /dev/null +++ b/libraries/android-bin/src/am.ts @@ -0,0 +1,69 @@ +import type { Adb } from "@yume-chan/adb"; +import { AdbCommandBase } from "@yume-chan/adb"; +import { ConcatStringStream, DecodeUtf8Stream } from "@yume-chan/stream-extra"; + +import { Cmd } from "./cmd.js"; +import type { IntentBuilder } from "./intent.js"; +import type { SingleUser } from "./utils.js"; +import { buildArguments } from "./utils.js"; + +export interface ActivityManagerStartActivityOptions { + displayId?: number; + windowingMode?: number; + forceStop?: boolean; + user?: SingleUser; + intent: IntentBuilder; +} + +const START_ACTIVITY_OPTIONS_MAP: Partial< + Record +> = { + displayId: "--display", + windowingMode: "--windowingMode", + forceStop: "-S", + user: "--user", +}; + +export class ActivityManager extends AdbCommandBase { + #cmd: Cmd; + + constructor(adb: Adb) { + super(adb); + this.#cmd = new Cmd(adb); + } + + async #cmdOrSubprocess(args: string[]) { + if (this.#cmd.supportsCmd) { + args.shift(); + return await this.#cmd.spawn(false, "activity", ...args); + } + + return this.adb.subprocess.spawn(args); + } + + async startActivity(options: ActivityManagerStartActivityOptions) { + let args = buildArguments( + ["am", "start-activity", "-W"], + options, + START_ACTIVITY_OPTIONS_MAP, + ); + + args = args.concat(options.intent.build()); + + const process = await this.#cmdOrSubprocess(args); + + const output = await process.stdout + .pipeThrough(new DecodeUtf8Stream()) + .pipeThrough(new ConcatStringStream()) + .then((output) => output.trim()); + + for (const line of output) { + if (line.startsWith("Error:")) { + throw new Error(line.substring("Error:".length).trim()); + } + if (line === "Complete") { + return; + } + } + } +} diff --git a/libraries/android-bin/src/index.ts b/libraries/android-bin/src/index.ts index db729acc..942cef46 100644 --- a/libraries/android-bin/src/index.ts +++ b/libraries/android-bin/src/index.ts @@ -1,10 +1,12 @@ // cspell: ignore logcat +export * from "./am.js"; export * from "./bu.js"; export * from "./bug-report.js"; export * from "./cmd.js"; export * from "./demo-mode.js"; export * from "./dumpsys.js"; +export * from "./intent.js"; export * from "./logcat.js"; export * from "./overlay-display.js"; export * from "./pm.js"; diff --git a/libraries/android-bin/src/intent.ts b/libraries/android-bin/src/intent.ts new file mode 100644 index 00000000..d3d3c0f3 --- /dev/null +++ b/libraries/android-bin/src/intent.ts @@ -0,0 +1,63 @@ +export class IntentBuilder { + #action: string | undefined; + #categories: string[] = []; + #packageName: string | undefined; + #component: string | undefined; + #data: string | undefined; + #type: string | undefined; + + setAction(action: string): this { + this.#action = action; + return this; + } + + addCategory(category: string): this { + this.#categories.push(category); + return this; + } + + setPackage(packageName: string): this { + this.#packageName = packageName; + return this; + } + + setComponent(component: string): this { + this.#component = component; + return this; + } + + setData(data: string): this { + this.#data = data; + return this; + } + + build(): string[] { + const result: string[] = []; + + if (this.#action) { + result.push("-a", this.#action); + } + + for (const category of this.#categories) { + result.push("-c", category); + } + + if (this.#packageName) { + result.push("-p", this.#packageName); + } + + if (this.#component) { + result.push("-n", this.#component); + } + + if (this.#data) { + result.push("-d", this.#data); + } + + if (this.#type) { + result.push("-t", this.#type); + } + + return result; + } +} diff --git a/libraries/android-bin/src/pm.ts b/libraries/android-bin/src/pm.ts index 4c8f26ae..47eb9361 100644 --- a/libraries/android-bin/src/pm.ts +++ b/libraries/android-bin/src/pm.ts @@ -17,6 +17,9 @@ import { } from "@yume-chan/stream-extra"; import { Cmd } from "./cmd.js"; +import type { IntentBuilder } from "./intent.js"; +import type { SingleUserOrAll } from "./utils.js"; +import { buildArguments } from "./utils.js"; export enum PackageManagerInstallLocation { Auto, @@ -101,7 +104,7 @@ export interface PackageManagerInstallOptions { /** * `--user` */ - userId: number; + user: SingleUserOrAll; /** * `--install-location` */ @@ -168,7 +171,7 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record< instantApp: "--instant", full: "--full", preload: "--preload", - userId: "--user", + user: "--user", installLocation: "--install-location", installReason: "--install-reason", forceUuid: "--force-uuid", @@ -192,7 +195,7 @@ export interface PackageManagerListPackagesOptions { listThirdParty: boolean; showVersionCode: boolean; listApexOnly: boolean; - user: "all" | "current" | number; + user: SingleUserOrAll; uid: number; filter: string; } @@ -225,7 +228,7 @@ export interface PackageManagerListPackagesResult { export interface PackageManagerUninstallOptions { keepData: boolean; - user: "all" | "current" | number; + user: SingleUserOrAll; versionCode: number; splitNames: string[]; } @@ -240,6 +243,17 @@ const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record< splitNames: "", }; +export interface PackageManagerResolveActivityOptions { + user?: SingleUserOrAll; + intent: IntentBuilder; +} + +const PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP: Partial< + Record +> = { + user: "--user", +}; + export class PackageManager extends AdbCommandBase { #cmd: Cmd; @@ -248,38 +262,11 @@ export class PackageManager extends AdbCommandBase { this.#cmd = new Cmd(adb); } - #buildArguments( - commands: string[], - options: Partial | undefined, - map: Record, - ): string[] { - const args = ["pm", ...commands]; - if (options) { - for (const [key, value] of Object.entries(options)) { - if (value) { - const option = map[key as keyof T]; - if (option) { - args.push(option); - switch (typeof value) { - case "number": - args.push(value.toString()); - break; - case "string": - args.push(value); - break; - } - } - } - } - } - return args; - } - #buildInstallArguments( options: Partial | undefined, ): string[] { - return this.#buildArguments( - ["install"], + return buildArguments( + ["pm", "install"], options, PACKAGE_MANAGER_INSTALL_OPTIONS_MAP, ); @@ -313,7 +300,7 @@ export class PackageManager extends AdbCommandBase { await sync.dispose(); } - // Starting from Android 7, `pm` is a only wrapper for `cmd package`, + // Starting from Android 7, `pm` is only a wrapper for `cmd package`, // and `cmd package` launches faster than `pm`. // But `cmd package` can't read `/data/local/tmp` folder due to SELinux policy, // so installing a file must use `pm`. @@ -434,8 +421,8 @@ export class PackageManager extends AdbCommandBase { async *listPackages( options?: Partial, ): AsyncGenerator { - const args = this.#buildArguments( - ["list", "packages"], + const args = buildArguments( + ["pm", "list", "packages"], options, PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP, ); @@ -461,8 +448,8 @@ export class PackageManager extends AdbCommandBase { packageName: string, options?: Partial, ): Promise { - const args = this.#buildArguments( - ["uninstall"], + const args = buildArguments( + ["pm", "uninstall"], options, PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP, ); @@ -480,4 +467,28 @@ export class PackageManager extends AdbCommandBase { throw new Error(output); } } + + async resolveActivity( + options: PackageManagerResolveActivityOptions, + ): Promise { + let args = buildArguments( + ["pm", "resolve-activity", "--components"], + options, + PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP, + ); + + args = args.concat(options.intent.build()); + + const process = await this.#cmdOrSubprocess(args); + const output = await process.stdout + .pipeThrough(new DecodeUtf8Stream()) + .pipeThrough(new ConcatStringStream()) + .then((output) => output.trim()); + + if (output === "No activity found") { + return undefined; + } + + return output; + } } diff --git a/libraries/android-bin/src/settings.ts b/libraries/android-bin/src/settings.ts index 601ef84a..3a6775b3 100644 --- a/libraries/android-bin/src/settings.ts +++ b/libraries/android-bin/src/settings.ts @@ -2,6 +2,7 @@ import type { Adb, AdbSubprocessWaitResult } from "@yume-chan/adb"; import { AdbCommandBase } from "@yume-chan/adb"; import { Cmd } from "./cmd.js"; +import type { SingleUser } from "./utils.js"; export type SettingsNamespace = "system" | "secure" | "global"; @@ -12,7 +13,7 @@ export enum SettingsResetMode { } export interface SettingsOptions { - user?: number | "current"; + user?: SingleUser; } export interface SettingsPutOptions extends SettingsOptions { diff --git a/libraries/android-bin/src/utils.ts b/libraries/android-bin/src/utils.ts new file mode 100644 index 00000000..8851a0b7 --- /dev/null +++ b/libraries/android-bin/src/utils.ts @@ -0,0 +1,29 @@ +export function buildArguments( + commands: string[], + options: Partial | undefined, + map: Partial>, +): string[] { + const args = commands; + if (options) { + for (const [key, value] of Object.entries(options)) { + if (value) { + const option = map[key as keyof T]; + if (option) { + args.push(option); + switch (typeof value) { + case "number": + args.push(value.toString()); + break; + case "string": + args.push(value); + break; + } + } + } + } + } + return args; +} + +export type SingleUser = number | "current"; +export type SingleUserOrAll = SingleUser | "all";