feat(bin): handle pm.install options based on API level

This commit is contained in:
Simon Chan 2025-08-25 01:52:05 +08:00
parent 9fdab3d677
commit 0185333bab
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD

View file

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