Untitled

 avatar
unknown
plain_text
11 hours ago
42 kB
6
Indexable
import os
import re
import time
import shutil
import queue
import tempfile
import threading
import subprocess
from pathlib import Path
from datetime import datetime
import tkinter as tk
from tkinter import ttk, filedialog, messagebox

DEFAULT_LAUNCHER = "com.google.android.apps.nexuslauncher"
DEFAULT_USER = "0"

DEFAULT_IMPORTANT_PACKAGES = {
    "com.google.android.deskclock",
    "com.google.android.apps.weather",
    "com.google.android.googlequicksearchbox",
}

GROUP_RULES = [
    ("Clock", re.compile(r"(deskclock|alarmclock|clock)", re.I)),
    ("Weather", re.compile(r"(weather|forecast)", re.I)),
    ("Google", re.compile(r"(googlequicksearchbox|searchwidget|smartspace|stocks)", re.I)),
    ("Launcher", re.compile(r"(nexuslauncher|launcher)", re.I)),
]


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("ADB Widget Manager v11 - All Widgets")
        self.geometry("1460x940")
        self.minsize(1180, 780)

        self.q = queue.Queue()
        self.watch_stop = threading.Event()
        self.watch_thread = None
        self.running_action = False

        self.adb_var = tk.StringVar(value=shutil.which("adb") or "adb")
        self.user_var = tk.StringVar(value=DEFAULT_USER)
        self.launcher_var = tk.StringVar(value=DEFAULT_LAUNCHER)
        self.work_user_var = tk.StringVar(value="auto")

        self.all_widgets = {}
        self.registered_providers = set()
        self.active_components = set()
        self.widget_rows = {}
        self.last_status = {}
        self.last_report = ""
        self.last_boot_id = None
        self.auto_repair_count = 0
        self.work_profiles = []

        self.build_ui()
        self.after(100, self.handle_queue)
        self.after(500, self.run_refresh_all_widgets)

    def build_ui(self):
        root = ttk.Frame(self, padding=12)
        root.pack(fill="both", expand=True)

        header = ttk.Frame(root)
        header.pack(fill="x", pady=(0, 10))
        ttk.Label(header, text="ADB Widget Manager v11", font=("Segoe UI", 18, "bold")).pack(side="left")
        ttk.Label(header, text="Work Profile control + all detected widget providers", font=("Segoe UI", 10)).pack(side="left", padx=(12, 0), pady=(7, 0))

        settings = ttk.LabelFrame(root, text="Settings")
        settings.pack(fill="x", pady=(0, 10))

        ttk.Label(settings, text="ADB:").grid(row=0, column=0, padx=8, pady=7, sticky="w")
        ttk.Entry(settings, textvariable=self.adb_var).grid(row=0, column=1, padx=8, pady=7, sticky="we")
        ttk.Button(settings, text="Choose adb.exe", command=self.pick_adb).grid(row=0, column=2, padx=8, pady=7)

        ttk.Label(settings, text="Launcher:").grid(row=1, column=0, padx=8, pady=7, sticky="w")
        ttk.Entry(settings, textvariable=self.launcher_var).grid(row=1, column=1, padx=8, pady=7, sticky="we")
        ttk.Label(settings, text="Main user:").grid(row=1, column=2, padx=(8, 2), pady=7, sticky="e")
        ttk.Entry(settings, textvariable=self.user_var, width=6).grid(row=1, column=3, padx=(2, 8), pady=7, sticky="w")

        ttk.Label(settings, text="Work profile user:").grid(row=2, column=0, padx=8, pady=7, sticky="w")
        ttk.Entry(settings, textvariable=self.work_user_var, width=12).grid(row=2, column=1, padx=8, pady=7, sticky="w")
        ttk.Button(settings, text="Detect Work Profile", command=self.run_detect_work_profile).grid(row=2, column=2, padx=8, pady=7, sticky="w")

        settings.columnconfigure(1, weight=1)

        actions = ttk.LabelFrame(root, text="Actions")
        actions.pack(fill="x", pady=(0, 10))

        self.watch_btn = ttk.Button(actions, text="Start Auto Watch", command=self.toggle_watch)
        self.watch_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.refresh_btn = ttk.Button(actions, text="Refresh All Widgets", command=self.run_refresh_all_widgets)
        self.refresh_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.repair_btn = ttk.Button(actions, text="Repair Selected Apps", command=self.run_repair_selected)
        self.repair_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.enable_btn = ttk.Button(actions, text="Enable Selected", command=self.run_enable_selected)
        self.enable_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.disable_btn = ttk.Button(actions, text="Disable Selected", command=self.run_disable_selected)
        self.disable_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.stop_work_btn = ttk.Button(actions, text="Stop Work Profile", command=self.run_stop_work_profile)
        self.stop_work_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.start_work_btn = ttk.Button(actions, text="Start Work Profile", command=self.run_start_work_profile)
        self.start_work_btn.pack(side="left", padx=5, pady=10, ipadx=7, ipady=8)

        self.save_btn = ttk.Button(actions, text="Save Log", command=self.save_log)
        self.save_btn.pack(side="right", padx=5, pady=10, ipadx=7, ipady=8)

        self.status_label = ttk.Label(actions, text="Ready.", wraplength=230)
        self.status_label.pack(side="left", fill="x", expand=True, padx=8)

        progress_box = ttk.LabelFrame(root, text="Progress")
        progress_box.pack(fill="x", pady=(0, 10))

        self.step_label = ttk.Label(progress_box, text="Idle")
        self.step_label.pack(anchor="w", padx=8, pady=(8, 2))

        self.progress = ttk.Progressbar(progress_box, mode="determinate", maximum=100)
        self.progress.pack(fill="x", padx=8, pady=(0, 8))

        cards = ttk.Frame(root)
        cards.pack(fill="x", pady=(0, 10))

        self.card_watch = self.card(cards, "Watch", "Off", 0)
        self.card_device = self.card(cards, "ADB", "–", 1)
        self.card_work = self.card(cards, "Work Profile", "–", 2)
        self.card_receivers = self.card(cards, "Receivers", "–", 3)
        self.card_providers = self.card(cards, "Providers", "–", 4)
        self.card_active = self.card(cards, "Active", "–", 5)
        self.card_repairs = self.card(cards, "Auto Repairs", "0", 6)

        for i in range(7):
            cards.columnconfigure(i, weight=1)

        panes = ttk.PanedWindow(root, orient="horizontal")
        panes.pack(fill="both", expand=True)

        left = ttk.Frame(panes)
        middle = ttk.Frame(panes)
        right = ttk.Frame(panes)
        panes.add(left, weight=4)
        panes.add(middle, weight=3)
        panes.add(right, weight=2)

        widget_box = ttk.LabelFrame(left, text="All detected widgets")
        widget_box.pack(fill="both", expand=True, padx=(0, 8))

        filter_bar = ttk.Frame(widget_box)
        filter_bar.pack(fill="x", padx=8, pady=(8, 0))

        ttk.Button(filter_bar, text="Select All", command=self.select_all).pack(side="left", padx=(0, 5))
        ttk.Button(filter_bar, text="Select None", command=self.select_none).pack(side="left", padx=(0, 5))
        ttk.Button(filter_bar, text="Only Missing Providers", command=self.select_missing_providers).pack(side="left", padx=(0, 5))
        ttk.Button(filter_bar, text="Only Registered", command=self.select_registered).pack(side="left", padx=(0, 5))

        ttk.Label(filter_bar, text="Filter:").pack(side="left", padx=(15, 4))
        self.filter_var = tk.StringVar(value="")
        filter_entry = ttk.Entry(filter_bar, textvariable=self.filter_var, width=24)
        filter_entry.pack(side="left", padx=(0, 5))
        filter_entry.bind("<KeyRelease>", lambda event: self.populate_widget_tree())

        cols = ("selected", "group", "package", "receiver", "registered", "active")
        self.widget_tree = ttk.Treeview(widget_box, columns=cols, show="headings", selectmode="extended")
        self.widget_tree.heading("selected", text="Sel")
        self.widget_tree.heading("group", text="Group")
        self.widget_tree.heading("package", text="Package")
        self.widget_tree.heading("receiver", text="Receiver")
        self.widget_tree.heading("registered", text="Provider")
        self.widget_tree.heading("active", text="Active")
        self.widget_tree.column("selected", width=42, anchor="center")
        self.widget_tree.column("group", width=85)
        self.widget_tree.column("package", width=260)
        self.widget_tree.column("receiver", width=420)
        self.widget_tree.column("registered", width=72, anchor="center")
        self.widget_tree.column("active", width=60, anchor="center")
        self.widget_tree.pack(side="left", fill="both", expand=True, padx=(8, 0), pady=8)
        self.widget_tree.bind("<Double-1>", self.repair_tree_row_double_click)
        self.widget_tree.bind("<space>", self.toggle_selected_rows)

        widget_scroll = ttk.Scrollbar(widget_box, orient="vertical", command=self.widget_tree.yview)
        self.widget_tree.configure(yscrollcommand=widget_scroll.set)
        widget_scroll.pack(side="right", fill="y", padx=(0, 8), pady=8)

        log_box = ttk.LabelFrame(middle, text="Live Log")
        log_box.pack(fill="both", expand=True, padx=(0, 8))

        self.live_log = tk.Text(log_box, wrap="word")
        self.live_log.pack(side="left", fill="both", expand=True, padx=(8, 0), pady=8)
        log_scroll = ttk.Scrollbar(log_box, orient="vertical", command=self.live_log.yview)
        self.live_log.configure(yscrollcommand=log_scroll.set)
        log_scroll.pack(side="right", fill="y", padx=(0, 8), pady=8)

        summary_box = ttk.LabelFrame(right, text="Summary")
        summary_box.pack(fill="both", expand=True)

        self.summary = tk.Text(summary_box, wrap="word")
        self.summary.pack(fill="both", expand=True, padx=8, pady=8)

    def card(self, parent, title, text, col):
        frame = ttk.LabelFrame(parent, text=title)
        frame.grid(row=0, column=col, sticky="nsew", padx=4)
        label = ttk.Label(frame, text=text, font=("Segoe UI", 9, "bold"), justify="center", wraplength=150)
        label.pack(fill="both", expand=True, padx=8, pady=12)
        return label

    def pick_adb(self):
        path = filedialog.askopenfilename(title="Choose adb.exe", filetypes=[("adb.exe", "adb.exe"), ("All files", "*.*")])
        if path:
            self.adb_var.set(path)

    def handle_queue(self):
        try:
            while True:
                kind, *rest = self.q.get_nowait()

                if kind == "log":
                    self.append_log(rest[0])
                elif kind == "status":
                    self.last_status = rest[0]
                    self.update_status_ui(rest[0])
                elif kind == "summary":
                    self.summary.delete("1.0", "end")
                    self.summary.insert("1.0", rest[0])
                elif kind == "widgets":
                    self.populate_widget_tree()
                elif kind == "busy":
                    self.running_action = True
                    self.progress.configure(mode="indeterminate")
                    self.progress.start(10)
                    self.step_label.configure(text=rest[0])
                    self.status_label.configure(text=rest[0])
                    self.set_action_buttons(False)
                elif kind == "done":
                    self.running_action = False
                    self.progress.stop()
                    self.progress.configure(mode="determinate", value=100)
                    self.step_label.configure(text=rest[0])
                    self.status_label.configure(text=rest[0])
                    self.set_action_buttons(True)
                elif kind == "watch":
                    enabled = rest[0]
                    self.card_watch.configure(text="On" if enabled else "Off")
                    self.watch_btn.configure(text="Stop Auto Watch" if enabled else "Start Auto Watch")
                elif kind == "error":
                    self.running_action = False
                    self.progress.stop()
                    self.progress.configure(mode="determinate", value=0)
                    self.status_label.configure(text="Error: " + rest[0])
                    self.step_label.configure(text="Error")
                    self.append_log("ERROR: " + rest[0])
                    self.set_action_buttons(True)
        except queue.Empty:
            pass

        self.after(100, self.handle_queue)

    def set_action_buttons(self, enabled):
        state = "normal" if enabled else "disabled"
        self.refresh_btn.configure(state=state)
        self.repair_btn.configure(state=state)
        self.enable_btn.configure(state=state)
        self.disable_btn.configure(state=state)
        self.save_btn.configure(state=state)
        self.stop_work_btn.configure(state=state)
        self.start_work_btn.configure(state=state)

    def append_log(self, text):
        stamp = datetime.now().strftime("%H:%M:%S")
        self.live_log.insert("end", f"[{stamp}] {text}\n")
        self.live_log.see("end")
        self.update_idletasks()

    def log(self, text):
        self.q.put(("log", text))

    def adb(self, *args, timeout=120, log_output=True):
        adb_path = self.adb_var.get().strip() or "adb"
        cmd = [adb_path] + list(args)
        flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0
        self.log("$ " + " ".join(cmd))

        try:
            proc = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
                creationflags=flags,
                encoding="utf-8",
                errors="replace",
            )
            out = proc.stdout.strip()
            err = proc.stderr.strip()

            if log_output:
                if out:
                    self.log(out[:2500] + (" ...[truncated]" if len(out) > 2500 else ""))
                if err:
                    self.log("STDERR: " + err[:2500] + (" ...[truncated]" if len(err) > 2500 else ""))

            return proc.returncode, out, err, " ".join(cmd)
        except subprocess.TimeoutExpired:
            self.log("TIMEOUT")
            return 124, "", "Timeout", " ".join(cmd)
        except Exception as exc:
            self.log("ERROR: " + repr(exc))
            return 1, "", repr(exc), " ".join(cmd)

    def run_refresh_all_widgets(self):
        self.run_worker("Refreshing all widgets...", self.workflow_refresh_all_widgets)

    def workflow_refresh_all_widgets(self):
        status = self.collect_status("Refresh all widgets")
        self.last_report = status["summary"]
        self.q.put(("widgets",))

    def collect_status(self, label="Status"):
        user = self.user_var.get().strip() or DEFAULT_USER
        status = {
            "label": label,
            "device_ok": False,
            "boot_id": "",
            "boot_completed": False,
            "receiver_count": 0,
            "provider_count": 0,
            "active_count": 0,
            "widgets_size": [],
            "diagnosis": "",
            "summary": "",
        }

        rc_dev, out_dev, err_dev, _ = self.adb("devices", timeout=40, log_output=False)
        status["device_ok"] = ("\tdevice" in out_dev) or ("\ndevice" in out_dev) or out_dev.strip().endswith("device")

        rc_boot, out_boot, err_boot, _ = self.adb("shell", "cat", "/proc/sys/kernel/random/boot_id", timeout=20, log_output=False)
        status["boot_id"] = out_boot.strip() if rc_boot == 0 else ""

        rc_completed, out_completed, err_completed, _ = self.adb("shell", "getprop", "sys.boot_completed", timeout=20, log_output=False)
        status["boot_completed"] = out_completed.strip() == "1"

        rc_recv, out_recv, err_recv, _ = self.adb(
            "shell", "cmd", "package", "query-receivers",
            "-a", "android.appwidget.action.APPWIDGET_UPDATE",
            "--user", user,
            timeout=180,
            log_output=False,
        )
        receivers = self.parse_receivers(out_recv if rc_recv == 0 else err_recv)

        rc_dump, out_dump, err_dump, _ = self.adb("shell", "dumpsys", "appwidget", timeout=180, log_output=False)
        dump = out_dump if rc_dump == 0 else err_dump

        self.registered_providers = self.parse_providers(dump)
        self.active_components = self.parse_active_components(dump)

        old_selected = {component: item.get("selected", True) for component, item in self.all_widgets.items()}
        widgets = {}

        for receiver in receivers:
            component = f"{receiver['package']}/{receiver['class']}"
            widgets[component] = {
                "package": receiver["package"],
                "class": receiver["class"],
                "group": self.group_for(component),
                "selected": old_selected.get(component, receiver["package"] in DEFAULT_IMPORTANT_PACKAGES),
            }

        for component in self.registered_providers:
            if component not in widgets:
                package, cls = component.split("/", 1)
                widgets[component] = {
                    "package": package,
                    "class": cls,
                    "group": self.group_for(component),
                    "selected": old_selected.get(component, package in DEFAULT_IMPORTANT_PACKAGES),
                }

        self.all_widgets = dict(sorted(widgets.items(), key=lambda kv: (kv[1]["group"], kv[1]["package"], kv[1]["class"])))

        status["receiver_count"] = len(receivers)
        status["provider_count"] = len(self.registered_providers)
        status["active_count"] = len(self.active_components.intersection(set(self.all_widgets.keys())))
        status["widgets_size"] = re.findall(r"widgets\.size=(\d+)", dump)
        status["diagnosis"] = self.make_diagnosis(status)
        status["summary"] = self.format_status(status)

        self.q.put(("status", status))
        self.q.put(("summary", status["summary"]))
        return status

    def parse_receivers(self, text):
        receivers = []
        seen = set()
        in_activity = False
        current_name = None

        for raw in text.splitlines():
            line = raw.strip()

            if line == "ActivityInfo:":
                in_activity = True
                current_name = None
                continue

            if line == "ApplicationInfo:":
                in_activity = False
                current_name = None
                continue

            if in_activity and line.startswith("name="):
                current_name = line.split("name=", 1)[1].split()[0]
                continue

            if in_activity and current_name and line.startswith("packageName="):
                package = line.split("packageName=", 1)[1].split()[0]
                component = f"{package}/{current_name}"
                if component not in seen:
                    receivers.append({"package": package, "class": current_name})
                    seen.add(component)
                current_name = None

        return receivers

    def parse_providers(self, dump):
        providers = set()
        section = dump
        match = re.search(r"Providers:\s*(.*?)\n\s*Widgets:", dump, re.S)

        if match:
            section = match.group(1)

        for package, cls in re.findall(r"ComponentInfo\{([^/}]+)/([^}]+)\}", section):
            providers.add(f"{package}/{cls}")

        return providers

    def parse_active_components(self, dump):
        active = set()
        section = ""
        match = re.search(r"Widgets:\s*(.*?)\n\s*Hosts:", dump, re.S)

        if match:
            section = match.group(1)

        for package, cls in re.findall(r"ComponentInfo\{([^/}]+)/([^}]+)\}", section):
            active.add(f"{package}/{cls}")

        return active

    def group_for(self, component):
        for name, pattern in GROUP_RULES:
            if pattern.search(component):
                return name
        return "Other"

    def make_diagnosis(self, status):
        if not status["device_ok"]:
            return "Device not connected"
        if not status["boot_completed"]:
            return "Boot not completed"
        if status["receiver_count"] > 0 and status["provider_count"] == 0:
            return "Repair needed: receivers exist but providers are zero"
        if status["provider_count"] > 0 and all(value == "0" for value in status["widgets_size"]):
            return "Providers exist but launcher has no active widget bindings"
        if status["provider_count"] > 0:
            return "Providers are registered"
        return "Unknown"

    def format_status(self, status):
        boot_short = status["boot_id"][:8] if status["boot_id"] else "unknown"
        widget_size = ", ".join(status["widgets_size"]) if status["widgets_size"] else "missing"
        selected_count = len(self.selected_components())
        selected_packages = len(self.selected_packages())

        return "\n".join([
            f"{status['label']}",
            "=" * 38,
            f"ADB device: {'OK' if status['device_ok'] else 'Not connected'}",
            f"Boot completed: {'Yes' if status['boot_completed'] else 'No'}",
            f"Boot ID: {boot_short}",
            f"Detected widget receivers: {status['receiver_count']}",
            f"Registered providers: {status['provider_count']}",
            f"Active detected widgets: {status['active_count']}",
            f"Pixel Launcher widgets.size: {widget_size}",
            f"Selected widgets: {selected_count}",
            f"Selected apps/packages: {selected_packages}",
            f"Diagnosis: {status['diagnosis']}",
            f"Auto repairs this session: {self.auto_repair_count}",
        ])

    def update_status_ui(self, status):
        self.card_device.configure(text="OK" if status.get("device_ok") else "Not connected")
        self.card_receivers.configure(text=str(status.get("receiver_count", 0)))
        self.card_providers.configure(text=str(status.get("provider_count", 0)))
        self.card_active.configure(text=str(status.get("active_count", 0)))
        self.card_repairs.configure(text=str(self.auto_repair_count))

        if self.work_user_var.get().strip() and self.work_user_var.get().strip() != "auto":
            self.card_work.configure(text=f"User {self.work_user_var.get().strip()}")

        self.populate_widget_tree()

    def populate_widget_tree(self):
        if not hasattr(self, "widget_tree"):
            return

        for item in self.widget_tree.get_children():
            self.widget_tree.delete(item)

        term = self.filter_var.get().strip().lower() if hasattr(self, "filter_var") else ""

        for component, item in self.all_widgets.items():
            package = item["package"]
            cls = item["class"]
            group = item["group"]

            if term and term not in component.lower() and term not in group.lower():
                continue

            selected = "✓" if item.get("selected") else ""
            registered = "Yes" if component in self.registered_providers else "No"
            active = "Yes" if component in self.active_components else "No"
            iid = component
            self.widget_tree.insert("", "end", iid=iid, values=(selected, group, package, cls, registered, active))

    def repair_tree_row_double_click(self, event=None):
        item = self.widget_tree.identify_row(event.y) if event else None
        if not item or item not in self.all_widgets:
            return "break"

        package = self.all_widgets[item]["package"]
        confirmed = messagebox.askyesno(
            "Repair App",
            "Reinstall the app package behind this widget?\n\n"
            f"Widget:\n{item}\n\n"
            f"Package:\n{package}\n\n"
            "This does not run pm clear and does not clear Pixel Launcher."
        )

        if confirmed:
            self.run_worker("Repairing app from double-click...", lambda: self.workflow_repair_packages([package], auto=False, reason=f"Double-click repair: {item}"))

        return "break"

    def toggle_tree_selected(self, event=None):
        item = self.widget_tree.identify_row(event.y) if event else None
        if item and item in self.all_widgets:
            self.all_widgets[item]["selected"] = not self.all_widgets[item].get("selected", False)
            self.populate_widget_tree()

    def toggle_selected_rows(self, event=None):
        for item in self.widget_tree.selection():
            if item in self.all_widgets:
                self.all_widgets[item]["selected"] = not self.all_widgets[item].get("selected", False)
        self.populate_widget_tree()
        return "break"

    def selected_components(self):
        return [component for component, item in self.all_widgets.items() if item.get("selected")]

    def selected_widgets(self):
        return [(component, self.all_widgets[component]) for component in self.selected_components()]

    def selected_packages(self):
        return sorted(set(item["package"] for component, item in self.selected_widgets()))

    def select_all(self):
        for item in self.all_widgets.values():
            item["selected"] = True
        self.populate_widget_tree()
        self.refresh_summary_selection()

    def select_none(self):
        for item in self.all_widgets.values():
            item["selected"] = False
        self.populate_widget_tree()
        self.refresh_summary_selection()

    def select_missing_providers(self):
        for component, item in self.all_widgets.items():
            item["selected"] = component not in self.registered_providers
        self.populate_widget_tree()
        self.refresh_summary_selection()

    def select_registered(self):
        for component, item in self.all_widgets.items():
            item["selected"] = component in self.registered_providers
        self.populate_widget_tree()
        self.refresh_summary_selection()

    def refresh_summary_selection(self):
        if self.last_status:
            self.last_status["summary"] = self.format_status(self.last_status)
            self.q.put(("summary", self.last_status["summary"]))

    def run_enable_selected(self):
        if not self.selected_components():
            messagebox.showwarning("No widgets selected", "Select at least one widget first.")
            return
        self.run_worker("Enabling selected widgets...", self.workflow_enable_selected)

    def workflow_enable_selected(self):
        user = self.user_var.get().strip() or DEFAULT_USER
        lines = ["Enable Selected Widgets report", "=" * 36]

        for component, item in self.selected_widgets():
            rc, out, err, cmd = self.adb("shell", "pm", "enable", "--user", user, component, timeout=80)
            lines.append(f"{component}: rc={rc}\n{out}\n{err}".strip())

        for package in self.selected_packages():
            self.adb("shell", "pm", "enable", "--user", user, package, timeout=80)

        status = self.collect_status("After enabling selected widgets")
        lines.append("")
        lines.append(status["summary"])
        self.last_report = "\n".join(lines)
        self.q.put(("summary", self.last_report))

    def run_disable_selected(self):
        if not self.selected_components():
            messagebox.showwarning("No widgets selected", "Select at least one widget first.")
            return

        confirmed = messagebox.askyesno(
            "Disable Selected Widgets",
            "This disables selected widget receiver components with pm disable-user.\n\nExisting widgets using those providers may disappear or stop working until enabled again.\n\nContinue?"
        )
        if confirmed:
            self.run_worker("Disabling selected widgets...", self.workflow_disable_selected)

    def workflow_disable_selected(self):
        user = self.user_var.get().strip() or DEFAULT_USER
        lines = ["Disable Selected Widgets report", "=" * 37]

        for component, item in self.selected_widgets():
            rc, out, err, cmd = self.adb("shell", "pm", "disable-user", "--user", user, component, timeout=80)
            lines.append(f"{component}: rc={rc}\n{out}\n{err}".strip())

        status = self.collect_status("After disabling selected widgets")
        lines.append("")
        lines.append(status["summary"])
        self.last_report = "\n".join(lines)
        self.q.put(("summary", self.last_report))

    def run_repair_selected(self):
        packages = self.selected_packages()
        if not packages:
            messagebox.showwarning("No widgets selected", "Select at least one widget first.")
            return

        confirmed = messagebox.askyesno(
            "Repair Selected Apps",
            f"This will reinstall {len(packages)} selected app package(s) over themselves using ADB.\n\nIt does not run pm clear and it does not clear Pixel Launcher.\n\nContinue?"
        )
        if confirmed:
            self.run_worker("Repairing selected apps...", lambda: self.workflow_repair_selected(auto=False))

    def workflow_repair_selected(self, auto=False):
        packages = self.selected_packages()
        return self.workflow_repair_packages(packages, auto=auto, reason="Selected repair")

    def workflow_repair_packages(self, packages, auto=False, reason="Repair"):
        self.running_action = True

        try:
            user = self.user_var.get().strip() or DEFAULT_USER
            launcher = self.launcher_var.get().strip() or DEFAULT_LAUNCHER

            before = self.collect_status(f"Before {reason}")
            package_paths = {}

            for package in packages:
                rc, out, err, _ = self.adb("shell", "pm", "path", package, timeout=80)
                paths = [line.replace("package:", "", 1).strip() for line in out.splitlines() if line.startswith("package:")] if rc == 0 else []
                package_paths[package] = paths
                self.log(f"{package}: {len(paths)} APK/split files found.")

            with tempfile.TemporaryDirectory(prefix="adb_widget_selected_repair_") as tmp:
                tmpdir = Path(tmp)
                local_by_package = {}

                for package in packages:
                    local_dir = tmpdir / package
                    local_dir.mkdir(parents=True, exist_ok=True)
                    local_files = []

                    for index, remote in enumerate(package_paths.get(package, [])):
                        local = local_dir / (f"{index:02d}_" + Path(remote).name)
                        rc, out, err, _ = self.adb("pull", remote, str(local), timeout=180)
                        if rc == 0 and local.exists():
                            local_files.append(str(local))

                    local_by_package[package] = local_files
                    self.log(f"{package}: {len(local_files)} local APK/split files ready.")

                for package in packages:
                    self.install_files(local_by_package.get(package, []), package)

            for package in packages:
                self.adb("shell", "pm", "enable", "--user", user, package, timeout=80)

            for component in self.selected_components():
                self.adb("shell", "pm", "enable", "--user", user, component, timeout=80)

            self.adb("shell", "appwidget", "grantbind", "--package", launcher, "--user", user, timeout=80)

            for package in packages:
                self.adb("shell", "monkey", "-p", package, "-c", "android.intent.category.LAUNCHER", "1", timeout=45)
                time.sleep(0.3)

            for package in packages:
                self.adb("shell", "am", "force-stop", package, timeout=45)

            self.adb("shell", "am", "force-stop", launcher, timeout=60)
            self.adb("shell", "input", "keyevent", "KEYCODE_HOME", timeout=30)
            time.sleep(3)

            after = self.collect_status(f"After {reason}")

            if auto:
                self.auto_repair_count += 1
                self.card_repairs.configure(text=str(self.auto_repair_count))

            self.last_report = self.build_repair_report(before, after, auto, packages)
            self.q.put(("summary", self.last_report))
            self.q.put(("widgets",))

        finally:
            self.running_action = False

    def install_files(self, files, label):
        if not files:
            self.log(f"{label}: no files to install.")
            return

        if len(files) == 1:
            self.adb("install", "-r", files[0], timeout=300)
        else:
            self.adb("install-multiple", "-r", *files, timeout=480)

    def build_repair_report(self, before, after, auto, packages):
        return "\n".join([
            "ADB Widget Manager v11 selected repair report",
            "=" * 52,
            datetime.now().isoformat(),
            f"Mode: {'Automatic' if auto else 'Manual'}",
            "Packages:",
            *[f"- {package}" for package in packages],
            "",
            "Before:",
            before["summary"],
            "",
            "After:",
            after["summary"],
        ])

    def detect_work_profiles(self):
        rc, out, err, _ = self.adb("shell", "pm", "list", "users", timeout=40)
        text = out if rc == 0 else err
        profiles = []

        for line in text.splitlines():
            match = re.search(r"UserInfo\{(\d+):([^:}]+):([^}]+)\}", line)
            if not match:
                continue

            user_id = match.group(1)
            name = match.group(2)
            flags = match.group(3)
            low = (name + " " + flags + " " + line).lower()

            if user_id != "0" and ("managed" in low or "work" in low or "profile" in low or "jobb" in low):
                profiles.append({"id": user_id, "name": name, "flags": flags, "raw": line.strip()})

        if not profiles:
            for line in text.splitlines():
                match = re.search(r"UserInfo\{(\d+):([^:}]+):([^}]+)\}", line)
                if match and match.group(1) != "0":
                    profiles.append({"id": match.group(1), "name": match.group(2), "flags": match.group(3), "raw": line.strip()})

        self.work_profiles = profiles

        if profiles:
            self.work_user_var.set(profiles[0]["id"])
            self.card_work.configure(text=f"User {profiles[0]['id']}\n{profiles[0]['name']}")
        else:
            self.card_work.configure(text="Not found")

        return profiles, text

    def work_user_id(self):
        value = self.work_user_var.get().strip()

        if value.lower() == "auto" or not value:
            profiles, _ = self.detect_work_profiles()
            if profiles:
                return profiles[0]["id"]
            return None

        return value

    def run_detect_work_profile(self):
        self.run_worker("Detecting work profile...", self.workflow_detect_work_profile)

    def workflow_detect_work_profile(self):
        profiles, raw = self.detect_work_profiles()
        lines = ["Work profile detection", "=" * 28, raw, ""]

        if profiles:
            lines.append("Detected profiles:")
            for profile in profiles:
                lines.append(f"- User {profile['id']}: {profile['name']} | {profile['flags']}")
        else:
            lines.append("No work profile was detected.")

        self.last_report = "\n".join(lines)
        self.q.put(("summary", self.last_report))

    def run_stop_work_profile(self):
        confirmed = messagebox.askyesno(
            "Stop Work Profile",
            "This uses the original v11 behavior:\n\nadb shell am stop-user -f <workProfileUserId>\n\nOn your phone Android may block this with SecurityException.\n\nContinue?"
        )
        if confirmed:
            self.run_worker("Stopping work profile...", self.workflow_stop_work_profile)

    def workflow_stop_work_profile(self):
        user_id = self.work_user_id()
        if not user_id:
            raise RuntimeError("No work profile user was found.")

        self.adb("shell", "am", "stop-user", "-f", user_id, timeout=80)
        time.sleep(2)
        profiles, raw = self.detect_work_profiles()

        self.last_report = "\n".join([
            "Stop Work Profile report",
            "=" * 28,
            f"Target user: {user_id}",
            "",
            raw,
        ])
        self.q.put(("summary", self.last_report))

    def run_start_work_profile(self):
        confirmed = messagebox.askyesno(
            "Start Work Profile",
            "This starts the work profile user again.\n\nContinue?"
        )
        if confirmed:
            self.run_worker("Starting work profile...", self.workflow_start_work_profile)

    def workflow_start_work_profile(self):
        user_id = self.work_user_id()
        if not user_id:
            raise RuntimeError("No work profile user was found.")

        self.adb("shell", "am", "start-user", user_id, timeout=80)
        time.sleep(2)
        profiles, raw = self.detect_work_profiles()

        self.last_report = "\n".join([
            "Start Work Profile report",
            "=" * 29,
            f"Target user: {user_id}",
            "",
            raw,
        ])
        self.q.put(("summary", self.last_report))

    def toggle_watch(self):
        if self.watch_thread and self.watch_thread.is_alive():
            self.stop_watch()
        else:
            self.start_watch()

    def start_watch(self):
        self.watch_stop.clear()
        self.watch_thread = threading.Thread(target=self.watch_loop, daemon=True)
        self.watch_thread.start()
        self.q.put(("watch", True))
        self.log("Auto Watch started.")

    def stop_watch(self):
        self.watch_stop.set()
        self.q.put(("watch", False))
        self.log("Auto Watch stopped.")

    def watch_loop(self):
        self.last_boot_id = None

        while not self.watch_stop.is_set():
            try:
                if self.running_action:
                    time.sleep(3)
                    continue

                status = self.collect_status("Auto Watch status")
                boot_id = status.get("boot_id") or ""

                if boot_id and self.last_boot_id and boot_id != self.last_boot_id:
                    self.log(f"Reboot detected: {self.last_boot_id[:8]} -> {boot_id[:8]}")
                    self.wait_for_boot_ready()
                    status = self.collect_status("Post-reboot status")

                if boot_id:
                    self.last_boot_id = boot_id

                important_missing = (
                    status.get("device_ok")
                    and status.get("boot_completed")
                    and status.get("receiver_count", 0) > 0
                    and status.get("provider_count", 0) == 0
                )

                if important_missing:
                    self.log("Auto Watch detected Providers=0 while receivers exist. Running selected automatic repair.")
                    if not self.selected_packages():
                        for component, item in self.all_widgets.items():
                            if item["package"] in DEFAULT_IMPORTANT_PACKAGES:
                                item["selected"] = True
                    self.workflow_repair_selected(auto=True)
                else:
                    self.log(f"Auto Watch check OK: providers={status.get('provider_count', 0)}, receivers={status.get('receiver_count', 0)}")

                for _ in range(20):
                    if self.watch_stop.is_set():
                        break
                    time.sleep(1)

            except Exception as exc:
                self.log("Auto Watch error: " + repr(exc))
                time.sleep(5)

        self.q.put(("watch", False))

    def wait_for_boot_ready(self):
        self.log("Waiting for device...")
        self.adb("wait-for-device", timeout=360, log_output=False)

        for _ in range(150):
            rc, out, err, _ = self.adb("shell", "getprop", "sys.boot_completed", timeout=10, log_output=False)
            if out.strip() == "1":
                self.log("Boot completed.")
                break
            time.sleep(2)

        self.log("Waiting 20 seconds for services to settle.")
        time.sleep(20)

    def run_worker(self, label, func):
        if self.running_action:
            return

        def worker():
            self.q.put(("busy", label))
            try:
                func()
                self.q.put(("done", "Done."))
            except Exception as exc:
                self.q.put(("error", repr(exc)))

        threading.Thread(target=worker, daemon=True).start()

    def save_log(self):
        path = filedialog.asksaveasfilename(
            title="Save Log",
            defaultextension=".txt",
            filetypes=[("Text file", "*.txt"), ("All files", "*.*")]
        )

        if not path:
            return

        content = [
            "=== Latest report ===",
            self.last_report or self.last_status.get("summary", ""),
            "",
            "=== Live log ===",
            self.live_log.get("1.0", "end"),
        ]

        Path(path).write_text("\n".join(content), encoding="utf-8", errors="replace")
        messagebox.showinfo("Saved", f"Saved:\n{path}")


if __name__ == "__main__":
    App().mainloop()
Editor is loading...
Leave a Comment