├── requirements.txt
├── preview.png
├── README.md
└── main.py
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyQt5
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehran-mousavi/Adb-AppManager-GUI/HEAD/preview.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android App Manager
2 |
3 | 
4 |
5 | Android App Manager is a Python-based GUI application that allows users to manage their Android applications via ADB (Android Debug Bridge). It provides functionality to list, search , disable, enable and uninstall applications.
6 |
7 | ## Features
8 |
9 | - List all installed applications on your Android device.
10 | - Application info
11 | - Debloating your phone
12 | - Disable any application.
13 | - Enable any application.
14 | - Uninstall any application.
15 | - Search for applications.
16 |
17 |
18 | ## Requirements
19 |
20 | - Python 3.x
21 | - PyQt5
22 | - ADB installed and set in PATH
23 |
24 | ## Usage
25 |
26 | 1. Connect your Android device to your computer.
27 | 2. Enable USB debugging on your Android device.
28 | 3. Run the script.
29 |
30 | ```bash
31 | pip install -r requirements.txt
32 | python main.py
33 | ```
34 |
35 | The application will start and display a GUI to manage your Android applications.
36 |
37 | ## Note
38 |
39 | This tool uses ADB commands to manage applications. Please ensure you understand the implications of disabling/enabling applications on your Android device. Always use this tool responsibly.
40 |
41 | ## Contributing
42 |
43 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
44 |
45 | ## License
46 |
47 | [MIT](https://choosealicense.com/licenses/mit/)
48 |
49 | ## Disclaimer
50 |
51 | This tool is for educational purposes only. The developer is not responsible for any misuse of this tool.
52 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import subprocess
3 | import json
4 | from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QLineEdit, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox
5 | from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
6 | from PyQt5.QtGui import QColor
7 |
8 | class AdbWorker(QThread):
9 | output = pyqtSignal(str)
10 |
11 | def __init__(self, command):
12 | super().__init__()
13 | self.command = command
14 |
15 | def run(self):
16 | try:
17 | result = subprocess.check_output(self.command, shell=True, text=True, stderr=subprocess.STDOUT)
18 | except subprocess.CalledProcessError as e:
19 | result = e.output
20 | self.output.emit(result)
21 |
22 | class AndroidAppManager(QMainWindow):
23 | def __init__(self):
24 | super().__init__()
25 | self.setWindowTitle("Android App Manager")
26 | self.setGeometry(100, 100, 800, 600)
27 | self.setStyleSheet("""
28 | QMainWindow {
29 | background-color: #2b2b2b;
30 | color: #ffffff;
31 | }
32 | QLabel, QPushButton, QComboBox, QLineEdit, QTableWidget {
33 | color: #ffffff;
34 | background-color: #3c3f41;
35 | border: 1px solid #555555;
36 | padding: 5px;
37 | border-radius: 3px;
38 | }
39 | QPushButton:hover {
40 | background-color: #4c5052;
41 | }
42 | QTableWidget {
43 | gridline-color: #555555;
44 | }
45 | QHeaderView::section {
46 | background-color: #3c3f41;
47 | color: #ffffff;
48 | padding: 5px;
49 | border: 1px solid #555555;
50 | }
51 | """)
52 |
53 | self.central_widget = QWidget()
54 | self.setCentralWidget(self.central_widget)
55 | self.layout = QVBoxLayout(self.central_widget)
56 |
57 | self.all_apps = [] # Store all apps for filtering
58 | self.workers = [] # Keep track of all running workers
59 | self.all_apps_output = "" # Initialize the attribute to store all apps output
60 | self.uad_info = {} # Will map package name to info
61 | self.load_uad_lists()
62 | self.setup_ui()
63 |
64 | def load_uad_lists(self):
65 | try:
66 | with open("uad_lists.json", "r", encoding="utf-8") as f:
67 | data = json.load(f)
68 | for entry in data:
69 | if "id" in entry:
70 | self.uad_info[entry["id"]] = entry
71 | except Exception as e:
72 | print(f"Failed to load uad_lists.json: {e}")
73 |
74 | def setup_ui(self):
75 | # Device selection
76 | device_layout = QHBoxLayout()
77 | self.device_combo = QComboBox()
78 | self.device_combo.setMinimumWidth(200)
79 | # Add event listener for device selection change
80 | self.device_combo.currentIndexChanged.connect(lambda: (self.all_apps.clear(), self.app_table.setRowCount(0), self.app_table.clearContents(), self.update_device_info(), self.load_all_apps()))
81 | self.refresh_devices_btn = QPushButton("Refresh Devices")
82 | self.refresh_devices_btn.clicked.connect(self.refresh_devices)
83 | device_layout.addWidget(QLabel("Select Device:"))
84 | device_layout.addWidget(self.device_combo)
85 | device_layout.addWidget(self.refresh_devices_btn)
86 | device_layout.addStretch()
87 | self.layout.addLayout(device_layout)
88 |
89 | # Device info
90 | self.device_info_label = QLabel("Device Info: Not connected")
91 | self.layout.addWidget(self.device_info_label)
92 |
93 | # Search
94 | search_layout = QHBoxLayout()
95 | self.search_input = QLineEdit()
96 | self.search_input.setPlaceholderText("Search apps...")
97 | self.search_btn = QPushButton("Search")
98 | self.search_btn.clicked.connect(self.search_apps)
99 | search_layout.addWidget(self.search_input)
100 | search_layout.addWidget(self.search_btn)
101 | self.layout.addLayout(search_layout)
102 |
103 | # App list
104 | self.app_table = QTableWidget()
105 | self.app_table.setColumnCount(4)
106 | self.app_table.setHorizontalHeaderLabels(["App Name", "Package Name", "Status", "Type"])
107 | self.app_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
108 | self.app_table.setSelectionBehavior(QTableWidget.SelectRows) # Change selection to row
109 | self.layout.addWidget(self.app_table)
110 | self.app_table.itemSelectionChanged.connect(self.update_app_desc)
111 |
112 | # App description area (moved below the grid)
113 | self.app_desc_label = QLabel()
114 | self.app_desc_label.setWordWrap(True)
115 | self.app_desc_label.setMinimumHeight(80)
116 | self.app_desc_label.setStyleSheet("background-color: #232629; color: #ffffff; border: 1px solid #555555; padding: 8px; border-radius: 3px;")
117 | self.layout.addWidget(self.app_desc_label)
118 | self.clear_app_desc()
119 |
120 | # Action buttons
121 | action_layout = QHBoxLayout()
122 | self.enable_btn = QPushButton("Enable")
123 | self.disable_btn = QPushButton("Disable")
124 | self.uninstall_btn = QPushButton("Uninstall")
125 | self.enable_btn.clicked.connect(self.enable_app)
126 | self.disable_btn.clicked.connect(self.disable_app)
127 | self.uninstall_btn.clicked.connect(self.uninstall_app)
128 | action_layout.addWidget(self.enable_btn)
129 | action_layout.addWidget(self.disable_btn)
130 | action_layout.addWidget(self.uninstall_btn)
131 | self.layout.addLayout(action_layout)
132 |
133 | self.refresh_devices()
134 |
135 | def clear_app_desc(self):
136 | self.app_desc_label.setText("App Description:
Select an app to see details.")
137 | self.app_desc_label.setStyleSheet("background-color: #232629; color: #ffffff; border: 1px solid #555555; padding: 8px; border-radius: 3px;")
138 |
139 | def update_app_desc(self):
140 | selected_items = self.app_table.selectedItems()
141 | if not selected_items:
142 | self.clear_app_desc()
143 | return
144 | row = selected_items[0].row()
145 | package = self.app_table.item(row, 1).text()
146 | info = self.uad_info.get(package)
147 | if not info:
148 | self.app_desc_label.setText(f"App Description:
No info available for {package}.")
149 | self.app_desc_label.setStyleSheet("background-color: #232629; color: #ffffff; border: 1px solid #555555; padding: 8px; border-radius: 3px;")
150 | return
151 | app_type = info.get("list", "Unknown")
152 | desc = info.get("description", "No description available.")
153 | removal = info.get("removal", "Recommended")
154 | if removal == "Recommended":
155 | removal_text = "Recommended: Safe to remove this app."
156 | elif removal == "Advanced":
157 | removal_text = "Important package. Removal is only recommended for advanced users."
158 | elif removal == "Expert":
159 | removal_text = "Critical package. Removing this may cause your device to become unusable (bricked)."
160 | else:
161 | removal_text = f"{removal}"
162 | html = f"""
163 | App Type: {app_type}
164 | Description: {desc}
165 | Removal: {removal_text}
166 | """
167 | self.app_desc_label.setText(html)
168 | self.app_desc_label.setStyleSheet("background-color: #232629; color: #ffffff; border: 1px solid #555555; padding: 8px; border-radius: 3px;")
169 |
170 | def run_adb_command(self, command, callback):
171 | worker = AdbWorker(command)
172 | worker.output.connect(callback)
173 | worker.finished.connect(lambda: self.workers.remove(worker)) # Remove worker from list when done
174 | self.workers.append(worker)
175 | worker.start()
176 |
177 | def refresh_devices(self):
178 | self.device_combo.clear()
179 | print("Refreshing devices...")
180 | self.run_adb_command("adb devices", self.update_device_list)
181 |
182 | def update_device_list(self, output):
183 | print(f"ADB output: {output}")
184 | devices = []
185 | for line in output.strip().split('\n')[1:]:
186 | if '\t' in line:
187 | device, status = line.split('\t')
188 | if status == 'device':
189 | devices.append(device)
190 | print(f"Detected devices: {devices}")
191 | self.device_combo.addItems(devices)
192 | if devices:
193 | self.device_combo.setCurrentIndex(0)
194 | # self.update_device_info()
195 | # self.load_all_apps() # Load all apps when a device is selected
196 | else:
197 | self.all_apps = [] # Clear the apps list
198 | self.app_table.setRowCount(0) # Clear the displayed rows in the app table
199 | self.app_table.clearContents() # Remove all items from the app table
200 | print("No devices detected")
201 |
202 | def update_device_info(self):
203 | device = self.device_combo.currentText()
204 | if device:
205 | self.run_adb_command(f"adb -s {device} shell getprop", self.display_device_info)
206 |
207 | def display_device_info(self, output):
208 | info = {}
209 | for line in output.split('\n'):
210 | if ':' in line:
211 | key, value = line.split(':', 1)
212 | info[key.strip()[1:-1]] = value.strip()[1:-1]
213 |
214 | device_info = f"Model: {info.get('ro.product.model', 'Unknown')}\t\t"
215 | device_info += f"Android Version: {info.get('ro.build.version.release', 'Unknown')}\t\t"
216 | device_info += f"API Level: {info.get('ro.build.version.sdk', 'Unknown')}"
217 | self.device_info_label.setText(f"Device Info:\n\n{device_info}")
218 |
219 | def load_all_apps(self):
220 | device = self.device_combo.currentText()
221 | if device:
222 | self.all_apps = [] # Clear the apps list
223 | self.app_table.setRowCount(0) # Clear the displayed rows in the app table
224 | self.app_table.clearContents() # Remove all items from the app table
225 | self.run_adb_command(f"adb -s {device} shell pm list packages -f -u", self.store_all_apps_output)
226 |
227 | def store_all_apps_output(self, output):
228 | self.all_apps_output = output # Store the output for later use
229 | self.store_all_apps()
230 |
231 | def store_all_apps(self):
232 | self.all_apps = []
233 | device = self.device_combo.currentText()
234 | if device:
235 | # Run the command to get disabled packages in a separate thread
236 | self.run_adb_command(f"adb -s {device} shell pm list packages -d", self.process_disabled_packages)
237 |
238 | def process_disabled_packages(self, disabled_output):
239 | disabled_packages = set(line.split(':')[1] for line in disabled_output.strip().split('\n') if line)
240 | for line in self.all_apps_output.strip().split('\n'):
241 | if '=' in line:
242 | path, package = line.rsplit('=', 1)
243 | app_name = package.split('.')[-1]
244 | status = "Disabled" if package in disabled_packages else "Enabled"
245 | app_type = "System" if "/system/" in path else "User"
246 | self.all_apps.append((app_name, package, status, app_type))
247 | self.display_apps(self.all_apps)
248 |
249 | def display_apps(self, apps):
250 | self.app_table.setRowCount(0)
251 | for app_name, package, status, app_type in apps:
252 | row = self.app_table.rowCount()
253 | self.app_table.insertRow(row)
254 | self.app_table.setItem(row, 0, QTableWidgetItem(app_name))
255 | self.app_table.setItem(row, 1, QTableWidgetItem(package))
256 | self.app_table.setItem(row, 2, QTableWidgetItem(status))
257 | self.app_table.setItem(row, 3, QTableWidgetItem(app_type))
258 | if status == "Disabled":
259 | for col in range(4):
260 | self.app_table.item(row, col).setBackground(QColor(244, 67, 54)) # Material Red
261 |
262 |
263 | def search_apps(self):
264 | query = self.search_input.text().lower()
265 | filtered_apps = [app for app in self.all_apps if query in app[1].lower()]
266 | self.display_apps(filtered_apps)
267 |
268 | def get_app_status(self, package):
269 | device = self.device_combo.currentText()
270 | result = subprocess.check_output(f"adb -s {device} shell pm list packages -d", shell=True, text=True)
271 | return "Disabled" if package in result else "Enabled"
272 |
273 | def enable_app(self):
274 | self.change_app_state("enable")
275 |
276 | def disable_app(self):
277 | self.change_app_state("disable")
278 |
279 | def change_app_state(self, action):
280 | device = self.device_combo.currentText()
281 | if not device:
282 | QMessageBox.warning(self, "Error", "No device selected")
283 | return
284 |
285 | selected_items = self.app_table.selectedItems()
286 | if not selected_items:
287 | QMessageBox.warning(self, "Error", "No app selected")
288 | return
289 |
290 | package = self.app_table.item(selected_items[0].row(), 1).text()
291 | command = f"adb -s {device} shell pm {action}{'-user' if action == 'disable' else ''} {package}"
292 | self.run_adb_command(command, lambda _: QTimer.singleShot(1000, self.load_all_apps)) # Delay before refreshing
293 |
294 | def uninstall_app(self):
295 | device = self.device_combo.currentText()
296 | if not device:
297 | QMessageBox.warning(self, "Error", "No device selected")
298 | return
299 |
300 | selected_items = self.app_table.selectedItems()
301 | if not selected_items:
302 | QMessageBox.warning(self, "Error", "No app selected")
303 | return
304 |
305 | row = selected_items[0].row()
306 | package = self.app_table.item(row, 1).text()
307 | app_type = self.app_table.item(row, 3).text()
308 |
309 | if app_type == "System":
310 | reply = QMessageBox.warning(self, "Warning",
311 | "You are about to uninstall a system app. This may cause system instability. "
312 | "Are you sure you want to proceed?",
313 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
314 | if reply == QMessageBox.No:
315 | return
316 |
317 | command = f"adb -s {device} shell pm uninstall --user 0 {package}"
318 | self.run_adb_command(command, lambda _: QTimer.singleShot(1000, self.load_all_apps))
319 |
320 | def closeEvent(self, event):
321 | for worker in self.workers:
322 | if worker.isRunning():
323 | worker.quit()
324 | worker.wait()
325 | super().closeEvent(event)
326 |
327 | if __name__ == "__main__":
328 | app = QApplication(sys.argv)
329 | window = AndroidAppManager()
330 | window.show()
331 | sys.exit(app.exec_())
332 |
--------------------------------------------------------------------------------