-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathandroid_debloater.py
More file actions
326 lines (274 loc) · 12.4 KB
/
android_debloater.py
File metadata and controls
326 lines (274 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
import re
import threading
import subprocess
import tkinter as tk
from pathlib import Path
from gui import GUI, DefaultPackageManager
from tkinter import ttk, filedialog, messagebox
class AndroidDebloater(GUI, DefaultPackageManager):
"""A GUI application to debloat Android devices using ADB."""
def __init__(self, root: tk.Tk, title: str):
"""
Initialize the AndroidDebloater application.
Args:
root (tk.Tk): The root Tkinter window.
title (str): The title of the application window.
"""
self.adb_active: bool = False
self.selected_apps: set = set()
self.devices: list = []
self.device_info: dict = {}
super().__init__(root, title)
self.adb_path: Path = Path("assets/adb/adb.exe")
self.package_command = lambda: self.debloat_selected(self.package_tree_holder)
def execute(self, command: str, print_log: bool = True) -> subprocess.CompletedProcess | None:
"""
Execute the given adb command.
Args:
command (str): The adb command to execute.
print_log (bool): Whether to print the command log. Defaults to True.
Returns:
subprocess.CompletedProcess | None: The result of the command execution.
"""
command_list = [self.adb_path] + command.split()
try:
return subprocess.run(
command_list,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW
)
except subprocess.CalledProcessError as e:
if print_log:
self.log_message(f"Error: {e.stderr}")
return None
def start_adb(self) -> None:
"""Start the adb server."""
threading.Thread(target=self._start_adb_thread).start()
def _start_adb_thread(self) -> None:
"""Start the adb server in a separate thread."""
try:
self.log_message("ADB server is starting...")
self.execute("start-server")
self.adb_active = True
self.log_message("ADB server started.")
self.get_device_name()
except Exception as e:
self.log_message(f"Failed to start ADB: {e}")
def stop_adb(self) -> None:
"""Stop the adb server."""
try:
if self.adb_active:
self.execute("kill-server")
self.adb_active = False
self.log_message("ADB server stopped.")
except Exception as e:
self.log_message(f"Failed to stop ADB: {e}")
def get_device_model(self, device: str) -> str:
"""
Get the model name of the connected device.
Args:
device (str): The device ID.
Returns:
str: The model name of the device.
"""
try:
result = self.execute(f"-s {device} shell getprop ro.product.model")
if result and result.returncode == 0:
return result.stdout.strip()
return "unknown"
except Exception as e:
self.log_message(f"Failed to get model for device {device}: {e}")
return "unknown"
def get_device_name(self) -> None:
"""Fetch the list of connected devices."""
if not self.adb_active:
self.log_message("ADB is not active. Start ADB first.")
return
threading.Thread(target=self._get_device_name_thread).start()
def _get_device_name_thread(self) -> None:
"""Fetch the list of connected devices in a separate thread."""
try:
result = self.execute("devices")
if not result:
return
devices_output = result.stdout.strip()
device_lines = devices_output.splitlines()[1:]
device_names = []
for line in device_lines:
device_id, status = line.split()
if status == "unauthorized":
self.show_permission_warning(device_id)
return
model = self.get_device_model(device_id)
device_names.append(f"{model} - {device_id}")
self.device_dropdown["values"] = device_names
if device_names:
self.device_var.set(device_names[0])
except Exception as e:
self.log_message(f"Failed to get device names: {e}")
def get_selected_device_id(self) -> tuple[str, str] | None:
"""
Get the ID of the selected device.
Returns:
tuple[str, str] | None: The device ID and model name.
"""
selected_device = self.device_var.get()
if selected_device:
model, device_id = selected_device.rsplit(" - ", 1)
return device_id, model
self.log_message("No Device Selected")
return None
def fetch_apps(self) -> None:
"""Fetch the list of installed applications on the selected device."""
threading.Thread(target=self._fetch_apps_thread).start()
def _fetch_apps_thread(self) -> None:
"""Fetch the list of installed applications in a separate thread."""
try:
device_info = self.get_selected_device_id()
if not device_info:
return
device, model = device_info
self.log_message(f"Fetching installed applications from {model} - {device}...")
result_all = self.execute(f"-s {device} shell pm list packages")
result_disabled = self.execute(f"-s {device} shell pm list packages -d")
result_system = self.execute(f"-s {device} shell pm list packages -s")
if not result_all or not result_disabled or not result_system:
raise Exception("Error fetching package lists.")
all_apps = [line.replace("package:", "").strip() for line in result_all.stdout.splitlines() if line.strip()]
disabled_apps = {line.replace("package:", "").strip() for line in result_disabled.stdout.splitlines() if line.strip()}
system_apps = {line.replace("package:", "").strip() for line in result_system.stdout.splitlines() if line.strip()}
self.app_list = [
{"package": app, "status": "Disabled" if app in disabled_apps else "Active",
"type": "System" if app in system_apps else "User"}
for app in all_apps
]
self.log_message(f"Loaded {len(self.app_list)} applications for {model} - {device}.")
self.update_app_tree()
except Exception as e:
self.log_message(f"Error fetching applications: {e}")
def load_applications(self) -> None:
"""Load the list of applications from the device."""
if not self.adb_active:
self.log_message("ADB is not active. Start ADB first.")
return
self.fetch_apps()
def debloat(self, apps: list[str]) -> None:
"""
Uninstall the selected applications from the device.
Args:
apps (list[str]): The list of application package names to uninstall.
"""
threading.Thread(target=self._debloat_thread, args=(apps,)).start()
def _debloat_thread(self, apps: list[str]) -> None:
"""Uninstall the selected applications in a separate thread."""
for app in apps:
try:
device_info = self.get_selected_device_id()
if not device_info:
return
device, _ = device_info
self.log_message(f"Debloating {app}...")
result = self.execute(f"-s {device} shell pm uninstall -k --user 0 {app}")
if result and result.returncode == 0:
self.log_message(f"Successfully debloated: {app}")
self.app_list = [
item for item in self.app_list if item["package"] != app
]
else:
raise Exception(result.stderr if result else "Unknown error")
except Exception as e:
self.log_message(f"Failed to debloat {app}: {e}\nDid you connect the device?")
self.update_app_tree()
def debloat_selected(self, package_tree: ttk.Treeview = None) -> None:
"""
Uninstall the selected applications from the tree view.
Args:
package_tree (ttk.Treeview, optional): The tree view containing the application packages.
"""
selected_items = package_tree.selection() if package_tree else self.app_tree.selection()
if not selected_items:
self.log_message("No application selected for debloating.")
return
selected_apps = [
package_tree.item(item, "values")[1] if package_tree else self.app_tree.item(item, "values")[0]
for item in selected_items
]
self.debloat(selected_apps)
def remove_apps_from_path(self) -> None:
"""Remove applications from paths listed in a text file."""
if not self.adb_active:
self.log_message("ADB is not active. Start ADB first.")
return
if not self._check_root_access():
self.log_message("Root Access Required")
return
# Run the file dialog in a separate thread to avoid freezing the GUI
threading.Thread(target=self._select_and_process_file).start()
def _select_and_process_file(self) -> None:
"""Open a file dialog to select a text file and process it in a separate thread."""
exe_directory = Path(__file__).parent.resolve()
initial_directory = exe_directory / 'assets'
txt_file_path = filedialog.askopenfilename(
title="Select Txt File",
initialdir=initial_directory,
filetypes=(("Text Files", "*.txt"), ("All Files", "*.*"))
)
if not txt_file_path:
self.log_message("No file selected!")
return
confirm = messagebox.askyesno(
"Confirm Removal",
"Removing files can damage your device. Do you want to proceed?"
)
if not confirm:
self.log_message("File removal canceled by user.")
return
threading.Thread(target=self._remove_apps_in_thread, args=(txt_file_path,)).start()
def _remove_apps_in_thread(self, txt_file_path: str) -> None:
"""Remove applications from paths listed in a text file in a separate thread."""
try:
serial_number, device_model = self.get_selected_device_id()
with open(txt_file_path, "r") as file:
lines = file.readlines()
if not lines:
self.log_message("The selected file is empty.")
return
if any("/system" in line for line in lines):
self.execute(f"-s {serial_number} shell su -c mount -o rw,remount /system")
self.log_message("System mounted as READ/WRITE")
pattern = re.compile(r"/[^ ]+")
for line in lines:
matches = pattern.findall(line)
for match in matches:
app_path = match
rm_command = f"-s {serial_number} shell rm -r {app_path}"
result = self.execute(rm_command, print_log=False)
try:
if result.returncode == 0:
self.log_message(f"Successfully removed: {app_path}")
else:
self.log_message(f"Failed to remove: {app_path}. Error: {result.stderr}")
except AttributeError:
self.log_message(f"{app_path} already does not exist on {device_model} ({serial_number}).")
self.log_message("DONE!")
except Exception as e:
self.log_message(f"An error occurred: {e}")
def _check_root_access(self) -> bool:
"""Check if the device has root access."""
serial_number, _ = self.get_selected_device_id()
command = f"-s {serial_number} shell su -c echo rooted"
result = self.execute(command)
try:
if result and "rooted" in result.stdout:
return True
except AttributeError:
return False
return False
if __name__ == '__main__':
root = tk.Tk()
app = AndroidDebloater(root, "Android Debloater")
app.set_icon(root, Path("assets/android_debloater.ico"))
root.mainloop()