From 0af1a27dcdfe67f7a3b90de58e08cd5352db711d Mon Sep 17 00:00:00 2001 From: jschwegmann Date: Thu, 9 Jan 2025 00:31:44 +0100 Subject: [PATCH] created basic gui for interacting with an android device via adb --- README.md | 30 ++++++++ adb-gui.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++ devices.json | 8 ++ requirements.txt | 18 +++++ run_app.command | 17 +++++ 5 files changed, 262 insertions(+) create mode 100644 README.md create mode 100644 adb-gui.py create mode 100644 devices.json create mode 100644 requirements.txt create mode 100755 run_app.command diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fb6872 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# ADB Unlock, Screenshot & Scrcpy Tool + +A simple Python GUI application that uses: +- **ADB** to unlock an Android device and take a screenshot +- **Scrcpy** to display and control an Android device (non-blocking) +- Tkinter for the GUI + +## Requirements + +- **Python 3** (with Tkinter installed): + - On most systems, Python 3 will already include the `tkinter` module. + - If you need to install it separately: + - Debian/Ubuntu: `sudo apt-get install python3-tk` + - macOS (Homebrew): `brew install python-tk` (though often included by default) + - Windows: `tkinter` is usually included in the official Python installer. +- **ADB** (Android SDK Platform Tools) available on your PATH. +- **Scrcpy** installed and on your PATH (for screen mirroring). +- A **`devices.json`** file in the same directory, containing your device serials. + +Example `devices.json`: + +```json +{ + "devices": [ + "emulator-5554", + "emulator-5556", + "device_serial_1", + "device_serial_2" + ] +} diff --git a/adb-gui.py b/adb-gui.py new file mode 100644 index 0000000..91f2d93 --- /dev/null +++ b/adb-gui.py @@ -0,0 +1,189 @@ +import json +import subprocess +import os +import tkinter as tk +from tkinter import ttk + + +def load_devices(config_file): + """ + Load device serials from a JSON config file. + Expects a structure like: + { + "devices": [ + "emulator-5554", + "my_device_serial" + ] + } + """ + try: + with open(config_file, 'r', encoding='utf-8') as f: + data = json.load(f) + return data.get("devices", []) + except (FileNotFoundError, json.JSONDecodeError): + return [] + + +def unlock_device(): + """ + Unlocks the selected device by sending ADB commands: + 1) Keyevent 26 (power) to wake up screen + 2) Keyevent 82 (menu) to unlock (simple locks) + """ + device_serial = selected_device.get() + if not device_serial: + status_label.config(text="Error: No device selected.") + return + + # Wake device (KEYCODE_POWER = 26) + subprocess.run(["adb", "-s", device_serial, "shell", "input", "keyevent", "26"]) + + # Send MENU key to unlock (KEYCODE_MENU = 82) + subprocess.run(["adb", "-s", device_serial, "shell", "input", "keyevent", "82"]) + + status_label.config(text=f"Device {device_serial} unlocked successfully.") + + +def take_screenshot(): + """ + Takes a screenshot from the selected device. + The screenshot file name is taken from the screenshot_name_entry widget. + Steps: + 1) Store the screenshot in /sdcard/ on the device. + 2) Pull it from the device to /Users/justinschwegmann/Downloads/. + """ + device_serial = selected_device.get() + if not device_serial: + status_label.config(text="Error: No device selected.") + return + + screenshot_name = screenshot_name_entry.get().strip() + if not screenshot_name: + status_label.config(text="Error: Screenshot name cannot be empty.") + return + + # Ensure the screenshot file has a .png extension + if not screenshot_name.lower().endswith(".png"): + screenshot_name += ".png" + + # Remote path on device + remote_screenshot_path = f"/sdcard/{screenshot_name}" + + # Local folder path + local_screenshot_folder = "/Users/justinschwegmann/Downloads/" + + # Combine folder + file for local path + local_screenshot_path = os.path.join(local_screenshot_folder, screenshot_name) + + # 1) Capture screenshot on the device + result = subprocess.run(["adb", "-s", device_serial, "shell", "screencap", "-p", remote_screenshot_path]) + if result.returncode != 0: + status_label.config(text=f"Error capturing screenshot on device {device_serial}.") + return + + # 2) Pull screenshot to local folder + result = subprocess.run(["adb", "-s", device_serial, "pull", remote_screenshot_path, local_screenshot_path]) + if result.returncode != 0: + status_label.config(text=f"Error pulling screenshot from device {device_serial}.") + return + + # (Optional) Remove screenshot from device after pulling + # subprocess.run(["adb", "-s", device_serial, "shell", "rm", remote_screenshot_path]) + + status_label.config(text=f"Screenshot saved to {local_screenshot_path}.") + + +def start_scrcpy(): + """ + Runs the scrcpy command with specified parameters in a non-blocking way: + scrcpy --video-codec=h265 -m1920 --max-fps=FPS --no-audio -K -s + The FPS is taken from the scrcpy_fps_entry widget (default: 60). + + Using subprocess.Popen() so the GUI remains responsive. + """ + device_serial = selected_device.get() + if not device_serial: + status_label.config(text="Error: No device selected.") + return + + # Get desired FPS from the user (default to 60 if not valid) + fps_value_str = scrcpy_fps_entry.get().strip() + if not fps_value_str.isdigit(): + fps_value_str = "60" # fallback default + + command = [ + "scrcpy", + "--video-codec=h265", + "-m", "1920", + f"--max-fps={fps_value_str}", + "--no-audio", + "-K", + "-s", device_serial + ] + + try: + # Launch scrcpy without blocking the GUI + subprocess.Popen(command) + + # We won't know when scrcpy closes (without additional logic), + # but the GUI remains usable now. + status_label.config(text=f"scrcpy started with FPS={fps_value_str}.") + except FileNotFoundError: + status_label.config(text="Error: scrcpy not found in PATH.") + + +# -------------- Main GUI code -------------- +if __name__ == "__main__": + CONFIG_FILE = "devices.json" + devices_list = load_devices(CONFIG_FILE) + + root = tk.Tk() + root.title("ADB Unlock, Screenshot & Scrcpy Tool") + root.geometry("450x350") + + main_frame = ttk.Frame(root, padding="10") + main_frame.pack(fill="both", expand=True) + + # -- Device selection -- + device_label = ttk.Label(main_frame, text="Select Device:") + device_label.pack(pady=5) + + selected_device = tk.StringVar() + device_combobox = ttk.Combobox( + main_frame, textvariable=selected_device, + values=devices_list, state="readonly" + ) + device_combobox.pack(pady=5) + if devices_list: + device_combobox.current(0) + + # -- Unlock button -- + unlock_button = ttk.Button(main_frame, text="Unlock Device", command=unlock_device) + unlock_button.pack(pady=5) + + # -- Screenshot name label/entry/button -- + screenshot_label = ttk.Label(main_frame, text="Screenshot Name (.png):") + screenshot_label.pack(pady=5) + + screenshot_name_entry = ttk.Entry(main_frame, width=30) + screenshot_name_entry.pack(pady=5) + + screenshot_button = ttk.Button(main_frame, text="Take Screenshot", command=take_screenshot) + screenshot_button.pack(pady=5) + + # -- scrcpy FPS label/entry/button -- + scrcpy_fps_label = ttk.Label(main_frame, text="scrcpy Max FPS:") + scrcpy_fps_label.pack(pady=5) + + scrcpy_fps_entry = ttk.Entry(main_frame, width=10) + scrcpy_fps_entry.insert(0, "60") # default to 60 + scrcpy_fps_entry.pack(pady=5) + + scrcpy_button = ttk.Button(main_frame, text="Start scrcpy", command=start_scrcpy) + scrcpy_button.pack(pady=5) + + # -- Status label -- + status_label = ttk.Label(main_frame, text="Status: Ready", foreground="blue") + status_label.pack(pady=10) + + root.mainloop() diff --git a/devices.json b/devices.json new file mode 100644 index 0000000..c6afad0 --- /dev/null +++ b/devices.json @@ -0,0 +1,8 @@ +{ + "devices": [ + "emulator-5554", + "emulator-5556", + "device_serial_1", + "device_serial_2" + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..40833ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +# ------------------------------------ +# Python dependencies for this project +# ------------------------------------ +# This project only uses Python's standard library modules: +# - json +# - subprocess +# - os +# - tkinter (comes bundled with Python on most systems) +# +# Therefore, there are no external Python packages needed +# to be installed via pip. +# +# You do, however, need "scrcpy" and "adb" installed on your system PATH. +# +# If you are missing tkinter (rare on some systems), you may need to install: +# - On Debian/Ubuntu: sudo apt-get install python3-tk +# - On macOS (Homebrew): brew install python-tk (usually tkinter is pre-included) +# - On Windows: usually included with official Python installer diff --git a/run_app.command b/run_app.command new file mode 100755 index 0000000..3c733a1 --- /dev/null +++ b/run_app.command @@ -0,0 +1,17 @@ +#!/bin/bash + +# --------------------------------------------------------- +# A simple macOS script to launch the ADB Unlock GUI app. +# --------------------------------------------------------- + +# 1) Find the directory of this script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# 2) Move into that directory +cd "$SCRIPT_DIR" + +# 3) Use python3 (make sure python3 is installed and on your PATH) +PYTHON=$(which python3) + +# 4) Run your Python script (adjust the file name if different) +"$PYTHON" adb-gui.py