Files
android-assistant/src/managers/transfer_manager.py
T

327 lines
13 KiB
Python

"""
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)