#!/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'\n' f'\n\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'\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"]*>\n", old).group(0) (out / "icebox.xml").write_text("\n" + head + package_info(*ICEBOX_INFO) + "".join(blocks) + "\n") (out / "lawnchair-pins.xml").write_text( "\n" f'\n' + package_info(*LAUNCHER_INFO) + f'\n' + "".join(f'\n' for p in pins) + "\n\n")