Lawrence DĀ¢Oliveiro wrote:
> comp.lang.python

Lawrence wisely deleted the iOS trolls and added the python experts, 
which I appreciate so much that below I post to the python group the code 
(as you wouldn't have seen the code in the original post).

Every suggestion from Lawrence to improve the code was apropos, so this is 
posted simply so that the python folks can see exactly what he was seeing.

See also the keyword-rich web-searchable specific code archive located at:
 *PSA: How to get a homescreen-organized list of APKs to backup/restore sans 
root to/from a PC (by package name & folder) without a mothership account!*
 
<https://xdaforums.com/t/psa-how-to-get-a-homescreen-organized-list-of-apks-to-backup-restore-sans-root-to-from-a-pc-by-package-name-folder-without-a-mothership-account.4798319/>

 # -------------------------------------------------------------------
 # apkhome.py 
 #  Replicates Android homescreen panes/folders/apps/apk(s) on the PC
 #   Usage: python apkhome.py
 # -------------------------------------------------------------------
 # v2.2 20260820 Added strict return-code validation for adb pull transfers
 # v2.1 20260819 Added automated batch-restoration-script generator 
 # v2.0 20260819 Added re-installation reference guide & ADB syntax options
 # v1.9 20260819 Added missing/uninstalled app tracking list and report
 # v1.8 20260819 Added _versio_ naming convention for package versioning
 # v1.7 20260819 Added end-of-run execution summary and tracking metrics
 # v1.6 20260819 Added cleanup prompt for existing ./apkhome directory
 # v1.5 20260819 Added custom fallback path for scrcpy's bundled adb.exe
 # v1.4 20260819 Added log file output (apkhome.log) alongside console
 # v1.3 20260819 Added Ctrl+C (KeyboardInterrupt) handling & cleanup
 # v1.2 20260819 Added interactive debug mode toggle for dry runs
 # v1.1 20260819 Added automated ADB split/monolithic APK pulling
 # v1.0 20260819 Combined SQL parsing and direct directory tree generation
 # -------------------------------------------------------------------
 # Creates a duplicate inventory of Android homescreen panels, folders, 
 # app icons, loose desktop shortcuts and Android application packages 
 # extracted from the Nova Launcher internal databases (nova.db). 
 # 
 # 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.
 # -------------------------------------------------------------------
 # Prerequisite: "nova.db" SQL database file created using the 
 # last-known-good-version of the Tesla Coil Nova Launcher 7.0.57
 #   <https://mobile.softpedia.com/apk/nova-launcher/7.0.57/>
 # 
 # A. On Android, Nova Settings > Backup & restore > Backup > Save
 #    /storage/emulated/0/0000/bck/nova_backup/2026-08-19_11-31.novabackup
 # B. Copy the nova backup to the desktop & rename it to a zip extension
 #    adb pull 2026-08-19_11-31.novabackup 2026-08-19_11-31.novabackup.zip
 # C. Unzip using 7-zip (or equivalent)
 #     08/19/2026  11:31 AM 7,430,144 nova.db
 #     08/19/2026  11:31 AM     6,455 nova.xml
 #     08/19/2026  11:31 AM     4,818 supportDetails.txt
 # 
 # Tested on Android 13 USA-spec Samsung Galaxy A32-5G (locked bootloader)
 # Tested on Windows 10, Python 3.14.1, with the phone on the Wi-Fi LAN
 # -------------------------------------------------------------------
 # Sample output (note that system apps may have defined APK names)
 # 
 # com.android.systemui_versio_13
 #    08/19/2026  09:14 PM   38,323,407 SystemUI.apk
 #                 1 File(s)   38,323,407 bytes
 #
 # com.simplemobiletools.clock_versio_5.11.2
 #    08/19/2026  09:04 PM   10,004,630 base.apk
 #                 1 File(s)   10,004,630 bytes
 # 
 #   com.trianguloy.instantintent_versio_0.1
 #    08/19/2026  09:06 PM      47,835 base.apk
 #    08/19/2026  09:06 PM      10,221 split_config.xhdpi.apk
 #                 2 File(s)      58,056 bytes
 #  
 #   com.chibatching.worldclockwidget_versio_20260316.0
 #    08/19/2026  09:04 PM   12,027,785 base.apk
 #    08/19/2026  09:04 PM          57,672 split_config.arm64_v8a.apk
 #    08/19/2026  09:04 PM          58,511 split_config.xhdpi.apk
 #                 3 File(s)   12,143,968 bytes
 #  
 #   com.neuracle.zulutime_versio_1.5
 #    08/19/2026  09:03 PM   21,619,972 base.apk
 #    08/19/2026  09:03 PM   25,650,697 split_config.arm64_v8a.apk
 #    08/19/2026  09:03 PM          45,465 split_config.en.apk
 #    08/19/2026  09:03 PM          67,159 split_config.xhdpi.apk
 #                 4 File(s)   47,383,293 bytes
 #  
 #   pixer.worldclock_versio_1.0.29
 #    08/19/2026  09:03 PM    6,161,133 base.apk
 #    08/19/2026  09:03 PM   14,546,665 split_config.arm64_v8a.apk
 #    08/19/2026  09:03 PM          29,018 split_config.en.apk
 #    08/19/2026  09:03 PM          16,730 split_config.es.apk
 #    08/19/2026  09:03 PM          68,714 split_config.xhdpi.apk
 #                 5 File(s)   20,822,260 bytes
 # -------------------------------------------------------------------
 # To re-install the exact package subversion using adb over Wi-Fi,
 # navigate into any specific app folder containing your pulled APKs, 
 # and then run the appropriate "adb install" command as shown below.
 #
 # For a single APK such as com.simplemobiletools.gallery.pro:
 #    adb install -r -d -i "com.android.vending" base.apk
 #
 # For multi-split install files:
 #    adb install-multiple base.apk split_config.arm64_v8a.apk 
split_config.en.apk
 #
 # Advanced flags for system migration / restores:
 #    -r : Replace existing application (updates/overwrites cleanly)
 #    -d : Allow version code downgrade (if the current device has a newer app)
 #    -i : Specify any desired custom installer package identifier tag 
 #
 # Combined Example:
 #    adb install-multiple -r -d -i "adb" base.apk split_config.arm64_v8a.apk
 # -------------------------------------------------------------------
 
 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
 }
 
 # List to track missing/uninstalled packages for the final report
 missing_apps_list = []
 
 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:
                 parts = line.strip().split("versionName=")
                 if len(parts) > 1:
                     raw_ver = parts[1].split()[0]
                     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)
 
                     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
                         )
                         paths_found = 0
                         pull_success_count = 0
 
                         for line in path_cmd.stdout.splitlines():
                             if line.startswith("package:"):
                                 remote_path = line.replace("package:", 
"").strip()
                                 if remote_path:
                                     paths_found += 1
                                     pull_result = subprocess.run(
                                         [adb_binary, "pull", remote_path, 
target_path],
                                         stdout=subprocess.DEVNULL, 
stderr=subprocess.DEVNULL
                                     )
                                     if pull_result.returncode == 0:
                                         pull_success_count += 1
 
                         if paths_found > 0 and pull_success_count == 
