Re: PSA: How to archive your EXACT homescreen(s) apps to Windows for perfect cross-platform backup/restore sans the net
Maria Sophia <[email protected]>
| Newsgroups | comp.mobile.android,alt.comp.microsoft.windows,alt.comp.os.windows-10 |
|---|---|
| Organization | BWH Usenet Archive (https://usenet.blueworldhosting.com) |
| Message-ID | <[email protected]> |
Maria Sophia wrote:
> # -------------------------------------------------------------------
> # WIP:
> # Need to consider how to rename the base.apk to the package name
> # Need to consider how to add the version number to the package name
> # Need to consider how to zip up splits
> # -------------------------------------------------------------------
Given nothing like this exists on the planet (that I'm aware of), it's
nice that it's working now, but the question now is what to improve.
I'm really torn here. The version is critical.
But it's nice having standard folder names too as versions can be anything.
I have to come up with an idea to know the version but be able to strip it.
As for zipping up the splits, I think I may leave them unzipped.
That way, the restore script can use adb to restore the splits easily.
I might leave the names exactly as they are on Android, since the folder
names (possibly with version numbers) will identify each app correctly.
If I add version numbers to the folders, that just complicates reinstall.
Maybe I'll add a Latin "_versio_" to the folder name, which I can strip
during re-installation, so that the version name won't complicate things?
# -------------------------------------------------------------------
# apkhome.py
# Parse nova.db, build homescreen folder/app skeleton and pull APKs
# python apkhome.py
# -------------------------------------------------------------------
# v1p8 20260819 Added _versio_ naming convention for package versioning
# v1p7 20260819 Added end-of-run execution summary and tracking metrics
# v1p6 20260819 Added optional cleanup prompt for existing ./apkhome directory
# v1p5 20260819 Added custom fallback path for scrcpy's bundled adb.exe
# v1p4 20260819 Added log file output (apkhome.log) alongside console
# v1p3 20260819 Added Ctrl+C (KeyboardInterrupt) handling & cleanup
# v1p2 20260819 Added interactive debug mode toggle for dry runs
# v1p1 20260819 Added automated ADB split/monolithic APK pulling
# v1p0 20260819 Combined SQL parsing and direct directory tree generation
# -------------------------------------------------------------------
# Reads nova.db, re-creates the homescreen folder and package hierarchy,
# checks for a connected ADB device, runs adb shell pm path <package>
# to dynamically locate and pull component files (base.apk plus splits)
# feeding results into the newly created matching homescreen hierarchy.
# -------------------------------------------------------------------
# WIP:
# Need to consider how to rename the base.apk to the package name
# Need to consider how to zip up splits
# -------------------------------------------------------------------
import os
import re
import shutil
import sqlite3
import subprocess
db_file = "nova.db"
base_dir = "./apkhome"
log_filename = "apkhome.log"
# Explicit fallback path for ADB based on a typical scrcpy setup
CUSTOM_ADB_PATH = r"C:\app\editor\android\scrcpy\adb.exe"
# Tracking metrics for summary
stats = {
"screens_processed": 0,
"folders_processed": 0,
"loose_apps_processed": 0,
"apps_attempted": 0,
"apps_success": 0,
"apps_failed": 0
}
if not os.path.exists(db_file):
print(f"Error: {db_file} not found. Please place it in the working directory.")
exit(1)
# Open log file for writing
log_file = open(log_filename, "w", encoding="utf-8")
def log_print(message):
"""Prints to console and writes to the log file simultaneously."""
print(message)
log_file.write(message + "\n")
log_file.flush()
# Helper to find the correct adb executable command/path
def get_adb_binary():
# 1. Try global 'adb' command first
try:
subprocess.run(["adb", "version"], capture_output=True, text=True, check=True)
return "adb"
except (subprocess.SubprocessError, FileNotFoundError):
pass
# 2. Try the custom scrcpy path fallback
if os.path.exists(CUSTOM_ADB_PATH):
return CUSTOM_ADB_PATH
return None
def get_app_version(binary, pkg):
"""Queries package version name via ADB, returns 'unknown' if it fails."""
try:
res = subprocess.run(
[binary, "shell", "dumpsys", "package", pkg],
capture_output=True, text=True, timeout=5
)
for line in res.stdout.splitlines():
if "versionName=" in line:
# Extracts part after versionName=
parts = line.strip().split("versionName=")
if len(parts) > 1:
raw_ver = parts[1].split()[0]
# Clean up illegal path characters from version strings
return re.sub(r'[\\/*?:"<>|]', "", raw_ver)
except Exception:
pass
return "unknown"
# --- SETUP & MODES PROMPT ---
log_print("--- Setup Mode ---")
mode_choice = input("Run in [D]ebug mode (mock files, no phone needed) or [L]ive mode (real ADB pull)? [d/l]: ").strip().lower()
log_file.write(f"User selected mode choice: {mode_choice}\n")
DEBUG_MODE = mode_choice.startswith('d')
# --- EXISTING DIRECTORY HANDLING ---
if os.path.exists(base_dir):
clean_choice = input(f"Existing '{base_dir}' folder detected. [C]lean/wipe it completely or [K]eep existing files? [c/k]: ").strip().lower()
log_file.write(f"User selected directory cleanup choice: {clean_choice}\n")
if clean_choice.startswith('c'):
try:
shutil.rmtree(base_dir)
log_print(f"[i] Successfully wiped existing '{base_dir}' directory.")
except Exception as e:
log_print(f"[!] Warning: Failed to fully remove '{base_dir}': {e}")
else:
log_print(f"[i] Keeping existing '{base_dir}' directory (merging/updating content).")
adb_binary = None
adb_active = False
if DEBUG_MODE:
log_print("\n[!] Running in DEBUG mode. ADB will be bypassed, and dummy APKs will be created.")
else:
log_print("\n[i] Running in LIVE mode. Locating ADB binary...")
adb_binary = get_adb_binary()
if not adb_binary:
log_print(f"Warning: 'adb' not found in PATH or at custom path ({CUSTOM_ADB_PATH}). Skipping APK pulling.")
adb_active = False
else:
log_print(f"Using ADB binary at: {adb_binary}")
try:
adb_check = subprocess.run([adb_binary, "devices"], capture_output=True, text=True, check=True)
devices = [line for line in adb_check.stdout.splitlines() if "\tdevice" in line]
if not devices:
log_print("Warning: No active ADB devices found. Directory skeleton will be built, but APK pulling will be skipped.")
adb_active = False
else:
log_print(f"ADB device detected: {devices[0].split()[0]}")
adb_active = True
except (subprocess.SubprocessError, FileNotFoundError):
log_print("Warning: Failed to execute ADB. Skipping APK pulling.")
adb_active = False
log_print(f"\nConnecting to {db_file} and processing layout...")
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
try:
# 1. Fetch distinct screen indices associated with desktop items
cursor.execute(
"SELECT DISTINCT screen FROM favorites WHERE container = -100 ORDER BY screen ASC;"
)
screens = cursor.fetchall()
for (screen_num,) in screens:
stats["screens_processed"] += 1
current_panel = f"panel_{screen_num}"
log_print(f"\n--- Processing: {current_panel} ---")
# 2. Find folders located directly on this specific screen (ignoring blank titles)
cursor.execute(
"SELECT _id, title FROM favorites WHERE container = -100 AND screen = ?"
" AND intent IS NULL AND title IS NOT NULL AND TRIM(title) != '';",
(screen_num,),
)
folders = cursor.fetchall()
for folder_id, folder_name in folders:
stats["folders_processed"] += 1
clean_folder_name = re.sub(r'[\\/*?:"<>|]', "", folder_name).strip()
if not clean_folder_name:
clean_folder_name = "unnamed_folder"
cursor.execute(
"SELECT title, intent FROM favorites WHERE container = ?;",
(folder_id,),
)
items = cursor.fetchall()
if items:
for app_title, intent in items:
stats["apps_attempted"] += 1
pkg = "Unknown"
if intent and "component=" in intent:
try:
part = intent.split("component=")[1]
pkg = part.split("/")[0]
except IndexError:
pkg = intent
# Fetch version if possible to build _versio_ suffix
version_str = "unknown"
if pkg != "Unknown":
if DEBUG_MODE:
version_str = "1.0.0-debug"
elif adb_active:
version_str = get_app_version(adb_binary, pkg)
# Apply _versio_ naming scheme
pkg_folder_name = f"{pkg}_versio_{version_str}" if pkg != "Unknown" else pkg
target_path = os.path.join(
base_dir, current_panel, clean_folder_name, pkg_folder_name
)
os.makedirs(target_path, exist_ok=True)
log_print(f" [Folder] {clean_folder_name} -> App: {pkg} (v:{version_str})")
# Pull APK or generate mock file depending on mode
if DEBUG_MODE and pkg != "Unknown":
dummy_apk_path = os.path.join(target_path, "base.apk")
with open(dummy_apk_path, "w") as f:
f.write("mock apk content")
log_print(f" [Mock ADB] Created test file at {dummy_apk_path}")
stats["apps_success"] += 1
elif adb_active and pkg != "Unknown":
path_cmd = subprocess.run(
[adb_binary, "shell", "pm", "path", pkg],
capture_output=True, text=True
)
pulled_any = False
for line in path_cmd.stdout.splitlines():
if line.startswith("package:"):
remote_path = line.replace("package:", "").strip()
if remote_path:
pulled_any = True
subprocess.run(
[adb_binary, "pull", remote_path, target_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if pulled_any:
stats["apps_success"] += 1
else:
stats["apps_failed"] += 1
log_print(f" [ADB] Warning: Could not find path for package (may be uninstalled): {pkg}")
else:
stats["apps_failed"] += 1
else:
target_path = os.path.join(base_dir, current_panel, clean_folder_name)
os.makedirs(target_path, exist_ok=True)
# 3. Find loose/top-level apps sitting directly on this screen
cursor.execute(
"SELECT title, intent FROM favorites WHERE container = -100 AND screen = ?"
" AND intent IS NOT NULL;",
(screen_num,),
)
loose_apps = cursor.fetchall()
if loose_apps:
for app_title, intent in loose_apps:
stats["loose_apps_processed"] += 1
stats["apps_attempted"] += 1
pkg = "Unknown"
if intent and "component=" in intent:
try:
part = intent.split("component=")[1]
pkg = part.split("/")[0]
except IndexError:
pkg = intent
# Fetch version if possible to build _versio_ suffix
version_str = "unknown"
if pkg != "Unknown":
if DEBUG_MODE:
version_str = "1.0.0-debug"
elif adb_active:
version_str = get_app_version(adb_binary, pkg)
pkg_folder_name = f"{pkg}_versio_{version_str}" if pkg != "Unknown" else pkg
target_path = os.path.join(
base_dir, current_panel, "loose_apps", pkg_folder_name
)
os.makedirs(target_path, exist_ok=True)
log_print(f" [Loose App] {pkg} (v:{version_str})")
if DEBUG_MODE and pkg != "Unknown":
dummy_apk_path = os.path.join(target_path, "base.apk")
with open(dummy_apk_path, "w") as f:
f.write("mock apk content")
log_print(f" [Mock ADB] Created test file at {dummy_apk_path}")
stats["apps_success"] += 1
elif adb_active and pkg != "Unknown":
path_cmd = subprocess.run(
[adb_binary, "shell", "pm", "path", pkg],
capture_output=True, text=True
)
pulled_any = False
for line in path_cmd.stdout.splitlines():
if line.startswith("package:"):
remote_path = line.replace("package:", "").strip()
if remote_path:
pulled_any = True
subprocess.run(
[adb_binary, "pull", remote_path, target_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if pulled_any:
stats["apps_success"] += 1
else:
stats["apps_failed"] += 1
log_print(f" [ADB] Warning: Could not find path for package (may be uninstalled): {pkg}")
else:
stats["apps_failed"] += 1
# --- EXECUTION SUMMARY BLOCK ---
summary_text = (
"\n"
"========================================\n"
" EXECUTION SUMMARY\n"
"========================================\n"
f"Panels/Screens Processed: {stats['screens_processed']}\n"
f"Folders Discovered: {stats['folders_processed']}\n"
f"Loose Apps Discovered: {stats['loose_apps_processed']}\n"
f"----------------------------------------\n"
f"Total App References: {stats['apps_attempted']}\n"
f"Successfully Pulled APKs: {stats['apps_success']}\n"
f"Missing / Uninstalled: {stats['apps_failed']}\n"
"========================================\n"
)
log_print(summary_text)
except KeyboardInterrupt:
log_print("\n\n[!] Operation canceled by user (Ctrl+C). Exiting safely...")
finally:
# Ensure database connection and log file are cleanly closed
conn.close()
log_file.close()
# end of apkhome.py