""" 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("<>", on_tree_expand) tree.bind("<>", 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 )