paths_found:
                             stats["apps_success"] += 1
                         else:
                             stats["apps_failed"] += 1
                             missing_apps_list.append(f"{current_panel} / 
{clean_folder_name} / {pkg}")
                             log_print(f"    [ADB] Warning: Failed to fully 
pull APK(s) for package: {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
 
                 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
                     )
                     paths_found = 0
                     pull_success_count = 0
 
                     for line in path_cmd.stdout.splitlines():
                         if line.startswith("package:"):
                             remote_path = line.replace("package:", "").strip()
                             if remote_path:
                                 paths_found += 1
                                 pull_result = subprocess.run(
                                     [adb_binary, "pull", remote_path, 
target_path],
                                     stdout=subprocess.DEVNULL, 
stderr=subprocess.DEVNULL
                                 )
                                 if pull_result.returncode == 0:
                                     pull_success_count += 1
 
                     if paths_found > 0 and pull_success_count == paths_found:
                         stats["apps_success"] += 1
                     else:
                         stats["apps_failed"] += 1
                         missing_apps_list.append(f"{current_panel} / 
loose_apps / {pkg}")
                         log_print(f"    [ADB] Warning: Failed to fully pull 
APK(s) for package: {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)
 
     # --- Missing apps detailed report ---
     if missing_apps_list:
         missing_report = "Missing / Uninstalled Apps Breakdown:\n"
         for item in missing_apps_list:
             missing_report += f"  - {item}\n"
         missing_report += "========================================\n"
         log_print(missing_report)
 
     # --- Optional Restoration Script Generation ---
     gen_script_choice = input("Would you like to generate an ADB batch 
restoration script for these pulled apps? [y/n]: ").strip().lower()
     log_file.write(f"User selected restoration script generation choice: 
{gen_script_choice}\n")
     
     if gen_script_choice.startswith('y'):
         restore_script_filename = "restore_apps.bat"
         commands = []
         
         if os.path.exists(base_dir):
             for root, dirs, files in os.walk(base_dir):
                 apks = [f for f in files if f.endswith(".apk")]
                 if apks:
                     # Sort so base.apk always comes first
                     apks.sort(key=lambda x: 0 if x == "base.apk" else 1)
                     apk_paths = [os.path.join(root, apk) for apk in apks]
                     
                     binary_to_use = adb_binary if adb_binary else "adb"
                     
                     if len(apk_paths) == 1:
                         cmd = f'"{binary_to_use}" install -r -d -i 
"com.android.vending" "{apk_paths[0]}"'
                     else:
                         joined_apks = '" "'.join(apk_paths)
                         cmd = f'"{binary_to_use}" install-multiple -r -d -i 
"com.android.vending" "{joined_apks}"'
                         
                     commands.append(cmd)
             
             with open(restore_script_filename, "w", encoding="utf-8") as 
r_file:
                 r_file.write("@echo off\n")
                 r_file.write(":: Generated by apkhome.py Restoration Script 
Generator\n")
                 r_file.write("echo Starting batch app restoration...\n\n")
                 for cmd in commands:
                     r_file.write(cmd + "\n")
                 r_file.write("\necho Restoration script completed.\n")
                 r_file.write("pause\n")
                 
             log_print(f"[i] Successfully generated restoration script: 
{restore_script_filename} ({len(commands)} apps targeted).")
 
 except KeyboardInterrupt:
     log_print("\n\n[!] Operation cancelled 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
-- 
-- 
https://mail.python.org/mailman3//lists/python-list.python.org

Reply via email to