* Added "Recheck for connected device feature" * WIP on progress bar

working for larger transfers * More generated debug files that I will
  probably use to help create tests
This commit is contained in:
2025-08-15 18:43:36 -05:00
parent 4512c73ef3
commit 19a555058e
20 changed files with 1895 additions and 24 deletions
+17
View File
@@ -0,0 +1,17 @@
"""
GUI Package for Android File Handler
Provides modular GUI components for the Android file transfer application.
"""
from .main_window import AndroidFileHandlerGUI, main
from .progress_handler import ProgressHandler
from .windows_browser import WindowsAndroidBrowser
from .linux_browser import LinuxAndroidBrowser
__all__ = [
"AndroidFileHandlerGUI",
"main",
"ProgressHandler",
"WindowsAndroidBrowser",
"LinuxAndroidBrowser",
]
+68
View File
@@ -0,0 +1,68 @@
"""
Linux Android Browser Module
Provides Android filesystem browsing capabilities for Linux using MTP.
"""
import os
from tkinter import messagebox, filedialog
class LinuxAndroidBrowser:
"""Android filesystem browser for Linux using MTP."""
def __init__(self, parent_window, mtp_manager, remote_path_var):
self.parent = parent_window
self.mtp_manager = mtp_manager
self.remote_path_var = remote_path_var
def show_browser(self):
"""Browse remote Android folders via MTP (Linux)"""
if not self.mtp_manager:
messagebox.showerror(
"Error",
"MTP manager not available. This feature is only supported on Linux.",
)
return
# First try using existing GVFS mount
gvfs_mount = self.mtp_manager.find_gvfs_mtp_mount()
if gvfs_mount:
try:
folder = filedialog.askdirectory(
initialdir=gvfs_mount, title="Select Android folder"
)
if folder:
# Convert filesystem path back to Android path
relative_path = os.path.relpath(folder, gvfs_mount)
if relative_path == ".":
android_path = "/sdcard"
else:
android_path = f"/sdcard/{relative_path}".replace("\\", "/")
self.remote_path_var.set(android_path)
return
except Exception as e:
print(f"GVFS browse failed: {e}")
# Fallback to jmtpfs
mount_point = self.mtp_manager.mount_mtp_device()
if mount_point:
try:
folder = filedialog.askdirectory(
initialdir=mount_point, title="Select Android folder"
)
if folder:
# Convert filesystem path back to Android path
relative_path = os.path.relpath(folder, mount_point)
if relative_path == ".":
android_path = "/sdcard"
else:
android_path = f"/sdcard/{relative_path}".replace("\\", "/")
self.remote_path_var.set(android_path)
finally:
self.mtp_manager.unmount_mtp_device()
else:
messagebox.showerror(
"Error",
"Could not mount Android device via MTP. "
"Make sure it's connected and set to 'File Transfer' mode.",
)
+430
View File
@@ -0,0 +1,430 @@
"""
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,
LinuxMTPManager,
get_platform_type,
is_adb_available,
)
except ImportError:
# Fall back to direct import (when run directly)
from adb_manager import (
ADBManager,
LinuxMTPManager,
get_platform_type,
is_adb_available,
)
from .progress_handler import ProgressHandler
from .windows_browser import WindowsAndroidBrowser
from .linux_browser import LinuxAndroidBrowser
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
if get_platform_type().startswith("linux"):
self.mtp_manager = LinuxMTPManager()
else:
self.mtp_manager = None
# Setup UI
self._setup_ui()
self._initialize_components()
self._initialize_app()
def _setup_ui(self):
"""Setup the user interface."""
# Window configuration
self.title("Android Folder Puller")
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="determinate")
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/Recheck button (will change based on device state)
self.start_btn = tk.Button(
self, text="Start Transfer", command=self.handle_button_click
)
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."""
# Progress handler
self.progress_handler = ProgressHandler(self, self.progress, self.status_label)
# Set up ADB callbacks
self.adb_manager.set_progress_callback(self.progress_handler.update_progress)
self.adb_manager.set_status_callback(self.progress_handler.set_status)
# Browser components
self.windows_browser = WindowsAndroidBrowser(
self, self.adb_manager, self.remote_path_var
)
self.linux_browser = LinuxAndroidBrowser(
self, self.mtp_manager, self.remote_path_var
)
def _initialize_app(self):
"""Initialize the application - check ADB and device."""
# Check adb availability
if not is_adb_available():
self.disable_controls()
self.progress_handler.set_status("ADB not found locally. Downloading...")
self.update()
success = self.adb_manager.download_and_extract_adb()
if success:
self.progress_handler.set_status("ADB downloaded and ready.")
self.enable_controls()
else:
self.progress_handler.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.check_device_connection()
def check_device_connection(self):
"""Check for device connection and update UI accordingly."""
self.progress_handler.set_status("Checking for connected device...")
self.update()
device = self.adb_manager.check_device()
if not device:
self.device_connected = False
self.disable_controls()
self.progress_handler.set_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.progress_handler.set_status(f"Device detected: {device}")
self._switch_to_transfer_mode()
self.enable_controls()
def browse_remote_folder(self):
"""Browse remote Android folders."""
if get_platform_type().startswith("linux") and self.mtp_manager:
# Use Linux MTP browser
self.linux_browser.show_browser()
else:
# Use Windows ADB browser
self.windows_browser.show_browser()
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")
# Don't disable start_btn here - it will be handled by button mode switching
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")
# Button state is handled by mode switching methods
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, 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 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, click 'Recheck for connected Android device' to try again."
)
result = messagebox.showinfo("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, 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:
messagebox.showerror(
"No Device",
"No Android device is connected. Please connect your device and enable USB debugging.",
)
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 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
# 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()
self.start_btn.config(
state="disabled"
) # Disable transfer button during transfer
# Reset progress bar in thread-safe way
self.progress_handler.reset_progress()
if direction == "pull":
self.progress_handler.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.progress_handler.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()
# 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.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()
# 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 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()
+78
View File
@@ -0,0 +1,78 @@
"""
Progress Handler Module
Handles progress bar updates and status messages in a thread-safe manner.
"""
import tkinter as tk
from tkinter import ttk
class ProgressHandler:
"""Handles progress bar updates and status messages."""
def __init__(
self, parent_widget, progress_bar: ttk.Progressbar, status_label: tk.Label
):
self.parent = parent_widget
self.progress_bar = progress_bar
self.status_label = status_label
def update_progress(self, percentage: int):
"""Update the progress bar (thread-safe)."""
# Debug output for large transfer troubleshooting
print(f"[DEBUG] Progress update called: {percentage}%")
# Schedule UI update on main thread using a proper closure
def update_ui():
self._update_progress_ui(percentage)
self.parent.after(0, update_ui)
def _update_progress_ui(self, percentage: int):
"""Internal method to update progress bar on main thread."""
# Ensure percentage is within valid range
percentage = max(0, min(100, percentage))
# Debug output for large transfer troubleshooting
current_value = self.progress_bar["value"]
print(f"[DEBUG] UI Progress update: {current_value} -> {percentage}%")
if (
hasattr(self, "_last_percentage")
and abs(percentage - self._last_percentage) >= 10
):
print(
f"[DEBUG] Major progress jump: {self._last_percentage}% -> {percentage}%"
)
self._last_percentage = percentage
try:
self.progress_bar["value"] = percentage
self.progress_bar.update() # Force immediate update
self.parent.update_idletasks()
print(f"[DEBUG] Progress bar updated successfully to {percentage}%")
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.parent.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.parent.update_idletasks()
def reset_progress(self):
"""Reset progress bar to 0 (thread-safe)."""
# Schedule UI update on main thread using a proper closure
def update_ui():
self._update_progress_ui(0)
self.parent.after(0, update_ui)
+426
View File
@@ -0,0 +1,426 @@
"""
Windows Android Browser Module
Provides Android filesystem browsing capabilities for Windows using ADB.
"""
import threading
import tkinter as tk
from tkinter import messagebox, ttk, simpledialog
class WindowsAndroidBrowser:
"""Android filesystem browser for Windows using ADB."""
def __init__(self, parent_window, adb_manager, remote_path_var):
self.parent = parent_window
self.adb_manager = adb_manager
self.remote_path_var = remote_path_var
def show_browser(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.parent)
browser_window.title("Browse Android Folders")
browser_window.geometry("500x400")
browser_window.transient(self.parent)
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.parent.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 - improved parsing to prevent truncation
parts = line.split()
if len(parts) >= 8:
# Method 1: Use regex to find time pattern and extract name after it
import re
time_pattern = r"\d{1,2}:\d{2}"
time_matches = list(re.finditer(time_pattern, line))
if time_matches:
# Take everything after the last time pattern
last_time_match = time_matches[-1]
folder_name = line[
last_time_match.end() :
].strip()
else:
# Fallback: join from part 8 (skip date/time fields)
folder_name = (
" ".join(parts[8:])
if len(parts) > 8
else parts[7]
)
# Additional validation and cleanup
folder_name = folder_name.strip()
if (
folder_name
and 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.parent.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.parent.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 - improved parsing to prevent truncation
parts = line.split()
if len(parts) >= 8:
# Method 1: Use regex to find time pattern and extract name after it
import re
time_pattern = r"\d{1,2}:\d{2}"
time_matches = list(re.finditer(time_pattern, line))
if time_matches:
# Take everything after the last time pattern
last_time_match = time_matches[-1]
folder_name = line[last_time_match.end() :].strip()
else:
# Fallback: join from part 8 (skip date/time fields)
folder_name = (
" ".join(parts[8:]) if len(parts) > 8 else parts[7]
)
# Additional validation and cleanup
folder_name = folder_name.strip()
if (
folder_name
and 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
)