Updated user instructions and added license agreement
This commit is contained in:
+54
-4
@@ -15,6 +15,52 @@ import time
|
||||
import re
|
||||
from typing import Optional, Tuple, Callable
|
||||
|
||||
def get_executable_directory() -> str:
|
||||
"""Get the directory containing the executable or script."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as executable (PyInstaller, cx_Freeze, etc.)
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# Running as script - use the script's directory
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def get_platform_tools_directory() -> str:
|
||||
"""Get platform-tools directory with executable and AppImage support."""
|
||||
base_dir = get_executable_directory()
|
||||
|
||||
# For AppImages running from temporary mounts, use persistent storage
|
||||
if sys.platform.startswith('linux') and ('tmp' in base_dir and '.mount_' in base_dir):
|
||||
# AppImage temporary mount - use persistent user directory
|
||||
home_dir = os.path.expanduser("~")
|
||||
persistent_dir = os.path.join(home_dir, ".android-file-handler", "platform-tools")
|
||||
os.makedirs(os.path.dirname(persistent_dir), exist_ok=True)
|
||||
return persistent_dir
|
||||
|
||||
# Check if we're in development mode (running from src/ directory)
|
||||
if not getattr(sys, 'frozen', False):
|
||||
# Running as script - check if we're in src/ directory or subdirectory
|
||||
if base_dir.endswith('src'):
|
||||
# Already in src directory - place platform-tools here
|
||||
return os.path.join(base_dir, "platform-tools")
|
||||
elif base_dir.endswith('gui') or os.path.basename(base_dir) in ['gui']:
|
||||
# In src/gui subdirectory - go up one level to src
|
||||
src_dir = os.path.dirname(base_dir)
|
||||
return os.path.join(src_dir, "platform-tools")
|
||||
else:
|
||||
# Not in src structure - assume we need to find/create src directory
|
||||
# This handles cases where the script might be run from project root
|
||||
current_dir = base_dir
|
||||
src_dir = os.path.join(current_dir, "src")
|
||||
if os.path.exists(src_dir):
|
||||
return os.path.join(src_dir, "platform-tools")
|
||||
else:
|
||||
# Fallback to current directory
|
||||
return os.path.join(base_dir, "src", "platform-tools")
|
||||
|
||||
# Running as executable - use directory next to binary
|
||||
return os.path.join(base_dir, "platform-tools")
|
||||
|
||||
|
||||
# Constants
|
||||
ADB_WIN_ZIP_URL = (
|
||||
"https://dl.google.com/android/repository/platform-tools-latest-windows.zip"
|
||||
@@ -22,9 +68,7 @@ ADB_WIN_ZIP_URL = (
|
||||
ADB_LINUX_ZIP_URL = (
|
||||
"https://dl.google.com/android/repository/platform-tools-latest-linux.zip"
|
||||
)
|
||||
LOCAL_ADB_FOLDER = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "platform-tools"
|
||||
)
|
||||
LOCAL_ADB_FOLDER = get_platform_tools_directory()
|
||||
OS_TYPE = sys.platform
|
||||
|
||||
# Determine ADB binary name based on platform
|
||||
@@ -100,7 +144,11 @@ class ADBManager:
|
||||
response.raise_for_status()
|
||||
|
||||
binary_archive = zipfile.ZipFile(io.BytesIO(response.content))
|
||||
binary_archive.extractall(LOCAL_ADB_FOLDER)
|
||||
|
||||
# Extract to parent directory since the zip contains platform-tools/ folder
|
||||
extract_to = os.path.dirname(LOCAL_ADB_FOLDER)
|
||||
os.makedirs(extract_to, exist_ok=True)
|
||||
binary_archive.extractall(extract_to)
|
||||
|
||||
if OS_TYPE.startswith("linux"):
|
||||
if os.path.exists(ADB_BINARY_PATH):
|
||||
@@ -781,3 +829,5 @@ def is_adb_available() -> bool:
|
||||
def get_platform_type() -> str:
|
||||
"""Get the current platform type."""
|
||||
return OS_TYPE
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
License Agreement Module
|
||||
Handles MIT license agreement functionality.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, scrolledtext
|
||||
|
||||
|
||||
def get_license_file_path():
|
||||
"""Get the path to the license agreement file."""
|
||||
# Get the directory where the application is running from
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running as executable
|
||||
app_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# Running as script - use directory containing the main script
|
||||
app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
return os.path.join(app_dir, "license_agreed.ini")
|
||||
|
||||
|
||||
def check_license_agreement():
|
||||
"""Check if user has already agreed to the license."""
|
||||
license_file = get_license_file_path()
|
||||
try:
|
||||
if os.path.exists(license_file):
|
||||
with open(license_file, 'r') as f:
|
||||
content = f.read().strip()
|
||||
return content == "1"
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def save_license_agreement():
|
||||
"""Save that the user has agreed to the license."""
|
||||
license_file = get_license_file_path()
|
||||
try:
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(license_file), exist_ok=True)
|
||||
with open(license_file, 'w') as f:
|
||||
f.write("1")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_mit_license_text():
|
||||
"""Get the MIT license text."""
|
||||
return """MIT License
|
||||
|
||||
Copyright (c) 2025 Jason Ross
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE."""
|
||||
|
||||
|
||||
class LicenseAgreementFrame(tk.Frame):
|
||||
"""License agreement UI frame that can be embedded in the main window."""
|
||||
|
||||
def __init__(self, parent, on_agree_callback):
|
||||
"""Initialize the license agreement frame.
|
||||
|
||||
Args:
|
||||
parent: Parent widget
|
||||
on_agree_callback: Function to call when user agrees to license
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.on_agree_callback = on_agree_callback
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the license agreement UI."""
|
||||
# Configure the frame to fill the window
|
||||
self.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||||
|
||||
# Header
|
||||
header_label = tk.Label(
|
||||
self,
|
||||
text="License Agreement",
|
||||
font=("Arial", 16, "bold")
|
||||
)
|
||||
header_label.pack(pady=(0, 10))
|
||||
|
||||
# Instruction
|
||||
instruction_label = tk.Label(
|
||||
self,
|
||||
text="Please read and accept the license agreement to continue using Android File Handler:",
|
||||
font=("Arial", 10),
|
||||
wraplength=500
|
||||
)
|
||||
instruction_label.pack(pady=(0, 15))
|
||||
|
||||
# License text area
|
||||
text_frame = tk.Frame(self)
|
||||
text_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
|
||||
|
||||
self.license_text = scrolledtext.ScrolledText(
|
||||
text_frame,
|
||||
wrap=tk.WORD,
|
||||
width=80,
|
||||
height=20,
|
||||
font=("Courier", 9),
|
||||
state=tk.DISABLED,
|
||||
bg="#f8f8f8",
|
||||
relief=tk.SUNKEN,
|
||||
bd=2
|
||||
)
|
||||
self.license_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Insert license text
|
||||
self.license_text.config(state=tk.NORMAL)
|
||||
self.license_text.insert(tk.END, get_mit_license_text())
|
||||
self.license_text.config(state=tk.DISABLED)
|
||||
|
||||
# Button frame
|
||||
button_frame = tk.Frame(self)
|
||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
|
||||
# Buttons
|
||||
self.disagree_btn = tk.Button(
|
||||
button_frame,
|
||||
text="Disagree & Exit",
|
||||
command=self.on_disagree,
|
||||
bg="#ff6b6b",
|
||||
fg="white",
|
||||
font=("Arial", 11, "bold"),
|
||||
width=15,
|
||||
height=2
|
||||
)
|
||||
self.disagree_btn.pack(side=tk.LEFT)
|
||||
|
||||
self.agree_btn = tk.Button(
|
||||
button_frame,
|
||||
text="I Agree",
|
||||
command=self.on_agree,
|
||||
bg="#4CAF50",
|
||||
fg="white",
|
||||
font=("Arial", 11, "bold"),
|
||||
width=15,
|
||||
height=2
|
||||
)
|
||||
self.agree_btn.pack(side=tk.RIGHT)
|
||||
|
||||
# Center text
|
||||
center_label = tk.Label(
|
||||
button_frame,
|
||||
text="You must agree to the license terms to use this software",
|
||||
font=("Arial", 9),
|
||||
fg="#666666"
|
||||
)
|
||||
center_label.pack(expand=True)
|
||||
|
||||
def on_agree(self):
|
||||
"""Handle user clicking Agree."""
|
||||
if save_license_agreement():
|
||||
self.on_agree_callback()
|
||||
else:
|
||||
messagebox.showerror(
|
||||
"Error",
|
||||
"Could not save license agreement. Please check file permissions and try again."
|
||||
)
|
||||
|
||||
def on_disagree(self):
|
||||
"""Handle user clicking Disagree."""
|
||||
# Ask for confirmation
|
||||
result = messagebox.askyesno(
|
||||
"Exit Application",
|
||||
"Are you sure you want to exit? You must agree to the license terms to use this software.",
|
||||
icon="warning"
|
||||
)
|
||||
if result:
|
||||
sys.exit(0)
|
||||
+117
-25
@@ -8,7 +8,7 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, filedialog, ttk
|
||||
from tkinter import messagebox, filedialog, ttk, scrolledtext
|
||||
|
||||
try:
|
||||
# Try relative import first (when used as module)
|
||||
@@ -26,52 +26,122 @@ except ImportError:
|
||||
try:
|
||||
# Try relative imports first
|
||||
from .file_browser import AndroidFileBrowser
|
||||
from .license_agreement import LicenseAgreementFrame, check_license_agreement
|
||||
except ImportError:
|
||||
# Fall back to direct imports
|
||||
from src.gui.file_browser import AndroidFileBrowser
|
||||
from src.gui.license_agreement import LicenseAgreementFrame, check_license_agreement
|
||||
|
||||
|
||||
class AndroidFileHandlerGUI(tk.Tk):
|
||||
"""Main GUI application for Android file transfers."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the main GUI application."""
|
||||
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.device_connected = False
|
||||
|
||||
# License agreement tracking
|
||||
self.license_agreed = check_license_agreement()
|
||||
self.license_frame = None
|
||||
self.main_ui_created = False
|
||||
|
||||
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"
|
||||
"Please ensure your Android device is securely connected 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."
|
||||
"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"
|
||||
" (You only need to do this once unless you disable it, reset settings, or wipe your device)\n"
|
||||
"4. Navigate back and go to System → Developer Options\n"
|
||||
"5. Find and enable 'USB debugging'\n"
|
||||
" (Tip: Use the search icon at the top if you can't find it)\n"
|
||||
"6. Connect via USB and tap 'Trust' when prompted\n"
|
||||
" (Checking 'Remember' is recommended for future transfers)\n\n"
|
||||
|
||||
"Ensure File Transfer mode is enabled:\n"
|
||||
"1. After connecting, swipe down to view notifications\n"
|
||||
"2. Look for a USB notification (often shows 'Charging over USB')\n"
|
||||
"3. Tap the notification and select 'File Transfer' or 'MTP' mode\n\n"
|
||||
|
||||
"Note: Menu names may vary by Android version:\n"
|
||||
"• Some devices show 'Developer options' under 'System'\n"
|
||||
"• Others may have it directly in the main Settings menu\n"
|
||||
"• Samsung devices might show 'Software information' instead of 'About phone'\n\n"
|
||||
|
||||
"If you're still having trouble:\n"
|
||||
"• Try a different USB cable or port (some cables only support charging)\n"
|
||||
"• Restart both your phone and computer\n"
|
||||
"• Make sure your phone screen is unlocked when connecting\n"
|
||||
"• Set your phone screen timeout to 30 minutes (especially for long transfers)\n\n"
|
||||
"• Use a different computer to test if the issue is computer-specific\n"
|
||||
|
||||
"Windows users: If you see a driver installation popup, please allow it to complete.\n"
|
||||
"Linux users: You may need to run 'sudo usermod -a -G plugdev $USER' and reboot.\n"
|
||||
|
||||
"After completing these steps, click 'Recheck for connected Android device' to try again."
|
||||
)
|
||||
# Setup UI
|
||||
self._setup_ui()
|
||||
self._initialize_components()
|
||||
self._initialize_app()
|
||||
|
||||
# Setup UI based on license status
|
||||
self._setup_initial_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the user interface."""
|
||||
def _setup_initial_ui(self):
|
||||
"""Setup the initial UI - either license agreement or main interface."""
|
||||
# Window configuration
|
||||
self.title("Android File Handler")
|
||||
self.geometry("520x320")
|
||||
self.minsize(520, 320)
|
||||
self.resizable(True, True)
|
||||
|
||||
if not self.license_agreed:
|
||||
# Show license agreement first
|
||||
self._show_license_agreement()
|
||||
else:
|
||||
# Show main interface directly
|
||||
self._show_main_interface()
|
||||
|
||||
def _show_license_agreement(self):
|
||||
"""Show the license agreement interface."""
|
||||
# Adjust window size for license agreement
|
||||
self.geometry("700x600")
|
||||
self.minsize(700, 600)
|
||||
|
||||
# Create license agreement frame
|
||||
self.license_frame = LicenseAgreementFrame(self, self._on_license_agreed)
|
||||
|
||||
def _on_license_agreed(self):
|
||||
"""Handle when user agrees to license."""
|
||||
self.license_agreed = True
|
||||
|
||||
# Remove license frame
|
||||
if self.license_frame:
|
||||
self.license_frame.destroy()
|
||||
self.license_frame = None
|
||||
|
||||
# Switch to main interface
|
||||
self._show_main_interface()
|
||||
|
||||
def _show_main_interface(self):
|
||||
"""Show the main application interface."""
|
||||
# Reset window size for main interface
|
||||
self.geometry("520x320")
|
||||
self.minsize(520, 320)
|
||||
|
||||
# Setup main UI
|
||||
self._setup_main_ui()
|
||||
self._initialize_components()
|
||||
self._initialize_app()
|
||||
|
||||
def _setup_main_ui(self):
|
||||
"""Setup the main user interface."""
|
||||
# Direction selection
|
||||
self.direction_var = tk.StringVar(value="pull")
|
||||
direction_frame = tk.Frame(self)
|
||||
@@ -265,16 +335,27 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
"""Initialize the application - check ADB and device."""
|
||||
# Check adb availability
|
||||
if not is_adb_available():
|
||||
# Show welcome message when platform-tools need to be downloaded
|
||||
messagebox.showinfo(
|
||||
"Welcome to Android File Transfer!",
|
||||
"Welcome to Android File Transfer! This application simplifies "
|
||||
"and speeds up file transfers over USB between computers and Android devices. "
|
||||
"Please do not delete or move the platform-tools folder that will be "
|
||||
"downloaded. These are tools written by Google "
|
||||
"and they are required for this application to function properly."
|
||||
)
|
||||
|
||||
self.disable_controls()
|
||||
self._update_status("ADB not found locally. Downloading...")
|
||||
self.update()
|
||||
success = self.adb_manager.download_and_extract_adb()
|
||||
self.adb_manager.download_and_extract_adb()
|
||||
success = is_adb_available()
|
||||
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."
|
||||
"Failed to download/access Android Debug Bridge tools. Please check your internet and for any blocking security pop-ups and restart."
|
||||
)
|
||||
messagebox.showerror("Error", "Failed to download ADB tools. Exiting.")
|
||||
self.quit()
|
||||
@@ -315,6 +396,17 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
"""Browse for local folder or file based on direction."""
|
||||
import tkinter.filedialog as fd
|
||||
import os
|
||||
from tkinter import messagebox
|
||||
|
||||
# Show helpful notification about folder selection behavior
|
||||
messagebox.showinfo(
|
||||
"File/Folder Selection Notice",
|
||||
"In this early stage of this program, if you are choosing a folder for transfer, "
|
||||
"the folder you are actually in will be the one that is chosen for transfer, "
|
||||
"not the one that you have highlighted/selected. File selection works "
|
||||
"as normal - click on it and it will be chosen for transfer. Only one folder or "
|
||||
"file may be moved at a time."
|
||||
)
|
||||
|
||||
direction = self.direction_var.get()
|
||||
|
||||
@@ -693,7 +785,7 @@ class AndroidFileHandlerGUI(tk.Tk):
|
||||
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._update_status("Transfer completed successfully. To start another transfer, please select another file or folder.")
|
||||
self.show_disable_debugging_reminder()
|
||||
# Clear paths and disable button for next transfer
|
||||
self.after(0, self._clear_paths_and_disable_button)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
Reference in New Issue
Block a user