got path selection to visually switch places on radio button selection
This commit is contained in:
@@ -23,6 +23,7 @@ This is a small Python GUI tool for managing Android device files via ADB (Andro
|
||||
- Include type hints for all function parameters and return values.
|
||||
- Use f-strings for string formatting.
|
||||
- No single letter variable names except for `e` when appropriate.
|
||||
- Do not recreate files I have deleted.
|
||||
|
||||
## UI guidelines
|
||||
|
||||
|
||||
+18
-10
@@ -195,11 +195,15 @@ class ADBManager:
|
||||
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."
|
||||
)
|
||||
if os.name == "nt":
|
||||
# Check if this is a root drive (like C:\, D:\, etc.)
|
||||
normalized_path = os.path.abspath(local_path)
|
||||
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
||||
if normalized_path == drive_root:
|
||||
# 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]
|
||||
|
||||
@@ -315,11 +319,15 @@ class ADBManager:
|
||||
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."
|
||||
)
|
||||
if os.name == "nt":
|
||||
# Check if this is a root drive (like C:\, D:\, etc.)
|
||||
normalized_path = os.path.abspath(local_path)
|
||||
drive_root = os.path.splitdrive(normalized_path)[0] + os.sep
|
||||
if normalized_path == drive_root:
|
||||
# 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]
|
||||
|
||||
|
||||
-728
@@ -1,728 +0,0 @@
|
||||
"""
|
||||
Android File Handler - GUI Module
|
||||
Provides the user interface for the Android file transfer application.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, filedialog, ttk, simpledialog
|
||||
from typing import Optional
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Setup UI
|
||||
self._setup_ui()
|
||||
self._setup_progress_handling()
|
||||
self._initialize_app()
|
||||
|
||||
def _setup_progress_handling(self):
|
||||
"""Set up progress handling callbacks."""
|
||||
# Set up ADB callbacks using our own methods for this legacy version
|
||||
self.adb_manager.set_progress_callback(self.update_progress)
|
||||
self.adb_manager.set_status_callback(self.set_status)
|
||||
|
||||
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",
|
||||
).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="indeterminate")
|
||||
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 button
|
||||
self.start_btn = tk.Button(
|
||||
self, text="Start Transfer", command=self.start_transfer
|
||||
)
|
||||
self.start_btn.pack(pady=10)
|
||||
|
||||
# Window close protocol
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_close)
|
||||
|
||||
def _initialize_app(self):
|
||||
"""Initialize the application - check ADB and device."""
|
||||
# Check adb availability
|
||||
if not is_adb_available():
|
||||
self.disable_controls()
|
||||
self.set_status("ADB not found locally. Downloading...")
|
||||
self.update()
|
||||
success = self.adb_manager.download_and_extract_adb()
|
||||
if success:
|
||||
self.set_status("ADB downloaded and ready.")
|
||||
self.enable_controls()
|
||||
else:
|
||||
self.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.set_status("Checking for connected device...")
|
||||
self.update()
|
||||
device = self.adb_manager.check_device()
|
||||
if not device:
|
||||
self.disable_controls()
|
||||
self.set_status(
|
||||
"No device detected. Enable USB debugging and connect your device."
|
||||
)
|
||||
self.show_enable_debugging_instructions()
|
||||
else:
|
||||
self.set_status(f"Device detected: {device}")
|
||||
|
||||
def browse_remote_folder(self):
|
||||
"""Browse remote Android folders."""
|
||||
messagebox.showinfo(
|
||||
"Feature Not Available",
|
||||
"Remote folder browsing is not available in this version. Please manually enter the path."
|
||||
)
|
||||
|
||||
def _show_android_filesystem_tree(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)
|
||||
browser_window.title("Browse Android Folders")
|
||||
browser_window.geometry("500x400")
|
||||
browser_window.transient(self)
|
||||
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.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 (last part after spaces)
|
||||
parts = line.split()
|
||||
if (
|
||||
len(parts) >= 8
|
||||
): # Android ls -la typically has 8+ parts
|
||||
folder_name = " ".join(
|
||||
parts[7:]
|
||||
) # Handle names with spaces (start from column 8)
|
||||
if 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.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.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 (last part after spaces)
|
||||
parts = line.split()
|
||||
if len(parts) >= 8: # Android ls -la typically has 8+ parts
|
||||
folder_name = " ".join(
|
||||
parts[7:]
|
||||
) # Handle names with spaces (start from column 8)
|
||||
if 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("<<TreeviewOpen>>", on_tree_expand)
|
||||
tree.bind("<<TreeviewSelect>>", 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
|
||||
)
|
||||
|
||||
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")
|
||||
self.start_btn.config(state="disabled")
|
||||
|
||||
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")
|
||||
self.start_btn.config(state="normal")
|
||||
|
||||
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, restart this app."
|
||||
)
|
||||
messagebox.showinfo("Enable USB Debugging", msg)
|
||||
|
||||
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."""
|
||||
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
|
||||
|
||||
# Start transfer
|
||||
self.current_transfer_id += 1
|
||||
transfer_id = self.current_transfer_id
|
||||
|
||||
self.disable_controls()
|
||||
# Reset progress bar in thread-safe way
|
||||
self._update_progress_ui(0)
|
||||
|
||||
if direction == "pull":
|
||||
self.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.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()
|
||||
|
||||
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()
|
||||
|
||||
def update_progress(self, percentage: int):
|
||||
"""Update the progress bar (thread-safe)."""
|
||||
|
||||
# Schedule UI update on main thread using a proper closure
|
||||
def update_ui():
|
||||
self._update_progress_ui(percentage)
|
||||
|
||||
self.after(0, update_ui)
|
||||
|
||||
def _update_progress_ui(self, percentage: int):
|
||||
"""Internal method to update progress bar on main thread."""
|
||||
# For indeterminate mode, start/stop animation based on progress
|
||||
try:
|
||||
if percentage > 0 and percentage < 100:
|
||||
self.progress.start(10) # Start animation
|
||||
elif percentage >= 100:
|
||||
self.progress.stop() # Stop animation when complete
|
||||
self.update_idletasks()
|
||||
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.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.update_idletasks()
|
||||
|
||||
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()
|
||||
+47
-18
@@ -80,46 +80,55 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
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))
|
||||
|
||||
# Remote folder path
|
||||
tk.Label(self, text="Remote folder path (Android device):").pack(
|
||||
anchor="w", padx=10, pady=(10, 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()
|
||||
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
|
||||
android_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
|
||||
android_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)
|
||||
)
|
||||
# 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()
|
||||
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
|
||||
computer_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
|
||||
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))
|
||||
@@ -260,6 +269,26 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
self.local_path_entry.config(state="normal")
|
||||
# Button state is handled by mode switching methods
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user