From d395d94992fb5ea2fbe1b3416b4c2e30a0f0205d Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 15 Aug 2025 16:47:45 -0500 Subject: [PATCH] Transfer is working, added tooling, debug scripts --- .github/copilot-instructions.md | 29 + debug_tools/README.md | 36 + debug_tools/debug_adb.py | 111 +++ debug_tools/test_parsing.py | 44 ++ poetry.lock | 486 ++++++++++++- pyproject.toml | 31 +- src/__init__.py | 24 +- src/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 598 bytes src/__pycache__/adb_manager.cpython-313.pyc | Bin 0 -> 15992 bytes src/__pycache__/gui.cpython-313.pyc | Bin 0 -> 30347 bytes src/adb_manager.py | 361 ++++++++++ src/android_file_handler.py | 459 ------------- src/gui.py | 712 ++++++++++++++++++++ src/main.py | 15 + test_parsing.py | 47 ++ 15 files changed, 1891 insertions(+), 464 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 debug_tools/README.md create mode 100644 debug_tools/debug_adb.py create mode 100644 debug_tools/test_parsing.py create mode 100644 src/__pycache__/__init__.cpython-313.pyc create mode 100644 src/__pycache__/adb_manager.cpython-313.pyc create mode 100644 src/__pycache__/gui.cpython-313.pyc create mode 100644 src/adb_manager.py delete mode 100644 src/android_file_handler.py create mode 100644 src/gui.py create mode 100644 src/main.py create mode 100644 test_parsing.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..d5b579d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,29 @@ +# Project Overview + +This is a small Python GUI tool for managing Android device files via ADB (Android Debug Bridge). +## Folder Structure + +- `/src`: Contains the source code for the application. + - `platform-tools/`: Directory where ADB binaries are stored. Do not modify or delete this folder. +- .github: Contains GitHub-related files, including workflows and issue templates. + - `copilot-instructions.md`: Instructions for GitHub Copilot to assist in code generation. Do not touch this file. + +## Libraries and Frameworks + +- Tkinter for the GUI. +- Requests for HTTP requests. +- Zipfile for handling zip archives. +- Poetry for package management. + +## Coding Standards + +- Follow PEP 8 guidelines for Python code. +- Use meaningful variable and function names. +- Write docstrings for all public modules, functions, and classes. +- Include type hints for all function parameters and return values. +- Use f-strings for string formatting. +- No single letter variable names except for `e` when appropriate. + +## UI guidelines + +- Application should have a modern and clean design. \ No newline at end of file diff --git a/debug_tools/README.md b/debug_tools/README.md new file mode 100644 index 0000000..21332af --- /dev/null +++ b/debug_tools/README.md @@ -0,0 +1,36 @@ +# Debug Tools + +This folder contains debugging utilities for troubleshooting the Android File Handler application. + +## Scripts + +### `debug_adb.py` + +Comprehensive ADB debugging script that helps diagnose device detection issues. + +**Usage:** + +```bash +poetry run python debug_tools/debug_adb.py +``` + +**What it checks:** + +- ADB binary path and existence +- Direct ADB command execution +- ADBManager device detection functionality +- Manual parsing of ADB output +- ADB version and server status + +**When to use:** + +- Android device is not being detected +- ADB connectivity issues +- Troubleshooting path problems +- Verifying ADB installation + +**Output interpretation:** + +- ✅ "Device found: [DEVICE_ID]" means ADB is working correctly +- ❌ "ADB binary not found" indicates path configuration issues +- Empty device list suggests USB debugging is not enabled or device is not connected diff --git a/debug_tools/debug_adb.py b/debug_tools/debug_adb.py new file mode 100644 index 0000000..09ca3c3 --- /dev/null +++ b/debug_tools/debug_adb.py @@ -0,0 +1,111 @@ +""" +Debug script for troubleshooting ADB device detection issues. + +This script provides comprehensive debugging information for ADB connectivity, +including binary path verification, device detection, and command execution testing. +""" + +import os +import sys + +# Add the src directory to Python path for imports +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), "src")) + +from adb_manager import ADBManager + + +def main(): + """Run comprehensive ADB debugging tests.""" + print("=== ADB Device Detection Debug ===\n") + + # Test 1: Check if ADB binary exists + adb_manager = ADBManager() + from adb_manager import ADB_BINARY_PATH + + adb_path = ADB_BINARY_PATH + print(f"1. ADB Binary Path: {adb_path}") + print(f" ADB Binary Exists: {os.path.exists(adb_path)}\n") + + if not os.path.exists(adb_path): + print("❌ ADB binary not found! Check the platform-tools directory structure.") + return + + # Test 2: Run ADB command directly + print("2. Testing ADB command directly...") + try: + import subprocess + + result = subprocess.run( + [adb_path, "devices"], capture_output=True, text=True, timeout=10 + ) + print(f" Return code: {result.returncode}") + print(f" STDOUT: {repr(result.stdout)}") + print(f" STDERR: {repr(result.stderr)}\n") + except Exception as e: + print(f" Error running ADB directly: {e}\n") + + # Test 3: Test ADBManager device detection + print("3. Testing with ADBManager...") + try: + result = adb_manager.run_adb_command(["devices"]) + print(f" Raw result: {result}") + print(f" Raw result type: {type(result)}") + + device = adb_manager.check_device() + if device: + print(f" Device found: {device}\n") + else: + print(" No device found by ADBManager\n") + except Exception as e: + print(f" Error with ADBManager: {e}\n") + + # Test 4: Manual parsing for debugging + print("4. Manual parsing of ADB output:") + try: + import subprocess + + result = subprocess.run( + [adb_path, "devices"], capture_output=True, text=True, timeout=10 + ) + print(f" Return code: {result.returncode}") + + lines = result.stdout.strip().split("\n") + print(" Output lines:") + for i, line in enumerate(lines): + print(f" Line {i}: {repr(line)}") + if "\t" in line and "device" in line: + device_id = line.split("\t")[0].strip() + print(f" ✅ Found device: {device_id}") + print() + except Exception as e: + print(f" Error in manual parsing: {e}\n") + + # Test 5: Additional ADB info + print("5. Additional ADB commands for debugging:") + try: + import subprocess + + # ADB version + version_result = subprocess.run( + [adb_path, "version"], capture_output=True, text=True, timeout=5 + ) + print(f" ADB Version: {version_result.stdout.strip()}") + + # Restart ADB server + subprocess.run([adb_path, "kill-server"], capture_output=True, timeout=5) + subprocess.run([adb_path, "start-server"], capture_output=True, timeout=5) + + # Check devices again after restart + devices_result = subprocess.run( + [adb_path, "devices"], capture_output=True, text=True, timeout=10 + ) + print(f" Restarting ADB server...") + print(f" After restart - Return code: {devices_result.returncode}") + print(f" After restart - Output: {repr(devices_result.stdout)}") + + except Exception as e: + print(f" Error in additional tests: {e}") + + +if __name__ == "__main__": + main() diff --git a/debug_tools/test_parsing.py b/debug_tools/test_parsing.py new file mode 100644 index 0000000..f5effb0 --- /dev/null +++ b/debug_tools/test_parsing.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +Test script to debug folder parsing +""" +import sys +import os +sys.path.insert(0, 'src') + +from adb_manager import ADBManager + +def main(): + adb = ADBManager() + print("Testing folder parsing...") + + result = adb.run_adb_command(['shell', 'ls', '-la', '/sdcard/']) + print(f"Return code: {result[2]}") + + lines = result[0].strip().split('\n') + print(f"Total lines: {len(lines)}") + print("\nFirst 10 lines:") + for i, line in enumerate(lines[:10]): + print(f" {i}: {repr(line)}") + + print("\nParsing directories:") + folders = [] + for i, line in enumerate(lines): + if line.startswith('d'): + parts = line.split() + print(f" Line {i}: {len(parts)} parts") + if len(parts) >= 8: # Changed from 9 to 8 + folder_name = ' '.join(parts[7:]) # Changed from 8 to 7 + print(f" Folder name: {repr(folder_name)}") + if folder_name not in ['.', '..']: + folders.append(folder_name) + print(f" ✅ Added: {repr(folder_name)}") + else: + print(f" ❌ Skipped: {repr(folder_name)}") + + print(f"\nFinal folders list ({len(folders)} items):") + for folder in sorted(folders): + print(f" - {repr(folder)}") + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 21cf34c..5e27a2b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,74 @@ # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "astroid" +version = "3.3.11" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, + {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, +] + +[[package]] +name = "black" +version = "25.1.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2025.8.3" @@ -101,6 +170,50 @@ files = [ {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "dill" +version = "0.4.0" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, + {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + [[package]] name = "idna" version = "3.10" @@ -116,6 +229,330 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"darwin\"" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.17.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, + {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, + {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, + {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, + {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, + {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, + {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, + {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, + {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, + {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, + {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, + {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, + {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, + {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, + {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, + {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyinstaller" +version = "6.15.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.15,>=3.8" +groups = ["dev"] +files = [ + {file = "pyinstaller-6.15.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:9f00c71c40148cd1e61695b2c6f1e086693d3bcf9bfa22ab513aa4254c3b966f"}, + {file = "pyinstaller-6.15.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cbcc8eb77320c60722030ac875883b564e00768fe3ff1721c7ba3ad0e0a277e9"}, + {file = "pyinstaller-6.15.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c33e6302bc53db2df1104ed5566bd980b3e0ee7f18416a6e3caa908c12a54542"}, + {file = "pyinstaller-6.15.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:eb902d0fed3bb1f8b7190dc4df5c11f3b59505767e0d56d1ed782b853938bbf3"}, + {file = "pyinstaller-6.15.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b4df862adae7cf1f08eff53c43ace283822447f7f528f72e4f94749062712f15"}, + {file = "pyinstaller-6.15.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b9ebf16ed0f99016ae8ae5746dee4cb244848a12941539e62ce2eea1df5a3f95"}, + {file = "pyinstaller-6.15.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:22193489e6a22435417103f61e7950363bba600ef36ec3ab1487303668c81092"}, + {file = "pyinstaller-6.15.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:18f743069849dbaee3e10900385f35795a5743eabab55e99dcc42f204e40a0db"}, + {file = "pyinstaller-6.15.0-py3-none-win32.whl", hash = "sha256:60da8f1b5071766b45c0f607d8bc3d7e59ba2c3b262d08f2e4066ba65f3544a2"}, + {file = "pyinstaller-6.15.0-py3-none-win_amd64.whl", hash = "sha256:cbea297e16eeda30b41c300d6ec2fd2abea4dbd8d8a32650eeec36431c94fcd9"}, + {file = "pyinstaller-6.15.0-py3-none-win_arm64.whl", hash = "sha256:f43c035621742cf2d19b84308c60e4e44e72c94786d176b8f6adcde351b5bd98"}, + {file = "pyinstaller-6.15.0.tar.gz", hash = "sha256:a48fc4644ee4aa2aa2a35e7b51f496f8fbd7eecf6a2150646bbf1613ad07bc2d"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30,<2024.8.26 || >2024.8.26", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2025.8" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2025.8" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064"}, + {file = "pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c"}, +] + +[package.dependencies] +packaging = ">=22.0" +setuptools = ">=42.0.0" + +[[package]] +name = "pylint" +version = "3.3.8" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83"}, + {file = "pylint-3.3.8.tar.gz", hash = "sha256:26698de19941363037e2937d3db9ed94fb3303fdadf7d98847875345a8bb6b05"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} +isort = ">=4.2.5,<5.13 || >5.13,<7" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "requests" version = "2.32.4" @@ -138,6 +575,51 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "tomlkit" +version = "0.13.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + [[package]] name = "urllib3" version = "2.2.3" @@ -158,5 +640,5 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" -python-versions = ">=3.8" -content-hash = "22724479fe8e759bcc703c31941828fa61f965e7af8dd57a4e89ea454887ddca" +python-versions = ">=3.13,<3.15" +content-hash = "271e389c31c4b4c771afcf1ec8670666300ee07673ba1c3ca3db2cf601237797" diff --git a/pyproject.toml b/pyproject.toml index fcdabdc..62b68cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,17 +7,44 @@ authors = [ ] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.13,<3.15" dependencies = ["requests>=2.32.4,<3.0.0"] [tool.poetry.dependencies] -python = ">=3.8" +python = ">=3.13,<3.15" requests = ">=2.32.4,<3.0.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" +[tool.poetry.scripts] +# Main application entry point +android-file-handler = "src.main:main" + +# Development and maintenance scripts +dev = "python src/main.py" +gui = "python src/gui.py" +test-adb = "python -c 'from src.adb_manager import ADBManager; m = ADBManager(); print(f\"ADB Available: {m.check_device()}\")" +clean-adb = "python -c 'import shutil, os; shutil.rmtree(\"src/platform-tools\", ignore_errors=True); print(\"ADB cache cleared\")'" + +[tool.poetry.group.dev.dependencies] +black = "^25.1.0" +mypy = "^1.5.0" +pylint = "^3.3.8" +pytest = "^8.4.1" +pyinstaller = "^6.15.0" + +[tool.black] +line-length = 88 +target-version = ['py313'] + +[tool.mypy] +python_version = "3.13" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true + [tool.poetry] package-mode = false diff --git a/src/__init__.py b/src/__init__.py index 47ae99e..eb1a62a 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +1,23 @@ -import src.android_file_handler as android_file_handler \ No newline at end of file +""" +Android File Handler - Package Initialization +Exposes the main components of the Android file handler application. +""" + +from .adb_manager import ( + ADBManager, + LinuxMTPManager, + get_adb_binary_path, + is_adb_available, + get_platform_type, +) +from .gui import AndroidFileHandlerGUI, main + +__all__ = [ + "ADBManager", + "LinuxMTPManager", + "AndroidFileHandlerGUI", + "get_adb_binary_path", + "is_adb_available", + "get_platform_type", + "main", +] diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67412df62d2d5a16ad1d962859db952d8452abc5 GIT binary patch literal 598 zcmY*WJ#Q2-5ViOF_7p;d=-qCS(4z<`Kmyqi6gbkYWchY|cUHXiYP<)zbo>XXsrgH8 zspx=HlotE4C&3g?Gvk>z^Tz9)og6vKzkFNVVuXJ9U~9qPtPh>@6-^MJ37!OJxC|gD zLkPrNYPNWb8uWkQi!x1Cm8^@ix#CcPz`dBVj&$huC7dc$#$ z|8$B_5!?Le<@0l<+!(OCZ-iQ2pMSX6CLYasPgygiQ=u5FXvg}6eJIQ($38M4*;I17 z=gT_D`k987_N$H;fqgve;57}BUR|Er$ZuSv5W4XYdB{A(9ug0!hmnJ~#n`5lHoB(N zj{O$x{ht5y{@-T9`BL2Tc>>?fq@?saTDuV0lNV2`OT)obB{N!)_u821lZGqPv6@%z zO#TWWg~eu2`1Ze_E>RyRw@pTVA^^@lXX!LJZw{5uG7beAVMxJ3@` R9w5B`C(6db=M;8b(O;<~t118h literal 0 HcmV?d00001 diff --git a/src/__pycache__/adb_manager.cpython-313.pyc b/src/__pycache__/adb_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40e8c2d52e44ac0e2bba2d3a00e03ec0e25d64c1 GIT binary patch literal 15992 zcmc(GdvF`andb~<25$l&0TSRtN zOM7uCf82dv4`u*DknGf5-F1=OJ^h;Qp09u3qd(j)DKS$J8Wyfh{rBw@^?Q6!gCU!_ zU!|j{iQr*+aC0uN<{s0sMXk@Jk7?=6wNP{8k;HaWrVZ}Ol;^C>3Jk@9G z%E(hgSm$*nX3Ra&2{9I#;QJ%fA^u<>Ix!s*`3`>HoD_+MB#A!~3rB+d(b&Y?bjYmK zOMFk?K0Y=Z5(Du_EGo706QS24!4N+ahz7!;nNT#|&cjIjB#s#u15s%*BuYN>UEIV$ zFC!a=X0c~rTINRPW~W24u{SV19XK%^N*tPs$7iLTot+cYzHlrSo(}nfv6)U74yV~E zhGt_@Bpwq_cg{`+;*&9PrXwDUO-mi{6q4c{ry|jb*eS`Eh|DGq{vVDt9f{7pPDZkA ze3{IVDwzpPoRArKWx@bCU+DEvFiR&S(Ptm-e+8L~)Hns_kHWJD=S~M_j;W&H#IfW0 zaYH99aNRl%+QIkIlP_c>HKq+U22x`djN=AqXChA)!8~q)C!h}a=2pQvUII@*7w}}u zKH12VU8n*Ya0m`)!75bo3N4mGo3e3J_T36~I0fgpiIf{iIV-rx2ri)<$}31+Nw;2b zlhSfhf;ISVz2G6G6+)%x4s+gW*$nh@l+cEF7f)S$$SHw5X|2PQmma*TlS^mCSXczQ_6LFBP6UD{WxEs) z#OHDaS817*LerBHcIM@Q-ksy45|Hosp@0;N^20Gn8b1{iPfD|aU}!u`mwqI5{}epI z@$myKj7ws098$k>CPd%tY1!!aN1~Cq-!D6J8{$(7+u?Om8ANYUnSK3d*;Qk0=DEJJ z?5D<>`?w_8s7`q>n1wSernFsfz{5rA1xIcV!n9YH*dm1DyxNtYoQnqW=<#rJcy%-Q zx!DOkJ-)%Kv}l82uIj{6NW=<=BzytSi#Rp8TnfegYPZ}TA|PZxu&-!~tvHYoqPM6o z>b;*DYsGSS_>8--(ke_}+4D!Q>0-yK#{0)azQ{YmlkS zW`MsSARPeShGif@5~4f=nTtS@!;lsRjhn!xLJN&dbL~cHm5=aLm?j^xaUdEGi2$1X zOej7To8aTI?2@$)=^GS1Fork#`FO~0Sii%sd}-|y%GG&`FRa{w-D@F&k7lu_E%or1 zu{zS$hLp8oap$L2-#SBC8;7q@{|wm~n3;&Wnj15P0uASCg3khw_JQe%)RVL1OJ zXUiI>F;=Zti9nzchNJW-E$DUv(F80MHKvMrl)$L%LUfp|r^b+6Nia}>8aTbw7!ek+ zXxOUM!qo=cw|~O*1F(%5om?Rr# z0w+Tg5mCy1cG)~BhC+Tay$uJUB8bcJPsacl{p3}oja;8c1;v}}gC$ANL-ZE4&Kgag zwaU75W!vq_w$&}qu2k;2c05(N_w1pJy)s$TzG81*tEfxz&#YAJOgeUEobIpI>=pMZ z#^kwUwVglw&f!&0>vH#P>yFx095-4p z2*ziasBTm@TwTxr9@RI)Sa8skFpQX45a5XMYSD0mn#-l|F5NJKR6$VZs-{t9lo>9f zAC0L?*S;#u=uiEQL+}-}_ZNev;VoLVmU#j_!H(pOhXPJPfL+n3Q81}m3{MSN#yZue zf>~XAhkweltkGcHq0sMB(1@oE#d3S zVi^%lL$8A}6O0q}hbSG0htXIZ_-{x8T=R{HxEvApJ>eP!#G9RsiSf_`uc{CIiAHtA zNHnY!D=G}%3h3kY`FwYgfV?^Y!?+j<%*cj#WF`~?T>KwExrt`P*b|XxKs-%eJ{g+> zT`MpN!U3P1AqaShR`o^jdXP)is=Uc4Z}lZS{Q;0(i9De~H!g7}Kj`&{+hHm)I~#~k z$@++dVyrr!Jgso_>c9B97GKEk9_CYe%TBhQ;bWeBG9@NTOAn~9DTw6;=nQg z=xWoQ*CLlMVY$gZMfyBpZS;5R#&C*__=8kn%+&C0S3Hj3jfxshQY>hytmXBT}wx zK{*}9_0NgZI0I=Gl-iJNQ});&5Q9^Z*FrKKk`PXL42m@DQMbzv8|=@%5v6~*5f-ls z#E)w<2!lyo@I~LE?pR9BKYi}$^Lx(i`JgjxX-iq!fLW}wb*9QY_KLKwNg*Iv@uoMxVS%E+LkJX(vAz3JC5>XMf<9wBjc|5!s$*seJQ6e?cADj zZp9K5-Re?SU`YE{t^If0b(h|J@6E--*VyYPQtpAX2QyYz+S;75Hm_P+?rdth;{TEV z=fl5v@o!(eW=!umnA&l0rDI@a)1kA28EaLx;pT6xTAzM&iq7&&PhWWY(w+-@e$ttC zZcjP4uemBN9enTL2mYndr>^chRgKA}!z)!slCC3p(>k{CSC1I z$CrAOWm{LR+t!?(Kdy67@la}p&HGtTNxzx8@k}|yAKQC(Lh>uKxxbzHmCM%O!u+a* zgS^ZJ#IPjWf`M5O%tC%RMzeGAAj)HEKsk!YXFrCIkdvXtr1?PHn zGfnw()mUt<1`=)OvtkU}sS-Z{L7%3*4AnZ86HJ zur27j8A%5~%tg5#t_dNT3bKxx8iPDD*c7$s^=HAS8G?i&s^K~pLYGj zuB7`w(sAI9t2XUwy6tMpxZFQ6e`NpA{*UJLj)ByUf#k7~dyKBUY@S}%Q=ZCsBgnLH zTv_tUjJx^+VX^f6=NE^Q?v_s-EqC12e_S^~1Iu65s3%37!3tM z67k6frBnjVi_qg;+%OUMXE0$fDV1J>I0*%^IRaK-;OWsIka?U~YME)Eb+a@(9f^Yt zGz2c>GK)QqS4itV!PGf(S03tZ>Y?hCsYtJ2Q)6=!>D;p+=U|q#(Wk&YY4D3gkqA+ zfH)^}VKFv0>tz+-I}OcbdPZhugK>$4haA}1W^o=lV^qEkW@FWR&`;V65kMVi&I9iZ zBr95$Dp#ysXX)QstZQZE^MOm@3*m*p4`2CV=h@zj-7(+$S7#Qw-aV6XSIwKjLgT3W zFLtN$`7B~+CKiVmqf~2Fi~yM5AQiv{D#$YhXCwb(3iJzQoh-@(d7Kd!RuBp}E7E*H z$m@@9D+q{az*{*YKNOJ$G;;YiAVyJn0r2Ml!5^v^p5*v%R6nXe0SY&uD{{?(Fq)?@ zLkmqY&ou+QdV*4}5-3j{9iRwe!EDQOz15tGZ#=F>Q_R&`AZIRhGyqL`WfmjchnEHz`FDp+RzJ z#W$hRLw+QCCkK3C2Cm;wLzU)&DDOL96K%^HDUtnz@ zhl#J@^V=8^p$QKQq6h{>6SLqJfSx!#5fbH!JpPea{yy}QjDTN6Tw2sGS(3KrKw4s} zLDO(t0C27^u%47`e(t*K*L64QlFuDaHVv;j1km%ox`XlCfhlOz34Oz@Af_k7Y3vjkLu^1etkbZ@^4FYKLKi zlICG%31X%+m5-Sl5E3uSHj2aqGo>j7GsSbTUy1UZ5sVnIcu>N?0@j}>SJ4hkRbk~F zV4};RsrUozUJuRrSu%?}z|88T1UH`qk_dnbG#0=mBBl5)MhKMNEJhM|BtVH6DgG5k z2#h(LBo~$nK;}WDG*uiTweo+2L8Qt&h%{#*()_1DWCdj^NgJzE#_D8E*Q&7_E)UN? zd;VF##4DB$EFf%GRHZAtsS58>(@I6>Jd<%&T-Zx6(wlO5ml{`HJ`M90`#w7K;i11d z@|j~BAZBF~Af~D0yy=|jT}uXr-Ft5DLSQ9(A6K8U)-N`$S~ur_kKeay?FHdJXJl&V zz25ihp&LWV-jO6fx@vtsz)LwQ3o-NQYxJ^|5-?f~$VB-w*aiaGrs@)$f0GE-;;~vHQVmK$M6Y z6^%i)!SA1l1^s>z52}a?fOrO@DvW{{Az$q9 z*Nr-^ZE5NnvpjcgV)>1fulE53C0N|LbZ|MiJbJBfc`Vi5gT<}uMuzKL>UuyyvTkO$ zw#6Xk+t$ry&a=cmpdeZIG;x(nO%EtY?j4~WT+L$NIt9tKrhma?y~)hE7N!=Z%QM*C zweB%+t|exjf@Ilv&H2lc2bf(itK%vcrF9CDYfuZxb$9_J>-%UPs)FkjBv=K>b@mUK zD67T=!Oan`XY4nEm-M$*nIn8cE> z*=v#QN6^RU=*Y3GwfOGuV7x@D@+9$4k>_Z|gWm>e&r7GJcxXm?j6V#T>A)Wbu1lsN zrv&hT(G4aXo$;Aj)tSma6Q70aSIZ1sAb?j#3{CN9|5H3)6k0%1lsyHL)O=R{E4Ien z1D}=Z4)j&=C_9RLc7q*94+XfZ_g^Nm!`I(|17BdFeL+B*D&9`#u8oFK7%T{&S~JD_ zccSAXS}Ym7OD|?)1zQhVfWVeoe1s+}FT$~MocQbJ)(M>KIJsE-CM$u~#=O>=#4?Ou zsvy9+HTT1But(&_5gXn z8rAja!r<_RX`Vb_OSB(Fk|w7oYXbuj0I!XdbHk%^W0-#h=zmgzOFO*Pl+ENAkW&Ee zPoq#o6ez;z!DW$~JsFvveyHo1Oc=wjO-dcmS$qXrfsOeiY`lbn=#?>vE+$@ul0;2T zt{^YSn-qu_iE84!z>_MR6CQYF%Ai(R$?coig~R_fMnu7S33Dh8$ff?dDCz0P8R2mT zOE+8tjS@Mudu~`gZ`40 zhzsj1uQ@d=uxU?gD(l(sKu=ZhmtKDFm#?Vhp@i%{GoRa&Ax1_4KBpvP7Mm}M`!He-K&C|X7{NKOy%a@ji zk`;Zcj{Pt(#n!C@w-3CU>RhImq-0swsVzF!~^Lr${flR1(Z5&s>03z!POR!|>%@MY(5)?D?3@Art3YR4!uq{VpH}YG%j_gLW+zNqw*=RLa&Uqr z{kV?!=~=j9hl??DqWRkaF$!R~lkbhqO-~Rfhb+)(D?sN2Qyv}?y(xyXeib9wOU2AY zkdFYNsyq?+LxU1PIyx&5WR-0MK~ZfaF72{I&7zSQ-7X(vc@qB?N2-5BKq>@g6QHDI z$U@~N)80GfbqI=!-(DQJHt~roRX%vOKjW#rQgXQ@?b(|0Z2io$9j_%S7e+4bSqv@3 ze>(F8Sey=BKJ@;P`Tc9|s)eV1m{>f1@l0m(mZhE#rxq-0o`x&7AK5aSyi487pn2{7 zWa0s%tE{?LLREHsSwYnb^x47RH*HTk__fN$E44qWm0brjjV+nVP4^6JRoT3L-ApyL zeYE?--RZ_}r5eAr(zqktxF^-P=UV);#=iN(zjHUtzwx-Y+~=sK{q&b5RCCu?4?IMh zqD^}VN%j>@#XJ!Nmtd2+NY(Tl(?x2#&*^Au{7 z4c~5Prhe+`DP{h_)?;OESUJcR|Jwf(X7Tl3yO`gjpm|iQh#k0yH)FYL;GDl4+^%K$ zlh(ElklhGVC83z&(4hGOM&Y+nND~6v#R#0B?*#9`Ar3f_0eF}WT}gm@fPe|SQxoEg zi}N{-`Ut0K0KUq?I2|w!mp3!i{R%=cLu2mpM_nP|U!d`ia(K0zyIwvD*GuG@*$8I? z_?r@P9I9IfX9A=bo>Es>M-74zLUA1~BG&82D%DYd+v^`WJI1KKM;|r{B+WZJ=0syZ z@DOlh11ks0${V>EPegYg@xYVy;5{}MnMjy5e$xqgjUPR9sC)`MNAPz=Z4a$#37pF| zkm>&;%u&3B(SL?$Fj1ul>d3{kcN4CN*B{{NaT;#IRWCnrFLaR&QvnHWEQ%0+6N_v; zs?9|?r`xcQ4Nu2Th|gk{i%8(ToX4{aL}Zr@$Y}kM3Au!@sVqy9WJ5M58)gD=fDK7p zhG7&|g#slA_ZXBr5+}}G(-goe1uBxt(uy^A?FZuGjju_ab-CnA8%ge0QGCbzfwkI(E0G^XmJBPkTjvk_&Rsiy>i5m<3;WmV>aU!- zeCo<~FMoGwG+o!VQrDGfY5$28n9k-7Fbnb>>o&>-X1qG8vgPN7qCCfHf zymxe+gZv*K@L3a|BD(vQUR??%%eJptyYfVL(EO>ln|dp#kGl5lqHa{tkiAjOV!o~( z@*lG;7>6EFT)jnIB1p%)X{ z?6!Z&(wuKy-@thmTbH^%?07&y_RE8G9k&;(Vg|5^^`Y^sa=mObGMwRg$eDQ`;~hMxD^Xf(aE4zIWibGY*b-$r&sB(gNEpZF=XI)Mwr4~PrPbN^hi zv?bZLM_2jeZLd|gJvb+d_!A2ii+B{Qao6`0nOQ0By4uo@EyXl63**!fKX7VgZw->k zcpDWeB@6x)4n_o#Ud;7lgu5>SJEFvY#^`4leHS8G4+8^cDmG~ri&1zJdoX$qBZ7VG zD`0>G@1zL)#0g9ohQJBGz1gg(kSy#IOOvDox+hF*JMMOws1gDflb>oU(sT**>RC zpHn5DQx@`P`W@AtqT1Ju6nC5kZxh;q*&&iG11ko&zoJL%t_!;|to|<#eE+~-4t;-U ze&3}77YV!Z literal 0 HcmV?d00001 diff --git a/src/__pycache__/gui.cpython-313.pyc b/src/__pycache__/gui.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48ad4c64c7f961a6cad5008c536572af60e778ea GIT binary patch literal 30347 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 bool: + """Check if there's enough disk space for ADB download.""" + try: + free_space = shutil.disk_usage(LOCAL_ADB_FOLDER)[2] + if free_space < 50 * 1024 * 1024: # 50MB minimum + raise Exception("Insufficient disk space") + return True + except OSError: + # Create directory if it doesn't exist + os.makedirs(LOCAL_ADB_FOLDER, exist_ok=True) + return True + + def download_and_extract_adb(self) -> bool: + """Download and extract ADB tools if not present.""" + if os.path.isfile(ADB_BINARY_PATH): + return True + + try: + # Determine the correct URL based on platform + if OS_TYPE.startswith("linux"): + adb_zip_url = ADB_LINUX_ZIP_URL + elif OS_TYPE.startswith("win"): + adb_zip_url = ADB_WIN_ZIP_URL + else: + print("Unsupported platform") + return False + + if not self.check_local_disk_space(): + return False + + self._update_status("Downloading platform-tools (ADB)...") + + response = requests.get(adb_zip_url, stream=True, timeout=30) + response.raise_for_status() + + binary_archive = zipfile.ZipFile(io.BytesIO(response.content)) + binary_archive.extractall(LOCAL_ADB_FOLDER) + + if OS_TYPE.startswith("linux"): + if os.path.exists(ADB_BINARY_PATH): + os.chmod(ADB_BINARY_PATH, 0o755) + else: + raise Exception("ADB binary not found after extraction") + + self._update_status("Downloaded and extracted platform-tools.") + return True + except Exception as e: + print(f"Failed to download ADB: {e}") + return False + + def run_adb_command(self, args: list, capture_output: bool = True): + """Run an ADB command and return output.""" + cmd = [ADB_BINARY_PATH] + args + try: + if capture_output: + p = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + return p.stdout.strip(), p.stderr.strip(), p.returncode + else: + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) + return p + except Exception as e: + if capture_output: + return None, str(e), -1 + else: + return None + + def check_device(self) -> Optional[str]: + """Check if an Android device is connected.""" + result = self.run_adb_command(["devices"], capture_output=True) + if isinstance(result, tuple) and len(result) == 3: + out, err, rc = result + if rc != 0 or not out: + return None + for line in out.splitlines(): + if line.endswith("\tdevice"): + return line.split()[0] + return None + + def parse_progress(self, text_line: str) -> Optional[int]: + """Parse progress percentage from ADB output.""" + m = re.search(r"\((\d{1,3})%\)", text_line) + if m: + pct = int(m.group(1)) + if 0 <= pct <= 100: + return pct + return None + + def pull_folder(self, remote_path: str, local_path: str) -> bool: + """Pull files from Android device to local machine.""" + cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path] + + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + + if proc.stdout: + for line in proc.stdout: + pct = self.parse_progress(line) + if pct is not None: + self._update_progress(pct) + self._update_status(line.strip()) + + proc.wait() + if proc.returncode == 0: + self._update_progress(100) + self._update_status("Transfer completed successfully.") + return True + else: + self._update_status(f"Transfer failed with code {proc.returncode}") + return False + + except Exception as e: + self._update_status(f"Transfer error: {e}") + return False + + def push_folder(self, local_path: str, remote_path: str) -> bool: + """Push files from local machine to Android device.""" + cmd = [ADB_BINARY_PATH, "push", local_path, remote_path] + + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + except Exception as e: + self._update_status(f"Failed to start adb: {e}") + return False + + self._update_status("Transferring files...") + + if proc.stdout: + for line in proc.stdout: + pct = self.parse_progress(line) + if pct is not None: + self._update_progress(pct) + self._update_status(line.strip()) + + proc.wait() + if proc.returncode == 0: + self._update_progress(100) + self._update_status("Transfer completed successfully.") + return True + else: + self._update_status(f"adb push failed with return code {proc.returncode}") + return False + + +class LinuxMTPManager: + """Manages MTP operations on Linux systems.""" + + def __init__(self): + self.mount_point = "/tmp/android_mtp" + + def mount_mtp_device(self) -> Optional[str]: + """Mount MTP device to filesystem using jmtpfs.""" + try: + # Create mount point + os.makedirs(self.mount_point, exist_ok=True) + + # Check if already mounted + result = subprocess.run( + ["mountpoint", self.mount_point], capture_output=True, text=True + ) + if result.returncode == 0: + return self.mount_point + + # First, try to unmount any existing GVFS MTP mounts + self._unmount_gvfs_mtp() + + # Kill any existing MTP processes that might be interfering + subprocess.run(["pkill", "-f", "gvfs-mtp"], capture_output=True) + subprocess.run(["pkill", "-f", "jmtpfs"], capture_output=True) + + # Wait a moment for processes to clean up + time.sleep(1) + + # Mount using jmtpfs + result = subprocess.run( + ["jmtpfs", self.mount_point], capture_output=True, text=True + ) + if result.returncode == 0: + return self.mount_point + else: + print(f"Failed to mount MTP device: {result.stderr}") + return None + except Exception as e: + print(f"Error mounting MTP device: {e}") + return None + + def _unmount_gvfs_mtp(self): + """Unmount any GVFS MTP mounts.""" + try: + # Find GVFS MTP mounts + result = subprocess.run(["mount"], capture_output=True, text=True) + for line in result.stdout.splitlines(): + if "gvfs" in line and "mtp" in line: + # Extract mount point from mount line + parts = line.split() + if len(parts) >= 3: + mount_point = parts[2] + subprocess.run( + ["fusermount", "-u", mount_point], capture_output=True + ) + + # Also try to unmount common GVFS locations + gvfs_locations = ["/run/user/*/gvfs/mtp*", "/media/*", "~/.gvfs/mtp*"] + + for location_pattern in gvfs_locations: + result = subprocess.run( + ["find", "/run/user", "-name", "mtp*", "-type", "d"], + capture_output=True, + text=True, + ) + for mount_point in result.stdout.strip().split("\n"): + if mount_point: + subprocess.run( + ["fusermount", "-u", mount_point], capture_output=True + ) + + except Exception as e: + print(f"Warning: Could not unmount GVFS MTP: {e}") + + def unmount_mtp_device(self) -> bool: + """Unmount MTP device.""" + try: + subprocess.run(["fusermount", "-u", self.mount_point], check=True) + return True + except subprocess.CalledProcessError as e: + print(f"Failed to unmount: {e}") + return False + + def find_gvfs_mtp_mount(self) -> Optional[str]: + """Find existing GVFS MTP mount point.""" + try: + # Check common GVFS mount locations (Linux only) + if sys.platform.startswith("linux"): + try: + if hasattr(os, "getuid"): + user_id = os.getuid() # type: ignore + gvfs_patterns = [ + f"/run/user/{user_id}/gvfs/mtp*", + "/media/*android*", + "/media/*MTP*", + ] + else: + # Fallback if getuid is not available + gvfs_patterns = [ + "/run/user/*/gvfs/mtp*", + "/media/*android*", + "/media/*MTP*", + ] + except (AttributeError, OSError): + # Fallback if getuid is not available or fails + gvfs_patterns = [ + "/run/user/*/gvfs/mtp*", + "/media/*android*", + "/media/*MTP*", + ] + + for pattern in gvfs_patterns: + matches = glob.glob(pattern) + if matches: + # Return the first valid mount point + for mount in matches: + if os.path.isdir(mount): + return mount + return None + except Exception as e: + print(f"Error finding GVFS mount: {e}") + return None + + +# Helper functions for standalone usage +def get_adb_binary_path() -> str: + """Get the path to the ADB binary.""" + return ADB_BINARY_PATH + + +def is_adb_available() -> bool: + """Check if ADB binary is available.""" + return os.path.isfile(ADB_BINARY_PATH) + + +def get_platform_type() -> str: + """Get the current platform type.""" + return OS_TYPE diff --git a/src/android_file_handler.py b/src/android_file_handler.py deleted file mode 100644 index 746ba57..0000000 --- a/src/android_file_handler.py +++ /dev/null @@ -1,459 +0,0 @@ -import os -import sys -import threading -import subprocess -import requests # type: ignore -import zipfile -import io -import shutil -import tkinter as tk -from tkinter import messagebox, filedialog, ttk - -ADB_WIN_ZIP_URL = "https://dl.google.com/android/repository/platform-tools-latest-windows.zip" -ADB_LINUX_ZIP_URL = "https://dl.google.com/android/repository/platform-tools-latest-linux.zip" -LOCAL_ADB_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "platform-tools") -OS_TYPE = f"{sys.platform}" - -if OS_TYPE.startswith("linux"): - ADB_BINARY_NAME = "adb" -elif OS_TYPE.startswith("win32"): - ADB_BINARY_NAME = "adb.exe" - -ADB_BINARY_PATH = os.path.join(LOCAL_ADB_FOLDER, ADB_BINARY_NAME) - -def check_local_disk_space(): - try: - # Check available disk space - free_space = shutil.disk_usage(LOCAL_ADB_FOLDER)[2] - if free_space < 50 * 1024 * 1024: # 50MB minimum - raise Exception("Insufficient disk space") - except OSError: - # Create directory if it doesn't exist - os.makedirs(LOCAL_ADB_FOLDER, exist_ok=True) -def download_and_extract_adb(): - if os.path.isfile(ADB_BINARY_PATH): - return True - try: - # Determine the correct URL based on platform - if OS_TYPE.startswith("linux"): - ADB_ZIP_URL = ADB_LINUX_ZIP_URL - elif OS_TYPE.startswith("win"): - ADB_ZIP_URL = ADB_WIN_ZIP_URL - else: - print("Unsupported platform") - return False - - check_local_disk_space() - - print("Downloading platform-tools (ADB)...") - - response = requests.get(ADB_ZIP_URL, stream=True, timeout=30) - - response.raise_for_status() - - binaryArchive = zipfile.ZipFile(io.BytesIO(response.content)) - - binaryArchive.extractall(LOCAL_ADB_FOLDER) - - if OS_TYPE.startswith("linux"): - if os.path.exists(ADB_BINARY_PATH): - os.chmod(ADB_BINARY_PATH, 0o755) - elif not os.path.exists(ADB_BINARY_PATH): - check_local_disk_space() - download_and_extract_adb() - else: - raise Exception("ADB binary not found after extraction") - - print("Downloaded and extracted platform-tools.") - return True - except Exception as e: - print(f"Failed to download ADB: {e}") - return False - -def run_adb_command(args, capture_output=True): - cmd = [ADB_BINARY_PATH] + args - try: - if capture_output: - p = subprocess.run(cmd, capture_output=True, text=True, timeout=15) - return p.stdout.strip(), p.stderr.strip(), p.returncode - else: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - return p - except Exception as e: - return None, str(e), -1 - -def check_device(): - out, err, rc = run_adb_command(["devices"]) - if rc != 0: - return None - for line in out.splitlines(): - if line.endswith("\tdevice"): - return line.split()[0] - return None - -class App(tk.Tk,): - - def __init__(self): - super().__init__() - self.title("Android Folder Puller") - self.geometry("520x320") - self.minsize(520, 320) # Set minimum window size with more vertical space - self.resizable(True, True) # Make window resizable - # Add to __init__ - self.direction_var = tk.StringVar(value="pull") - direction_frame = tk.Frame(self) - direction_frame.pack(anchor="w", padx=10, pady=(10,0)) - tk.Radiobutton(direction_frame, text="Pull (Android → Computer)", - variable=self.direction_var, value="pull").pack(side="left") - tk.Radiobutton(direction_frame, text="Push (Computer → Android)", - variable=self.direction_var, value="push").pack(side="left", padx=(20,0)) - - - # UI Elements - tk.Label(self, text="Remote folder path (Android device):").pack(anchor="w", padx=10, pady=(10,0)) - self.remote_path_var = tk.StringVar() - remote_path_frame = tk.Frame(self) - remote_path_frame.pack(fill="x", padx=10) - self.remote_path_entry = tk.Entry(remote_path_frame, textvariable=self.remote_path_var, width=50) - self.remote_path_entry.pack(side="left", fill="x", expand=True) - - if OS_TYPE.startswith("linux"): - tk.Button(remote_path_frame, text="Browse...", command=self.linux_browse_remote_folder).pack(side="right", padx=(5,0)) - else: - tk.Button(remote_path_frame, text="Browse...", command=self.browse_local_folder).pack(side="right", padx=(5,0)) - - tk.Label(self, text="Local destination folder (Computer):").pack(anchor="w", padx=10, pady=(10,0)) - self.local_path_var = tk.StringVar() - local_path_frame = tk.Frame(self) - local_path_frame.pack(fill="x", padx=10) - self.local_path_entry = tk.Entry(local_path_frame, textvariable=self.local_path_var, width=50) - self.local_path_entry.pack(side="left", fill="x", expand=True) - tk.Button(local_path_frame, text="Browse...", command=self.browse_local_folder).pack(side="right", padx=(5,0)) - - self.progress = ttk.Progressbar(self, orient="horizontal", mode="determinate") - self.progress.pack(fill="x", padx=10, pady=(20, 5)) - - self.status_label = tk.Label(self, text="Status: Idle") - self.status_label.pack(anchor="w", padx=10, fill="x") - - self.start_btn = tk.Button(self, text="Start Transfer", command=self.start_transfer) - self.start_btn.pack(pady=10) - - self.protocol("WM_DELETE_WINDOW", self.on_close) - - # Check adb availability - if not os.path.isfile(ADB_BINARY_PATH): - self.disable_controls() - self.status_label.config(text="ADB not found locally. Downloading...") - self.update() - success = download_and_extract_adb() - if success: - self.status_label.config(text="ADB downloaded and ready.") - self.enable_controls() - else: - self.status_label.config(text="Failed to download ADB. Please check your internet and restart.") - messagebox.showerror("Error", "Failed to download ADB tools. Exiting.") - self.quit() - return - - # Check device connected - self.status_label.config(text="Checking for connected device...") - self.update() - device = check_device() - if not device: - self.disable_controls() - self.status_label.config(text="No device detected. Enable USB debugging and connect your device.") - self.show_enable_debugging_instructions() - else: - self.status_label.config(text=f"Device detected: {device}") - def linux_mount_mtp_device(self): - """Mount MTP device to filesystem using jmtpfs""" - mount_point = "/tmp/android_mtp" - try: - # Create mount point - os.makedirs(mount_point, exist_ok=True) - - # Check if already mounted - result = subprocess.run(["mountpoint", mount_point], - capture_output=True, text=True) - if result.returncode == 0: - return mount_point - - # First, try to unmount any existing GVFS MTP mounts - self._unmount_gvfs_mtp() - - # Kill any existing MTP processes that might be interfering - subprocess.run(["pkill", "-f", "gvfs-mtp"], capture_output=True) - subprocess.run(["pkill", "-f", "jmtpfs"], capture_output=True) - - # Wait a moment for processes to clean up - import time - time.sleep(1) - - # Mount using jmtpfs - result = subprocess.run(["jmtpfs", mount_point], - capture_output=True, text=True) - if result.returncode == 0: - return mount_point - else: - print(f"Failed to mount MTP device: {result.stderr}") - return None - except Exception as e: - print(f"Error mounting MTP device: {e}") - return None - - def _unmount_gvfs_mtp(self): - """Unmount any GVFS MTP mounts""" - try: - # Find GVFS MTP mounts - result = subprocess.run(["mount"], capture_output=True, text=True) - for line in result.stdout.splitlines(): - if "gvfs" in line and "mtp" in line: - # Extract mount point from mount line - parts = line.split() - if len(parts) >= 3: - mount_point = parts[2] - subprocess.run(["fusermount", "-u", mount_point], - capture_output=True) - - # Also try to unmount common GVFS locations - gvfs_locations = [ - "/run/user/*/gvfs/mtp*", - "/media/*", - "~/.gvfs/mtp*" - ] - - for location_pattern in gvfs_locations: - result = subprocess.run(["find", "/run/user", "-name", "mtp*", "-type", "d"], - capture_output=True, text=True) - for mount_point in result.stdout.strip().split('\n'): - if mount_point: - subprocess.run(["fusermount", "-u", mount_point], - capture_output=True) - - except Exception as e: - print(f"Warning: Could not unmount GVFS MTP: {e}") - - def linux_unmount_mtp_device(self): - """Unmount MTP device""" - mount_point = "/tmp/android_mtp" - try: - subprocess.run(["fusermount", "-u", mount_point], check=True) - return True - except subprocess.CalledProcessError as e: - print(f"Failed to unmount: {e}") - return False - - def linux_browse_remote_folder(self): - """Browse remote Android folders via MTP""" - # First try using existing GVFS mount - gvfs_mount = self._find_gvfs_mtp_mount() - if gvfs_mount: - try: - folder = filedialog.askdirectory(initialdir=gvfs_mount, - title="Select Android folder") - if folder: - # Convert filesystem path back to Android path - relative_path = os.path.relpath(folder, gvfs_mount) - # Clean up the path for Android - if relative_path == ".": - android_path = "/sdcard" - else: - android_path = f"/sdcard/{relative_path}".replace("\\", "/") - self.remote_path_var.set(android_path) - return - except Exception as e: - print(f"GVFS browse failed: {e}") - - # Fallback to jmtpfs - mount_point = self.linux_mount_mtp_device() - if mount_point: - try: - folder = filedialog.askdirectory(initialdir=mount_point, - title="Select Android folder") - if folder: - # Convert filesystem path back to Android path - relative_path = os.path.relpath(folder, mount_point) - if relative_path == ".": - android_path = "/sdcard" - else: - android_path = f"/sdcard/{relative_path}".replace("\\", "/") - self.remote_path_var.set(android_path) - finally: - self.linux_unmount_mtp_device() - else: - messagebox.showerror("Error", "Could not mount Android device via MTP. Make sure it's connected and set to 'File Transfer' mode.") - - def _find_gvfs_mtp_mount(self): - """Find existing GVFS MTP mount point""" - try: - # Check common GVFS mount locations - import glob - user_id = os.getuid() - gvfs_patterns = [ - f"/run/user/{user_id}/gvfs/mtp*", - "/media/*android*", - "/media/*MTP*" - ] - - for pattern in gvfs_patterns: - matches = glob.glob(pattern) - if matches: - # Return the first valid mount point - for mount in matches: - if os.path.isdir(mount): - return mount - return None - except Exception as e: - print(f"Error finding GVFS mount: {e}") - return None - - def browse_local_folder(self): - folder = filedialog.askdirectory() - if folder: - self.local_path_var.set(folder) - - def disable_controls(self): - self.remote_path_entry.config(state="disabled") - self.local_path_entry.config(state="disabled") - self.start_btn.config(state="disabled") - - def enable_controls(self): - self.remote_path_entry.config(state="normal") - self.local_path_entry.config(state="normal") - self.start_btn.config(state="normal") - - def show_enable_debugging_instructions(self): - msg = ( - "To enable USB debugging:\n" - "1. Open Settings → About phone.\n" - "2. Tap 'Build number' seven times to unlock Developer Options.\n" - "3. Go back to Settings → Developer Options.\n" - "4. Enable 'USB debugging'.\n" - "5. Connect your phone via USB and accept the prompt to allow debugging.\n\n" - "After enabling, restart this app." - ) - messagebox.showinfo("Enable USB Debugging", msg) - - def show_disable_debugging_reminder(self): - msg = ( - "Transfer completed.\n\n" - "For security, disable USB debugging when done:\n" - "Settings → Developer Options → disable 'USB debugging'." - ) - messagebox.showinfo("Disable USB Debugging", msg) - - def start_transfer(self): - remote_path = self.remote_path_var.get().strip() - local_path = self.local_path_var.get().strip() - direction = self.direction_var.get() - - if not remote_path: - messagebox.showerror("Input Error", "Remote folder path cannot be empty.") - return - if not local_path or not os.path.isdir(local_path): - messagebox.showerror("Input Error", "Please select a valid local destination folder.") - return - - self.disable_controls() - self.progress["value"] = 0 - - if direction == "pull": - self.status_label.config(text="Starting pull transfer...") - threading.Thread(target=self.pull_folder, args=(remote_path, local_path), daemon=True).start() - elif direction == "push": - self.status_label.config(text="Starting push transfer...") - threading.Thread(target=self.push_folder, args=(local_path, remote_path), daemon=True).start() - else: - self.report_error("Invalid transfer direction selected.") - self.enable_controls() - - def pull_folder(self, remote_path, local_path): - cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path] - - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1) - - for line in proc.stdout: - pct = self.parse_progress(line) - if pct is not None: - self.update_progress(pct) - self.set_status(line.strip()) - - proc.wait() - if proc.returncode == 0: - self.update_progress(100) - self.set_status("Transfer completed successfully.") - self.show_disable_debugging_reminder() - else: - self.report_error(f"Transfer failed with code {proc.returncode}") - - except Exception as e: - self.report_error(f"Transfer error: {e}") - finally: - self.enable_controls() - - def push_folder(self, local_path,remote_path): - # Run adb push command with progress parsing - cmd = [ADB_BINARY_PATH, "push", f"{local_path}", f"{remote_path}" ] - - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1) - except Exception as e: - self.report_error(f"Failed to start adb: {e}") - return - - self.set_status("Transferring files...") - - for line in proc.stdout: - # adb progress lines look like: - # 12345 KB/s (100%) - # or partial output, parse percentage from line - pct = self.parse_progress(line) - if pct is not None: - self.update_progress(pct) - self.set_status(line.strip()) - - proc.wait() - if proc.returncode == 0: - self.update_progress(100) - self.set_status("Transfer completed successfully.") - self.enable_controls() - self.show_disable_debugging_reminder() - else: - self.report_error(f"adb pull failed with return code {proc.returncode}") - - def parse_progress(self, text_line): - import re - m = re.search(r"\((\d{1,3})%\)", text_line) - if m: - pct = int(m.group(1)) - if 0 <= pct <= 100: - return pct - return None - - def update_progress(self, pct): - self.progress["value"] = pct - self.update_idletasks() - - def set_status(self, msg): - self.status_label.config(text="Status: " + msg) - self.update_idletasks() - - def report_error(self, message): - self.status_label.config(text="Error: " + message) - messagebox.showerror("Error", message) - self.enable_controls() - - def on_close(self): - self.destroy() - -if __name__ == "__main__": - if sys.platform not in ["win32", "linux"]: - tk.Tk().withdraw() - messagebox.showerror("Unsupported OS", "This application only supports Windows or Linux.") - sys.exit(1) - - app = App() - app.mainloop() diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..6c12858 --- /dev/null +++ b/src/gui.py @@ -0,0 +1,712 @@ +""" +Android File Handler - GUI Module +Provides the user interface for the Android file transfer application. +""" + +import os +import sys +import threading +import tkinter as tk +from tkinter import messagebox, filedialog, ttk, simpledialog +from typing import Optional + +try: + # Try relative import first (when used as module) + from .adb_manager import ( + ADBManager, + LinuxMTPManager, + get_platform_type, + is_adb_available, + ) +except ImportError: + # Fall back to direct import (when run directly) + from adb_manager import ( + ADBManager, + LinuxMTPManager, + get_platform_type, + is_adb_available, + ) + + +class AndroidFileHandlerGUI(tk.Tk): + """Main GUI application for Android file transfers.""" + + def __init__(self): + super().__init__() + + # Initialize business logic + self.adb_manager = ADBManager() + self.adb_manager.set_progress_callback(self.update_progress) + self.adb_manager.set_status_callback(self.set_status) + + if get_platform_type().startswith("linux"): + self.mtp_manager = LinuxMTPManager() + else: + self.mtp_manager = None + + # Setup UI + self._setup_ui() + self._initialize_app() + + def _setup_ui(self): + """Setup the user interface.""" + # Window configuration + self.title("Android Folder Puller") + self.geometry("520x320") + self.minsize(520, 320) + self.resizable(True, True) + + # Direction selection + self.direction_var = tk.StringVar(value="pull") + direction_frame = tk.Frame(self) + direction_frame.pack(anchor="w", padx=10, pady=(10, 0)) + tk.Radiobutton( + direction_frame, + text="Pull (Android → Computer)", + variable=self.direction_var, + value="pull", + ).pack(side="left") + tk.Radiobutton( + direction_frame, + text="Push (Computer → Android)", + variable=self.direction_var, + value="push", + ).pack(side="left", padx=(20, 0)) + + # Remote folder path + tk.Label(self, text="Remote folder path (Android device):").pack( + anchor="w", padx=10, pady=(10, 0) + ) + self.remote_path_var = tk.StringVar() + remote_path_frame = tk.Frame(self) + remote_path_frame.pack(fill="x", padx=10) + self.remote_path_entry = tk.Entry( + remote_path_frame, textvariable=self.remote_path_var, width=50 + ) + self.remote_path_entry.pack(side="left", fill="x", expand=True) + + # Browse button for remote path + tk.Button( + remote_path_frame, text="Browse...", command=self.browse_remote_folder + ).pack(side="right", padx=(5, 0)) + + # Local folder path + tk.Label(self, text="Local destination folder (Computer):").pack( + anchor="w", padx=10, pady=(10, 0) + ) + self.local_path_var = tk.StringVar() + local_path_frame = tk.Frame(self) + local_path_frame.pack(fill="x", padx=10) + self.local_path_entry = tk.Entry( + local_path_frame, textvariable=self.local_path_var, width=50 + ) + self.local_path_entry.pack(side="left", fill="x", expand=True) + tk.Button( + local_path_frame, text="Browse...", command=self.browse_local_folder + ).pack(side="right", padx=(5, 0)) + + # Progress bar + self.progress = ttk.Progressbar(self, orient="horizontal", mode="determinate") + self.progress.pack(fill="x", padx=10, pady=(20, 5)) + + # Status label + self.status_label = tk.Label(self, text="Status: Idle") + self.status_label.pack(anchor="w", padx=10, fill="x") + + # Start button + self.start_btn = tk.Button( + self, text="Start Transfer", command=self.start_transfer + ) + self.start_btn.pack(pady=10) + + # Window close protocol + self.protocol("WM_DELETE_WINDOW", self.on_close) + + def _initialize_app(self): + """Initialize the application - check ADB and device.""" + # Check adb availability + if not is_adb_available(): + self.disable_controls() + self.set_status("ADB not found locally. Downloading...") + self.update() + success = self.adb_manager.download_and_extract_adb() + if success: + self.set_status("ADB downloaded and ready.") + self.enable_controls() + else: + self.set_status( + "Failed to download ADB. Please check your internet and restart." + ) + messagebox.showerror("Error", "Failed to download ADB tools. Exiting.") + self.quit() + return + + # Check device connected + self.set_status("Checking for connected device...") + self.update() + device = self.adb_manager.check_device() + if not device: + self.disable_controls() + self.set_status( + "No device detected. Enable USB debugging and connect your device." + ) + self.show_enable_debugging_instructions() + else: + self.set_status(f"Device detected: {device}") + + def browse_remote_folder(self): + """Browse remote Android folders via MTP (Linux)""" + # For Linux - use MTP browsing + if get_platform_type().startswith("linux") and self.mtp_manager: + # First try using existing GVFS mount + gvfs_mount = self.mtp_manager.find_gvfs_mtp_mount() + if gvfs_mount: + try: + folder = filedialog.askdirectory( + initialdir=gvfs_mount, title="Select Android folder" + ) + if folder: + # Convert filesystem path back to Android path + relative_path = os.path.relpath(folder, gvfs_mount) + if relative_path == ".": + android_path = "/sdcard" + else: + android_path = f"/sdcard/{relative_path}".replace("\\", "/") + self.remote_path_var.set(android_path) + return + except Exception as e: + print(f"GVFS browse failed: {e}") + + # Fallback to jmtpfs + mount_point = self.mtp_manager.mount_mtp_device() + if mount_point: + try: + folder = filedialog.askdirectory( + initialdir=mount_point, title="Select Android folder" + ) + if folder: + # Convert filesystem path back to Android path + relative_path = os.path.relpath(folder, mount_point) + if relative_path == ".": + android_path = "/sdcard" + else: + android_path = f"/sdcard/{relative_path}".replace("\\", "/") + self.remote_path_var.set(android_path) + finally: + self.mtp_manager.unmount_mtp_device() + else: + messagebox.showerror( + "Error", + "Could not mount Android device via MTP. " + "Make sure it's connected and set to 'File Transfer' mode.", + ) + else: + # For Windows - show common Android paths dialog + self._show_android_filesystem_tree() + + def _show_android_filesystem_tree(self): + """Show a browsable Android folder tree for Windows users.""" + # Check if device is connected + device = self.adb_manager.check_device() + if not device: + messagebox.showerror( + "No Device", + "No Android device connected. Please connect your device and enable USB debugging.", + ) + return + + # Create browsable folder dialog + browser_window = tk.Toplevel(self) + browser_window.title("Browse Android Folders") + browser_window.geometry("500x400") + browser_window.transient(self) + browser_window.grab_set() + + tk.Label( + browser_window, + text="Browse Android device folders:", + font=("Arial", 10, "bold"), + ).pack(pady=10) + + # Create treeview for folder browsing + tree_frame = tk.Frame(browser_window) + tree_frame.pack(fill="both", expand=True, padx=10) + + # Treeview with scrollbars + tree_scroll_y = tk.Scrollbar(tree_frame) + tree_scroll_y.pack(side="right", fill="y") + + tree_scroll_x = tk.Scrollbar(tree_frame, orient="horizontal") + tree_scroll_x.pack(side="bottom", fill="x") + + tree = ttk.Treeview( + tree_frame, + yscrollcommand=tree_scroll_y.set, + xscrollcommand=tree_scroll_x.set, + ) + tree.pack(side="left", fill="both", expand=True) + + tree_scroll_y.config(command=tree.yview) + tree_scroll_x.config(command=tree.xview) + + # Current path display + path_frame = tk.Frame(browser_window) + path_frame.pack(fill="x", padx=10, pady=5) + tk.Label(path_frame, text="Current path:").pack(side="left") + current_path_var = tk.StringVar(value="/sdcard") + current_path_label = tk.Label( + path_frame, textvariable=current_path_var, font=("Courier", 9) + ) + current_path_label.pack(side="left", padx=(5, 0)) + + def load_folders_async(parent_item, path): + """Load folders asynchronously to avoid UI freezing.""" + + def load_in_thread(): + try: + # Ensure path ends with / for proper directory listing + list_path = path if path.endswith("/") else path + "/" + + # Use ls -la to get detailed listing with file type information + result = self.adb_manager.run_adb_command( + ["shell", "ls", "-la", list_path] + ) + + if not isinstance(result, tuple) or len(result) != 3: + self.after( + 0, + lambda: tree.insert( + parent_item, + "end", + text="(Error loading folders)", + values=[""], + ), + ) + return + + stdout, stderr, returncode = result + + # Update UI in main thread - always remove Loading... first + def update_tree(): + # First, remove any existing dummy children + children = tree.get_children(parent_item) + for child in children: + child_text = tree.item(child, "text") + if child_text in [ + "Loading...", + "(No Folders)", + "(Error loading folders)", + "(Permission denied)", + ]: + tree.delete(child) + + # Check for errors + if returncode != 0: + if stderr and "Permission denied" in stderr: + tree.insert( + parent_item, + "end", + text="(Permission denied)", + values=[""], + ) + else: + tree.insert( + parent_item, + "end", + text="(Error loading folders)", + values=[""], + ) + return + + if not stdout or not stdout.strip(): + tree.insert( + parent_item, "end", text="(No Folders)", values=[""] + ) + return + + # Parse ls -la output to find directories + folders = [] + lines = stdout.strip().split("\n") + for line in lines: + if line.startswith("d"): + # Extract folder name (last part after spaces) + parts = line.split() + if ( + len(parts) >= 8 + ): # Android ls -la typically has 8+ parts + folder_name = " ".join( + parts[7:] + ) # Handle names with spaces (start from column 8) + if folder_name not in [ + ".", + "..", + ] and not folder_name.startswith("."): + folders.append(folder_name) + + # Add folders to tree + if folders: + for folder in sorted(folders): + folder_path = f"{path.rstrip('/')}/{folder}" + item = tree.insert( + parent_item, + "end", + text=folder, + values=[folder_path], + ) + # Add a dummy child to make it expandable + tree.insert(item, "end", text="Loading...") + else: + # No folders found, show indicator + tree.insert( + parent_item, "end", text="(No Folders)", values=[""] + ) + + self.after(0, update_tree) + + except Exception as e: + print(f"Error loading folders from {path}: {e}") + + def error_update(): + # Remove Loading... even on error + children = tree.get_children(parent_item) + for child in children: + child_text = tree.item(child, "text") + if child_text == "Loading...": + tree.delete(child) + tree.insert( + parent_item, + "end", + text="(Error loading folders)", + values=[""], + ) + + self.after(0, error_update) + + # Run in background thread + threading.Thread(target=load_in_thread, daemon=True).start() + + def load_folders(parent_item, path): + """Load folders from Android device using ADB (legacy sync version for initial load).""" + # First, remove any existing dummy children + children = tree.get_children(parent_item) + for child in children: + child_text = tree.item(child, "text") + if child_text in [ + "Loading...", + "(No Folders)", + "(Error loading folders)", + "(Permission denied)", + ]: + tree.delete(child) + + try: + # Ensure path ends with / for proper directory listing + list_path = path if path.endswith("/") else path + "/" + + # Use ls -la to get detailed listing with file type information + result = self.adb_manager.run_adb_command( + ["shell", "ls", "-la", list_path] + ) + + if not isinstance(result, tuple) or len(result) != 3: + tree.insert( + parent_item, "end", text="(Error loading folders)", values=[""] + ) + return [] + + stdout, stderr, returncode = result + if returncode != 0: + if stderr and "Permission denied" in stderr: + tree.insert( + parent_item, "end", text="(Permission denied)", values=[""] + ) + else: + tree.insert( + parent_item, + "end", + text="(Error loading folders)", + values=[""], + ) + return [] + + if not stdout or not stdout.strip(): + tree.insert(parent_item, "end", text="(No Folders)", values=[""]) + return [] + + # Parse ls -la output to find directories + folders = [] + lines = stdout.strip().split("\n") + for line in lines: + if line.startswith("d"): + # Extract folder name (last part after spaces) + parts = line.split() + if len(parts) >= 8: # Android ls -la typically has 8+ parts + folder_name = " ".join( + parts[7:] + ) # Handle names with spaces (start from column 8) + if folder_name not in [ + ".", + "..", + ] and not folder_name.startswith("."): + folders.append(folder_name) + + # Add folders to tree + if folders: + for folder in sorted(folders): + folder_path = f"{path.rstrip('/')}/{folder}" + item = tree.insert( + parent_item, "end", text=folder, values=[folder_path] + ) + # Add a dummy child to make it expandable + tree.insert(item, "end", text="Loading...") + else: + # No folders found, show indicator + tree.insert(parent_item, "end", text="(No Folders)", values=[""]) + + return folders + except Exception as e: + print(f"Error loading folders from {path}: {e}") + tree.insert( + parent_item, "end", text="(Error loading folders)", values=[""] + ) + return [] + + def on_tree_expand(event): + """Handle tree expansion - load subfolders dynamically.""" + item = tree.selection()[0] if tree.selection() else tree.focus() + if not item: + return + + # Get the path + folder_path = ( + tree.item(item, "values")[0] if tree.item(item, "values") else None + ) + if not folder_path: # Skip items without valid paths (like "(No Folders)") + return + + current_path_var.set(folder_path) + + # Check if we need to load subfolders + children = tree.get_children(item) + has_loading = any( + tree.item(child, "text") == "Loading..." for child in children + ) + + # Only load if we have a "Loading..." placeholder - use async version + if has_loading: + load_folders_async(item, folder_path) + + def on_tree_select(event): + """Handle tree selection - update current path.""" + item = tree.selection()[0] if tree.selection() else None + if item: + folder_path = ( + tree.item(item, "values")[0] if tree.item(item, "values") else None + ) + if folder_path: # Only update if valid path + current_path_var.set(folder_path) + + # Bind events + tree.bind("<>", on_tree_expand) + tree.bind("<>", on_tree_select) + + # Determine which path to use - prefer /sdcard, fallback to /storage/emulated/0 + primary_path = "/sdcard" + fallback_path = "/storage/emulated/0" + + # Test if /sdcard is accessible (with trailing slash for directory listing) + test_result = self.adb_manager.run_adb_command( + ["shell", "ls", "-la", primary_path + "/"] + ) + if ( + isinstance(test_result, tuple) + and len(test_result) == 3 + and test_result[2] == 0 + ): + # /sdcard is accessible + android_path = primary_path + else: + # /sdcard not accessible, use fallback + android_path = fallback_path + + # Create single "Android" root item + android_item = tree.insert("", "end", text="Android", values=[android_path]) + tree.insert(android_item, "end", text="Loading...") + + # Set initial path + current_path_var.set(android_path) + + # Expand and load the Android root immediately + tree.item(android_item, open=True) + load_folders(android_item, android_path) + + # Select the Android item + tree.selection_set(android_item) + + # Buttons + button_frame = tk.Frame(browser_window) + button_frame.pack(pady=10) + + def select_current_folder(): + """Select the currently highlighted folder.""" + current_path = current_path_var.get() + if current_path and current_path.strip(): + self.remote_path_var.set(current_path) + browser_window.destroy() + else: + messagebox.showwarning("No Selection", "Please select a valid folder.") + + def enter_custom_path(): + """Allow user to enter a custom path.""" + browser_window.destroy() + custom_path = simpledialog.askstring( + "Custom Path", + "Enter custom Android folder path:", + initialvalue="/sdcard/", + ) + if custom_path: + self.remote_path_var.set(custom_path.strip()) + + tk.Button( + button_frame, text="Select This Folder", command=select_current_folder + ).pack(side="left", padx=5) + tk.Button( + button_frame, text="Enter Custom Path", command=enter_custom_path + ).pack(side="left", padx=5) + tk.Button(button_frame, text="Cancel", command=browser_window.destroy).pack( + side="left", padx=5 + ) + + def browse_local_folder(self): + """Browse for local folder.""" + folder = filedialog.askdirectory() + if folder: + self.local_path_var.set(folder) + + def disable_controls(self): + """Disable UI controls during operations.""" + self.remote_path_entry.config(state="disabled") + self.local_path_entry.config(state="disabled") + self.start_btn.config(state="disabled") + + def enable_controls(self): + """Enable UI controls after operations.""" + self.remote_path_entry.config(state="normal") + self.local_path_entry.config(state="normal") + self.start_btn.config(state="normal") + + def show_enable_debugging_instructions(self): + """Show instructions for enabling USB debugging.""" + msg = ( + "To enable USB debugging:\n" + "1. Open Settings → About phone.\n" + "2. Tap 'Build number' seven times to unlock Developer Options.\n" + "3. Go back to Settings → Developer Options.\n" + "4. Enable 'USB debugging'.\n" + "5. Connect your phone via USB and accept the prompt to allow debugging.\n\n" + "After enabling, restart this app." + ) + messagebox.showinfo("Enable USB Debugging", msg) + + def show_disable_debugging_reminder(self): + """Show reminder to disable USB debugging after transfer.""" + msg = ( + "Transfer completed.\n\n" + "For security, disable USB debugging when done:\n" + "Settings → Developer Options → disable 'USB debugging'." + ) + messagebox.showinfo("Disable USB Debugging", msg) + + def start_transfer(self): + """Start the file transfer operation.""" + remote_path = self.remote_path_var.get().strip() + local_path = self.local_path_var.get().strip() + direction = self.direction_var.get() + + # Validate inputs + if not remote_path: + messagebox.showerror("Input Error", "Remote folder path cannot be empty.") + return + if not local_path or not os.path.isdir(local_path): + messagebox.showerror( + "Input Error", "Please select a valid local destination folder." + ) + return + + # Start transfer + self.disable_controls() + self.progress["value"] = 0 + + if direction == "pull": + self.set_status("Starting pull transfer...") + threading.Thread( + target=self._pull_thread, args=(remote_path, local_path), daemon=True + ).start() + elif direction == "push": + self.set_status("Starting push transfer...") + threading.Thread( + target=self._push_thread, args=(local_path, remote_path), daemon=True + ).start() + else: + self.report_error("Invalid transfer direction selected.") + self.enable_controls() + + def _pull_thread(self, remote_path: str, local_path: str): + """Thread function for pull operations.""" + try: + success = self.adb_manager.pull_folder(remote_path, local_path) + if success: + self.show_disable_debugging_reminder() + except Exception as e: + self.report_error(f"Pull operation failed: {e}") + finally: + self.enable_controls() + + def _push_thread(self, local_path: str, remote_path: str): + """Thread function for push operations.""" + try: + success = self.adb_manager.push_folder(local_path, remote_path) + if success: + self.show_disable_debugging_reminder() + except Exception as e: + self.report_error(f"Push operation failed: {e}") + finally: + self.enable_controls() + + def update_progress(self, percentage: int): + """Update the progress bar.""" + self.progress["value"] = percentage + self.update_idletasks() + + def set_status(self, message: str): + """Update the status label.""" + self.status_label.config(text=f"Status: {message}") + self.update_idletasks() + + def report_error(self, message: str): + """Report an error to the user.""" + self.status_label.config(text=f"Error: {message}") + messagebox.showerror("Error", message) + self.enable_controls() + + def on_close(self): + """Handle window close event.""" + self.destroy() + + +def main(): + """Main function to run the application.""" + if sys.platform not in ["win32", "linux"]: + root = tk.Tk() + root.withdraw() + messagebox.showerror( + "Unsupported OS", "This application only supports Windows or Linux." + ) + sys.exit(1) + + app = AndroidFileHandlerGUI() + app.mainloop() + + +if __name__ == "__main__": + main() diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..0597dec --- /dev/null +++ b/src/main.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +""" +Android File Handler - Main Entry Point +Simple entry point to launch the Android file transfer application. +""" + +try: + # Try relative import first (when used as module) + from .gui import main +except ImportError: + # Fall back to direct import (when run directly) + from gui import main + +if __name__ == "__main__": + main() diff --git a/test_parsing.py b/test_parsing.py new file mode 100644 index 0000000..657b108 --- /dev/null +++ b/test_parsing.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" +Test script to debug folder parsing +""" +import sys +import os + +sys.path.insert(0, "src") + +from adb_manager import ADBManager + + +def main(): + adb = ADBManager() + print("Testing folder parsing...") + + result = adb.run_adb_command(["shell", "ls", "-la", "/sdcard/"]) + print(f"Return code: {result[2]}") + + lines = result[0].strip().split("\n") + print(f"Total lines: {len(lines)}") + print("\nFirst 10 lines:") + for i, line in enumerate(lines[:10]): + print(f" {i}: {repr(line)}") + + print("\nParsing directories:") + folders = [] + for i, line in enumerate(lines): + if line.startswith("d"): + parts = line.split() + print(f" Line {i}: {len(parts)} parts") + if len(parts) >= 8: # Changed from 9 to 8 + folder_name = " ".join(parts[7:]) # Changed from 8 to 7 + print(f" Folder name: {repr(folder_name)}") + if folder_name not in [".", ".."]: + folders.append(folder_name) + print(f" ✅ Added: {repr(folder_name)}") + else: + print(f" ❌ Skipped: {repr(folder_name)}") + + print(f"\nFinal folders list ({len(folders)} items):") + for folder in sorted(folders): + print(f" - {repr(folder)}") + + +if __name__ == "__main__": + main()