126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import path from "node:path";
|
|
|
|
const root = path.resolve(import.meta.dir, "..");
|
|
const adb = process.env.ADB ?? "adb";
|
|
const args = process.argv.slice(2);
|
|
const command = args[0] ?? "help";
|
|
|
|
function option(name: string): string | undefined {
|
|
const index = args.indexOf(name);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
}
|
|
|
|
function options(name: string): string[] {
|
|
const values: string[] = [];
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const value = args[index + 1];
|
|
if (args[index] === name && value) values.push(value);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function requireSerial(): string {
|
|
const serial = option("--serial");
|
|
if (!serial)
|
|
throw new Error("Specify exactly one target with --serial <adb-serial>");
|
|
return serial;
|
|
}
|
|
|
|
function run(serial: string, commandArgs: string[]): void {
|
|
const result = spawnSync(adb, ["-s", serial, ...commandArgs], {
|
|
stdio: "inherit",
|
|
});
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0)
|
|
throw new Error(`adb ${commandArgs.join(" ")} failed`);
|
|
}
|
|
|
|
function confirm(flag: string): void {
|
|
if (!args.includes(flag))
|
|
throw new Error(`Refusing this device-changing operation without ${flag}`);
|
|
}
|
|
|
|
function replacePackages(
|
|
serial: string,
|
|
launcher: string,
|
|
note: string,
|
|
uninstall: string[],
|
|
): void {
|
|
if (uninstall.length !== 2) {
|
|
throw new Error("Provide exactly two explicit --uninstall package names");
|
|
}
|
|
for (const packageName of uninstall) run(serial, ["uninstall", packageName]);
|
|
run(serial, ["install", "-r", launcher]);
|
|
run(serial, ["install", "-r", note]);
|
|
}
|
|
|
|
function packageFromApk(file: string): string {
|
|
const result = spawnSync("apkanalyzer", ["manifest", "print", file], {
|
|
encoding: "utf8",
|
|
});
|
|
if (result.error) throw result.error;
|
|
const match = result.stdout.match(/\bpackage="([^"]+)"/);
|
|
if (result.status !== 0 || !match)
|
|
throw new Error(`Could not infer the package name from ${file}`);
|
|
return match[1]!;
|
|
}
|
|
|
|
if (command === "help") {
|
|
console.log(`ADBMe guarded deployment commands
|
|
|
|
bun run adbme status --serial SERIAL
|
|
bun run adbme grant-settings --serial SERIAL --app rip.crit.ms.adbme --confirm-permissions
|
|
bun run adbme set-language --serial SERIAL --confirm-system-changes
|
|
bun run adbme patch-and-replace --serial SERIAL --confirm-replace
|
|
bun run adbme replace --serial SERIAL --uninstall OLD_LAUNCHER --uninstall OLD_NOTE --launcher APK --note APK --confirm-replace
|
|
|
|
CHANGE_CONFIGURATION cannot be granted to an ordinary APK on stock Android. It requires a privileged or OEM-signed app.`);
|
|
} else if (command === "status") {
|
|
run(requireSerial(), ["get-state"]);
|
|
} else if (command === "grant-settings") {
|
|
const serial = requireSerial();
|
|
const app = option("--app");
|
|
if (!app)
|
|
throw new Error("Specify the installed companion app with --app <package>");
|
|
confirm("--confirm-permissions");
|
|
run(serial, ["shell", "appops", "set", app, "WRITE_SETTINGS", "allow"]);
|
|
console.log(
|
|
"[ok] WRITE_SETTINGS app-op enabled. CHANGE_CONFIGURATION was not attempted because stock Android rejects it for ordinary apps.",
|
|
);
|
|
} else if (command === "set-language") {
|
|
const serial = requireSerial();
|
|
confirm("--confirm-system-changes");
|
|
run(serial, ["shell", "cmd", "locale", "set", "en-US"]);
|
|
} else if (command === "replace") {
|
|
const serial = requireSerial();
|
|
const launcher = option("--launcher");
|
|
const note = option("--note");
|
|
const uninstall = options("--uninstall");
|
|
if (!launcher || !note)
|
|
throw new Error("Provide --launcher APK and --note APK");
|
|
confirm("--confirm-replace");
|
|
replacePackages(serial, launcher, note, uninstall);
|
|
} else if (command === "patch-and-replace") {
|
|
const serial = requireSerial();
|
|
confirm("--confirm-replace");
|
|
const patched = spawnSync("bun", ["run", "patch-apks"], {
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
});
|
|
if (patched.error) throw patched.error;
|
|
if (patched.status !== 0) throw new Error("APK patching failed");
|
|
const oldLauncher = packageFromApk(
|
|
path.join(root, "inputs", "MoanLauncher.apk"),
|
|
);
|
|
const oldNote = packageFromApk(path.join(root, "inputs", "MoanNote.apk"));
|
|
console.log(`[ok] Inferred original packages: ${oldLauncher}, ${oldNote}`);
|
|
replacePackages(
|
|
serial,
|
|
path.join(root, "outputs", "MoanLauncher_patched.apk"),
|
|
path.join(root, "outputs", "MoanNote_patched.apk"),
|
|
[oldLauncher, oldNote],
|
|
);
|
|
} else {
|
|
throw new Error(`Unknown ADBMe command: ${command}`);
|
|
}
|