* Elected to go with simple text animation progress symbol for the moment

* Unified around one folder browser approach
* Added cancellation functionality
This commit is contained in:
2025-08-25 11:18:48 -05:00
committed by Jason Ross
parent 1bd77ca375
commit 80efa57153
9 changed files with 265 additions and 254 deletions
+412
View File
@@ -0,0 +1,412 @@
"""
Windows Android Browser Module
Provides Android filesystem browsing capabilities for Windows using ADB.
"""
import threading
import tkinter as tk
from tkinter import messagebox, ttk
class AndroidFileBrowser:
"""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.")
tk.Button(
button_frame, text="Select This Folder", command=select_current_folder
).pack(side="left", padx=5)
tk.Button(button_frame, text="Cancel", command=browser_window.destroy).pack(
side="left", padx=5
)