diff --git a/libraries/android-bin/src/pm.ts b/libraries/android-bin/src/pm.ts index b97431cf..4c8f26ae 100644 --- a/libraries/android-bin/src/pm.ts +++ b/libraries/android-bin/src/pm.ts @@ -1,6 +1,7 @@ // cspell:ignore dont // cspell:ignore instantapp // cspell:ignore apks +// cspell:ignore versioncode import type { Adb } from "@yume-chan/adb"; import { @@ -9,7 +10,11 @@ import { escapeArg, } from "@yume-chan/adb"; import type { Consumable, ReadableStream } from "@yume-chan/stream-extra"; -import { ConcatStringStream, DecodeUtf8Stream } from "@yume-chan/stream-extra"; +import { + ConcatStringStream, + DecodeUtf8Stream, + SplitStringStream, +} from "@yume-chan/stream-extra"; import { Cmd } from "./cmd.js"; @@ -218,6 +223,23 @@ export interface PackageManagerListPackagesResult { uid?: number | undefined; } +export interface PackageManagerUninstallOptions { + keepData: boolean; + user: "all" | "current" | number; + versionCode: number; + splitNames: string[]; +} + +const PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP: Record< + keyof PackageManagerUninstallOptions, + string +> = { + keepData: "-k", + user: "--user", + versionCode: "--versionCode", + splitNames: "", +}; + export class PackageManager extends AdbCommandBase { #cmd: Cmd; @@ -297,10 +319,9 @@ export class PackageManager extends AdbCommandBase { // so installing a file must use `pm`. const args = this.#buildInstallArguments(options); args.push(filePath); - const process = await AdbSubprocessNoneProtocol.raw( - this.adb, - args.map(escapeArg).join(" "), - ); + const process = await this.adb.subprocess.spawn(args.map(escapeArg), { + protocols: [AdbSubprocessNoneProtocol], + }); const output = await process.stdout .pipeThrough(new DecodeUtf8Stream()) @@ -348,6 +369,8 @@ export class PackageManager extends AdbCommandBase { ]); } + // TODO: install: support split apk formats (`adb install-multiple`) + static parsePackageListItem( line: string, ): PackageManagerListPackagesResult { @@ -399,9 +422,18 @@ export class PackageManager extends AdbCommandBase { }; } - async listPackages( + async #cmdOrSubprocess(args: string[]) { + if (this.#cmd.supportsCmd) { + args.shift(); + return await this.#cmd.spawn(false, "package", ...args); + } + + return this.adb.subprocess.spawn(args); + } + + async *listPackages( options?: Partial, - ): Promise { + ): AsyncGenerator { const args = this.#buildArguments( ["list", "packages"], options, @@ -410,12 +442,42 @@ export class PackageManager extends AdbCommandBase { if (options?.filter) { args.push(options.filter); } - const output = await this.adb.subprocess.spawnAndWaitLegacy(args); - return output - .split("\n") - .filter((line) => !!line) - .map((line) => PackageManager.parsePackageListItem(line)); + + const process = await this.#cmdOrSubprocess(args); + const reader = process.stdout + .pipeThrough(new DecodeUtf8Stream()) + .pipeThrough(new SplitStringStream("\n")) + .getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + yield PackageManager.parsePackageListItem(value); + } } - // TODO: install: support split apk formats (`adb install-multiple`) + async uninstall( + packageName: string, + options?: Partial, + ): Promise { + const args = this.#buildArguments( + ["uninstall"], + options, + PACKAGE_MANAGER_UNINSTALL_OPTIONS_MAP, + ); + args.push(packageName); + if (options?.splitNames) { + args.push(...options.splitNames); + } + + const process = await this.#cmdOrSubprocess(args); + const output = await process.stdout + .pipeThrough(new DecodeUtf8Stream()) + .pipeThrough(new ConcatStringStream()) + .then((output) => output.trim()); + if (output !== "Success") { + throw new Error(output); + } + } }