""" 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 managers.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), daemon=True ) transfer_thread.start() return True 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 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)