693 lines
28 KiB
Python
693 lines
28 KiB
Python
"""
|
|
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,
|
|
is_adb_available,
|
|
)
|
|
except ImportError:
|
|
# Fall back to direct import (when run directly)
|
|
from adb_manager import (
|
|
ADBManager,
|
|
is_adb_available,
|
|
)
|
|
|
|
try:
|
|
# Try relative imports first
|
|
from .file_browser import AndroidFileBrowser
|
|
except ImportError:
|
|
# Fall back to direct imports
|
|
from src.gui.file_browser import AndroidFileBrowser
|
|
|
|
|
|
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
|
|
self.troubleshooting_steps = (
|
|
"Android device appears to have been disconnected and/or USB debugging is disabled.\n"
|
|
"Please ensure your Android device is securely connected to the computer at both ends.\n\n"
|
|
"To enable USB debugging:\n"
|
|
"1. Connect your device to the computer via USB.\n"
|
|
"2. Open Settings → About phone.\n"
|
|
"3. Tap 'Build number' seven times to unlock Developer Options.\n"
|
|
"4. Go back to Settings → Developer Options.\n"
|
|
"5. Enable 'USB debugging'.\n"
|
|
"6. Connect your phone via USB and accept the prompt to allow debugging.\n\n"
|
|
"Additionally, ensure your device is set to 'File Transfer' mode:\n"
|
|
"1. When you connect your device, swipe down to check the notification panel.\n"
|
|
"2. Look for a 'USB' notification that will tell you what mode the device is in (usually 'Charging over USB') and tap it.\n"
|
|
"3. Select 'File Transfer' or 'MTP' mode.\n\n"
|
|
"After enabling USB debugging and file transfer mode, please click the 'Recheck for connected Android device' button to try again."
|
|
)
|
|
# Setup UI
|
|
self._setup_ui()
|
|
self._initialize_components()
|
|
self._initialize_app()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup the user interface."""
|
|
# Window configuration
|
|
self.title("Android File Handler")
|
|
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",
|
|
command=self._on_direction_change,
|
|
).pack(side="left")
|
|
tk.Radiobutton(
|
|
direction_frame,
|
|
text="Push (Computer → Android)",
|
|
variable=self.direction_var,
|
|
value="push",
|
|
command=self._on_direction_change,
|
|
).pack(side="left", padx=(20, 0))
|
|
|
|
# Create a container frame for the path sections that can be reordered
|
|
self.path_container = tk.Frame(self)
|
|
self.path_container.pack(fill="x", padx=10, pady=(10, 0))
|
|
|
|
# Create the Android device path section
|
|
self.android_frame = tk.Frame(self.path_container)
|
|
self.android_label = tk.Label(self.android_frame, text="Android device:")
|
|
self.android_label.pack(anchor="w")
|
|
|
|
android_path_frame = tk.Frame(self.android_frame)
|
|
android_path_frame.pack(fill="x", pady=(0, 10))
|
|
self.remote_path_var = tk.StringVar(value="Please select file or folder ->")
|
|
self.remote_path_display = tk.Label(
|
|
android_path_frame,
|
|
textvariable=self.remote_path_var,
|
|
anchor="w"
|
|
)
|
|
self.remote_path_display.pack(side="left", fill="x", expand=True)
|
|
tk.Button(
|
|
android_path_frame, text="Browse...", command=self.browse_remote_folder
|
|
).pack(side="right", padx=(5, 0))
|
|
|
|
# Create the Computer path section
|
|
self.computer_frame = tk.Frame(self.path_container)
|
|
self.computer_label = tk.Label(self.computer_frame, text="Computer:")
|
|
self.computer_label.pack(anchor="w")
|
|
|
|
computer_path_frame = tk.Frame(self.computer_frame)
|
|
computer_path_frame.pack(fill="x", pady=(0, 10))
|
|
self.local_path_var = tk.StringVar(value="Please select file or folder ->")
|
|
self.local_path_display = tk.Label(
|
|
computer_path_frame,
|
|
textvariable=self.local_path_var,
|
|
anchor="w"
|
|
)
|
|
self.local_path_display.pack(side="left", fill="x", expand=True)
|
|
tk.Button(
|
|
computer_path_frame, text="Browse...", command=self.browse_local_folder
|
|
).pack(side="right", padx=(5, 0))
|
|
|
|
# Initially arrange for pull (Android on top)
|
|
self._arrange_path_sections()
|
|
|
|
# Status label
|
|
self.status_label = tk.Label(self, text="Status: Idle")
|
|
self.status_label.pack(anchor="w", padx=10, fill="x", pady=(20, 5))
|
|
|
|
# Start/Recheck button (will change based on device state)
|
|
self.start_btn = tk.Button(
|
|
self, text="Start Transfer", command=self.handle_button_click, state="disabled"
|
|
)
|
|
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."""
|
|
# Transfer animation state
|
|
self.transfer_animation_job = None
|
|
self.transfer_dots = 0
|
|
|
|
# Set up ADB callbacks to our own methods
|
|
self.adb_manager.set_progress_callback(self._update_progress)
|
|
self.adb_manager.set_status_callback(self._update_status)
|
|
|
|
# Browser component
|
|
self.browser = AndroidFileBrowser(
|
|
self, self.adb_manager, self.remote_path_var, self._validate_paths_and_update_button
|
|
)
|
|
|
|
def _update_progress(self, percentage):
|
|
"""Handle progress updates (simplified)."""
|
|
# We ignore the percentage and just rely on status updates
|
|
pass
|
|
|
|
def _update_status(self, message: str):
|
|
"""Update status label (thread-safe)."""
|
|
# Don't update status if transfer animation is running
|
|
if self.transfer_animation_job is not None:
|
|
return # Ignore ADB status updates during transfer animation
|
|
|
|
def update_ui():
|
|
self.status_label.config(text=message)
|
|
self.update_idletasks()
|
|
|
|
self.after(0, update_ui)
|
|
|
|
def _validate_paths_and_update_button(self):
|
|
"""Check if both paths are selected and update button state accordingly."""
|
|
remote_path = self.remote_path_var.get().strip()
|
|
local_path = self.local_path_var.get().strip()
|
|
|
|
# Check if both paths are set and not the default placeholder text
|
|
remote_selected = remote_path and remote_path != "Please select file or folder ->"
|
|
local_selected = local_path and local_path != "Please select file or folder ->"
|
|
|
|
# Only manage state if button is in "Start Transfer" mode
|
|
if self.start_btn.cget("text") == "Start Transfer":
|
|
if remote_selected and local_selected:
|
|
self.start_btn.config(state="normal")
|
|
else:
|
|
self.start_btn.config(state="disabled")
|
|
|
|
def _clear_paths_and_disable_button(self):
|
|
"""Clear path selections and disable the transfer button."""
|
|
self.remote_path_var.set("Please select file or folder ->")
|
|
self.local_path_var.set("Please select file or folder ->")
|
|
# Only disable if button is in "Start Transfer" mode
|
|
if self.start_btn.cget("text") == "Start Transfer":
|
|
self.start_btn.config(state="disabled")
|
|
|
|
def _start_transfer_animation(self):
|
|
"""Start the 'Transferring...' animation."""
|
|
print("[DEBUG] Starting transfer animation")
|
|
self.transfer_dots = 0
|
|
self.transfer_animation_job = True # Mark as active before starting
|
|
self._animate_transfer_text()
|
|
|
|
def _animate_transfer_text(self):
|
|
"""Animate the transfer text with dots."""
|
|
if self.transfer_animation_job is not None:
|
|
dots = "." * (self.transfer_dots + 1)
|
|
status_text = f"Transferring{dots}"
|
|
self.status_label.config(text=status_text)
|
|
print(f"[DEBUG] Animation update: {status_text}") # Debug output
|
|
self.update_idletasks() # Force immediate UI update
|
|
self.transfer_dots = (self.transfer_dots + 1) % 5 # Cycle 0-4 dots
|
|
# Schedule next update in 500ms
|
|
self.transfer_animation_job = self.after(500, self._animate_transfer_text)
|
|
|
|
def _stop_transfer_animation(self):
|
|
"""Stop the transfer animation."""
|
|
print("[DEBUG] Stopping transfer animation")
|
|
if self.transfer_animation_job is not None:
|
|
if isinstance(self.transfer_animation_job, str): # It's an after job ID
|
|
self.after_cancel(self.transfer_animation_job)
|
|
self.transfer_animation_job = None
|
|
|
|
def _initialize_app(self):
|
|
"""Initialize the application - check ADB and device."""
|
|
# Check adb availability
|
|
if not is_adb_available():
|
|
self.disable_controls()
|
|
self._update_status("ADB not found locally. Downloading...")
|
|
self.update()
|
|
success = self.adb_manager.download_and_extract_adb()
|
|
if success:
|
|
self._update_status("ADB downloaded and ready.")
|
|
self.enable_controls()
|
|
else:
|
|
self._update_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._update_status("Checking for connected device...")
|
|
self.update()
|
|
device = self.adb_manager.check_device()
|
|
if not device:
|
|
self.device_connected = False
|
|
self.disable_controls()
|
|
self._update_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._update_status(f"Device detected: {device}")
|
|
self._switch_to_transfer_mode()
|
|
self.enable_controls()
|
|
|
|
def browse_remote_folder(self):
|
|
"""Browse remote Android files and folders."""
|
|
# Pass the current direction to the browser
|
|
direction = self.direction_var.get()
|
|
self.browser.show_browser(direction)
|
|
|
|
def browse_local_folder(self):
|
|
"""Browse for local folder or file based on direction."""
|
|
import tkinter.filedialog as fd
|
|
import os
|
|
|
|
direction = self.direction_var.get()
|
|
|
|
# Set initial directory to user's home directory
|
|
initial_dir = os.path.expanduser("~")
|
|
|
|
if direction == "push":
|
|
# For push, show file selection first, then folder selection if cancelled
|
|
|
|
# First try file selection
|
|
selected_path = fd.askopenfilename(
|
|
title="Select file to push to Android device",
|
|
initialdir=initial_dir,
|
|
filetypes=[("All files", "*.*")]
|
|
)
|
|
|
|
# If no file was selected, offer folder selection as an alternative
|
|
if not selected_path:
|
|
selected_path = fd.askdirectory(
|
|
title="Select folder to push to Android device",
|
|
initialdir=initial_dir
|
|
)
|
|
|
|
# Only set the path if something was actually selected
|
|
# If user cancels both dialogs, selected_path will be empty and nothing happens
|
|
if selected_path:
|
|
self.local_path_var.set(selected_path)
|
|
self._validate_paths_and_update_button()
|
|
|
|
else: # pull direction
|
|
# For pull, only allow folder selection (destination)
|
|
folder = filedialog.askdirectory(
|
|
title="Select destination folder for pulled files",
|
|
initialdir=initial_dir
|
|
)
|
|
if folder:
|
|
self.local_path_var.set(folder)
|
|
self._validate_paths_and_update_button()
|
|
|
|
def disable_controls(self):
|
|
"""Disable UI controls during operations."""
|
|
# Path displays are already read-only (Labels), no need to disable them
|
|
# Don't disable start_btn here - it will be handled by button mode switching
|
|
pass
|
|
|
|
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."""
|
|
# Path displays are already read-only (Labels), no need to enable them
|
|
# Button state is handled by mode switching methods
|
|
pass
|
|
|
|
def _on_direction_change(self):
|
|
"""Handle radio button direction change."""
|
|
self._arrange_path_sections()
|
|
|
|
def _arrange_path_sections(self):
|
|
"""Arrange the Android and Computer path sections based on direction."""
|
|
# Remove both frames from container
|
|
self.android_frame.pack_forget()
|
|
self.computer_frame.pack_forget()
|
|
|
|
direction = self.direction_var.get()
|
|
if direction == "pull":
|
|
# Pull: Android → Computer (Android on top)
|
|
self.android_frame.pack(fill="x", pady=(0, 5))
|
|
self.computer_frame.pack(fill="x")
|
|
else: # push
|
|
# Push: Computer → Android (Computer on top)
|
|
self.computer_frame.pack(fill="x", pady=(0, 5))
|
|
self.android_frame.pack(fill="x")
|
|
|
|
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
|
|
)
|
|
# Only enable if both paths are selected, otherwise keep disabled
|
|
self._validate_paths_and_update_button()
|
|
|
|
def _switch_to_cancel_mode(self):
|
|
"""Switch button to cancel transfer mode."""
|
|
self.start_btn.config(
|
|
text="Cancel Transfer", command=self.cancel_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 cancel_transfer(self):
|
|
"""Cancel the current transfer."""
|
|
print("[DEBUG] Cancel transfer requested")
|
|
|
|
# Cancel the actual ADB process
|
|
cancelled = self.adb_manager.cancel_transfer()
|
|
|
|
# Increment transfer ID to invalidate current transfer
|
|
self.current_transfer_id += 1
|
|
|
|
# Stop animation and restore UI
|
|
self._stop_transfer_animation()
|
|
|
|
# Use after() to ensure status update happens after animation stops
|
|
def update_cancelled_status():
|
|
if cancelled:
|
|
self.status_label.config(text="Transfer cancelled by user.")
|
|
else:
|
|
self.status_label.config(text="Transfer cancellation failed.")
|
|
self.update_idletasks()
|
|
|
|
self.after(0, update_cancelled_status)
|
|
self.enable_controls()
|
|
|
|
# Restore proper button state
|
|
if self.device_connected:
|
|
self._switch_to_transfer_mode()
|
|
else:
|
|
self._switch_to_recheck_mode()
|
|
|
|
def show_enable_debugging_instructions(self):
|
|
"""Show instructions to connect device, enable file transfer, and to enable USB debugging."""
|
|
msg = (
|
|
self.troubleshooting_steps + "\n\n"
|
|
)
|
|
result = messagebox.showinfo(
|
|
"Check device connection and 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, please 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:
|
|
msg = (
|
|
self.troubleshooting_steps + "\n\n"
|
|
)
|
|
messagebox.showerror("No Android Device Detected", msg)
|
|
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 path cannot be empty.")
|
|
return
|
|
|
|
# For push operations, validate local path
|
|
if direction == "push":
|
|
if not local_path or (not os.path.isfile(local_path) and not os.path.isdir(local_path)):
|
|
messagebox.showerror(
|
|
"Input Error", "Please select a valid local file or folder."
|
|
)
|
|
return
|
|
else: # pull operations
|
|
if not local_path or not os.path.isdir(local_path):
|
|
messagebox.showerror(
|
|
"Input Error", "Please select a valid local destination folder for pulled files."
|
|
)
|
|
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()
|
|
# Switch button to cancel mode during transfer
|
|
self._switch_to_cancel_mode()
|
|
# Start the transfer animation
|
|
self._start_transfer_animation()
|
|
|
|
# Determine if we're dealing with files or folders
|
|
if direction == "pull":
|
|
# For pull, check if remote path is a file
|
|
is_file = self._is_remote_file(remote_path)
|
|
threading.Thread(
|
|
target=self._transfer_thread,
|
|
args=(direction, remote_path, local_path, transfer_id, is_file),
|
|
daemon=True,
|
|
).start()
|
|
elif direction == "push":
|
|
# For push, check if local path is a file
|
|
is_file = os.path.isfile(local_path)
|
|
threading.Thread(
|
|
target=self._transfer_thread,
|
|
args=(direction, local_path, remote_path, transfer_id, is_file),
|
|
daemon=True,
|
|
).start()
|
|
else:
|
|
self.report_error("Invalid transfer direction selected.")
|
|
self.enable_controls()
|
|
|
|
def _transfer_thread(self, direction: str, source_path: str, dest_path: str, transfer_id: int, is_file: bool):
|
|
"""Unified thread function for all transfer operations."""
|
|
try:
|
|
# Check if this transfer is still current
|
|
if self.current_transfer_id != transfer_id:
|
|
return
|
|
|
|
# Get the appropriate transfer method
|
|
transfer_method, transfer_type = self._get_file_transfer_methods(direction, is_file)
|
|
|
|
# For pull operations with files, we need to construct the full destination path
|
|
if direction == "pull" and is_file:
|
|
# Extract filename from source and append to destination directory
|
|
filename = source_path.split("/")[-1]
|
|
full_dest_path = os.path.join(dest_path, filename)
|
|
success = transfer_method(source_path, full_dest_path)
|
|
else:
|
|
success = transfer_method(source_path, dest_path)
|
|
|
|
if success and self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
transfer_desc = "File" if is_file else "Folder"
|
|
self._update_status(f"{transfer_desc} transfer completed successfully.")
|
|
self.show_disable_debugging_reminder()
|
|
# Clear paths and disable button for next transfer
|
|
self.after(0, self._clear_paths_and_disable_button)
|
|
except Exception as e:
|
|
if self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
transfer_desc = "file" if is_file else "folder"
|
|
self.report_error(f"{direction.capitalize()} {transfer_desc} operation failed: {e}")
|
|
finally:
|
|
if self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
self.enable_controls()
|
|
# Restore proper button state after transfer
|
|
self.after(0, self._restore_button_state)
|
|
|
|
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._stop_transfer_animation()
|
|
self._update_status("Transfer completed successfully.")
|
|
self.show_disable_debugging_reminder()
|
|
# Clear paths and disable button for next transfer
|
|
self.after(0, self._clear_paths_and_disable_button)
|
|
except Exception as e:
|
|
if self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
self.report_error(f"Pull operation failed: {e}")
|
|
finally:
|
|
if self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
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._stop_transfer_animation()
|
|
self._update_status("Transfer completed successfully.")
|
|
self.show_disable_debugging_reminder()
|
|
# Clear paths and disable button for next transfer
|
|
self.after(0, self._clear_paths_and_disable_button)
|
|
except Exception as e:
|
|
if self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
self.report_error(f"Push operation failed: {e}")
|
|
finally:
|
|
if self.current_transfer_id == transfer_id:
|
|
self._stop_transfer_animation()
|
|
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 _is_remote_file(self, remote_path: str) -> bool:
|
|
"""Check if the remote path points to a file (not a directory)."""
|
|
try:
|
|
# Use ls -la to check if it's a file
|
|
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."""
|
|
if direction == "pull":
|
|
if is_file:
|
|
return self.adb_manager.pull_file, "file"
|
|
else:
|
|
return self.adb_manager.pull_folder, "folder"
|
|
else: # push
|
|
if is_file:
|
|
return self.adb_manager.push_file, "file"
|
|
else:
|
|
return self.adb_manager.push_folder, "folder"
|
|
|
|
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()
|