Untitled
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