Transfer is working, added tooling, debug scripts

This commit is contained in:
2025-08-25 11:18:48 -05:00
committed by Jason Ross
parent e3e57e7bec
commit d395d94992
15 changed files with 1891 additions and 464 deletions
+29
View File
@@ -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.
+36
View File
@@ -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
+111
View File
@@ -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()
+44
View File
@@ -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()
Generated
+484 -2
View File
@@ -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"
+29 -2
View File
@@ -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
+23 -1
View File
@@ -1 +1,23 @@
import src.android_file_handler as android_file_handler
"""
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",
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
+361
View File
@@ -0,0 +1,361 @@
"""
Android File Handler - Business Logic Module
Handles ADB operations, device management, and file transfers.
"""
import os
import sys
import subprocess
import requests
import zipfile
import io
import shutil
import glob
import time
import re
from typing import Optional, Tuple, Callable
# Constants
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 = sys.platform
# Determine ADB binary name based on platform
if OS_TYPE.startswith("linux"):
ADB_BINARY_NAME = "adb"
elif OS_TYPE.startswith("win"):
ADB_BINARY_NAME = "adb.exe"
else:
ADB_BINARY_NAME = "adb"
ADB_BINARY_PATH = os.path.join(LOCAL_ADB_FOLDER, ADB_BINARY_NAME)
class ADBManager:
"""Manages ADB operations and Android device communication."""
def __init__(self):
self.progress_callback: Optional[Callable[[int], None]] = None
self.status_callback: Optional[Callable[[str], None]] = None
def set_progress_callback(self, callback: Callable[[int], None]):
"""Set callback function for progress updates."""
self.progress_callback = callback
def set_status_callback(self, callback: Callable[[str], None]):
"""Set callback function for status updates."""
self.status_callback = callback
def _update_progress(self, percentage: int):
"""Internal method to update progress."""
if self.progress_callback:
self.progress_callback(percentage)
def _update_status(self, message: str):
"""Internal method to update status."""
if self.status_callback:
self.status_callback(message)
def check_local_disk_space(self) -> 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
-459
View File
@@ -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()
+712
View File
@@ -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("<<TreeviewOpen>>", on_tree_expand)
tree.bind("<<TreeviewSelect>>", 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()
+15
View File
@@ -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()
+47
View File
@@ -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()