diff --git a/license_agreed.ini b/license_agreed.ini new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/license_agreed.ini @@ -0,0 +1 @@ +1 diff --git a/poetry.lock b/poetry.lock index b046bad..37769b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -50,8 +50,6 @@ mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -298,9 +296,6 @@ files = [ {file = "coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -316,25 +311,6 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "filelock" version = "3.19.1" @@ -394,31 +370,6 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["build"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.1.0" @@ -510,7 +461,6 @@ files = [ [package.dependencies] mypy_extensions = ">=1.0.0" pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] @@ -704,7 +654,6 @@ files = [ ] [package.dependencies] -importlib_metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} packaging = ">=22.0" setuptools = ">=42.0.0" @@ -722,11 +671,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -908,49 +855,6 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev", "test"] -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] -markers = {dev = "python_version < \"3.11\"", test = "python_full_version <= \"3.11.0a6\""} - [[package]] name = "typing-extensions" version = "4.14.1" @@ -997,34 +901,12 @@ files = [ distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["build"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [metadata] lock-version = "2.1" -python-versions = "<3.13,>=3.9" -content-hash = "224bda1d49801be6e0e993ae685177d1b6cce307395933b8ff1f25a6b378dab4" +python-versions = "<3.13,>=3.12" +content-hash = "dfb6152cb0fb34dcda59387fbaef4f445b0f38437bd7bdfe25b38b62b14d527e" diff --git a/pyproject.toml b/pyproject.toml index 81eb1ea..e609d06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,11 @@ authors = [ ] license = { text = "MIT" } readme = "README.md" -requires-python = "<3.13,>=3.9" +requires-python = "<3.13,>=3.12" dependencies = ["requests>=2.32.4,<3.0.0"] [tool.poetry.dependencies] -python = "<3.13,>=3.9" +python = "<3.13,>=3.12" requests = ">=2.32.4,<3.0.0" [build-system] @@ -22,11 +22,6 @@ build-backend = "poetry.core.masonry.api" # Main application entry point android-file-handler = "src.main:main" -# Development and maintenance scripts -dev = "python src/main.py" -gui = "python src/gui.py" -test-adb = "python -c 'from src.adb_manager import ADBManager; m = ADBManager(); print(f\"ADB Available: {m.check_device()}\")" -clean-adb = "python -c 'import shutil, os; shutil.rmtree(\"src/platform-tools\", ignore_errors=True); print(\"ADB cache cleared\")'" [tool.poetry.group.dev.dependencies] black = "^25.0.0" diff --git a/src/adb_manager.py b/src/adb_manager.py index 0937989..992f355 100644 --- a/src/adb_manager.py +++ b/src/adb_manager.py @@ -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 + + diff --git a/src/gui/license_agreement.py b/src/gui/license_agreement.py new file mode 100644 index 0000000..ef37c9f --- /dev/null +++ b/src/gui/license_agreement.py @@ -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) diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 58ebf26..afe41ef 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -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) diff --git a/src/license_agreed.ini b/src/license_agreed.ini new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/src/license_agreed.ini @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/test_parsing.py b/test_parsing.py deleted file mode 100644 index 657b108..0000000 --- a/test_parsing.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to debug folder parsing -""" -import sys -import os - -sys.path.insert(0, "src") - -from adb_manager import ADBManager - - -def main(): - adb = ADBManager() - print("Testing folder parsing...") - - result = adb.run_adb_command(["shell", "ls", "-la", "/sdcard/"]) - print(f"Return code: {result[2]}") - - lines = result[0].strip().split("\n") - print(f"Total lines: {len(lines)}") - print("\nFirst 10 lines:") - for i, line in enumerate(lines[:10]): - print(f" {i}: {repr(line)}") - - print("\nParsing directories:") - folders = [] - for i, line in enumerate(lines): - if line.startswith("d"): - parts = line.split() - print(f" Line {i}: {len(parts)} parts") - if len(parts) >= 8: # Changed from 9 to 8 - folder_name = " ".join(parts[7:]) # Changed from 8 to 7 - print(f" Folder name: {repr(folder_name)}") - if folder_name not in [".", ".."]: - folders.append(folder_name) - print(f" ✅ Added: {repr(folder_name)}") - else: - print(f" ❌ Skipped: {repr(folder_name)}") - - print(f"\nFinal folders list ({len(folders)} items):") - for folder in sorted(folders): - print(f" - {repr(folder)}") - - -if __name__ == "__main__": - main()