Action release fix binaries not found #13
@@ -444,20 +444,20 @@ jobs:
|
||||
merge-multiple: true
|
||||
path: ./binaries
|
||||
|
||||
- name: List downloaded binaries (debug)
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
echo "Downloaded files:"
|
||||
ls -la ./binaries || true
|
||||
mkdir -p ./release-files
|
||||
find ./binaries -name "*.exe" -exec cp {} ./release-files/ \; || true
|
||||
find ./binaries -name "*.deb" -exec cp {} ./release-files/ \; || true
|
||||
find ./binaries -name "*.rpm" -exec cp {} ./release-files/ \; || true
|
||||
find ./binaries -name "*.pkg.tar.*" -exec cp {} ./release-files/ \; || true
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
name: Release v${{ steps.version.outputs.version }}
|
||||
files: |
|
||||
./binaries/android-file-handler.exe
|
||||
./binaries/android-file-handler_*.deb
|
||||
./binaries/android-file-handler-*.rpm
|
||||
./binaries/android-file-handler-*-x86_64.pkg.tar.*
|
||||
files: ./release-files/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
@@ -515,7 +515,7 @@ jobs:
|
||||
|
||||
- name: Checkout wiki repo
|
||||
run: |
|
||||
git clone "https://github.com/${GITHUB_REPOSITORY}.wiki.git" wiki
|
||||
git clone "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.wiki.git" wiki
|
||||
|
||||
- name: Sync WIKI.md to Wiki/Home.md
|
||||
run: |
|
||||
@@ -536,7 +536,7 @@ jobs:
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add Home.md
|
||||
git commit -m "Sync WIKI.md from main repo [skip ci]" || echo "No changes to commit"
|
||||
git push
|
||||
git push "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.wiki.git"
|
||||
else
|
||||
echo "No changes in $SRC_FILE; wiki is up to date."
|
||||
fi
|
||||
@@ -0,0 +1,142 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
A Python GUI tool for managing Android device files via ADB (Android Debug Bridge). The application automatically downloads ADB platform-tools if needed, detects connected devices, and provides a Tkinter-based interface for transferring files between Android devices and local systems.
|
||||
|
||||
**Supported Platforms:** Windows, Linux (Debian, Arch, RHEL/Fedora)
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Installation
|
||||
```sh
|
||||
poetry install
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
```sh
|
||||
poetry run python -m src.main
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Testing
|
||||
```sh
|
||||
|
||||
# Test if application runs
|
||||
poetry run python -m src.main
|
||||
|
||||
# Run all tests
|
||||
poetry run pytest tests/ -v
|
||||
|
||||
# Run tests with coverage
|
||||
poetry run pytest tests/ -v --cov
|
||||
|
||||
# Run specific test file
|
||||
poetry run pytest tests/core/test_adb_manager.py -v
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```sh
|
||||
# Format code with Black
|
||||
poetry run black src/ tests/
|
||||
|
||||
# Lint with flake8
|
||||
poetry run flake8 src/ tests/
|
||||
|
||||
# Type checking with mypy
|
||||
poetry run mypy src/
|
||||
```
|
||||
|
||||
### Building and Packaging
|
||||
|
||||
#### Local Development Build (Linux)
|
||||
```sh
|
||||
poetry run python scripts/build_package_linux.py
|
||||
```
|
||||
|
||||
#### Platform-Specific Builds
|
||||
```sh
|
||||
# Windows executable (PyInstaller)
|
||||
poetry run pyinstaller scripts/spec_scripts/android-file-handler-windows.spec
|
||||
|
||||
# Linux packages use distro-specific spec files:
|
||||
# - android-file-handler-debian.spec
|
||||
# - android-file-handler-arch.spec
|
||||
# - android-file-handler-rhel.spec
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
- **src/core/**: Core ADB functionality
|
||||
- `adb_manager.py`: Main ADB interface and operations coordinator
|
||||
- `adb_command.py`: Command execution wrapper
|
||||
- `file_transfer.py`: File transfer logic
|
||||
- `platform_tools.py`: ADB binary management and download
|
||||
- `platform_utils.py`: Platform detection utilities
|
||||
- `progress_tracker.py`: Transfer progress tracking
|
||||
|
||||
- **src/gui/**: GUI components
|
||||
- `main_window.py`: Main application window (entry point for GUI)
|
||||
- `components/`: Reusable UI widgets (file browser, path selectors, etc.)
|
||||
- `handlers/`: Event and animation handlers
|
||||
- `dialogs/`: Dialog windows (license agreement, device instructions, etc.)
|
||||
|
||||
- **src/managers/**: Business logic coordination
|
||||
- `device_manager.py`: Device detection and management
|
||||
- `transfer_manager.py`: Coordinates transfers between GUI and ADB manager
|
||||
|
||||
- **src/utils/**: Utility modules
|
||||
- `file_deduplication.py`: File deduplication logic
|
||||
|
||||
- **scripts/**: Build and packaging scripts
|
||||
- `build_package_linux.py`: Unified Linux packaging script (uses DISTRO_TYPE env var)
|
||||
- `spec_scripts/`: PyInstaller spec files for each platform
|
||||
|
||||
- **tests/**: Test suite mirroring src/ structure
|
||||
|
||||
### Application Flow
|
||||
|
||||
1. **Startup**: `src/main.py` → License check → `gui/main_window.py:main()`
|
||||
2. **ADB Setup**: ADBManager checks for platform-tools, downloads if needed
|
||||
3. **Device Detection**: DeviceManager checks for connected devices
|
||||
4. **File Transfer**: TransferManager coordinates UI updates with ADB file operations
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Import Fallbacks**: Most modules use try/except for relative vs. direct imports to support both module and direct execution
|
||||
- **Manager Pattern**: Business logic separated into DeviceManager, TransferManager, ADBManager
|
||||
- **Component Composition**: GUI built from reusable components in `gui/components/`
|
||||
- **Threading**: File transfers run in background threads; UI updates via callbacks
|
||||
|
||||
## CI/CD
|
||||
|
||||
The project uses GitHub Actions for multi-platform builds (`.github/workflows/release.yml`):
|
||||
- Runs tests on Linux and Windows
|
||||
- Builds binaries for Windows, Debian, Arch, and RHEL
|
||||
- Packages using PyInstaller + fpm
|
||||
- Supports manual workflow dispatch with configurable jobs
|
||||
- Optional GitHub release creation and S3 upload
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Follow PEP 8 guidelines
|
||||
- Use type hints for all function parameters and return values
|
||||
- Write docstrings for all public modules, functions, and classes
|
||||
- Use f-strings for string formatting
|
||||
- No single-letter variable names except `e` for exceptions
|
||||
- Always run Python commands through Poetry
|
||||
- Do not recreate deleted files
|
||||
- Do not change user-facing text unless asked
|
||||
- Always run the application to test if it will run and have it run successfully before declaring an iteration complete
|
||||
|
||||
## Notes
|
||||
|
||||
- **ADB Binaries**: Stored in `src/platform-tools/` - do not modify or delete
|
||||
- **Python Version**: Requires Python 3.12 (< 3.13)
|
||||
- **Package Mode**: Poetry is configured with `package-mode = false`
|
||||
- **License**: First-run license agreement required on Windows
|
||||
+2
-2
@@ -40,10 +40,10 @@ pyinstaller = "^5.13.0"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py313']
|
||||
target-version = ['py312']
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.13"
|
||||
python_version = "3.12"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
+68
-28
@@ -115,7 +115,7 @@ class ADBManager:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def is_device_connected(self, device_id: str = None) -> bool:
|
||||
def is_device_connected(self, device_id: Optional[str] = None) -> bool:
|
||||
"""Check if a specific device is connected."""
|
||||
devices = self.get_devices()
|
||||
if not devices:
|
||||
@@ -135,7 +135,7 @@ class ADBManager:
|
||||
"""Get the currently selected device."""
|
||||
return self.selected_device
|
||||
|
||||
def list_files(self, path: str, device_id: str = None) -> list[dict]:
|
||||
def list_files(self, path: str, device_id: Optional[str] = None) -> list[dict]:
|
||||
"""List files in the specified path on the device."""
|
||||
device_args = []
|
||||
target_device = device_id or self.selected_device
|
||||
@@ -192,9 +192,9 @@ class ADBManager:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def pull_file(self, remote_path: str, local_path: str,
|
||||
def pull_file(self, remote_path: str, local_path: str,
|
||||
progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
device_id: str = None) -> Tuple[bool, str]:
|
||||
device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Pull a file from device to local system."""
|
||||
try:
|
||||
# Create local directory if it doesn't exist
|
||||
@@ -217,7 +217,7 @@ class ADBManager:
|
||||
|
||||
def pull_folder(self, remote_path: str, local_path: str,
|
||||
progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
device_id: str = None) -> Tuple[bool, str]:
|
||||
device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Pull a folder from device to local system."""
|
||||
try:
|
||||
# Create local directory
|
||||
@@ -237,7 +237,7 @@ class ADBManager:
|
||||
|
||||
def push_file(self, local_path: str, remote_path: str,
|
||||
progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
device_id: str = None) -> Tuple[bool, str]:
|
||||
device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Push a file from local system to device."""
|
||||
try:
|
||||
if not os.path.exists(local_path):
|
||||
@@ -254,7 +254,7 @@ class ADBManager:
|
||||
|
||||
def push_folder(self, local_path: str, remote_path: str,
|
||||
progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
device_id: str = None) -> Tuple[bool, str]:
|
||||
device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Push a folder from local system to device."""
|
||||
try:
|
||||
if not os.path.exists(local_path):
|
||||
@@ -269,15 +269,15 @@ class ADBManager:
|
||||
except Exception as e:
|
||||
return False, f"Error pushing folder: {str(e)}"
|
||||
|
||||
def delete_file(self, remote_path: str, device_id: str = None) -> Tuple[bool, str]:
|
||||
def delete_file(self, remote_path: str, device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Delete a file on the device."""
|
||||
device_args = []
|
||||
target_device = device_id or self.selected_device
|
||||
if target_device:
|
||||
device_args = ["-s", target_device]
|
||||
|
||||
|
||||
args = device_args + ["shell", "rm", "-f", remote_path]
|
||||
|
||||
|
||||
try:
|
||||
stdout, stderr, returncode = self.command_runner.run_adb_command(args)
|
||||
if returncode == 0:
|
||||
@@ -286,16 +286,16 @@ class ADBManager:
|
||||
return False, f"Failed to delete file: {stderr}"
|
||||
except Exception as e:
|
||||
return False, f"Error deleting file: {str(e)}"
|
||||
|
||||
def create_folder(self, remote_path: str, device_id: str = None) -> Tuple[bool, str]:
|
||||
|
||||
def create_folder(self, remote_path: str, device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Create a folder on the device."""
|
||||
device_args = []
|
||||
target_device = device_id or self.selected_device
|
||||
if target_device:
|
||||
device_args = ["-s", target_device]
|
||||
|
||||
|
||||
args = device_args + ["shell", "mkdir", "-p", remote_path]
|
||||
|
||||
|
||||
try:
|
||||
stdout, stderr, returncode = self.command_runner.run_adb_command(args)
|
||||
if returncode == 0:
|
||||
@@ -304,16 +304,16 @@ class ADBManager:
|
||||
return False, f"Failed to create folder: {stderr}"
|
||||
except Exception as e:
|
||||
return False, f"Error creating folder: {str(e)}"
|
||||
|
||||
def delete_folder(self, remote_path: str, device_id: str = None) -> Tuple[bool, str]:
|
||||
|
||||
def delete_folder(self, remote_path: str, device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Delete a folder on the device."""
|
||||
device_args = []
|
||||
target_device = device_id or self.selected_device
|
||||
if target_device:
|
||||
device_args = ["-s", target_device]
|
||||
|
||||
|
||||
args = device_args + ["shell", "rm", "-rf", remote_path]
|
||||
|
||||
|
||||
try:
|
||||
stdout, stderr, returncode = self.command_runner.run_adb_command(args)
|
||||
if returncode == 0:
|
||||
@@ -322,16 +322,16 @@ class ADBManager:
|
||||
return False, f"Failed to delete folder: {stderr}"
|
||||
except Exception as e:
|
||||
return False, f"Error deleting folder: {str(e)}"
|
||||
|
||||
def move_item(self, old_path: str, new_path: str, device_id: str = None) -> Tuple[bool, str]:
|
||||
|
||||
def move_item(self, old_path: str, new_path: str, device_id: Optional[str] = None) -> Tuple[bool, str]:
|
||||
"""Move/rename a file or folder on the device."""
|
||||
device_args = []
|
||||
target_device = device_id or self.selected_device
|
||||
if target_device:
|
||||
device_args = ["-s", target_device]
|
||||
|
||||
|
||||
args = device_args + ["shell", "mv", old_path, new_path]
|
||||
|
||||
|
||||
try:
|
||||
stdout, stderr, returncode = self.command_runner.run_adb_command(args)
|
||||
if returncode == 0:
|
||||
@@ -340,8 +340,8 @@ class ADBManager:
|
||||
return False, f"Failed to move item: {stderr}"
|
||||
except Exception as e:
|
||||
return False, f"Error moving item: {str(e)}"
|
||||
|
||||
def get_file_info(self, remote_path: str, device_id: str = None) -> Optional[dict]:
|
||||
|
||||
def get_file_info(self, remote_path: str, device_id: Optional[str] = None) -> Optional[dict]:
|
||||
"""Get information about a file or folder on the device."""
|
||||
device_args = []
|
||||
target_device = device_id or self.selected_device
|
||||
@@ -390,19 +390,59 @@ class ADBManager:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def pull_folder_with_dedup(self, remote_path: str, local_path: str,
|
||||
progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
device_id: Optional[str] = None) -> Tuple[bool, Optional[dict]]:
|
||||
"""Pull a folder from device with deduplication support.
|
||||
|
||||
Args:
|
||||
remote_path: Remote folder path on device
|
||||
local_path: Local destination path
|
||||
progress_callback: Optional progress callback
|
||||
device_id: Optional specific device ID
|
||||
|
||||
Returns:
|
||||
Tuple of (success, stats_dict) where stats contains transfer information
|
||||
"""
|
||||
# For now, just call the regular pull_folder
|
||||
# TODO: Implement actual deduplication logic
|
||||
success, message = self.pull_folder(remote_path, local_path, progress_callback, device_id)
|
||||
stats = {'message': message} if success else None
|
||||
return success, stats
|
||||
|
||||
def push_folder_with_dedup(self, local_path: str, remote_path: str,
|
||||
progress_callback: Optional[Callable[[int, int], None]] = None,
|
||||
device_id: Optional[str] = None) -> Tuple[bool, Optional[dict]]:
|
||||
"""Push a folder to device with deduplication support.
|
||||
|
||||
Args:
|
||||
local_path: Local folder path
|
||||
remote_path: Remote destination path on device
|
||||
progress_callback: Optional progress callback
|
||||
device_id: Optional specific device ID
|
||||
|
||||
Returns:
|
||||
Tuple of (success, stats_dict) where stats contains transfer information
|
||||
"""
|
||||
# For now, just call the regular push_folder
|
||||
# TODO: Implement actual deduplication logic
|
||||
success, message = self.push_folder(local_path, remote_path, progress_callback, device_id)
|
||||
stats = {'message': message} if success else None
|
||||
return success, stats
|
||||
|
||||
def deduplicate_files(self, folder_path: str, progress_callback: Optional[Callable[[str], None]] = None) -> Tuple[int, list]:
|
||||
"""Find and optionally remove duplicate files in a folder."""
|
||||
deduplicator = FileDeduplicator()
|
||||
|
||||
|
||||
if progress_callback:
|
||||
deduplicator.set_progress_callback(progress_callback)
|
||||
|
||||
|
||||
duplicates = deduplicator.find_duplicates(folder_path)
|
||||
|
||||
|
||||
if duplicates:
|
||||
removed_count = deduplicator.remove_duplicates(duplicates)
|
||||
return removed_count, duplicates
|
||||
|
||||
|
||||
return 0, []
|
||||
|
||||
# --- Legacy/compatibility helpers expected by older tests ---
|
||||
|
||||
@@ -9,8 +9,8 @@ from typing import Optional, Callable, Dict, Any
|
||||
|
||||
class ProgressTracker:
|
||||
"""Tracks progress for file transfer operations."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.progress_callback: Optional[Callable[[int], None]] = None
|
||||
self.status_callback: Optional[Callable[[str], None]] = None
|
||||
|
||||
@@ -26,12 +26,12 @@ class ProgressTracker:
|
||||
'total_files': 0,
|
||||
'files_to_transfer': 0
|
||||
}
|
||||
|
||||
def set_progress_callback(self, callback: Callable[[int], None]):
|
||||
|
||||
def set_progress_callback(self, callback: Callable[[int], None]) -> None:
|
||||
"""Set callback function for progress updates."""
|
||||
self.progress_callback = callback
|
||||
|
||||
def set_status_callback(self, callback: Callable[[str], None]):
|
||||
def set_status_callback(self, callback: Callable[[str], None]) -> None:
|
||||
"""Set callback function for status updates."""
|
||||
self.status_callback = callback
|
||||
|
||||
@@ -77,12 +77,12 @@ class ProgressTracker:
|
||||
if self.progress_callback:
|
||||
self.progress_callback(int(value))
|
||||
|
||||
def update_status(self, message: str):
|
||||
def update_status(self, message: str) -> None:
|
||||
"""Update status message."""
|
||||
if self.status_callback:
|
||||
self.status_callback(message)
|
||||
|
||||
def update_transfer_progress(self, current_file: int, total_files: int):
|
||||
def update_transfer_progress(self, current_file: int, total_files: int) -> None:
|
||||
"""Update transfer progress for file counting."""
|
||||
self.transfer_progress['current_file'] = current_file
|
||||
self.transfer_progress['total_files'] = total_files
|
||||
@@ -91,15 +91,15 @@ class ProgressTracker:
|
||||
if self.status_callback:
|
||||
self.status_callback(progress_message)
|
||||
|
||||
def reset_transfer_progress(self):
|
||||
def reset_transfer_progress(self) -> None:
|
||||
"""Reset transfer progress counters."""
|
||||
self.transfer_progress = {
|
||||
'current_file': 0,
|
||||
'total_files': 0,
|
||||
'files_to_transfer': 0
|
||||
}
|
||||
|
||||
def set_files_to_transfer(self, count: int):
|
||||
|
||||
def set_files_to_transfer(self, count: int) -> None:
|
||||
"""Set the total number of files to transfer."""
|
||||
self.transfer_progress['files_to_transfer'] = count
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ except ImportError:
|
||||
from core.adb_manager import ADBManager, is_adb_available
|
||||
|
||||
try:
|
||||
# Try relative imports first
|
||||
# Try relative imports first (when used as module)
|
||||
from .components.file_browser import AndroidFileBrowser
|
||||
from .handlers.animation_handler import AnimationHandler
|
||||
from .dialogs.dialog_manager import DialogManager
|
||||
@@ -27,11 +27,11 @@ try:
|
||||
from ..managers.device_manager import DeviceManager
|
||||
from ..managers.transfer_manager import TransferManager
|
||||
except ImportError:
|
||||
# Fall back to direct imports
|
||||
from components.file_browser import AndroidFileBrowser
|
||||
from handlers.animation_handler import AnimationHandler
|
||||
from dialogs.dialog_manager import DialogManager
|
||||
from components.ui_components import (
|
||||
# Fall back to absolute imports from src package
|
||||
from gui.components.file_browser import AndroidFileBrowser
|
||||
from gui.handlers.animation_handler import AnimationHandler
|
||||
from gui.dialogs.dialog_manager import DialogManager
|
||||
from gui.components.ui_components import (
|
||||
PathSelectorFrame, DirectionSelector, StatusLabel,
|
||||
TransferButton, LicenseManager
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ try:
|
||||
from ..gui.handlers.animation_handler import AnimationHandler
|
||||
from ..gui.dialogs.dialog_manager import DialogManager
|
||||
except ImportError:
|
||||
from device_manager import DeviceManager
|
||||
from managers.device_manager import DeviceManager
|
||||
from gui.handlers.animation_handler import AnimationHandler
|
||||
from gui.dialogs.dialog_manager import DialogManager
|
||||
|
||||
@@ -67,56 +67,48 @@ class TransferManager:
|
||||
"""
|
||||
self.controls_callback = callback
|
||||
|
||||
def start_transfer(self, direction: str, source_path: str, dest_path: str,
|
||||
def start_transfer(self, direction: str, source_path: str, dest_path: str,
|
||||
completion_callback: Optional[Callable] = None) -> bool:
|
||||
"""Start a file transfer operation.
|
||||
|
||||
|
||||
Args:
|
||||
direction: Transfer direction ('pull' or 'push')
|
||||
source_path: Source file or folder path
|
||||
dest_path: Destination path
|
||||
completion_callback: Callback to call when transfer completes
|
||||
|
||||
|
||||
Returns:
|
||||
True if transfer was started successfully, False otherwise
|
||||
"""
|
||||
# Increment transfer ID for cancellation support
|
||||
self.current_transfer_id += 1
|
||||
transfer_id = self.current_transfer_id
|
||||
|
||||
|
||||
# Determine if transferring a file or folder
|
||||
if direction == "pull":
|
||||
is_file = self._is_remote_file(source_path)
|
||||
else:
|
||||
is_file = os.path.isfile(source_path)
|
||||
|
||||
|
||||
# Disable controls during transfer
|
||||
if 'disable_controls' in self.ui_callbacks:
|
||||
self.ui_callbacks['disable_controls']()
|
||||
|
||||
|
||||
# Start transfer in background thread
|
||||
transfer_thread = threading.Thread(
|
||||
target=self._transfer_thread,
|
||||
args=(direction, source_path, dest_path, transfer_id, is_file, completion_callback),
|
||||
args=(direction, source_path, dest_path, transfer_id, is_file),
|
||||
daemon=True
|
||||
)
|
||||
transfer_thread.start()
|
||||
return True
|
||||
|
||||
def cancel_transfer(self) -> None:
|
||||
"""Cancel the current transfer operation."""
|
||||
# Increment transfer ID to invalidate current transfer
|
||||
self.current_transfer_id += 1
|
||||
|
||||
# Cancel ADB operation
|
||||
self.device_manager.cancel_current_operation()
|
||||
|
||||
def _is_remote_file(self, remote_path: str) -> bool:
|
||||
"""Check if a remote path is a file.
|
||||
|
||||
|
||||
Args:
|
||||
remote_path: Path on Android device
|
||||
|
||||
|
||||
Returns:
|
||||
True if path is a file, False if it's a folder
|
||||
"""
|
||||
@@ -126,53 +118,7 @@ class TransferManager:
|
||||
except Exception:
|
||||
# If we can't determine, assume it's a folder for safety
|
||||
return False
|
||||
|
||||
def _transfer_thread(self, direction: str, source_path: str, dest_path: str,
|
||||
transfer_id: int, is_file: bool, completion_callback: Optional[Callable]):
|
||||
"""Handle file transfer in background thread.
|
||||
|
||||
Args:
|
||||
direction: Transfer direction ('pull' or 'push')
|
||||
source_path: Source file or folder path
|
||||
dest_path: Destination path
|
||||
transfer_id: Transfer ID for cancellation
|
||||
is_file: True if transferring a file
|
||||
completion_callback: Callback for completion
|
||||
"""
|
||||
try:
|
||||
# Check if transfer is still valid
|
||||
if transfer_id != self.current_transfer_id:
|
||||
return
|
||||
|
||||
# Get ADB manager from device manager
|
||||
adb_manager = self.device_manager.adb_manager
|
||||
|
||||
# Perform the transfer
|
||||
if direction == "pull":
|
||||
if is_file:
|
||||
success, stats = adb_manager.pull_file(source_path, dest_path)
|
||||
else:
|
||||
success, stats = adb_manager.pull_folder(source_path, dest_path)
|
||||
operation = "pulled from Android device"
|
||||
else: # push
|
||||
if is_file:
|
||||
success, stats = adb_manager.push_file(source_path, dest_path)
|
||||
else:
|
||||
success, stats = adb_manager.push_folder(source_path, dest_path)
|
||||
operation = "pushed to Android device"
|
||||
|
||||
# Check if transfer was cancelled
|
||||
if transfer_id != self.current_transfer_id:
|
||||
return
|
||||
|
||||
# Call completion callback on main thread
|
||||
if completion_callback:
|
||||
self.parent.after(0, lambda: completion_callback(success, stats, operation))
|
||||
|
||||
except Exception as e:
|
||||
# Handle errors on main thread
|
||||
if transfer_id == self.current_transfer_id and 'show_error' in self.ui_callbacks:
|
||||
self.parent.after(0, lambda: self.ui_callbacks["show_error"](f"Transfer error: {str(e)}"))
|
||||
|
||||
def cancel_transfer(self) -> bool:
|
||||
"""Cancel the current transfer operation.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user