reorganized file and folder structure to reflect responsibilities

This commit is contained in:
2025-09-22 14:38:14 -05:00
parent 79b36ce454
commit b1cafc3106
20 changed files with 58 additions and 158 deletions
+4
View File
@@ -0,0 +1,4 @@
"""
Managers Package
Business logic managers for device and transfer operations.
"""
+172
View File
@@ -0,0 +1,172 @@
"""
Device Manager Module
Handles Android device connection and ADB operations for the GUI.
"""
import tkinter as tk
from tkinter import messagebox
from typing import Optional, Callable
try:
from ..core.adb_manager import ADBManager, is_adb_available
except ImportError:
from core.adb_manager import ADBManager, is_adb_available
class DeviceManager:
"""Manages Android device connections and ADB operations."""
def __init__(self, parent_window: tk.Tk, status_callback: Optional[Callable[[str], None]] = None):
"""Initialize the device manager.
Args:
parent_window: The main window instance
status_callback: Callback function for status updates
"""
self.parent = parent_window
self.status_callback = status_callback
self.adb_manager = ADBManager()
self.device_connected = False
# Set up ADB callbacks
self.adb_manager.set_status_callback(self._on_adb_status_update)
self.adb_manager.set_progress_callback(self._on_adb_progress_update)
def initialize_adb(self) -> bool:
"""Initialize ADB and download tools if needed.
Returns:
True if ADB is available and ready, False otherwise
"""
if not is_adb_available():
# Show welcome message when platform-tools need to be downloaded
messagebox.showinfo(
"Welcome to Android File Transfer!",
"Welcome to Android File Transfer! This application simplifies "
"and speeds up file transfers over USB between computers and Android devices. "
"Please do not delete or move the platform-tools folder that will be "
"downloaded. These are tools written by Google "
"and they are required for this application to function properly."
)
self._update_status("ADB not found locally. Downloading...")
self.parent.update()
self.adb_manager.download_and_extract_adb()
success = is_adb_available()
if success:
self._update_status("ADB downloaded and ready.")
return True
else:
self._update_status(
"Failed to download/access Android Debug Bridge tools. "
"Please check your internet and for any blocking security pop-ups and restart."
)
messagebox.showerror("Error", "Failed to download ADB tools. Exiting.")
return False
return True
def check_device_connection(self) -> Optional[str]:
"""Check for device connection and update status.
Returns:
Device ID if connected, None otherwise
"""
self._update_status("Checking for connected device...")
self.parent.update()
device = self.adb_manager.check_device()
if device:
self.device_connected = True
self._update_status(f"Device detected: {device}")
return device
else:
self.device_connected = False
self._update_status(
"No Android devices detected. Please check the USB connection at both ends is "
"securely inserted, USB debugging is enabled, and that File Transfer mode is turned on."
)
return None
def is_remote_file(self, remote_path: str) -> bool:
"""Check if the remote path points to a file (not a directory).
Args:
remote_path: Path on the Android device
Returns:
True if it's a file, False if it's a directory or check fails
"""
try:
result = self.adb_manager.run_adb_command(["shell", "ls", "-la", remote_path])
if isinstance(result, tuple) and len(result) == 3:
stdout, stderr, returncode = result
if returncode == 0 and stdout:
# If the output starts with '-', it's a regular file
return stdout.strip().startswith('-')
return False
except Exception:
return False
def get_file_transfer_methods(self, direction: str, is_file: bool):
"""Get the appropriate transfer methods based on direction and type.
Args:
direction: Transfer direction ('pull' or 'push')
is_file: Whether transferring a file (True) or folder (False)
Returns:
Tuple of (transfer_method, transfer_type)
"""
if direction == "pull":
if is_file:
return self.adb_manager.pull_file, "file"
else:
return self.adb_manager.pull_folder_with_dedup, "folder"
else: # push
if is_file:
return self.adb_manager.push_file, "file"
else:
return self.adb_manager.push_folder_with_dedup, "folder"
def cancel_current_operation(self) -> None:
"""Cancel the current ADB operation."""
try:
self.adb_manager.cancel_current_operation()
except Exception as e:
print(f"Error cancelling operation: {e}")
def cancel_transfer(self) -> bool:
"""Cancel the current transfer operation.
Returns:
True if transfer was cancelled successfully, False otherwise
"""
return self.adb_manager.cancel_transfer()
def _update_status(self, message: str) -> None:
"""Update status through callback if available.
Args:
message: Status message to display
"""
if self.status_callback:
self.status_callback(message)
def _on_adb_status_update(self, message: str) -> None:
"""Handle status updates from ADB manager.
Args:
message: Status message from ADB operations
"""
self._update_status(message)
def _on_adb_progress_update(self, percentage: int) -> None:
"""Handle progress updates from ADB manager.
Args:
percentage: Progress percentage (0-100)
"""
# Currently we ignore progress updates and rely on status updates
pass
+394
View File
@@ -0,0 +1,394 @@
"""
Transfer Manager Module
Handles file transfer operations and coordination between GUI and ADB manager.
"""
import threading
import os
from typing import Optional, Callable, Tuple, Dict, Any
try:
from .device_manager import DeviceManager
from ..gui.handlers.animation_handler import AnimationHandler
from ..gui.dialogs.dialog_manager import DialogManager
except ImportError:
from device_manager import DeviceManager
from gui.handlers.animation_handler import AnimationHandler
from gui.dialogs.dialog_manager import DialogManager
class TransferManager:
"""Manages file transfer operations and coordination."""
def __init__(self, parent_window, device_manager: DeviceManager,
animation_handler: AnimationHandler, dialog_manager: DialogManager):
"""Initialize the transfer manager.
Args:
parent_window: The main window instance
device_manager: Device manager instance
animation_handler: Animation handler instance
dialog_manager: Dialog manager instance
"""
self.parent = parent_window
self.device_manager = device_manager
self.animation_handler = animation_handler
self.dialog_manager = dialog_manager
# Transfer tracking
self.current_transfer_id = 0
# UI callbacks
self.ui_callbacks = {}
def set_ui_callback(self, name: str, callback: Callable) -> None:
"""Set a UI callback function.
Args:
name: Name of the callback
callback: Function to call
"""
self.ui_callbacks[name] = callback
def set_status_callback(self, callback: Callable[[str], None]) -> None:
"""Set the status update callback.
Args:
callback: Function to call for status updates
"""
self.ui_callbacks['status'] = callback
self.status_callback = callback
def set_controls_callback(self, callback: Callable[[], None]) -> None:
"""Set the controls update callback.
Args:
callback: Function to call for enabling/disabling controls
"""
self.controls_callback = callback
def start_transfer(self, direction: str, source_path: str, dest_path: str,
completion_callback: Optional[Callable] = None) -> bool:
"""Start a file transfer operation.
Args:
direction: Transfer direction ('pull' or 'push')
source_path: Source file or folder path
dest_path: Destination path
completion_callback: Callback to call when transfer completes
Returns:
True if transfer was started successfully, False otherwise
"""
# Increment transfer ID for cancellation support
self.current_transfer_id += 1
transfer_id = self.current_transfer_id
# Determine if transferring a file or folder
if direction == "pull":
is_file = self._is_remote_file(source_path)
else:
is_file = os.path.isfile(source_path)
# Disable controls during transfer
if 'disable_controls' in self.ui_callbacks:
self.ui_callbacks['disable_controls']()
# Start transfer in background thread
transfer_thread = threading.Thread(
target=self._transfer_thread,
args=(direction, source_path, dest_path, transfer_id, is_file, completion_callback),
daemon=True
)
transfer_thread.start()
return True
def cancel_transfer(self) -> None:
"""Cancel the current transfer operation."""
# Increment transfer ID to invalidate current transfer
self.current_transfer_id += 1
# Cancel ADB operation
self.device_manager.cancel_current_operation()
def _is_remote_file(self, remote_path: str) -> bool:
"""Check if a remote path is a file.
Args:
remote_path: Path on Android device
Returns:
True if path is a file, False if it's a folder
"""
try:
# Use device manager to check if path is a file
return self.device_manager.is_remote_file(remote_path)
except Exception:
# If we can't determine, assume it's a folder for safety
return False
def _transfer_thread(self, direction: str, source_path: str, dest_path: str,
transfer_id: int, is_file: bool, completion_callback: Optional[Callable]):
"""Handle file transfer in background thread.
Args:
direction: Transfer direction ('pull' or 'push')
source_path: Source file or folder path
dest_path: Destination path
transfer_id: Transfer ID for cancellation
is_file: True if transferring a file
completion_callback: Callback for completion
"""
try:
# Check if transfer is still valid
if transfer_id != self.current_transfer_id:
return
# Get ADB manager from device manager
adb_manager = self.device_manager.adb_manager
# Perform the transfer
if direction == "pull":
if is_file:
success, stats = adb_manager.pull_file(source_path, dest_path)
else:
success, stats = adb_manager.pull_folder(source_path, dest_path)
operation = "pulled from Android device"
else: # push
if is_file:
success, stats = adb_manager.push_file(source_path, dest_path)
else:
success, stats = adb_manager.push_folder(source_path, dest_path)
operation = "pushed to Android device"
# Check if transfer was cancelled
if transfer_id != self.current_transfer_id:
return
# Call completion callback on main thread
if completion_callback:
self.parent.after(0, lambda: completion_callback(success, stats, operation))
except Exception as e:
# Handle errors on main thread
if transfer_id == self.current_transfer_id and 'show_error' in self.ui_callbacks:
self.parent.after(0, lambda: self.ui_callbacks["show_error"](f"Transfer error: {str(e)}"))
# Start transfer in background thread
self.current_transfer_id += 1
transfer_id = self.current_transfer_id
threading.Thread(
target=self._transfer_thread,
args=(direction, source_path, dest_path, transfer_id, is_file),
daemon=True
).start()
return True
def cancel_transfer(self) -> bool:
"""Cancel the current transfer operation.
Returns:
True if transfer was cancelled successfully, False otherwise
"""
# Cancel the actual ADB process
cancelled = self.device_manager.cancel_transfer()
# Increment transfer ID to invalidate current transfer
self.current_transfer_id += 1
# Stop animation and restore UI
self.animation_handler.stop_animation()
# Update status
status = "Transfer cancelled by user." if cancelled else "Transfer cancellation failed."
self._update_status(status)
return cancelled
def _validate_transfer_paths(self, direction: str, source_path: str, dest_path: str) -> bool:
"""Validate transfer paths based on direction.
Args:
direction: Transfer direction ('pull' or 'push')
source_path: Source path
dest_path: Destination path
Returns:
True if paths are valid, False otherwise
"""
if not source_path or not dest_path:
self.dialog_manager.show_error("Input Error", "Both source and destination paths are required.")
return False
if direction == "push":
# For push operations, validate local source path exists
if not os.path.exists(source_path):
self.dialog_manager.show_error(
"Input Error",
f"Source path does not exist: {source_path}"
)
return False
else: # pull operations
# For pull operations, validate local destination is a directory
if not os.path.isdir(dest_path):
self.dialog_manager.show_error(
"Input Error",
"Destination must be a valid directory for pulled files."
)
return False
return True
def _transfer_thread(self, direction: str, source_path: str, dest_path: str,
transfer_id: int, is_file: bool) -> None:
"""Background thread function for transfer operations.
Args:
direction: Transfer direction ('pull' or 'push')
source_path: Source path
dest_path: Destination path
transfer_id: Transfer ID for thread safety
is_file: True if transferring a file, False for folder
"""
try:
# Check if this transfer is still current
if self.current_transfer_id != transfer_id:
return
# Recheck device connectivity before proceeding
device = self.device_manager.adb_manager.check_device()
if not device:
if self.current_transfer_id == transfer_id:
self.animation_handler.stop_animation()
self.parent.after(0, self._handle_device_disconnection)
return
# Get the appropriate transfer method
transfer_method, transfer_type = self.device_manager.get_file_transfer_methods(direction, is_file)
# Perform the transfer
success, stats = self._execute_transfer(
direction, source_path, dest_path, transfer_method, transfer_type, is_file
)
if success and self.current_transfer_id == transfer_id:
self.animation_handler.stop_animation()
transfer_desc = "File" if is_file else "Folder"
self._update_status(
f"{transfer_desc} transfer completed successfully. "
"To start another transfer, please select another file or folder."
)
# Show debugging reminder and transfer statistics
self.parent.after(0, self.dialog_manager.show_disable_debugging_reminder)
if stats is not None:
self.parent.after(0, lambda: self.dialog_manager.show_transfer_stats(
stats, direction.capitalize(), self.device_manager.adb_manager.deduplicator
))
except Exception as e:
if self.current_transfer_id == transfer_id:
self.animation_handler.stop_animation()
transfer_desc = "file" if is_file else "folder"
error_msg = f"{direction.capitalize()} {transfer_desc} operation failed: {e}"
self.parent.after(0, lambda: self.dialog_manager.show_error("Transfer Error", error_msg))
finally:
if self.current_transfer_id == transfer_id:
self.animation_handler.stop_animation()
if self.controls_callback:
self.parent.after(0, self.controls_callback)
def _execute_transfer(self, direction: str, source_path: str, dest_path: str,
transfer_method: Callable, transfer_type: str, is_file: bool) -> Tuple[bool, Optional[Dict[str, Any]]]:
"""Execute the actual transfer operation.
Args:
direction: Transfer direction
source_path: Source path
dest_path: Destination path
transfer_method: Transfer method to call
transfer_type: Type of transfer ('file' or 'folder')
is_file: True if transferring a file
Returns:
Tuple of (success, stats_dict or None)
"""
if direction == "pull" and is_file:
# For pull operations with files, construct the full destination path
filename = source_path.split("/")[-1]
full_dest_path = os.path.join(dest_path, filename)
success = transfer_method(source_path, full_dest_path)
return success, None
else:
# For folder transfers or push operations
if transfer_type == "folder":
# Folder transfers return (success, stats)
return transfer_method(source_path, dest_path)
else:
# File transfers return just success
success = transfer_method(source_path, dest_path)
return success, None
def _handle_device_disconnection(self) -> None:
"""Handle device disconnection during transfers."""
self.device_manager.device_connected = False
self._update_status(
"No Android devices detected. Please check the USB connection at both ends is "
"securely inserted, USB debugging is enabled, and that File Transfer mode is turned on."
)
self.dialog_manager.show_enable_debugging_instructions()
def _update_status(self, message: str) -> None:
"""Update status through callback if available.
Args:
message: Status message to display
"""
if self.status_callback:
self.status_callback(message)
def handle_adb_status_update(self, message: str) -> None:
"""Handle status updates from ADB operations.
Args:
message: Status message from ADB
"""
# Check for transfer progress updates
if message.startswith("TRANSFER_PROGRESS:"):
parts = message.split(":")
if len(parts) == 3:
current = int(parts[1])
total = int(parts[2])
self.animation_handler.update_transfer_progress(current, total)
return
# Check for animation state changes
if self.animation_handler.is_animation_running():
if "Scanning for duplicates" in message and not self.animation_handler.is_scanning():
# Already scanning, ignore duplicate messages
return
elif "Starting transfer" in message or "Transferring" in message:
# Switch from scanning to transfer animation
self.animation_handler.stop_animation()
self.animation_handler.start_transfer_animation()
return
elif ("Duplicate scan complete" in message or
"No duplicates found" in message or
"All files already exist" in message):
# Allow these messages to show briefly before transfer starts
self.animation_handler.stop_animation()
self._update_status(message)
return
else:
# Don't update status if animation is running (except for specific cases)
return
elif "Scanning for duplicates" in message:
# Start scanning animation
self.animation_handler.start_scanning_animation()
return
# Normal status update
self._update_status(message)