├── 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 | ![Preview](preview.png) 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 | --------------------------------------------------------------------------------