From 0185333bab91465fb8d808378b5fe944dd523d91 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 25 Aug 2025 01:52:05 +0800 Subject: [PATCH] feat(bin): handle pm.install options based on API level --- libraries/android-bin/src/pm.ts | 301 +++++++++++++++----------------- 1 file changed, 143 insertions(+), 158 deletions(-) diff --git a/libraries/android-bin/src/pm.ts b/libraries/android-bin/src/pm.ts index 0780138a..3feea15b 100644 --- a/libraries/android-bin/src/pm.ts +++ b/libraries/android-bin/src/pm.ts @@ -2,6 +2,7 @@ // cspell:ignore instantapp // cspell:ignore apks // cspell:ignore versioncode +// cspell:ignore dexopt import type { Adb, AdbNoneProtocolProcess } from "@yume-chan/adb"; import { AdbServiceBase, escapeArg } from "@yume-chan/adb"; @@ -31,154 +32,83 @@ export enum PackageManagerInstallReason { UserRequest, } -// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd -export interface PackageManagerInstallOptions { - /** - * `-R` - */ - skipExisting: boolean; - /** - * `-i` - */ - installerPackageName: string; - /** - * `-t` - */ - allowTest: boolean; - /** - * `-f` - */ - internalStorage: boolean; - /** - * `-d` - */ - requestDowngrade: boolean; - /** - * `-g` - */ - grantRuntimePermissions: boolean; - /** - * `--restrict-permissions` - */ - restrictPermissions: boolean; - /** - * `--dont-kill` - */ - doNotKill: boolean; - /** - * `--originating-uri` - */ - originatingUri: string; - /** - * `--referrer` - */ - refererUri: string; - /** - * `-p` - */ - inheritFrom: string; - /** - * `--pkg` - */ - packageName: string; - /** - * `--abi` - */ - abi: string; - /** - * `--ephemeral`/`--instant`/`--instantapp` - */ - instantApp: boolean; - /** - * `--full` - */ - full: boolean; - /** - * `--preload` - */ - preload: boolean; - /** - * `--user` - */ - user: SingleUserOrAll; - /** - * `--install-location` - */ - installLocation: PackageManagerInstallLocation; - /** - * `--install-reason` - */ - installReason: PackageManagerInstallReason; - /** - * `--force-uuid` - */ - forceUuid: string; - /** - * `--apex` - */ - apex: boolean; - /** - * `--force-non-staged` - */ - forceNonStaged: boolean; - /** - * `--staged` - */ - staged: boolean; - /** - * `--force-queryable` - */ - forceQueryable: boolean; - /** - * `--enable-rollback` - */ - enableRollback: boolean; - /** - * `--staged-ready-timeout` - */ - stagedReadyTimeout: number; - /** - * `--skip-verification` - */ - skipVerification: boolean; - /** - * `--bypass-low-target-sdk-block` - */ - bypassLowTargetSdkBlock: boolean; +interface OptionDefinition { + type: T; + name: string; + minApiLevel?: number; + maxApiLevel?: number; } -export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record< - keyof PackageManagerInstallOptions, - string -> = { - skipExisting: "-R", - installerPackageName: "-i", - allowTest: "-t", - internalStorage: "-f", - requestDowngrade: "-d", - grantRuntimePermissions: "-g", - restrictPermissions: "--restrict-permissions", - doNotKill: "--dont-kill", - originatingUri: "--originating-uri", - refererUri: "--referrer", - inheritFrom: "-p", - packageName: "--pkg", - abi: "--abi", - instantApp: "--instant", - full: "--full", - preload: "--preload", - user: "--user", - installLocation: "--install-location", - installReason: "--install-reason", - forceUuid: "--force-uuid", - apex: "--apex", - forceNonStaged: "--force-non-staged", - staged: "--staged", - forceQueryable: "--force-queryable", - enableRollback: "--enable-rollback", - stagedReadyTimeout: "--staged-ready-timeout", - skipVerification: "--skip-verification", - bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block", +function option( + name: string, + minApiLevel?: number, + maxApiLevel?: number, +): OptionDefinition { + return { + name, + minApiLevel, + maxApiLevel, + } as OptionDefinition; +} + +// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java;l=3046;drc=6d14d35d0241f6fee145f8e54ffd77252e8d29fd +export const PackageManagerInstallOptions = { + forwardLock: option("-l", undefined, 28), + replaceExisting: option("-r", undefined, 27), + skipExisting: option("-R", 28), + installerPackageName: option("-i"), + allowTest: option("-t"), + externalStorage: option("-s", undefined, 28), + internalStorage: option("-f"), + requestDowngrade: option("-d"), + grantRuntimePermissions: option("-g", 23), + restrictPermissions: option("--restrict-permissions", 29), + doNotKill: option("--dont-kill"), + originatingUri: option("--originating-uri"), + refererUri: option("--referrer"), + inheritFrom: option("-p", 24), + packageName: option("--pkg", 28), + abi: option("--abi", 21), + instantApp: option("--ephemeral", 24), + full: option("--full", 26), + preload: option("--preload", 28), + user: option("--user", 21), + installLocation: option( + "--install-location", + 24, + ), + installReason: option("--install-reason", 29), + updateOwnership: option("--update-ownership", 34), + forceUuid: option("--force-uuid", 24), + forceSdk: option("--force-sdk", 24), + apex: option("--apex", 29), + forceNonStaged: option("--force-non-staged", 31), + multiPackage: option("--multi-package", 29), + staged: option("--staged", 29), + nonStaged: option("--non-staged", 35), + forceQueryable: option("--force-queryable", 30), + enableRollback: option("--enable-rollback", 29), + rollbackImpactLevel: option("--rollback-impact-level", 35), + wait: option("--wait", 30, 30), + noWait: option("--no-wait", 30, 30), + stagedReadyTimeout: option("--staged-ready-timeout", 31), + skipVerification: option("--skip-verification", 30), + skipEnable: option("--skip-enable", 34), + bypassLowTargetSdkBlock: option( + "--bypass-low-target-sdk-block", + 34, + ), + ignoreDexoptProfile: option("--ignore-dexopt-profile", 35), + packageSource: option("--package-source", 35), + dexoptCompilerFilter: option("--dexopt-compiler-filter", 35), + disableAutoInstallDependencies: option( + "--disable-auto-install-dependencies", + 36, + ), +} as const; + +export type PackageManagerInstallOptions = { + [K in keyof typeof PackageManagerInstallOptions]: (typeof PackageManagerInstallOptions)[K]["type"]; }; export interface PackageManagerListPackagesOptions { @@ -258,12 +188,10 @@ const PACKAGE_MANAGER_RESOLVE_ACTIVITY_OPTIONS_MAP: Partial< function buildInstallArguments( command: string, options: Optional | undefined, + apiLevel: number | undefined, ): string[] { - const args = buildArguments( - [PackageManager.ServiceName, command], - options, - PACKAGE_MANAGER_INSTALL_OPTIONS_MAP, - ); + const args = [PackageManager.ServiceName, command]; + if (!options?.skipExisting) { /* * | behavior | previous version | modern version | @@ -278,6 +206,59 @@ function buildInstallArguments( */ args.push("-r"); } + + if (!options) { + return args; + } + + for (const [key, value] of Object.entries(options)) { + if (value === undefined || value === null) { + continue; + } + + const option = + PackageManagerInstallOptions[ + key as keyof PackageManagerInstallOptions + ]; + + if (option === undefined) { + continue; + } + + if (apiLevel !== undefined) { + if ( + option.minApiLevel !== undefined && + apiLevel < option.minApiLevel + ) { + continue; + } + if ( + option.maxApiLevel !== undefined && + apiLevel > option.maxApiLevel + ) { + continue; + } + } + + switch (typeof value) { + case "boolean": + if (value) { + args.push(option.name); + } + break; + case "number": + args.push(option.name, value.toString()); + break; + case "string": + args.push(option.name, escapeArg(value)); + break; + default: + throw new Error( + `Unsupported type for option ${key}: ${typeof value}`, + ); + } + } + return args; } @@ -285,10 +266,10 @@ export class PackageManager extends AdbServiceBase { static readonly ServiceName = "package"; static readonly CommandName = "pm"; - #apiLevel: number; + #apiLevel: number | undefined; #cmd: Cmd.NoneProtocolService; - constructor(adb: Adb, apiLevel = 0) { + constructor(adb: Adb, apiLevel?: number) { super(adb); this.#apiLevel = apiLevel; @@ -304,7 +285,7 @@ export class PackageManager extends AdbServiceBase { apks: readonly string[], options?: Optional, ): Promise { - const args = buildInstallArguments("install", options); + const args = buildInstallArguments("install", options, this.#apiLevel); args[0] = PackageManager.CommandName; // WIP: old version of pm doesn't support multiple apks args.push(...apks.map(escapeArg)); @@ -369,7 +350,7 @@ export class PackageManager extends AdbServiceBase { return; } - const args = buildInstallArguments("install", options); + const args = buildInstallArguments("install", options, this.#apiLevel); args.push("-S", size.toString()); const process = await this.#cmd.spawn(args); @@ -498,7 +479,7 @@ export class PackageManager extends AdbServiceBase { // `cmd package` doesn't support `path` command on Android 7 and 8. let process: AdbNoneProtocolProcess; - if (this.#apiLevel <= 27) { + if (this.#apiLevel !== undefined && this.#apiLevel <= 27) { args[0] = PackageManager.CommandName; process = await this.adb.subprocess.noneProtocol.spawn(args); } else { @@ -584,7 +565,11 @@ export class PackageManager extends AdbServiceBase { async sessionCreate( options?: Optional, ): Promise { - const args = buildInstallArguments("install-create", options); + const args = buildInstallArguments( + "install-create", + options, + this.#apiLevel, + ); const output = await this.#cmd .spawn(args) @@ -670,7 +655,7 @@ export class PackageManager extends AdbServiceBase { // but the "Success" message is not forwarded back to the client, // causing this function to fail with an empty message. let process: AdbNoneProtocolProcess; - if (this.#apiLevel <= 25) { + if (this.#apiLevel !== undefined && this.#apiLevel <= 25) { args[0] = PackageManager.CommandName; process = await this.adb.subprocess.noneProtocol.spawn(args); } else {