Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit bf76ce00 authored by Simon Chan's avatar Simon Chan
Browse files

feat(bin): support `pm list packages`

parent d9685acc
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -147,7 +147,7 @@ export class AdbDaemonTransport implements AdbTransport {
                AdbFeature.AbbExec,
                "remount_shell",
                "track_app",
                "sendrecv_v2",
                AdbFeature.SendReceiveV2,
                "sendrecv_v2_brotli",
                "sendrecv_v2_lz4",
                "sendrecv_v2_zstd",
+3 −0
Original line number Diff line number Diff line
import { AdbCommandBase } from "@yume-chan/adb";

export class DumpSys extends AdbCommandBase {}
+137 −15
Original line number Diff line number Diff line
@@ -177,6 +177,47 @@ export const PACKAGE_MANAGER_INSTALL_OPTIONS_MAP: Record<
    bypassLowTargetSdkBlock: "--bypass-low-target-sdk-block",
};

export interface PackageManagerListPackagesOptions {
    listDisabled: boolean;
    listEnabled: boolean;
    showSourceDir: boolean;
    showInstaller: boolean;
    listSystem: boolean;
    showUid: boolean;
    listThirdParty: boolean;
    showVersionCode: boolean;
    listApexOnly: boolean;
    user: "all" | "current" | number;
    uid: number;
    filter: string;
}

export const PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP: Record<
    keyof PackageManagerListPackagesOptions,
    string
> = {
    listDisabled: "-d",
    listEnabled: "-e",
    showSourceDir: "-f",
    showInstaller: "-i",
    listSystem: "-s",
    showUid: "-U",
    listThirdParty: "-3",
    showVersionCode: "--show-versioncode",
    listApexOnly: "--apex-only",
    user: "--user",
    uid: "--uid",
    filter: "",
};

export interface PackageManagerListPackagesResult {
    packageName: string;
    sourceDir?: string | undefined;
    versionCode?: number | undefined;
    installer?: string | undefined;
    uid?: number | undefined;
}

export class PackageManager extends AdbCommandBase {
    private _cmd: Cmd;

@@ -185,17 +226,16 @@ export class PackageManager extends AdbCommandBase {
        this._cmd = new Cmd(adb);
    }

    private buildInstallArgs(
        options?: Partial<PackageManagerInstallOptions>
    private buildArguments<T>(
        commands: string[],
        options: Partial<T> | undefined,
        map: Record<keyof T, string>
    ): string[] {
        const args = ["pm", "install"];
        const args = ["pm", ...commands];
        if (options) {
            for (const [key, value] of Object.entries(options)) {
                if (value) {
                    const option =
                        PACKAGE_MANAGER_INSTALL_OPTIONS_MAP[
                            key as keyof PackageManagerInstallOptions
                        ];
                    const option = map[key as keyof T];
                    if (option) {
                        args.push(option);
                        switch (typeof value) {
@@ -213,11 +253,21 @@ export class PackageManager extends AdbCommandBase {
        return args;
    }

    private buildInstallArguments(
        options: Partial<PackageManagerInstallOptions> | undefined
    ): string[] {
        return this.buildArguments(
            ["install"],
            options,
            PACKAGE_MANAGER_INSTALL_OPTIONS_MAP
        );
    }

    public async install(
        apks: string[],
        options?: Partial<PackageManagerInstallOptions>
    ): Promise<string> {
        const args = this.buildInstallArgs(options);
        const args = this.buildInstallArguments(options);
        // WIP: old version of pm doesn't support multiple apks
        args.push(...apks);
        return await this.adb.subprocess.spawnAndWaitLegacy(args);
@@ -241,7 +291,11 @@ export class PackageManager extends AdbCommandBase {
            await sync.dispose();
        }

        const args = this.buildInstallArgs(options);
        // Starting from Android 7, `pm` is a only 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`.
        const args = this.buildInstallArguments(options);
        args.push(filePath);
        const process = await AdbSubprocessNoneProtocol.raw(
            this.adb,
@@ -260,16 +314,15 @@ export class PackageManager extends AdbCommandBase {
        stream: ReadableStream<Consumable<Uint8Array>>,
        options?: Partial<PackageManagerInstallOptions>
    ): Promise<ReadableStream<string>> {
        // Android 7 added both `cmd` command and streaming install support,
        // we can't detect whether `pm` supports streaming install,
        // so we detect `cmd` command support instead.
        if (!this._cmd.supportsCmd) {
            return this.pushAndInstallStream(stream, options);
        }

        // Android 7 added `cmd package` and piping apk to stdin,
        // the source code suggests using `cmd package` over `pm`, but didn't say why.
        // However, `cmd package install` can't read `/data/local/tmp` folder due to SELinux policy,
        // so even ADB today is still using `pm install` for non-streaming installs.
        const args = this.buildInstallArgs(options);
        // Remove `pm` from args, final command will be `cmd package install`
        const args = this.buildInstallArguments(options);
        // Remove `pm` from args, final command will starts with `cmd package install`
        args.shift();
        args.push("-S", size.toString());
        const process = await this._cmd.spawn(false, "package", ...args);
@@ -277,5 +330,74 @@ export class PackageManager extends AdbCommandBase {
        return process.stdout.pipeThrough(new DecodeUtf8Stream());
    }

    public static parsePackageListItem(
        line: string
    ): PackageManagerListPackagesResult {
        line = line.substring("package:".length);

        let packageName: string;
        let sourceDir: string | undefined;
        let versionCode: number | undefined;
        let installer: string | undefined;
        let uid: number | undefined;

        // Parse backwards
        let index = line.indexOf(" uid:");
        if (index !== -1) {
            uid = Number.parseInt(line.substring(index + " uid:".length), 10);
            line = line.substring(0, index);
        }

        index = line.indexOf(" installer=");
        if (index !== -1) {
            installer = line.substring(index + " installer=".length);
            line = line.substring(0, index);
        }

        index = line.indexOf(" versionCode:");
        if (index !== -1) {
            versionCode = Number.parseInt(
                line.substring(index + " versionCode:".length),
                10
            );
            line = line.substring(0, index);
        }

        // `sourceDir` may contain `=` so use `lastIndexOf`
        index = line.lastIndexOf("=");
        if (index !== -1) {
            sourceDir = line.substring(0, index);
            packageName = line.substring(index + "=".length);
        } else {
            packageName = line;
        }

        return {
            packageName,
            sourceDir,
            versionCode,
            installer,
            uid,
        };
    }

    public async listPackages(
        options?: Partial<PackageManagerListPackagesOptions>
    ): Promise<PackageManagerListPackagesResult[]> {
        const args = this.buildArguments(
            ["list", "packages"],
            options,
            PACKAGE_MANAGER_LIST_PACKAGES_OPTIONS_MAP
        );
        if (options?.filter) {
            args.push(options.filter);
        }
        const output = await this.adb.subprocess.spawnAndWaitLegacy(args);
        return output
            .split("\n")
            .filter((line) => !!line)
            .map(PackageManager.parsePackageListItem);
    }

    // TODO: install: support split apk formats (`adb install-multiple`)
}