docs: add software-vision and android-backup-restore skills with android scripts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BluscreamandClaude Opus 5.5 committed 2026-10-30 13:00:44 +01:00
1 parent a16a879b1e
commit 5e6abadb95
12 files changed
+1261 -1

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Reinstall each restored app with its original installer (pm set-installer refuses root).
Same APKs, `install-multiple -r -i <installer>` keeps data. The device comes from ANDROID_SERIAL, as for adb. Usage: fix-installers.py BACKUP_DIR WORK_DIR
"""
import re
import subprocess
import sys
import time
from pathlib import Path
backup, work = Path(sys.argv[1]), Path(sys.argv[2])
log_file = (work / "restore.log").open("a")
def log(msg: str) -> None:
line = f"{time.strftime('%H:%M:%S')} {msg}"
print(line, flush=True)
log_file.write(line + "\n")
log_file.flush()
def adb(*args: str, timeout: int = 900) -> str:
r = subprocess.run(["adb", *args], capture_output=True, text=True, timeout=timeout)
return (r.stdout + r.stderr).strip()
def installers(text: str) -> dict:
# `pm list packages -i` prints "package:PKG", with -f "package:PATH=PKG".
return dict(re.findall(r"[:=]([\w.]+)\s+installer=(\S+)", text))
want = installers((backup / "rooted-fresh/packages-3.txt").read_text())
now = installers(adb("shell", "pm list packages -3 -i"))
disabled = set(re.findall(r"package:([\w.]+)", adb("shell", "pm list packages -d")))
log("=== installer fix-up start")
for pkg, inst in sorted(want.items()):
if inst == "null" or pkg not in now or now[pkg] == inst or inst not in now | {"com.android.vending": 1}:
continue
apks = sorted(str(p) for p in (backup / "apks" / pkg).glob("*.apk"))
if not apks:
continue
# Reinstalling re-enables a frozen app; disable it again afterwards.
out = adb("install-multiple", "-r", "-d", "-i", inst, "--bypass-low-target-sdk-block", *apks)
if pkg in disabled:
adb("shell", f"su -c 'pm disable-user --user 0 {pkg}'")
log(f"{pkg}: installer {now[pkg]} -> {inst}: {'OK' if 'Success' in out else 'FAILED ' + out[-200:]}")
log("=== installer fix-up done")
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Emit SQL that copies apps' keystore2 keys from an old ROM's DB into the live one, under the new UIDs.
Only works when both ROMs use the same TEE (compare keyentry.km_uuid). Keys super-encrypted with the
old lock-screen credential (blobmetadata tag 0, EncryptedBy) cannot be moved and are skipped.
Usage: keystore-transplant.py OLD_DB [--replace] OLD_UID:NEW_UID [...] > transplant.sql
--replace first delete the app's keys that already exist on the new ROM (needed when it regenerated them)
Apply with the apps force-stopped: `su -c 'sqlite3 /data/misc/keystore/persistent.sqlite < transplant.sql'`, reboot.
"""
import secrets
import sqlite3
import sys
DOMAIN_APP = 0
TAG_ENCRYPTED_BY = 0
def lit(v: object) -> str:
if v is None:
return "NULL"
if isinstance(v, bytes):
return f"X'{v.hex()}'"
if isinstance(v, str):
return "'" + v.replace("'", "''") + "'"
return str(v)
def delete_keys(where: str) -> list[str]:
ids = f"(SELECT id FROM keyentry WHERE {where})"
return [f"DELETE FROM blobmetadata WHERE blobentryid IN (SELECT id FROM blobentry WHERE keyentryid IN {ids});",
f"DELETE FROM blobentry WHERE keyentryid IN {ids};",
f"DELETE FROM keyparameter WHERE keyentryid IN {ids};",
f"DELETE FROM keymetadata WHERE keyentryid IN {ids};",
f"DELETE FROM grant WHERE keyentryid IN {ids};",
f"DELETE FROM keyentry WHERE {where};"]
args = sys.argv[1:]
old = sqlite3.connect(f"file:{args.pop(0)}?mode=ro", uri=True)
replace = "--replace" in args
pairs = [tuple(map(int, a.split(":"))) for a in args if a != "--replace"]
# Older keystore2 schemas have no blobentry.state; every blob there is current (0).
blob_state = "state" if any(c[1] == "state" for c in old.execute("PRAGMA table_info(blobentry)")) else "0"
out = ["BEGIN;"]
for old_ns, new_ns in pairs:
if replace:
out += delete_keys(f"domain={DOMAIN_APP} AND namespace={new_ns}")
keys = old.execute("SELECT id, key_type, alias, state, km_uuid FROM keyentry WHERE domain=? AND namespace=?",
(DOMAIN_APP, old_ns)).fetchall()
for kid, key_type, alias, state, km_uuid in keys:
blobs = old.execute(f"SELECT id, subcomponent_type, blob, {blob_state} FROM blobentry WHERE keyentryid=?", (kid,)).fetchall()
if any(old.execute("SELECT 1 FROM blobmetadata WHERE blobentryid=? AND tag=?", (b[0], TAG_ENCRYPTED_BY)).fetchone()
for b in blobs):
print(f"-- skip {old_ns} {alias!r}: super-encrypted with the old lock-screen credential", file=sys.stderr)
continue
new_id = secrets.randbits(63) * (1 if secrets.randbits(1) else -1)
# The old key wins over one the app generated under the same alias on the new ROM.
out += delete_keys(f"domain={DOMAIN_APP} AND namespace={new_ns} AND alias={lit(alias)}")
out.append(f"INSERT INTO keyentry (id,key_type,domain,namespace,alias,state,km_uuid) VALUES "
f"({new_id},{key_type},{DOMAIN_APP},{new_ns},{lit(alias)},{state},{lit(km_uuid)});")
for bid, sub, blob, bstate in blobs:
out.append(f"INSERT INTO blobentry (subcomponent_type,keyentryid,blob,state) VALUES ({sub},{new_id},{lit(blob)},{bstate});")
for tag, data in old.execute("SELECT tag, data FROM blobmetadata WHERE blobentryid=?", (bid,)):
out.append(f"INSERT INTO blobmetadata (blobentryid,tag,data) VALUES ((SELECT max(id) FROM blobentry WHERE keyentryid={new_id}),{tag},{lit(data)});")
for tag, data, level in old.execute("SELECT tag, data, security_level FROM keyparameter WHERE keyentryid=?", (kid,)):
out.append(f"INSERT INTO keyparameter (keyentryid,tag,data,security_level) VALUES ({new_id},{tag},{lit(data)},{level});")
for tag, data in old.execute("SELECT tag, data FROM keymetadata WHERE keyentryid=?", (kid,)):
out.append(f"INSERT INTO keymetadata (keyentryid,tag,data) VALUES ({new_id},{tag},{lit(data)});")
print(f"-- {old_ns}->{new_ns} {alias!r}: {len(blobs)} blobs", file=sys.stderr)
out.append("COMMIT;")
print("\n".join(out))
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Cold-start an app's launcher activity the way Android 17 accepts from the shell. Usage: launch.sh PKG [WAIT] [SHOT.png]
p=$1 w=${2:-6} shot=${3:-}
act=$(adb shell "cmd package resolve-activity --brief -c android.intent.category.LAUNCHER $p" </dev/null | tr -d '\r' | tail -1)
adb shell "am start -S -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n '$act'" </dev/null >/dev/null
sleep "$w"
[ -n "$shot" ] && adb exec-out screencap -p > "$shot" </dev/null
true
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Snapshot phone health: memory, load, crashes/ANRs since boot, low-memory kills, log errors, cold launches.
# Usage: measure.sh OUT_FILE [launch-package ...]
set -uo pipefail
out=$1; shift
a() { adb shell "$@" </dev/null; }
{
echo "# $(date '+%F %T') $(a getprop ro.build.display.id) / Android $(a getprop ro.build.version.release)"
echo "## system"
a 'uptime; cat /proc/loadavg'
a 'grep -E "MemTotal|MemAvailable|Cached:|SwapTotal|SwapFree" /proc/meminfo'
a 'su -c "cat /proc/sys/vm/swappiness; cat /sys/block/zram0/mm_stat 2>/dev/null"'
echo "procs: $(a 'ls -d /proc/[0-9]* | wc -l')"
echo "## top cpu"
a 'top -b -n 1 -m 12 -o PID,USER,%CPU,%MEM,NAME' | tail -13
echo "## dropbox since boot"
boot=$(a 'date -d @$(( $(date +%s) - $(cut -d. -f1 /proc/uptime) )) "+%Y-%m-%d %H:%M:%S"')
a "dumpsys dropbox" | awk -v b="$boot" '$1" "$2 >= b && $3 ~ /anr|crash|watchdog|tombstone|wtf|lowmem|native/ {print $3}' | sort | uniq -c
echo "## crash buffer (FATAL / ANR per process)"
a 'logcat -d -b crash' | grep -oE "Process: [^,]+|FATAL EXCEPTION|>>> [^ ]+ <<<" | sort | uniq -c | sort -rn | head -20
a 'logcat -d -b events' | grep -oE "am_anr: \[[^]]*" | awk -F, '{print "ANR " $3}' | sort | uniq -c
echo "## lmkd / am_kill"
a 'logcat -d -b events' | grep -cE "am_kill|lmk_kill" | sed 's/^/kills: /'
a 'logcat -d -b events' | grep -oE "am_kill: \[[^]]*" | awk -F, '{print $5}' | sort | uniq -c | sort -rn | head -10
echo "## logcat error tags (top 25)"
a 'logcat -d *:E' | awk '{print $6}' | sort | uniq -c | sort -rn | head -25
echo "## thermal / battery"
a 'dumpsys battery | grep -E "level|temperature|status"; for z in /sys/class/thermal/thermal_zone*; do echo "$(cat $z/type) $(cat $z/temp)"; done 2>/dev/null | sort -k2 -n | tail -5'
if [ $# -gt 0 ]; then
echo "## cold launches (ms, am start -W -S, 3 runs)"
for p in "$@"; do
t=""
for _ in 1 2 3; do
act=$(a "cmd package resolve-activity --brief -c android.intent.category.LAUNCHER $p" | tr -d '\r' | tail -1)
v=$(a "am start -W -S -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n '$act'" | awk '/TotalTime/{print $2}')
t="$t ${v:-fail}"; a 'input keyevent KEYCODE_HOME'; sleep 2
done
echo "$p:$t"
done
fi
} 2>&1 | tee "$out"
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Rebuild the Nova home screen in Lawnchair, plus the Ice Box shortcuts Nova had pinned.
Usage: nova-to-lawnchair.py NOVA_DB LAWNCHAIR_DB OLD_ICEBOX_SHORTCUT_XML OUT_DIR
Writes OUT_DIR/launcher_5_5_5.db (Lawnchair schema, Nova's items), OUT_DIR/icebox.xml
(shortcut_service package file with only the pinned shortcuts) and OUT_DIR/lawnchair-pins.xml
(shortcut_service launcher file pinning them for Lawnchair).
"""
import re
import shutil
import sqlite3
import sys
from pathlib import Path
LAUNCHER = "app.lawnchair.nightly"
ICEBOX = "com.catchingnow.icebox"
# From /data/system/packages.xml on the new ROM: versionCode, lastUpdateTime, SHA-256 of the cert.
LAUNCHER_INFO = ("1600020100", "1792700106881", "dHw2RbNXJYsuI+hR5TyWdH/grdAH5bos2X6MhVcuTcU=")
ICEBOX_INFO = ("154821395", "1792699567277", "gvW+cC4DIBdGDKg3b22r1yWp5S3SSSDdXl17WF+/4bY=")
# Nova-only containers (gesture/dock-swipe slots); Lawnchair has nothing to put them in.
SKIP_CONTAINERS = {-207, -208}
ITEM_APP, ITEM_FOLDER, ITEM_WIDGET, ITEM_DEEP_SHORTCUT = 0, 2, 4, 6
FLAG_ID_NOT_VALID = 1 # Launcher3 restore flag: allocate and bind a fresh widget id on load
MISSING_PACKAGES = {"com.dot.gallery"} # not installed on the new ROM
# Google's smartspace widget on row 0 of the first page: Lawnchair reserves that row for its own.
SKIP_IDS = {56}
nova_db, lc_db, icebox_xml, out = map(Path, sys.argv[1:5])
out.mkdir(parents=True, exist_ok=True)
def package_info(version: str, updated: str, sig: str) -> str:
return (f'<package-info version="{version}" last_udpate_time="{updated}" shadow="0" allow-backup="1" '
f'allow-backup-initialized="1" bk_src_version="-1" bk_src_backup-allowed="0">\n'
f'<signature hash="{sig}" />\n</package-info>\n')
nova = sqlite3.connect(nova_db)
nova.row_factory = sqlite3.Row
rows = [r for r in nova.execute("select * from favorites where _id >= 0 order by _id")
if r["container"] not in SKIP_CONTAINERS and r["_id"] not in SKIP_IDS]
folders = {r["_id"] for r in rows if r["itemType"] == ITEM_FOLDER}
kept = []
for r in rows:
pkg = re.search(r"component=([\w.]+)/", r["intent"] or "")
if pkg and pkg.group(1) in MISSING_PACKAGES:
print(f"skip {r['title']}: {pkg.group(1)} not installed")
continue
if r["container"] >= 0 and r["container"] not in folders:
continue
kept.append(r)
db = out / "launcher_5_5_5.db"
shutil.copy(lc_db, db)
lc = sqlite3.connect(db)
lc.execute("delete from favorites")
pins = []
for r in kept:
widget = r["itemType"] == ITEM_WIDGET
hotseat = r["container"] == -101
in_folder = r["container"] >= 0
x, y = int(max(r["cellX"] or 0, 0)), int(max(r["cellY"] or 0, 0))
lc.execute(
"insert into favorites (_id,title,intent,container,screen,cellX,cellY,spanX,spanY,itemType,"
"appWidgetId,appWidgetProvider,restored,profileId,rank,options) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(r["_id"], r["title"], r["intent"], r["container"],
x if hotseat else (0 if in_folder else r["screen"]),
x, 0 if hotseat else y, int(r["spanX"]), int(r["spanY"]), r["itemType"],
r["appWidgetId"] if widget else -1, r["appWidgetProvider"],
FLAG_ID_NOT_VALID if widget else 0, 0, r["rank"], 0))
if r["itemType"] == ITEM_DEEP_SHORTCUT:
pins.append(re.search(r"S\.shortcut_id=([^;]+)", r["intent"]).group(1))
lc.commit()
print(f"{len(kept)} items written, pins: {pins}")
old = Path(icebox_xml).read_text()
blocks = [m.group(0) for m in re.finditer(r'<shortcut id="([^"]+)".*?</shortcut>\n', old, re.S) if m.group(1) in pins]
assert len(blocks) == len(pins), f"found {len(blocks)} of {len(pins)} shortcuts"
head = re.search(r"<package name=[^>]*>\n", old).group(0)
(out / "icebox.xml").write_text("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + head
+ package_info(*ICEBOX_INFO) + "".join(blocks) + "</package>\n")
(out / "lawnchair-pins.xml").write_text(
"<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
f'<launcher-pins package-name="{LAUNCHER}" launcher-user="0">\n' + package_info(*LAUNCHER_INFO)
+ f'<package package-name="{ICEBOX}" package-user="0">\n'
+ "".join(f'<pin value="{p}" />\n' for p in pins) + "</package>\n</launcher-pins>\n")
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Replace an app with a differently signed build while keeping its data, permissions,
# installer and frozen state. Data is also saved to BACKUP_DIR before the uninstall.
# Usage: reinstall-keep-data.sh PACKAGE BACKUP_DIR APK [APK ...]
set -euo pipefail
pkg=$1 bak=$2; shift 2
a() { adb shell "$@" </dev/null; }
su_() { a "su -c '$1'"; }
mkdir -p "$bak"
installer=$(a "pm list packages -3 -i $pkg" | tr -d '\r' | grep "package:$pkg " | sed 's/.*installer=//')
disabled=$(a "pm list packages -d $pkg" | tr -d '\r' | grep -cx "package:$pkg" || true)
perms=$(a "dumpsys package $pkg" | sed -n '/runtime permissions:/,/^ *[a-z]* .*:$/p' | grep -oE '[a-zA-Z0-9_.]+: granted=true' | cut -d: -f1 | sort -u | tr '\n' ' ')
echo "$pkg: installer=$installer disabled=$disabled perms=[$perms]"
dirs="/data/data/$pkg /data/user_de/0/$pkg"
[ "$(su_ "[ -d /data/media/0/Android/data/$pkg ] && echo y")" = y ] && dirs="$dirs /data/media/0/Android/data/$pkg"
su_ "am force-stop $pkg"
# The phone writes the tar to stdout; exec-out keeps it binary-safe.
adb exec-out "su -c 'tar -cf - --exclude=$pkg/cache --exclude=$pkg/code_cache $dirs 2>/dev/null'" > "$bak/$pkg.tar"
echo "saved $(stat -c %s "$bak/$pkg.tar") bytes to $bak/$pkg.tar"
[ "$(stat -c %s "$bak/$pkg.tar")" -gt 10240 ] || { echo "backup suspiciously small, aborting"; exit 1; }
a "pm uninstall $pkg"
iopt=(); [ -n "$installer" ] && [ "$installer" != null ] && iopt=(-i "$installer")
adb install-multiple -r "${iopt[@]}" --bypass-low-target-sdk-block "$@"
uid=$(su_ "stat -c %u /data/data/$pkg")
cache_gid=$((uid - 10000 + 20000))
adb exec-in "su -c 'tar -xf - -C /'" < "$bak/$pkg.tar"
su_ "while pgrep -x tar >/dev/null; do sleep 0.3; done"
for d in /data/data/$pkg /data/user_de/0/$pkg; do
su_ "chown -R $uid:$uid $d; for c in cache code_cache; do mkdir -p $d/\$c; chown $uid:$cache_gid $d/\$c; chmod 2771 $d/\$c; done; chcon -hR \"\$(stat -c %C $d)\" $d"
done
case " $dirs " in *Android/data*)
su_ "d=/data/media/0/Android/data/$pkg; chown -R $uid:1078 \$d; find \$d -type d -exec chmod 2770 {} +; find \$d -type f -exec chmod 660 {} +; chcon -R u:object_r:media_rw_data_file:s0 \$d" ;;
esac
for p in $perms; do a "pm grant $pkg $p" 2>/dev/null || true; done
[ "$disabled" = 1 ] && su_ "pm disable-user --user 0 $pkg"
echo "$pkg: reinstalled as uid $uid, $(a "dumpsys package $pkg" | grep -m1 versionName | xargs)"
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Restore user apps, app data and permissions onto a freshly flashed, rooted phone.
Sources (from the backup folder):
apks/<pkg>/*.apk APK + splits
rooted-fresh/dumpsys-package.txt installer, granted runtime permissions
rooted-fresh/packages-3.txt which packages are user apps
rooted-fresh/packages-disabled.txt frozen/disabled state
rooted/apps-manifest.json special app-ops (overlay, all-files, ...)
<work>/data/<pkg>, <work>/user_de/0/<pkg> unpacked data-fresh.tar
The device comes from ANDROID_SERIAL, as for adb. Usage: restore.py BACKUP_DIR WORK_DIR [--data-only] [pkg ...] (no pkgs = all user apps)
--data-only only put the data back (apps already installed and updated): keeps the current APK,
installer, permissions and frozen state. Never re-run a full restore over updated apps.
Every action and its result is appended to WORK_DIR/restore.log.
"""
import json
import re
import shlex
import subprocess
import sys
import time
from pathlib import Path
SKIP = {"com.topjohnwu.magisk"} # installed fresh, its data must not be reused
# Dropped on restore: regenerated by the app, and the bulk of most backups.
VOLATILE = ["cache", "code_cache", "app_webview/Default/Cache", "app_webview/Default/Code Cache"]
backup, work = Path(sys.argv[1]), Path(sys.argv[2])
data_only = "--data-only" in sys.argv[3:]
only = set(sys.argv[3:]) - {"--data-only"}
log_file = (work / "restore.log").open("a")
def log(msg: str) -> None:
line = f"{time.strftime('%H:%M:%S')} {msg}"
print(line, flush=True)
log_file.write(line + "\n")
log_file.flush()
def adb(*args: str, timeout: int = 300, stdin=None) -> subprocess.CompletedProcess:
return subprocess.run(["adb", *args], capture_output=True, text=stdin is None,
timeout=timeout, stdin=stdin)
def su(cmd: str, timeout: int = 300) -> str:
r = adb("shell", f"su -c {shlex.quote(cmd)}", timeout=timeout)
return (r.stdout + r.stderr).strip()
def parse_dumpsys(text: str) -> dict:
"""installer + granted runtime permissions per package."""
out = {}
for block in re.split(r"\n Package \[", text)[1:]:
name = block.split("]", 1)[0]
installer = re.search(r"\n installerPackageName=(\S+)", block)
runtime = block.split("runtime permissions:", 1)
perms = re.findall(r"\n ([\w.]+): granted=true", runtime[1]) if len(runtime) > 1 else []
out[name] = {"installer": installer.group(1) if installer else None, "perms": sorted(set(perms))}
return out
fresh = backup / "rooted-fresh"
meta = parse_dumpsys((fresh / "dumpsys-package.txt").read_text(errors="replace"))
manifest = json.loads((backup / "rooted/apps-manifest.json").read_text())["apps"]
user_pkgs = sorted(set(re.findall(r"=([\w.]+)\s+installer=", (fresh / "packages-3.txt").read_text())) - SKIP)
disabled = set(re.findall(r"package:([\w.]+)", (fresh / "packages-disabled.txt").read_text()))
targets = [p for p in user_pkgs if not only or p in only]
log(f"=== restore start: {len(targets)} packages")
# Pass 1: install every APK set, so installers like Obtainium exist before pass 2 names them.
for pkg in [] if data_only else targets:
apks = sorted(str(p) for p in (backup / "apks" / pkg).glob("*.apk"))
if not apks:
log(f"{pkg}: NO APK, skipped")
continue
# --bypass-low-target-sdk-block: Android 14+ refuses targetSdk < 24 (Automagic workarounds).
r = subprocess.run(["adb", "install-multiple", "-r", "-d", "-g",
"--bypass-low-target-sdk-block", *apks],
capture_output=True, text=True, timeout=900)
log(f"{pkg}: install {'OK' if 'Success' in r.stdout else 'FAILED ' + (r.stdout + r.stderr).strip()[-200:]}")
installed = set(re.findall(r"package:([\w.]+)", adb("shell", "pm list packages").stdout))
# Pass 2: data, ownership, SELinux labels, installer, permissions, disabled state.
for pkg in targets:
if pkg not in installed:
continue
info = meta.get(pkg, {})
su(f"am force-stop {pkg}")
uid = su(f"stat -c %u /data/data/{pkg}")
if not uid.isdigit():
log(f"{pkg}: no data dir after install ({uid}), data skipped")
continue
cache_gid = int(uid) - 10000 + 20000 # u0_aNNN_cache
members = [m for m in (f"data/{pkg}", f"user_de/0/{pkg}") if (work / m).is_dir()]
if members:
for m in members:
su(f"find /data/{m} -mindepth 1 -maxdepth 1 ! -name lib -exec rm -rf {{}} +")
excludes = [f"--exclude={m}/{v}" for m in members for v in VOLATILE]
tar = subprocess.Popen(["tar", "-cf", "-", "-C", str(work), *excludes, *members],
stdout=subprocess.PIPE)
r = adb("exec-in", "su -c 'tar -xf - -C /data'", stdin=tar.stdout, timeout=1800)
tar.wait()
# exec-in returns once our side is sent; the phone's tar may still be writing.
su("while pgrep -x tar >/dev/null; do sleep 0.3; done", timeout=600)
for m in members:
su(f"chown -R {uid}:{uid} /data/{m}; "
f"for d in cache code_cache; do mkdir -p /data/{m}/$d; chown {uid}:{cache_gid} /data/{m}/$d; "
# restorecon gives only app_data_file:s0; the per-app MLS categories that
# installd put on the top directory must be copied onto its contents.
f"chmod 2771 /data/{m}/$d; done; chcon -hR \"$(stat -c %C /data/{m})\" /data/{m}",
timeout=900)
wrong = su(f"find {' '.join('/data/' + m for m in members)} ! -user {uid} | wc -l")
log(f"{pkg}: data {'+'.join(members)} rc={r.returncode} uid={uid} wrong_owner={wrong}")
else:
log(f"{pkg}: no data in backup")
if data_only:
continue
inst = info.get("installer")
if inst and inst != "null" and inst in installed | {"com.android.vending"}:
log(f"{pkg}: installer {inst} -> {su(f'pm set-installer {pkg} {inst}') or 'OK'}")
# -g granted everything at install; revoke what was not granted before.
wanted = set(info.get("perms", []))
now = re.findall(r"\n\s+([\w.]+): granted=true", su(f"dumpsys package {pkg} | sed -n '/runtime permissions:/,$p'"))
revoked = [p for p in set(now) - wanted]
for p in revoked:
su(f"pm revoke {pkg} {p}")
for p in wanted - set(now):
su(f"pm grant {pkg} {p}")
ops = manifest.get(pkg, {}).get("specialAppOps", {})
for op, mode in ops.items():
su(f"appops set {pkg} {op} {mode}")
log(f"{pkg}: perms granted={len(wanted)} revoked={len(revoked)} appops={','.join(ops) or '-'}")
if pkg in disabled:
log(f"{pkg}: disable-user -> {su(f'pm disable-user --user 0 {pkg}')}")
log("=== restore done")
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Launch every user app with a launcher activity, one by one: unfreeze if frozen, cold start,
# wait, screenshot, collect its crash/ANR/keystore log lines, check it is alive and in front,
# then stop it and re-freeze. Writes OUT_DIR/results.tsv, OUT_DIR/<pkg>.png, OUT_DIR/<pkg>.log.
# Usage: test-all-apps.sh OUT_DIR [pkg ...]
set -uo pipefail
out=$1; shift
mkdir -p "$out"
a() { adb shell "$@" </dev/null; }
WAIT=${WAIT:-8}
if [ $# -gt 0 ]; then pkgs=("$@"); else
mapfile -t pkgs < <(a 'pm list packages -3' | tr -d '\r' | sed 's/package://' | sort)
fi
frozen=$(a 'pm list packages -d' | tr -d '\r' | sed 's/package://')
printf 'pkg\twas_frozen\tlaunch_ms\talive\tforeground\tcrash\tanr\tkeystore\tnote\n' > "$out/results.tsv"
for p in "${pkgs[@]}"; do
wf=0; grep -qx "$p" <<<"$frozen" && wf=1
[ $wf = 1 ] && a "su -c 'pm enable --user 0 $p'" >/dev/null
act=$(a "cmd package resolve-activity --brief -c android.intent.category.LAUNCHER $p" | tr -d '\r' | tail -1)
if [[ "$act" != */* ]]; then
[ $wf = 1 ] && a "su -c 'pm disable-user --user 0 $p'" >/dev/null
printf '%s\t%s\t-\t-\t-\t-\t-\t-\tno launcher activity\n' "$p" "$wf" >> "$out/results.tsv"; continue
fi
a 'logcat -c'
ms=$(a "am start -W -S -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n '$act'" | awk '/TotalTime/{print $2}')
sleep "$WAIT"
adb exec-out screencap -p > "$out/$p.png" </dev/null
pid=$(a "pidof $p" | tr -d '\r')
fg=$(a 'dumpsys activity activities' | grep -m1 -E 'topResumedActivity|mResumedActivity' | grep -c "$p/")
a 'logcat -d' > "$out/$p.log"
crash=$(grep -cE "FATAL EXCEPTION|Process: $p|Fatal signal" "$out/$p.log")
anr=$(grep -cE "ANR in $p" "$out/$p.log")
ks=$(grep -ciE "AEADBadTag|KeyStoreException|VERIFICATION_FAILED|KeyPermanentlyInvalidated|UnrecoverableKey|BadPaddingException|file is not a database|SQLiteDatabaseCorrupt" "$out/$p.log")
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n' "$p" "$wf" "${ms:-?}" "${pid:+yes}" "$fg" "$crash" "$anr" "$ks" >> "$out/results.tsv"
echo "$p frozen=$wf ms=${ms:-?} alive=${pid:+yes} fg=$fg crash=$crash anr=$anr ks=$ks"
a "am force-stop $p; input keyevent KEYCODE_HOME"
[ $wf = 1 ] && a "su -c 'pm disable-user --user 0 $p'" >/dev/null
done