Transfer is working, added tooling, debug scripts
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
@@ -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
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user