From 7df056ef60eccd6e1c09fd40ad692236528fccc6 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 15 Aug 2025 18:43:36 -0500 Subject: [PATCH] * Added "Recheck for connected device feature" * WIP on progress bar working for larger transfers * More generated debug files that I will probably use to help create tests --- .gitignore | 7 +- debug_tools/debug_adb.py | 118 +++++- debug_tools/debug_main.py | 63 +++ debug_tools/debug_parsing.py | 95 +++++ debug_tools/test_large_transfer_progress.py | 104 +++++ debug_tools/test_minimal_progress.py | 59 +++ debug_tools/test_modular_progress.py | 44 ++ debug_tools/test_modular_successive.py | 51 +++ debug_tools/test_normal_flow.py | 75 ++++ debug_tools/test_real_workflow.py | 67 +++ src/__pycache__/__init__.cpython-313.pyc | Bin 598 -> 0 bytes src/__pycache__/adb_manager.cpython-313.pyc | Bin 15992 -> 0 bytes src/__pycache__/gui.cpython-313.pyc | Bin 30347 -> 0 bytes src/adb_manager.py | 192 ++++++++- src/gui/__init__.py | 17 + src/gui/linux_browser.py | 68 ++++ src/gui/main_window.py | 430 ++++++++++++++++++++ src/gui/progress_handler.py | 78 ++++ src/gui/windows_browser.py | 426 +++++++++++++++++++ src/main.py | 25 +- 20 files changed, 1895 insertions(+), 24 deletions(-) create mode 100644 debug_tools/debug_main.py create mode 100644 debug_tools/debug_parsing.py create mode 100644 debug_tools/test_large_transfer_progress.py create mode 100644 debug_tools/test_minimal_progress.py create mode 100644 debug_tools/test_modular_progress.py create mode 100644 debug_tools/test_modular_successive.py create mode 100644 debug_tools/test_normal_flow.py create mode 100644 debug_tools/test_real_workflow.py delete mode 100644 src/__pycache__/__init__.cpython-313.pyc delete mode 100644 src/__pycache__/adb_manager.cpython-313.pyc delete mode 100644 src/__pycache__/gui.cpython-313.pyc create mode 100644 src/gui/__init__.py create mode 100644 src/gui/linux_browser.py create mode 100644 src/gui/main_window.py create mode 100644 src/gui/progress_handler.py create mode 100644 src/gui/windows_browser.py diff --git a/.gitignore b/.gitignore index 3e2a44b..52c7f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -src/platform-tools \ No newline at end of file +src/platform-tools + +# Python cache files +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/debug_tools/debug_adb.py b/debug_tools/debug_adb.py index 09ca3c3..94dad2d 100644 --- a/debug_tools/debug_adb.py +++ b/debug_tools/debug_adb.py @@ -1,26 +1,128 @@ """ -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. +Debug large transfer ADB output """ -import os import sys +import subprocess +import re +import time +import os # 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 +from adb_manager import ADB_BINARY_PATH + + +def debug_large_transfer_output(): + """Debug what ADB output looks like for large transfers.""" + + print("=== DEBUGGING LARGE TRANSFER ADB OUTPUT ===") + print("This will show you what ADB actually outputs during large transfers.") + print("Use Ctrl+C to stop when you see enough output.\n") + + # Get user input for paths + remote_path = input("Enter remote path (e.g., /sdcard/DCIM): ").strip() + if not remote_path: + remote_path = "/sdcard/DCIM" + + local_path = input("Enter local path (e.g., C:\\temp\\debug_transfer): ").strip() + if not local_path: + local_path = "C:\\temp\\debug_transfer" + + print(f"\nDebugging transfer: {remote_path} -> {local_path}") + print("ADB output analysis:\n") + + cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path] + + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + + line_count = 0 + progress_lines = [] + + if proc.stdout: + for line in proc.stdout: + line_count += 1 + line_clean = line.strip() + + # Check for progress information + progress_match = re.search(r"\((\d{1,3})%\)", line) + + # Log interesting lines + if ( + progress_match + or "%" in line + or line_count <= 20 + or line_count % 100 == 0 + ): + print(f"Line {line_count:4d}: {line_clean}") + + if progress_match: + pct = int(progress_match.group(1)) + progress_lines.append((line_count, pct, line_clean)) + + # Show periodic updates for very long transfers + if line_count % 500 == 0: + print(f"... ({line_count} lines processed) ...") + + proc.wait() + + print(f"\n=== TRANSFER COMPLETED ===") + print(f"Total lines: {line_count}") + print(f"Return code: {proc.returncode}") + print(f"Progress lines found: {len(progress_lines)}") + + if progress_lines: + print("\nProgress line analysis:") + for line_num, pct, text in progress_lines: + print(f" Line {line_num}: {pct}% - {text}") + else: + print("No explicit progress percentages found in output!") + print("This explains why large transfers don't show progress.") + + except KeyboardInterrupt: + print(f"\n\nStopped at line {line_count}") + if proc: + proc.terminate() + + if progress_lines: + print(f"Progress lines found so far: {len(progress_lines)}") + for line_num, pct, text in progress_lines[-5:]: # Show last 5 + print(f" Line {line_num}: {pct}% - {text}") + + except Exception as e: + print(f"Error: {e}") def main(): - """Run comprehensive ADB debugging tests.""" + """Debug large transfer functionality.""" + print("Choose debug option:") + print("1. Debug large transfer ADB output") + print("2. Run original ADB detection tests") + + choice = input("Enter choice (1 or 2): ").strip() + + if choice == "1": + debug_large_transfer_output() + else: + debug_original_adb_detection() + + +def debug_original_adb_detection(): + """Original ADB debugging functionality.""" + from adb_manager import ADBManager + 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}") diff --git a/debug_tools/debug_main.py b/debug_tools/debug_main.py new file mode 100644 index 0000000..a437b35 --- /dev/null +++ b/debug_tools/debug_main.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Debug version of main window with console output +""" + +import sys + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +class DebugAndroidFileHandlerGUI(AndroidFileHandlerGUI): + """Debug version with console output.""" + + def _initialize_components(self): + """Initialize GUI components and handlers with debug output.""" + super()._initialize_components() + + # Wrap the progress callback with debug output + original_update_progress = self.progress_handler.update_progress + + def debug_update_progress(percentage): + print(f"DEBUG: Progress update called with {percentage}%") + return original_update_progress(percentage) + + # Wrap the status callback with debug output + original_set_status = self.progress_handler.set_status + + def debug_set_status(message): + print(f"DEBUG: Status update called with '{message}'") + return original_set_status(message) + + # Replace the callbacks + self.progress_handler.update_progress = debug_update_progress + self.progress_handler.set_status = debug_set_status + + # Update ADB manager callbacks + self.adb_manager.set_progress_callback(self.progress_handler.update_progress) + self.adb_manager.set_status_callback(self.progress_handler.set_status) + + print("DEBUG: Callbacks set up with debug wrappers") + + +def main(): + """Main function to run the debug application.""" + if sys.platform not in ["win32", "linux"]: + import tkinter as tk + from tkinter import messagebox + + root = tk.Tk() + root.withdraw() + messagebox.showerror( + "Unsupported OS", "This application only supports Windows or Linux." + ) + sys.exit(1) + + app = DebugAndroidFileHandlerGUI() + app.mainloop() + + +if __name__ == "__main__": + main() diff --git a/debug_tools/debug_parsing.py b/debug_tools/debug_parsing.py new file mode 100644 index 0000000..d3e8f93 --- /dev/null +++ b/debug_tools/debug_parsing.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Debug folder name parsing from ADB ls output +""" + +import sys +import subprocess + +sys.path.insert(0, "src") + +from adb_manager import ADB_BINARY_PATH + + +def debug_folder_parsing(): + """Debug folder name parsing from ls -la output.""" + + print("=== DEBUGGING FOLDER NAME PARSING ===") + print("This will show actual ADB ls output and how it's parsed.\n") + + path = input("Enter Android path to list (e.g., /sdcard): ").strip() + if not path: + path = "/sdcard" + + print(f"Running: adb shell ls -la '{path}'\n") + + try: + cmd = [ADB_BINARY_PATH, "shell", "ls", "-la", path] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + print(f"Return code: {result.returncode}") + print(f"Raw output:\n{repr(result.stdout)}") + print(f"Error output: {repr(result.stderr)}") + print("\n" + "=" * 50) + print("PARSED OUTPUT:") + print("=" * 50) + + if result.stdout and result.stdout.strip(): + lines = result.stdout.strip().split("\n") + print(f"Total lines: {len(lines)}\n") + + for i, line in enumerate(lines): + print(f"Line {i+1}: {repr(line)}") + + if line.startswith("d"): + # Current parsing logic + parts = line.split() + print(f" Parts ({len(parts)}): {parts}") + + if len(parts) >= 8: + current_name = " ".join(parts[7:]) + print(f" Current parsing result: '{current_name}'") + + # Improved parsing - try different approaches + # Method 1: Find the last occurrence of time pattern and take everything after + import re + + time_pattern = r"\d{2}:\d{2}" + time_match = list(re.finditer(time_pattern, line)) + if time_match: + last_time = time_match[-1] + improved_name = line[last_time.end() :].strip() + print(f" Improved parsing result: '{improved_name}'") + + if improved_name != current_name: + print(f" *** DIFFERENCE DETECTED! ***") + + # Method 2: Manual field parsing (more reliable) + # drwxrwxrwx root root 2024-01-01 12:00 folder_name + tokens = line.split() + if len(tokens) >= 6: + # Skip permissions, links, user, group, size, date, time + # Take everything from index 8 (or find after time pattern) + manual_name = ( + " ".join(tokens[8:]) + if len(tokens) > 8 + else (tokens[7] if len(tokens) == 8 else "") + ) + print(f" Manual parsing result: '{manual_name}'") + else: + print( + f" *** NOT ENOUGH PARTS! Expected >= 8, got {len(parts)}" + ) + + print() + else: + print("No output received!") + + except subprocess.TimeoutExpired: + print("Command timed out!") + except Exception as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + debug_folder_parsing() diff --git a/debug_tools/test_large_transfer_progress.py b/debug_tools/test_large_transfer_progress.py new file mode 100644 index 0000000..94c524e --- /dev/null +++ b/debug_tools/test_large_transfer_progress.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Test improved progress calculation for large transfers +""" + +import sys +import time +import threading + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +def test_large_transfer_progress(): + """Test improved progress calculation.""" + + class TestApp(AndroidFileHandlerGUI): + def __init__(self): + super().__init__() + self.progress_updates = [] + + # Capture progress updates + original_update = self.progress_handler.update_progress + + def capture_progress(percentage): + self.progress_updates.append((time.time(), percentage)) + print(f"Progress: {percentage}%") + return original_update(percentage) + + self.progress_handler.update_progress = capture_progress + self.adb_manager.set_progress_callback( + self.progress_handler.update_progress + ) + + app = TestApp() + + def simulate_large_transfer(): + """Simulate a large transfer with many output lines.""" + time.sleep(1) + + print("=== Testing Large Transfer Progress ===") + + # Simulate the internal progress logic without actual ADB + start_time = time.time() + last_update_time = start_time + last_progress = 0 + + print("Simulating 2000 lines of output (like a large transfer)...") + + for line_count in range(1, 2001): + current_time = time.time() + + # Simulate the improved progress logic + elapsed_time = current_time - start_time + time_since_last_update = current_time - last_update_time + + should_update = False + new_progress = last_progress + + # Time-based progress (update every 2 seconds) + if time_since_last_update >= 2.0 and last_progress < 95: + if line_count > 100: + activity_factor = min(line_count / 1000, 50) + time_factor = min(elapsed_time / 60, 40) + new_progress = min(activity_factor + time_factor, 95) + else: + new_progress = min(last_progress + 10, 95) + should_update = True + + # Line-based progress + elif line_count % 50 == 0 and last_progress < 90: + increment = max(1, min(5, 90 // (line_count // 50 + 1))) + new_progress = min(last_progress + increment, 90) + should_update = True + + if should_update and new_progress > last_progress: + app.adb_manager._update_progress(int(new_progress)) + last_progress = new_progress + last_update_time = current_time + + # Speed up simulation + if line_count % 100 == 0: + time.sleep(0.1) + + # Complete the transfer + app.adb_manager._update_progress(100) + + print(f"\nTotal progress updates: {len(app.progress_updates)}") + print("Progress timeline:") + start_time = app.progress_updates[0][0] if app.progress_updates else time.time() + for timestamp, percentage in app.progress_updates[-10:]: # Show last 10 + elapsed = timestamp - start_time + print(f" {elapsed:.1f}s: {percentage}%") + + threading.Thread(target=simulate_large_transfer, daemon=True).start() + + # Run for limited time + app.after(15000, app.destroy) # Close after 15 seconds + app.mainloop() + + +if __name__ == "__main__": + test_large_transfer_progress() diff --git a/debug_tools/test_minimal_progress.py b/debug_tools/test_minimal_progress.py new file mode 100644 index 0000000..1fd2d06 --- /dev/null +++ b/debug_tools/test_minimal_progress.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Minimal test to verify progress updates work in modular GUI +""" + +import sys +import time +import threading + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +def test_minimal_progress(): + """Test minimal progress updates.""" + print("Creating GUI...") + app = AndroidFileHandlerGUI() + + def test_updates(): + time.sleep(1) # Wait for GUI to be ready + print("Starting test updates...") + + # Test direct progress handler calls + print("Testing progress handler directly...") + app.progress_handler.update_progress(25) + time.sleep(0.5) + app.progress_handler.update_progress(50) + time.sleep(0.5) + app.progress_handler.update_progress(75) + time.sleep(0.5) + app.progress_handler.update_progress(100) + + time.sleep(1) + + # Test ADB manager calls (should go through callbacks) + print("Testing through ADB manager...") + app.adb_manager._update_progress(0) + time.sleep(0.5) + app.adb_manager._update_progress(30) + time.sleep(0.5) + app.adb_manager._update_progress(60) + time.sleep(0.5) + app.adb_manager._update_progress(90) + time.sleep(0.5) + app.adb_manager._update_progress(100) + + print("Test completed!") + + # Start test in background + threading.Thread(target=test_updates, daemon=True).start() + + # Run GUI + print("Starting GUI mainloop...") + app.mainloop() + + +if __name__ == "__main__": + test_minimal_progress() diff --git a/debug_tools/test_modular_progress.py b/debug_tools/test_modular_progress.py new file mode 100644 index 0000000..705d64a --- /dev/null +++ b/debug_tools/test_modular_progress.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +Debug test for progress callback in modular GUI +""" + +import sys +import os + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +def test_callbacks(): + """Test if callbacks are being set properly.""" + app = AndroidFileHandlerGUI() + + print("Testing callback setup...") + print(f"ADB Manager progress callback: {app.adb_manager.progress_callback}") + print(f"ADB Manager status callback: {app.adb_manager.status_callback}") + print(f"Progress handler: {app.progress_handler}") + print( + f"Progress handler update_progress method: {app.progress_handler.update_progress}" + ) + print(f"Progress handler set_status method: {app.progress_handler.set_status}") + + # Test direct call + print("\nTesting direct progress update...") + app.progress_handler.update_progress(50) + app.update() + + print("\nTesting direct status update...") + app.progress_handler.set_status("Testing status") + app.update() + + print( + "\nTest completed. Check if progress bar shows 50% and status shows 'Testing status'" + ) + + app.mainloop() + + +if __name__ == "__main__": + test_callbacks() diff --git a/debug_tools/test_modular_successive.py b/debug_tools/test_modular_successive.py new file mode 100644 index 0000000..4809eff --- /dev/null +++ b/debug_tools/test_modular_successive.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Test successive transfers with modular GUI +""" + +import sys +import time +import threading + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +def simulate_transfer(app, transfer_name): + """Simulate a transfer with progress updates.""" + print(f"Starting {transfer_name}") + app.progress_handler.set_status(f"Starting {transfer_name}...") + app.progress_handler.reset_progress() + + # Simulate progress updates + for i in range(0, 101, 10): + time.sleep(0.1) # Simulate work + app.progress_handler.update_progress(i) + app.progress_handler.set_status(f"{transfer_name}: {i}% complete") + + app.progress_handler.set_status(f"{transfer_name} completed!") + print(f"Completed {transfer_name}") + + +def test_successive_transfers(): + """Test successive transfers to ensure progress bar works correctly.""" + app = AndroidFileHandlerGUI() + + def run_transfers(): + time.sleep(1) # Wait for GUI to be ready + simulate_transfer(app, "Transfer 1") + time.sleep(0.5) + simulate_transfer(app, "Transfer 2") + time.sleep(0.5) + simulate_transfer(app, "Transfer 3") + print("All transfers completed!") + + # Start transfers in background thread + threading.Thread(target=run_transfers, daemon=True).start() + + app.mainloop() + + +if __name__ == "__main__": + test_successive_transfers() diff --git a/debug_tools/test_normal_flow.py b/debug_tools/test_normal_flow.py new file mode 100644 index 0000000..ac9dd32 --- /dev/null +++ b/debug_tools/test_normal_flow.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +Debug test following normal application flow +""" + +import sys +import os +import time +import threading + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +def test_normal_flow(): + """Test following the normal application flow exactly.""" + app = AndroidFileHandlerGUI() + + def debug_transfer(): + # Wait for app to initialize + time.sleep(2) + + print("=== DEBUGGING NORMAL APPLICATION FLOW ===") + print(f"App ADB manager: {app.adb_manager}") + print(f"App progress handler: {app.progress_handler}") + print(f"ADB manager progress callback: {app.adb_manager.progress_callback}") + print(f"ADB manager status callback: {app.adb_manager.status_callback}") + + # Set up a test transfer + app.remote_path_var.set("/sdcard/Download") + app.local_path_var.set(r"C:\temp") + + print("\n=== SIMULATING START TRANSFER BUTTON CLICK ===") + + # Check if local path exists + if not os.path.exists(r"C:\temp"): + os.makedirs(r"C:\temp", exist_ok=True) + + # Test the exact method that gets called + app.current_transfer_id += 1 + transfer_id = app.current_transfer_id + print(f"Transfer ID: {transfer_id}") + + app.disable_controls() + app.progress_handler.reset_progress() + + print("\n=== TESTING PROGRESS UPDATES DIRECTLY ===") + app.adb_manager._update_progress(25) + app.update() + time.sleep(0.5) + + app.adb_manager._update_progress(50) + app.update() + time.sleep(0.5) + + app.adb_manager._update_progress(75) + app.update() + time.sleep(0.5) + + app.adb_manager._update_progress(100) + app.update() + + print("=== PROGRESS UPDATES COMPLETED ===") + + app.enable_controls() + + # Start debug in background + threading.Thread(target=debug_transfer, daemon=True).start() + + app.mainloop() + + +if __name__ == "__main__": + test_normal_flow() diff --git a/debug_tools/test_real_workflow.py b/debug_tools/test_real_workflow.py new file mode 100644 index 0000000..81df5a6 --- /dev/null +++ b/debug_tools/test_real_workflow.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +Test real transfer workflow +""" + +import sys +import os +import time +import threading + +sys.path.insert(0, "src") + +from gui.main_window import AndroidFileHandlerGUI + + +def test_real_transfer_workflow(): + """Test the real transfer workflow with console output.""" + + # Add debug output to see what's happening + class DebugApp(AndroidFileHandlerGUI): + def __init__(self): + super().__init__() + + # Wrap callbacks with debug output + original_progress = self.adb_manager.progress_callback + original_status = self.adb_manager.status_callback + + def debug_progress(percentage): + print(f"PROGRESS: {percentage}%") + if original_progress: + original_progress(percentage) + + def debug_status(message): + print(f"STATUS: {message}") + if original_status: + original_status(message) + + self.adb_manager.set_progress_callback(debug_progress) + self.adb_manager.set_status_callback(debug_status) + + app = DebugApp() + + def simulate_transfer(): + time.sleep(2) # Wait for app to be ready + + print("=== SIMULATING BUTTON CLICK ===") + + # Set up paths (use a path that doesn't exist to avoid actual transfer) + app.remote_path_var.set("/nonexistent/path") + app.local_path_var.set(r"C:\temp") + + # Create temp dir if needed + os.makedirs(r"C:\temp", exist_ok=True) + + # Call start_transfer method directly + print("Calling start_transfer()...") + app.start_transfer() + + # Start simulation + threading.Thread(target=simulate_transfer, daemon=True).start() + + print("Starting app...") + app.mainloop() + + +if __name__ == "__main__": + test_real_transfer_workflow() diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 67412df62d2d5a16ad1d962859db952d8452abc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598 zcmY*WJ#Q2-5ViOF_7p;d=-qCS(4z<`Kmyqi6gbkYWchY|cUHXiYP<)zbo>XXsrgH8 zspx=HlotE4C&3g?Gvk>z^Tz9)og6vKzkFNVVuXJ9U~9qPtPh>@6-^MJ37!OJxC|gD zLkPrNYPNWb8uWkQi!x1Cm8^@ix#CcPz`dBVj&$huC7dc$#$ z|8$B_5!?Le<@0l<+!(OCZ-iQ2pMSX6CLYasPgygiQ=u5FXvg}6eJIQ($38M4*;I17 z=gT_D`k987_N$H;fqgve;57}BUR|Er$ZuSv5W4XYdB{A(9ug0!hmnJ~#n`5lHoB(N zj{O$x{ht5y{@-T9`BL2Tc>>?fq@?saTDuV0lNV2`OT)obB{N!)_u821lZGqPv6@%z zO#TWWg~eu2`1Ze_E>RyRw@pTVA^^@lXX!LJZw{5uG7beAVMxJ3@` R9w5B`C(6db=M;8b(O;<~t118h diff --git a/src/__pycache__/adb_manager.cpython-313.pyc b/src/__pycache__/adb_manager.cpython-313.pyc deleted file mode 100644 index 40e8c2d52e44ac0e2bba2d3a00e03ec0e25d64c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15992 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 diff --git a/src/__pycache__/gui.cpython-313.pyc b/src/__pycache__/gui.cpython-313.pyc deleted file mode 100644 index 48ad4c64c7f961a6cad5008c536572af60e778ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30347 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 Optional[int]: - """Parse progress percentage from ADB output.""" + """Parse progress percentage from ADB output with enhanced patterns for large transfers.""" + # Original pattern: (XX%) m = re.search(r"\((\d{1,3})%\)", text_line) if m: pct = int(m.group(1)) if 0 <= pct <= 100: return pct + + # Additional patterns for large transfers + # Pattern: XX% complete + m = re.search(r"(\d{1,3})%\s+complete", text_line, re.IGNORECASE) + if m: + pct = int(m.group(1)) + if 0 <= pct <= 100: + return pct + + # Pattern: transferred XX% + m = re.search(r"transferred\s+(\d{1,3})%", text_line, re.IGNORECASE) + if m: + pct = int(m.group(1)) + if 0 <= pct <= 100: + return pct + + # Pattern: XX files pulled/pushed (XX%) + m = re.search( + r"\d+\s+files?\s+(?:pulled|pushed).*?\((\d{1,3})%\)", + text_line, + re.IGNORECASE, + ) + 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.""" + # Normalize paths for better compatibility + local_path = os.path.normpath(local_path) + remote_path = remote_path.strip() + + # Ensure local directory exists + try: + os.makedirs(local_path, exist_ok=True) + except Exception as e: + self._update_status(f"Failed to create local directory: {e}") + return False + + # For Windows root drives, ensure proper formatting + if os.name == "nt" and len(local_path) == 3 and local_path.endswith(":\\"): + # Root drive path like C:\ - this might cause issues with ADB + self._update_status( + "Warning: Transferring to root drive. Consider using a subfolder." + ) + cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path] + # Debug output for troubleshooting + self._update_status(f"Command: adb pull '{remote_path}' '{local_path}'") + try: # Start with initial progress self._update_progress(0) @@ -171,19 +220,59 @@ class ADBManager: line_count = 0 last_progress = 0 + start_time = time.time() + last_update_time = start_time + if proc.stdout: for line in proc.stdout: line_count += 1 + current_time = time.time() pct = self.parse_progress(line) + if pct is not None: + # Use explicit progress when available self._update_progress(pct) last_progress = pct + last_update_time = current_time else: - # If no explicit progress, simulate some progress based on activity - if line_count % 5 == 0 and last_progress < 90: - estimated_progress = min(last_progress + 5, 90) - self._update_progress(estimated_progress) - last_progress = estimated_progress + # Improved progress estimation for large transfers + elapsed_time = current_time - start_time + time_since_last_update = current_time - last_update_time + + # Calculate progress based on multiple factors + should_update = False + new_progress = last_progress + + # Time-based progress (update every 2 seconds) + if time_since_last_update >= 2.0 and last_progress < 95: + # Estimate progress based on activity and time + if line_count > 100: + # For large transfers, use a logarithmic approach + activity_factor = min( + line_count / 1000, 50 + ) # Max 50% from activity + time_factor = min( + elapsed_time / 60, 40 + ) # Max 40% from time (assumes 1-2 min transfers) + new_progress = min(activity_factor + time_factor, 95) + else: + # For smaller transfers, use the original approach + new_progress = min(last_progress + 10, 95) + + should_update = True + + # Line-based progress (for very active transfers) + elif line_count % 50 == 0 and last_progress < 90: + # More conservative line-based updates + increment = max(1, min(5, 90 // (line_count // 50 + 1))) + new_progress = min(last_progress + increment, 90) + should_update = True + + # Update progress if needed + if should_update and new_progress > last_progress: + self._update_progress(int(new_progress)) + last_progress = new_progress + last_update_time = current_time self._update_status(line.strip()) @@ -193,7 +282,16 @@ class ADBManager: self._update_status("Transfer completed successfully.") return True else: - self._update_status(f"Transfer failed with code {proc.returncode}") + # Capture error output for better debugging + error_msg = f"Transfer failed with code {proc.returncode}" + if hasattr(proc, "stderr") and proc.stderr: + try: + stderr_output = proc.stderr.read() + if stderr_output: + error_msg += f". Error: {stderr_output}" + except: + pass + self._update_status(error_msg) return False except Exception as e: @@ -202,8 +300,27 @@ class ADBManager: def push_folder(self, local_path: str, remote_path: str) -> bool: """Push files from local machine to Android device.""" + # Normalize paths for better compatibility + local_path = os.path.normpath(local_path) + remote_path = remote_path.strip() + + # Validate local path exists + if not os.path.exists(local_path): + self._update_status(f"Local path does not exist: {local_path}") + return False + + # For Windows root drives, ensure proper formatting + if os.name == "nt" and len(local_path) == 3 and local_path.endswith(":\\"): + # Root drive path like C:\ - this might cause issues with ADB + self._update_status( + "Warning: Pushing from root drive. Consider using a subfolder." + ) + cmd = [ADB_BINARY_PATH, "push", local_path, remote_path] + # Debug output for troubleshooting + self._update_status(f"Command: adb push '{local_path}' '{remote_path}'") + try: # Start with initial progress self._update_progress(0) @@ -222,19 +339,59 @@ class ADBManager: line_count = 0 last_progress = 0 + start_time = time.time() + last_update_time = start_time + if proc.stdout: for line in proc.stdout: line_count += 1 + current_time = time.time() pct = self.parse_progress(line) + if pct is not None: + # Use explicit progress when available self._update_progress(pct) last_progress = pct + last_update_time = current_time else: - # If no explicit progress, simulate some progress based on activity - if line_count % 5 == 0 and last_progress < 90: - estimated_progress = min(last_progress + 5, 90) - self._update_progress(estimated_progress) - last_progress = estimated_progress + # Improved progress estimation for large transfers + elapsed_time = current_time - start_time + time_since_last_update = current_time - last_update_time + + # Calculate progress based on multiple factors + should_update = False + new_progress = last_progress + + # Time-based progress (update every 2 seconds) + if time_since_last_update >= 2.0 and last_progress < 95: + # Estimate progress based on activity and time + if line_count > 100: + # For large transfers, use a logarithmic approach + activity_factor = min( + line_count / 1000, 50 + ) # Max 50% from activity + time_factor = min( + elapsed_time / 60, 40 + ) # Max 40% from time (assumes 1-2 min transfers) + new_progress = min(activity_factor + time_factor, 95) + else: + # For smaller transfers, use the original approach + new_progress = min(last_progress + 10, 95) + + should_update = True + + # Line-based progress (for very active transfers) + elif line_count % 50 == 0 and last_progress < 90: + # More conservative line-based updates + increment = max(1, min(5, 90 // (line_count // 50 + 1))) + new_progress = min(last_progress + increment, 90) + should_update = True + + # Update progress if needed + if should_update and new_progress > last_progress: + self._update_progress(int(new_progress)) + last_progress = new_progress + last_update_time = current_time self._update_status(line.strip()) @@ -244,7 +401,16 @@ class ADBManager: self._update_status("Transfer completed successfully.") return True else: - self._update_status(f"adb push failed with return code {proc.returncode}") + # Capture error output for better debugging + error_msg = f"Push failed with code {proc.returncode}" + if hasattr(proc, "stderr") and proc.stderr: + try: + stderr_output = proc.stderr.read() + if stderr_output: + error_msg += f". Error: {stderr_output}" + except: + pass + self._update_status(error_msg) return False diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..f12d5ab --- /dev/null +++ b/src/gui/__init__.py @@ -0,0 +1,17 @@ +""" +GUI Package for Android File Handler +Provides modular GUI components for the Android file transfer application. +""" + +from .main_window import AndroidFileHandlerGUI, main +from .progress_handler import ProgressHandler +from .windows_browser import WindowsAndroidBrowser +from .linux_browser import LinuxAndroidBrowser + +__all__ = [ + "AndroidFileHandlerGUI", + "main", + "ProgressHandler", + "WindowsAndroidBrowser", + "LinuxAndroidBrowser", +] diff --git a/src/gui/linux_browser.py b/src/gui/linux_browser.py new file mode 100644 index 0000000..bb7339e --- /dev/null +++ b/src/gui/linux_browser.py @@ -0,0 +1,68 @@ +""" +Linux Android Browser Module +Provides Android filesystem browsing capabilities for Linux using MTP. +""" + +import os +from tkinter import messagebox, filedialog + + +class LinuxAndroidBrowser: + """Android filesystem browser for Linux using MTP.""" + + def __init__(self, parent_window, mtp_manager, remote_path_var): + self.parent = parent_window + self.mtp_manager = mtp_manager + self.remote_path_var = remote_path_var + + def show_browser(self): + """Browse remote Android folders via MTP (Linux)""" + if not self.mtp_manager: + messagebox.showerror( + "Error", + "MTP manager not available. This feature is only supported on Linux.", + ) + return + + # 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.", + ) diff --git a/src/gui/main_window.py b/src/gui/main_window.py new file mode 100644 index 0000000..ee2ac36 --- /dev/null +++ b/src/gui/main_window.py @@ -0,0 +1,430 @@ +""" +Main Window Module +Core GUI application window for Android file transfers. +""" + +import os +import sys +import threading +import tkinter as tk +from tkinter import messagebox, filedialog, ttk + +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, + ) + +from .progress_handler import ProgressHandler +from .windows_browser import WindowsAndroidBrowser +from .linux_browser import LinuxAndroidBrowser + + +class AndroidFileHandlerGUI(tk.Tk): + """Main GUI application for Android file transfers.""" + + def __init__(self): + super().__init__() + + # Initialize business logic + self.adb_manager = ADBManager() + + # Transfer tracking for thread safety + self.current_transfer_id = 0 + self.device_connected = False # Track device connection state + + if get_platform_type().startswith("linux"): + self.mtp_manager = LinuxMTPManager() + else: + self.mtp_manager = None + + # Setup UI + self._setup_ui() + self._initialize_components() + 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/Recheck button (will change based on device state) + self.start_btn = tk.Button( + self, text="Start Transfer", command=self.handle_button_click + ) + self.start_btn.pack(pady=10) + + # Window close protocol + self.protocol("WM_DELETE_WINDOW", self.on_close) + + def _initialize_components(self): + """Initialize GUI components and handlers.""" + # Progress handler + self.progress_handler = ProgressHandler(self, self.progress, self.status_label) + + # Set up ADB callbacks + self.adb_manager.set_progress_callback(self.progress_handler.update_progress) + self.adb_manager.set_status_callback(self.progress_handler.set_status) + + # Browser components + self.windows_browser = WindowsAndroidBrowser( + self, self.adb_manager, self.remote_path_var + ) + self.linux_browser = LinuxAndroidBrowser( + self, self.mtp_manager, self.remote_path_var + ) + + def _initialize_app(self): + """Initialize the application - check ADB and device.""" + # Check adb availability + if not is_adb_available(): + self.disable_controls() + self.progress_handler.set_status("ADB not found locally. Downloading...") + self.update() + success = self.adb_manager.download_and_extract_adb() + if success: + self.progress_handler.set_status("ADB downloaded and ready.") + self.enable_controls() + else: + self.progress_handler.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.check_device_connection() + + def check_device_connection(self): + """Check for device connection and update UI accordingly.""" + self.progress_handler.set_status("Checking for connected device...") + self.update() + device = self.adb_manager.check_device() + if not device: + self.device_connected = False + self.disable_controls() + self.progress_handler.set_status( + "No device detected. Enable USB debugging and connect your device." + ) + self._switch_to_recheck_mode() + self.show_enable_debugging_instructions() + else: + self.device_connected = True + self.progress_handler.set_status(f"Device detected: {device}") + self._switch_to_transfer_mode() + self.enable_controls() + + def browse_remote_folder(self): + """Browse remote Android folders.""" + if get_platform_type().startswith("linux") and self.mtp_manager: + # Use Linux MTP browser + self.linux_browser.show_browser() + else: + # Use Windows ADB browser + self.windows_browser.show_browser() + + 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") + # Don't disable start_btn here - it will be handled by button mode switching + + def enable_controls(self): + """Enable UI controls after operations (thread-safe).""" + # Schedule UI update on main thread + self.after(0, self._enable_controls_ui) + + def _enable_controls_ui(self): + """Internal method to enable controls on main thread.""" + self.remote_path_entry.config(state="normal") + self.local_path_entry.config(state="normal") + # Button state is handled by mode switching methods + + def _switch_to_recheck_mode(self): + """Switch button to recheck device mode.""" + self.start_btn.config( + text="Recheck for connected Android device", + command=self.recheck_device, + state="normal", + ) + + def _switch_to_transfer_mode(self): + """Switch button to transfer mode.""" + self.start_btn.config( + text="Start Transfer", command=self.start_transfer, state="normal" + ) + + def handle_button_click(self): + """Handle button click - delegates to appropriate method based on device state.""" + if self.device_connected: + self.start_transfer() + else: + self.recheck_device() + + def recheck_device(self): + """Recheck for connected Android device.""" + # Temporarily disable the button during recheck + self.start_btn.config(state="disabled") + self.start_btn.config(text="Checking...") + + # Use after() to allow UI to update before blocking operation + self.after(100, self._perform_device_recheck) + + def _perform_device_recheck(self): + """Perform the actual device recheck.""" + self.check_device_connection() + + 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, click 'Recheck for connected Android device' to try again." + ) + result = messagebox.showinfo("Enable USB Debugging", msg) + # After user clicks OK, ensure recheck button is enabled + self.after(0, self._enable_recheck_after_dialog) + + def _enable_recheck_after_dialog(self): + """Re-enable recheck button after user dismisses the dialog.""" + if not self.device_connected: + self._switch_to_recheck_mode() + + def show_disable_debugging_reminder(self): + """Show reminder to disable USB debugging after transfer (thread-safe).""" + # Schedule UI update on main thread + self.after(0, self._show_debugging_reminder_ui) + + def _show_debugging_reminder_ui(self): + """Internal method to show debugging reminder on main thread.""" + 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.""" + # Double-check device is still connected before starting transfer + if not self.device_connected: + messagebox.showerror( + "No Device", + "No Android device is connected. Please connect your device and enable USB debugging.", + ) + self._switch_to_recheck_mode() + return + + 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 + + # Final device check before transfer + device = self.adb_manager.check_device() + if not device: + self.device_connected = False + messagebox.showerror( + "Device Disconnected", + "Android device was disconnected. Please reconnect and try again.", + ) + self._switch_to_recheck_mode() + return + + # Start transfer + self.current_transfer_id += 1 + transfer_id = self.current_transfer_id + + self.disable_controls() + self.start_btn.config( + state="disabled" + ) # Disable transfer button during transfer + # Reset progress bar in thread-safe way + self.progress_handler.reset_progress() + + if direction == "pull": + self.progress_handler.set_status("Starting pull transfer...") + threading.Thread( + target=self._pull_thread, + args=(remote_path, local_path, transfer_id), + daemon=True, + ).start() + elif direction == "push": + self.progress_handler.set_status("Starting push transfer...") + threading.Thread( + target=self._push_thread, + args=(local_path, remote_path, transfer_id), + daemon=True, + ).start() + else: + self.report_error("Invalid transfer direction selected.") + self.enable_controls() + + def _pull_thread(self, remote_path: str, local_path: str, transfer_id: int): + """Thread function for pull operations.""" + try: + # Check if this transfer is still current + if self.current_transfer_id != transfer_id: + return + + success = self.adb_manager.pull_folder(remote_path, local_path) + if success and self.current_transfer_id == transfer_id: + self.show_disable_debugging_reminder() + except Exception as e: + if self.current_transfer_id == transfer_id: + self.report_error(f"Pull operation failed: {e}") + finally: + if self.current_transfer_id == transfer_id: + self.enable_controls() + # Restore proper button state after transfer + self.after(0, self._restore_button_state) + + def _push_thread(self, local_path: str, remote_path: str, transfer_id: int): + """Thread function for push operations.""" + try: + # Check if this transfer is still current + if self.current_transfer_id != transfer_id: + return + + success = self.adb_manager.push_folder(local_path, remote_path) + if success and self.current_transfer_id == transfer_id: + self.show_disable_debugging_reminder() + except Exception as e: + if self.current_transfer_id == transfer_id: + self.report_error(f"Push operation failed: {e}") + finally: + if self.current_transfer_id == transfer_id: + self.enable_controls() + # Restore proper button state after transfer + self.after(0, self._restore_button_state) + + def _restore_button_state(self): + """Restore the correct button state based on device connection.""" + if self.device_connected: + self._switch_to_transfer_mode() + else: + self._switch_to_recheck_mode() + + def report_error(self, message: str): + """Report an error to the user (thread-safe).""" + # Schedule UI update on main thread + self.after(0, lambda: self._report_error_ui(message)) + + def _report_error_ui(self, message: str): + """Internal method to report error on main thread.""" + self.status_label.config(text=f"Error: {message}") + messagebox.showerror("Error", message) + self.enable_controls() + + def on_close(self): + """Handle window close event.""" + self.destroy() + + +def main(): + """Main function to run the application.""" + if sys.platform not in ["win32", "linux"]: + root = tk.Tk() + root.withdraw() + messagebox.showerror( + "Unsupported OS", "This application only supports Windows or Linux." + ) + sys.exit(1) + + app = AndroidFileHandlerGUI() + app.mainloop() + + +if __name__ == "__main__": + main() diff --git a/src/gui/progress_handler.py b/src/gui/progress_handler.py new file mode 100644 index 0000000..2bd5362 --- /dev/null +++ b/src/gui/progress_handler.py @@ -0,0 +1,78 @@ +""" +Progress Handler Module +Handles progress bar updates and status messages in a thread-safe manner. +""" + +import tkinter as tk +from tkinter import ttk + + +class ProgressHandler: + """Handles progress bar updates and status messages.""" + + def __init__( + self, parent_widget, progress_bar: ttk.Progressbar, status_label: tk.Label + ): + self.parent = parent_widget + self.progress_bar = progress_bar + self.status_label = status_label + + def update_progress(self, percentage: int): + """Update the progress bar (thread-safe).""" + # Debug output for large transfer troubleshooting + print(f"[DEBUG] Progress update called: {percentage}%") + + # Schedule UI update on main thread using a proper closure + def update_ui(): + self._update_progress_ui(percentage) + + self.parent.after(0, update_ui) + + def _update_progress_ui(self, percentage: int): + """Internal method to update progress bar on main thread.""" + # Ensure percentage is within valid range + percentage = max(0, min(100, percentage)) + + # Debug output for large transfer troubleshooting + current_value = self.progress_bar["value"] + print(f"[DEBUG] UI Progress update: {current_value} -> {percentage}%") + + if ( + hasattr(self, "_last_percentage") + and abs(percentage - self._last_percentage) >= 10 + ): + print( + f"[DEBUG] Major progress jump: {self._last_percentage}% -> {percentage}%" + ) + self._last_percentage = percentage + + try: + self.progress_bar["value"] = percentage + self.progress_bar.update() # Force immediate update + self.parent.update_idletasks() + print(f"[DEBUG] Progress bar updated successfully to {percentage}%") + except Exception as e: + print(f"[DEBUG] Error updating progress bar: {e}") + + def set_status(self, message: str): + """Update the status label (thread-safe).""" + + # Schedule UI update on main thread using a proper closure + def update_ui(): + self._set_status_ui(message) + + self.parent.after(0, update_ui) + + def _set_status_ui(self, message: str): + """Internal method to update status on main thread.""" + self.status_label.config(text=f"Status: {message}") + self.parent.update_idletasks() + + def reset_progress(self): + """Reset progress bar to 0 (thread-safe).""" + + # Schedule UI update on main thread using a proper closure + def update_ui(): + self._update_progress_ui(0) + + self.parent.after(0, update_ui) diff --git a/src/gui/windows_browser.py b/src/gui/windows_browser.py new file mode 100644 index 0000000..f9289a6 --- /dev/null +++ b/src/gui/windows_browser.py @@ -0,0 +1,426 @@ +""" +Windows Android Browser Module +Provides Android filesystem browsing capabilities for Windows using ADB. +""" + +import threading +import tkinter as tk +from tkinter import messagebox, ttk, simpledialog + + +class WindowsAndroidBrowser: + """Android filesystem browser for Windows using ADB.""" + + def __init__(self, parent_window, adb_manager, remote_path_var): + self.parent = parent_window + self.adb_manager = adb_manager + self.remote_path_var = remote_path_var + + def show_browser(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.parent) + browser_window.title("Browse Android Folders") + browser_window.geometry("500x400") + browser_window.transient(self.parent) + 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.parent.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 - improved parsing to prevent truncation + parts = line.split() + if len(parts) >= 8: + # Method 1: Use regex to find time pattern and extract name after it + import re + + time_pattern = r"\d{1,2}:\d{2}" + time_matches = list(re.finditer(time_pattern, line)) + + if time_matches: + # Take everything after the last time pattern + last_time_match = time_matches[-1] + folder_name = line[ + last_time_match.end() : + ].strip() + else: + # Fallback: join from part 8 (skip date/time fields) + folder_name = ( + " ".join(parts[8:]) + if len(parts) > 8 + else parts[7] + ) + + # Additional validation and cleanup + folder_name = folder_name.strip() + + if ( + folder_name + and 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.parent.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.parent.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 - improved parsing to prevent truncation + parts = line.split() + if len(parts) >= 8: + # Method 1: Use regex to find time pattern and extract name after it + import re + + time_pattern = r"\d{1,2}:\d{2}" + time_matches = list(re.finditer(time_pattern, line)) + + if time_matches: + # Take everything after the last time pattern + last_time_match = time_matches[-1] + folder_name = line[last_time_match.end() :].strip() + else: + # Fallback: join from part 8 (skip date/time fields) + folder_name = ( + " ".join(parts[8:]) if len(parts) > 8 else parts[7] + ) + + # Additional validation and cleanup + folder_name = folder_name.strip() + + if ( + folder_name + and folder_name not in [".", ".."] + and not folder_name.startswith(".") + ): + folders.append(folder_name) + + # Add folders to tree + if folders: + for folder in sorted(folders): + folder_path = f"{path.rstrip('/')}/{folder}" + item = tree.insert( + parent_item, "end", text=folder, values=[folder_path] + ) + # Add a dummy child to make it expandable + tree.insert(item, "end", text="Loading...") + else: + # No folders found, show indicator + tree.insert(parent_item, "end", text="(No Folders)", values=[""]) + + return folders + except Exception as e: + print(f"Error loading folders from {path}: {e}") + tree.insert( + parent_item, "end", text="(Error loading folders)", values=[""] + ) + return [] + + def on_tree_expand(event): + """Handle tree expansion - load subfolders dynamically.""" + item = tree.selection()[0] if tree.selection() else tree.focus() + if not item: + return + + # Get the path + folder_path = ( + tree.item(item, "values")[0] if tree.item(item, "values") else None + ) + if not folder_path: # Skip items without valid paths (like "(No Folders)") + return + + current_path_var.set(folder_path) + + # Check if we need to load subfolders + children = tree.get_children(item) + has_loading = any( + tree.item(child, "text") == "Loading..." for child in children + ) + + # Only load if we have a "Loading..." placeholder - use async version + if has_loading: + load_folders_async(item, folder_path) + + def on_tree_select(event): + """Handle tree selection - update current path.""" + item = tree.selection()[0] if tree.selection() else None + if item: + folder_path = ( + tree.item(item, "values")[0] if tree.item(item, "values") else None + ) + if folder_path: # Only update if valid path + current_path_var.set(folder_path) + + # Bind events + tree.bind("<>", on_tree_expand) + tree.bind("<>", on_tree_select) + + # Determine which path to use - prefer /sdcard, fallback to /storage/emulated/0 + primary_path = "/sdcard" + fallback_path = "/storage/emulated/0" + + # Test if /sdcard is accessible (with trailing slash for directory listing) + test_result = self.adb_manager.run_adb_command( + ["shell", "ls", "-la", primary_path + "/"] + ) + if ( + isinstance(test_result, tuple) + and len(test_result) == 3 + and test_result[2] == 0 + ): + # /sdcard is accessible + android_path = primary_path + else: + # /sdcard not accessible, use fallback + android_path = fallback_path + + # Create single "Android" root item + android_item = tree.insert("", "end", text="Android", values=[android_path]) + tree.insert(android_item, "end", text="Loading...") + + # Set initial path + current_path_var.set(android_path) + + # Expand and load the Android root immediately + tree.item(android_item, open=True) + load_folders(android_item, android_path) + + # Select the Android item + tree.selection_set(android_item) + + # Buttons + button_frame = tk.Frame(browser_window) + button_frame.pack(pady=10) + + def select_current_folder(): + """Select the currently highlighted folder.""" + current_path = current_path_var.get() + if current_path and current_path.strip(): + self.remote_path_var.set(current_path) + browser_window.destroy() + else: + messagebox.showwarning("No Selection", "Please select a valid folder.") + + def enter_custom_path(): + """Allow user to enter a custom path.""" + browser_window.destroy() + custom_path = simpledialog.askstring( + "Custom Path", + "Enter custom Android folder path:", + initialvalue="/sdcard/", + ) + if custom_path: + self.remote_path_var.set(custom_path.strip()) + + tk.Button( + button_frame, text="Select This Folder", command=select_current_folder + ).pack(side="left", padx=5) + tk.Button( + button_frame, text="Enter Custom Path", command=enter_custom_path + ).pack(side="left", padx=5) + tk.Button(button_frame, text="Cancel", command=browser_window.destroy).pack( + side="left", padx=5 + ) diff --git a/src/main.py b/src/main.py index 0597dec..3907a15 100644 --- a/src/main.py +++ b/src/main.py @@ -6,10 +6,31 @@ Simple entry point to launch the Android file transfer application. try: # Try relative import first (when used as module) - from .gui import main + from .gui.main_window import main except ImportError: # Fall back to direct import (when run directly) - from gui import main + try: + from gui.main_window import main + except ImportError: + from gui import AndroidFileHandlerGUI + import sys + + def main(): + """Main function to run the application.""" + if sys.platform not in ["win32", "linux"]: + import tkinter as tk + from tkinter import messagebox + + 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()