diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 7df4d792c..d59a92417 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -27,6 +27,39 @@ jobs: ndk-version: r28c link-to-sdk: true local-cache: true + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + file_operations/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Setup Rust for Android + run: | + cd file_operations + chmod +x setup_rust_android.sh + ./setup_rust_android.sh + - name: Verify Rust setup + run: | + cd file_operations + echo "🔍 Verifying Rust Android targets..." + rustup target list --installed | grep android || echo "No Android targets found" + echo "🔍 Verifying cargo configuration..." + if [ -f .cargo/config.toml ]; then + echo "✅ .cargo/config.toml exists" + else + echo "❌ .cargo/config.toml missing" + exit 1 + fi - name: Check formatting using spotless uses: gradle/actions/setup-gradle@v3 with: @@ -42,6 +75,39 @@ jobs: with: distribution: "temurin" java-version: 17 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + file_operations/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Setup Rust for Android + run: | + cd file_operations + chmod +x setup_rust_android.sh + ./setup_rust_android.sh + - name: Verify Rust setup + run: | + cd file_operations + echo "🔍 Verifying Rust Android targets..." + rustup target list --installed | grep android || echo "No Android targets found" + echo "🔍 Verifying cargo configuration..." + if [ -f .cargo/config.toml ]; then + echo "✅ .cargo/config.toml exists" + else + echo "❌ .cargo/config.toml missing" + exit 1 + fi - name: Build with Gradle uses: gradle/actions/setup-gradle@v3 with: diff --git a/.github/workflows/android-debug-artifact-ondemand.yml b/.github/workflows/android-debug-artifact-ondemand.yml index 39eefaa4a..4dd72ba16 100644 --- a/.github/workflows/android-debug-artifact-ondemand.yml +++ b/.github/workflows/android-debug-artifact-ondemand.yml @@ -46,6 +46,39 @@ jobs: ndk-version: r28c link-to-sdk: true local-cache: true + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + file_operations/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Setup Rust for Android + run: | + cd file_operations + chmod +x setup_rust_android.sh + ./setup_rust_android.sh + - name: Verify Rust setup + run: | + cd file_operations + echo "🔍 Verifying Rust Android targets..." + rustup target list --installed | grep android || echo "No Android targets found" + echo "🔍 Verifying cargo configuration..." + if [ -f .cargo/config.toml ]; then + echo "✅ .cargo/config.toml exists" + else + echo "❌ .cargo/config.toml missing" + exit 1 + fi - name: Build with Gradle uses: gradle/actions/setup-gradle@v3 with: diff --git a/.github/workflows/android-debug-artifact-release.yml b/.github/workflows/android-debug-artifact-release.yml index d4052fdd1..e837df793 100644 --- a/.github/workflows/android-debug-artifact-release.yml +++ b/.github/workflows/android-debug-artifact-release.yml @@ -20,6 +20,39 @@ jobs: ndk-version: r28c link-to-sdk: true local-cache: true + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + file_operations/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Setup Rust for Android + run: | + cd file_operations + chmod +x setup_rust_android.sh + ./setup_rust_android.sh + - name: Verify Rust setup + run: | + cd file_operations + echo "🔍 Verifying Rust Android targets..." + rustup target list --installed | grep android || echo "No Android targets found" + echo "🔍 Verifying cargo configuration..." + if [ -f .cargo/config.toml ]; then + echo "✅ .cargo/config.toml exists" + else + echo "❌ .cargo/config.toml missing" + exit 1 + fi - name: Build with Gradle uses: gradle/actions/setup-gradle@v3 with: diff --git a/.github/workflows/android-feature.yml b/.github/workflows/android-feature.yml old mode 100644 new mode 100755 index abb78f30f..23aca0ec1 --- a/.github/workflows/android-feature.yml +++ b/.github/workflows/android-feature.yml @@ -23,6 +23,7 @@ jobs: distribution: "temurin" java-version: 17 - name: Set up NDK + id: setup-ndk uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 with: ndk-version: r28c @@ -32,6 +33,8 @@ jobs: uses: gradle/actions/setup-gradle@v3 with: arguments: spotlessCheck --stacktrace + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} build: name: Build debug and run Jacoco tests @@ -43,6 +46,39 @@ jobs: with: distribution: "temurin" java-version: 17 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + file_operations/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Setup Rust for Android + run: | + cd file_operations + chmod +x setup_rust_android.sh + ./setup_rust_android.sh + - name: Verify Rust setup + run: | + cd file_operations + echo "🔍 Verifying Rust Android targets..." + rustup target list --installed | grep android || echo "No Android targets found" + echo "🔍 Verifying cargo configuration..." + if [ -f .cargo/config.toml ]; then + echo "✅ .cargo/config.toml exists" + else + echo "❌ .cargo/config.toml missing" + exit 1 + fi - name: Build with Gradle uses: gradle/actions/setup-gradle@v3 with: diff --git a/.github/workflows/android-main.yml b/.github/workflows/android-main.yml index 18f7e5531..ea0959f7a 100644 --- a/.github/workflows/android-main.yml +++ b/.github/workflows/android-main.yml @@ -45,6 +45,39 @@ jobs: with: distribution: "temurin" java-version: 17 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + file_operations/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Setup Rust for Android + run: | + cd file_operations + chmod +x setup_rust_android.sh + ./setup_rust_android.sh + - name: Verify Rust setup + run: | + cd file_operations + echo "🔍 Verifying Rust Android targets..." + rustup target list --installed | grep android || echo "No Android targets found" + echo "🔍 Verifying cargo configuration..." + if [ -f .cargo/config.toml ]; then + echo "✅ .cargo/config.toml exists" + else + echo "❌ .cargo/config.toml missing" + exit 1 + fi - name: Build with Gradle uses: gradle/actions/setup-gradle@v3 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce87f0623..180c4af72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,7 @@ If we feel your PR is a significant help to us, we'll award you a bounty with an - Please follow [Android/JAVA code style](https://source.android.com/docs/setup/contribute/code-style) for writing any code. Please see [this](https://github.com/TeamAmaze/AmazeFileManager/issues/986) issue too. - Also, follow [Android Material Design guidelines](https://m3.material.io/get-started) in case you make changes to any UI element. + - The `file_operations` module contains native code in Rust. Please see [file_operations/src/main/rust/README.md](file_operations/src/main/rust/README.md) for more details on how to work with it. ## PS: please make sure to diff --git a/build.gradle b/build.gradle index 81bda2e8f..379e3c33b 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { classpath libs.kotlin.gradle.plugin classpath libs.easylauncher classpath libs.jacoco.android.plugin + classpath libs.rust.android.gradle // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/file_operations/.cargo/config.toml.template b/file_operations/.cargo/config.toml.template new file mode 100644 index 000000000..306ec4642 --- /dev/null +++ b/file_operations/.cargo/config.toml.template @@ -0,0 +1,24 @@ +# Cargo configuration for Android cross-compilation +# This file configures the linkers for different Android architectures +# Generated from config.toml.template by setup_rust_android.sh + +[target.aarch64-linux-android] +linker = "{{NDK_PATH}}/aarch64-linux-android{{API_LEVEL}}-clang{{EXE_SUFFIX}}" + +[target.armv7-linux-androideabi] +linker = "{{NDK_PATH}}/armv7a-linux-androideabi{{API_LEVEL}}-clang{{EXE_SUFFIX}}" + +[target.i686-linux-android] +linker = "{{NDK_PATH}}/i686-linux-android{{API_LEVEL}}-clang{{EXE_SUFFIX}}" + +[target.x86_64-linux-android] +linker = "{{NDK_PATH}}/x86_64-linux-android{{API_LEVEL}}-clang{{EXE_SUFFIX}}" + +# Note: This file is auto-generated. Do not edit manually. +# To regenerate, run: ./setup_rust_android.sh +# +# Configuration details: +# - API level: {{API_LEVEL}} (matching minSdkVersion from build.gradle) +# - Platform: {{PLATFORM}} +# - NDK version: {{NDK_VERSION}} +# - Android SDK: {{ANDROID_SDK_PATH}} \ No newline at end of file diff --git a/file_operations/.gitignore b/file_operations/.gitignore index ed6456e9a..f2869aa55 100644 --- a/file_operations/.gitignore +++ b/file_operations/.gitignore @@ -1,5 +1,11 @@ -/build +# Rust build artifacts +target/ +**/*.rs.bk +Cargo.lock -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild -.cxx/ \ No newline at end of file +# Auto-generated configuration (generated from template) +.cargo/config.toml + +# Android build artifacts +.cxx/ +build/ \ No newline at end of file diff --git a/file_operations/Cargo.toml b/file_operations/Cargo.toml new file mode 100644 index 000000000..7c2b53db8 --- /dev/null +++ b/file_operations/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rootoperations" +version = "0.1.0" +edition = "2024" + +[lib] +name = "rootoperations" +crate-type = ["cdylib"] +path = "src/main/rust/lib.rs" + +[dependencies] +jni = "0.21" +libc = "0.2" + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.11" +log = "0.4" \ No newline at end of file diff --git a/file_operations/build.gradle b/file_operations/build.gradle index 032d02250..5400d6445 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -1,10 +1,12 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'com.google.devtools.ksp' +apply plugin: 'org.mozilla.rust-android-gradle.rust-android' android { namespace "com.amaze.filemanager.fileoperations" compileSdk libs.versions.compileSdk.get().toInteger() + ndkVersion libs.versions.ndk.get() defaultConfig { minSdkVersion libs.versions.minSdk.get().toInteger() @@ -14,12 +16,6 @@ android { consumerProguardFiles "consumer-rules.pro" multiDexEnabled true - - externalNativeBuild { - cmake { - cFlags '-std=c11' - } - } } buildTypes { @@ -46,21 +42,40 @@ android { } } - externalNativeBuild { - cmake { - path file('src/main/c/CMakeLists.txt') - } - } - kotlinOptions { jvmTarget = '17' } } +// Rust configuration +cargo { + module = "./" + libname = "rootoperations" + targets = ["arm", "arm64", "x86", "x86_64"] + profile = "release" + + // Optional: Use cargo and rustc from the path + rustcCommand = "rustc" + cargoCommand = "cargo" + pythonCommand = "python3" +} + kotlin { jvmToolchain(17) } +// Ensure Rust libraries are built before Android tasks +afterEvaluate { + android.libraryVariants.all { variant -> + variant.outputs.all { output -> + def buildTask = tasks.findByName("cargoBuild") + if (buildTask) { + tasks.getByName("merge${variant.name.capitalize()}JniLibFolders").dependsOn buildTask + } + } + } +} + dependencies { modules { module("org.bouncycastle:bcprov-jdk15on") { diff --git a/file_operations/doc/RUST_MIGRATION.md b/file_operations/doc/RUST_MIGRATION.md new file mode 100644 index 000000000..2b92c0574 --- /dev/null +++ b/file_operations/doc/RUST_MIGRATION.md @@ -0,0 +1,277 @@ +# C++ to Rust Migration Guide + +This document outlines the migration from C++ to Rust in the `file_operations` module using Mozilla's Rust Android Gradle plugin. + +## Overview + +The migration replaces the C++ implementation in `src/main/c/rootoperations.c` with a Rust implementation while maintaining the same JNI interface for seamless integration with the existing Kotlin code. + +## What Was Migrated + +### Original C++ Code +- **File**: `src/main/c/rootoperations.c` +- **Function**: `Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_isDirectory` +- **Purpose**: Checks if a given path is a directory using the `stat()` system call +- **Build System**: CMake + +### New Rust Code +- **File**: `src/lib.rs` +- **Function**: Same JNI function signature +- **Purpose**: Identical functionality using Rust's `std::fs::metadata()` +- **Build System**: Cargo with Mozilla's Rust Android Gradle plugin + +## Prerequisites + +1. **Rust Installation**: Install Rust from https://rustup.rs/ +2. **Android NDK**: Install via Android Studio → Tools → SDK Manager → SDK Tools → NDK +3. **Environment Variables**: Set `ANDROID_HOME` or `ANDROID_NDK_ROOT` +4. **Rust-compatible IDE**: Before Android Studio gets first-class Rust support, consider using [Visual Studio Code](https://code.visualstudio.com) with Rust extensions for coding. + +## Setup Instructions + +### 1. Automated Setup (Recommended) + +The project includes an automated setup script that detects your system configuration and generates the correct configuration files: + +```bash +cd file_operations +./setup_rust_android.sh +``` + +This script will: +- **Auto-detect your operating system** (macOS, Linux, Windows) +- **Find your Android SDK** path (from `ANDROID_HOME`, `ANDROID_SDK_ROOT`, or common locations) +- **Detect the latest NDK version** installed +- **Extract API level** from your `build.gradle` +- **Install necessary Rust targets** for Android +- **Generate `.cargo/config.toml`** with correct linker paths + +### 2. Manual Setup (If Needed) + +If the automated setup doesn't work, you can manually configure: + +1. **Install Android Rust targets:** + ```bash + rustup target add aarch64-linux-android + rustup target add armv7-linux-androideabi + rustup target add i686-linux-android + rustup target add x86_64-linux-android + ``` + +2. **Create `.cargo/config.toml`** based on your system: + - Copy `.cargo/config.toml.template` to `.cargo/config.toml` + - Replace placeholders with your actual paths: + - `{{NDK_PATH}}`: Path to your NDK toolchain binaries + - `{{API_LEVEL}}`: Your minimum API level (e.g., 21) + - `{{EXE_SUFFIX}}`: `.exe` for Windows, empty for Unix systems + +### 3. System Requirements + +**Android SDK/NDK:** +- Android SDK installed (via Android Studio or command-line tools) +- NDK installed (via SDK Manager) +- Set `ANDROID_HOME` environment variable + +**Supported Platforms:** +- macOS (`darwin-x86_64`) +- Linux (`linux-x86_64`) +- Windows (`windows-x86_64`) + +### 4. Build Configuration + +The `build.gradle` file now includes: +- Mozilla's Rust Android Gradle plugin +- Cargo configuration for cross-compilation +- Automatic building of Rust code during Android build + +## File Structure + +``` +file_operations/ +├── src/ +│ └── main/ +│ └── rust/ +│ ├── lib.rs # Main library file (module organizer) +│ └── rootoperations.rs # Root operations functions +├── .cargo/ +│ ├── config.toml # Auto-generated Cargo configuration for Android +│ └── config.toml.template # Template for generating config.toml +├── Cargo.toml # Rust project configuration +├── setup_rust_android.sh # Automated setup script +├── build.gradle # Updated with Rust plugin +└── RUST_MIGRATION.md # This file +``` + +## Key Changes + +### 1. Build System +- **Before**: CMake with `externalNativeBuild` +- **After**: Cargo with Mozilla's Rust Android Gradle plugin + +### 2. Dependencies +- **Added**: `jni`, `libc`, `android_logger`, `log` crates +- **Removed**: CMake build configuration + +### 3. Function Implementation +- **Before**: C function using `stat()` system call +- **After**: Rust function using `std::fs::metadata()` + +### 4. Error Handling +- **Before**: errno-based error handling +- **After**: Rust's `Result` type with proper error handling + +### 5. Modular Structure +- **Before**: Single C file with all functions +- **After**: Organized module structure: + - `lib.rs`: Main library file that organizes and re-exports modules + - `rootoperations.rs`: Root-specific operations (migrated from C) + +## Testing + +The Rust implementation includes unit tests: +```bash +cd file_operations +cargo test +``` + +## Building + +The Rust code is automatically built when you build the Android project: +```bash +./gradlew :file_operations:build +``` + +## Advantages of the Migration + +1. **Memory Safety**: Rust prevents memory leaks and buffer overflows +2. **Better Error Handling**: Rust's type system catches errors at compile time +3. **Cross-Platform**: Easier to port to other platforms +4. **Modern Tooling**: Cargo package manager and ecosystem +5. **Performance**: Comparable performance to C++ with better safety + +## Troubleshooting + +### Configuration Issues +First, try re-running the automated setup script: +```bash +cd file_operations +./setup_rust_android.sh +``` + +This will re-detect your system configuration and regenerate `.cargo/config.toml`. + +### NDK Path Issues +If the automated setup fails to find your NDK, ensure your Android SDK is properly configured: +```bash +export ANDROID_HOME=/path/to/android/sdk +# The script will automatically find the NDK in $ANDROID_HOME/ndk/[version] +``` + +### Linker Not Found +If you see linker errors, verify your NDK installation: +1. Check that NDK is installed via Android Studio → SDK Manager → SDK Tools → NDK +2. The setup script will automatically use the latest NDK version found +3. If needed, manually verify the generated `.cargo/config.toml` paths exist + +### Build Errors +Clean and rebuild: +```bash +cd file_operations +cargo clean +cd .. +./gradlew clean +./gradlew :file_operations:build +``` + +### Cross-Platform Issues +The setup script supports: +- **macOS**: Automatically detects `darwin-x86_64` platform +- **Linux**: Automatically detects `linux-x86_64` platform +- **Windows**: Automatically detects `windows-x86_64` platform and adds `.exe` suffix + +If you encounter platform-specific issues, check the generated linker paths in `.cargo/config.toml`. + +## Extending the Codebase + +The modular structure makes it easy to add new functionality: + +### Adding a New Module + +1. **Create a new `.rs` file** in `src/main/rust/` (e.g., `network_ops.rs`) +2. **Add the module declaration** in `lib.rs`: + ```rust + pub mod network_ops; + ``` +3. **Re-export functions** in `lib.rs` for easy access: + ```rust + pub use network_ops::{ + download_file, + upload_file, + }; + ``` + +### Adding JNI Functions + +To add new JNI functions that can be called from Kotlin: + +1. **Add the function** in the appropriate module: + ```rust + #[no_mangle] + pub extern "system" fn Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_newFunction( + env: JNIEnv, + _class: JClass, + param: JString, + ) -> jboolean { + // Implementation + } + ``` + +2. **Add the corresponding method** in `NativeOperations.kt`: + ```kotlin + @JvmStatic + external fun newFunction(param: String?): Boolean + ``` + +### Example: Adding Compression Operations + +```rust +// In src/main/rust/compression_ops.rs +pub mod compression_ops { + use jni::objects::{JClass, JString}; + use jni::JNIEnv; + use jni::sys::jboolean; + + #[no_mangle] + pub extern "system" fn Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_compressFile( + env: JNIEnv, + _class: JClass, + source: JString, + destination: JString, + ) -> jboolean { + // Implementation + } +} +``` + +## Future Enhancements + +1. **Add More Functions**: Migrate additional C++ functions to Rust +2. **Async Operations**: Use Rust's async/await for non-blocking operations +3. **Better Logging**: Enhanced logging with structured logs +4. **Performance Monitoring**: Add metrics collection +5. **Cross-Platform Support**: Extend to other platforms (iOS, Windows, etc.) +6. **Error Types**: Create custom error types for better error handling +7. **Benchmarking**: Add performance benchmarks for critical operations + +## Resources + +- [Mozilla Rust Android Gradle Plugin](https://github.com/mozilla/rust-android-gradle) +- [JNI for Rust](https://docs.rs/jni/latest/jni/) +- [Android NDK Documentation](https://developer.android.com/ndk) +- [Rust Cross-Compilation](https://rust-lang.github.io/rustup/cross-compilation.html) + +## Support + +For questions or issues with the migration, please refer to: +- [Mozilla Rust Android Gradle Issues](https://github.com/mozilla/rust-android-gradle/issues) +- [Rust Android Development](https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html) \ No newline at end of file diff --git a/file_operations/setup_rust_android.sh b/file_operations/setup_rust_android.sh new file mode 100755 index 000000000..132c3b955 --- /dev/null +++ b/file_operations/setup_rust_android.sh @@ -0,0 +1,182 @@ +#!/bin/bash + +# Setup script for Rust Android development +# This script: +# 1. Installs necessary Rust targets for Android +# 2. Auto-detects system configuration +# 3. Generates .cargo/config.toml with correct linker paths + +set -e + +echo "🦀 Setting up Rust for Android development..." + +# Function to detect operating system +detect_os() { + case "$(uname -s)" in + Darwin*) echo "darwin-x86_64" ;; + Linux*) echo "linux-x86_64" ;; + CYGWIN*|MINGW*|MSYS*) echo "windows-x86_64" ;; + *) echo "unknown" ;; + esac +} + +# Function to find Android SDK path +find_android_sdk() { + if [ -n "$ANDROID_HOME" ]; then + echo "$ANDROID_HOME" + elif [ -n "$ANDROID_SDK_ROOT" ]; then + echo "$ANDROID_SDK_ROOT" + elif [ -d "$HOME/Android/Sdk" ]; then + echo "$HOME/Android/Sdk" + elif [ -d "$HOME/Library/Android/sdk" ]; then + echo "$HOME/Library/Android/sdk" + else + echo "" + fi +} + +# Function to find the latest NDK version +find_ndk_version() { + local sdk_path="$1" + local ndk_dir="$sdk_path/ndk" + + if [ -d "$ndk_dir" ]; then + # Find the latest version (highest version number) + ls -1 "$ndk_dir" | sort -V | tail -n 1 + else + echo "" + fi +} + +# Function to extract API level from build.gradle and version catalog +extract_api_level() { + local build_gradle="build.gradle" + local version_catalog="../gradle/libs.versions.toml" + + # First, check if build.gradle uses version catalog + if [ -f "$build_gradle" ] && grep -q "libs.versions.minSdk" "$build_gradle"; then + # Extract from version catalog + if [ -f "$version_catalog" ]; then + grep "^minSdk" "$version_catalog" | cut -d '"' -f 2 + else + echo "21" # Default fallback + fi + elif [ -f "$build_gradle" ]; then + # Look for direct minSdkVersion in build.gradle + grep -E "minSdkVersion|minSdk" "$build_gradle" | head -n 1 | sed 's/.*[[:space:]]\([0-9]\+\).*/\1/' + else + echo "21" # Default fallback + fi +} + +# Detect system configuration +OS_PLATFORM=$(detect_os) +echo "📱 Detected OS: $OS_PLATFORM" + +if [ "$OS_PLATFORM" = "unknown" ]; then + echo "❌ Unsupported operating system. Please configure manually." + exit 1 +fi + +# Find Android SDK +ANDROID_SDK_PATH=$(find_android_sdk) +if [ -z "$ANDROID_SDK_PATH" ]; then + echo "❌ Android SDK not found. Please set ANDROID_HOME or ANDROID_SDK_ROOT environment variable." + echo " Or install Android SDK to ~/Android/Sdk (Linux) or ~/Library/Android/sdk (macOS)" + exit 1 +fi + +echo "📁 Found Android SDK: $ANDROID_SDK_PATH" + +# Find NDK version +NDK_VERSION=$(find_ndk_version "$ANDROID_SDK_PATH") +if [ -z "$NDK_VERSION" ]; then + echo "❌ NDK not found in $ANDROID_SDK_PATH/ndk" + echo " Please install NDK through Android Studio SDK Manager" + exit 1 +fi + +echo "🔨 Found NDK version: $NDK_VERSION" + +# Extract API level +API_LEVEL=$(extract_api_level) +echo "📋 Using API level: $API_LEVEL" + +# Set executable suffix for Windows +EXE_SUFFIX="" +if [ "$OS_PLATFORM" = "windows-x86_64" ]; then + EXE_SUFFIX=".exe" +fi + +# Construct NDK path +NDK_PATH="$ANDROID_SDK_PATH/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/$OS_PLATFORM/bin" + +# Verify NDK path exists +if [ ! -d "$NDK_PATH" ]; then + echo "❌ NDK toolchain not found at: $NDK_PATH" + echo " Please check your NDK installation" + exit 1 +fi + +echo "🛠️ NDK toolchain path: $NDK_PATH" + +# Install Rust targets for Android +echo "📦 Installing Rust targets for Android..." +rustup target add aarch64-linux-android +rustup target add armv7-linux-androideabi +rustup target add i686-linux-android +rustup target add x86_64-linux-android + +# Create .cargo directory if it doesn't exist +mkdir -p .cargo + +# Generate config.toml from template +echo "⚙️ Generating .cargo/config.toml..." +if [ ! -f ".cargo/config.toml.template" ]; then + echo "❌ Template file .cargo/config.toml.template not found" + exit 1 +fi + +# Use sed to replace placeholders in template +sed -e "s|{{NDK_PATH}}|$NDK_PATH|g" \ + -e "s|{{API_LEVEL}}|$API_LEVEL|g" \ + -e "s|{{EXE_SUFFIX}}|$EXE_SUFFIX|g" \ + -e "s|{{PLATFORM}}|$OS_PLATFORM|g" \ + -e "s|{{NDK_VERSION}}|$NDK_VERSION|g" \ + -e "s|{{ANDROID_SDK_PATH}}|$ANDROID_SDK_PATH|g" \ + .cargo/config.toml.template > .cargo/config.toml + +echo "✅ Successfully generated .cargo/config.toml" + +# Verify generated linkers exist +echo "🔍 Verifying linkers..." +LINKERS=( + "aarch64-linux-android${API_LEVEL}-clang${EXE_SUFFIX}" + "armv7a-linux-androideabi${API_LEVEL}-clang${EXE_SUFFIX}" + "i686-linux-android${API_LEVEL}-clang${EXE_SUFFIX}" + "x86_64-linux-android${API_LEVEL}-clang${EXE_SUFFIX}" +) + +for linker in "${LINKERS[@]}"; do + if [ -f "$NDK_PATH/$linker" ]; then + echo " ✅ $linker" + else + echo " ❌ $linker (not found)" + fi +done + +echo "" +echo "🎉 Rust Android setup complete!" +echo "" +echo "📋 Configuration summary:" +echo " - OS Platform: $OS_PLATFORM" +echo " - Android SDK: $ANDROID_SDK_PATH" +echo " - NDK Version: $NDK_VERSION" +echo " - API Level: $API_LEVEL" +echo " - Config file: .cargo/config.toml" +echo "" +echo "🚀 You can now build for Android using:" +echo " cargo build --target aarch64-linux-android" +echo " cargo build --target armv7-linux-androideabi" +echo " cargo build --target i686-linux-android" +echo " cargo build --target x86_64-linux-android" \ No newline at end of file diff --git a/file_operations/src/main/AndroidManifest.xml b/file_operations/src/main/AndroidManifest.xml index 1f08914bd..f71066fed 100644 --- a/file_operations/src/main/AndroidManifest.xml +++ b/file_operations/src/main/AndroidManifest.xml @@ -1,5 +1,2 @@ - - - / - \ No newline at end of file + + \ No newline at end of file diff --git a/file_operations/src/main/c/CMakeLists.txt b/file_operations/src/main/c/CMakeLists.txt deleted file mode 100644 index a6a2a47fc..000000000 --- a/file_operations/src/main/c/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# For more information about using CMake with Android Studio, read the -# documentation: https://d.android.com/studio/projects/add-native-code.html - -cmake_minimum_required(VERSION 3.10.2) - -project("rootoperations") - -add_library(rootoperations SHARED rootoperations.c) - -find_library(log-lib log) - -target_link_libraries(rootoperations ${log-lib}) -target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "-Wl,-z,max-page-size=16384") \ No newline at end of file diff --git a/file_operations/src/main/c/rootoperations.c b/file_operations/src/main/c/rootoperations.c deleted file mode 100644 index 2fcf855db..000000000 --- a/file_operations/src/main/c/rootoperations.c +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include -#include -#include -#include -#include - -JNIEXPORT jboolean JNICALL Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_isDirectory( - JNIEnv * env, - jobject thiz, - jstring path - ) { - struct stat path_stat; - const char * cPath = (*env)->GetStringUTFChars(env, path, NULL); - int returnCode = stat(cPath, &path_stat); - (*env)->ReleaseStringUTFChars(env, path, cPath); - - if(returnCode == -1) { - switch (errno) { - case ELOOP: - return true; - default: - return false; - } - } - - //This follows links - return S_ISDIR(path_stat.st_mode); -} \ No newline at end of file diff --git a/file_operations/src/main/rust/README.md b/file_operations/src/main/rust/README.md new file mode 100644 index 000000000..bf01726e9 --- /dev/null +++ b/file_operations/src/main/rust/README.md @@ -0,0 +1,216 @@ +# Rust File Operations Module + +This directory contains the Rust implementation of file operations for the Amaze File Manager, which +was migrated from C/C++ to Rust for more modern, maintainable more safe code, with Rust's inherent +type-safety and memory-safety features. + +Please refer to [RUST_MIGRATION.md](../doc/RUST_MIGRATION.md) for detailed information +about the migration process, key changes, and advantages of using Rust. + +## Module Structure + +The code is organized into separate modules for better maintainability and extensibility: + +### `lib.rs` - Main Library File +- **Purpose**: Organizes and re-exports functionality from various modules +- **Content**: Module declarations and re-exports for easy access +- **Note**: This is the entry point for the Rust library + +### `rootoperations.rs` - Root Operations +- **Purpose**: Contains root-specific file operations (migrated from C++) +- **Functions**: + - `Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_isDirectory` - JNI function for directory checking + - `check_is_directory` - Helper function to check if a path is a directory + - `check_path_exists` - Helper function to check if a path exists + - `check_is_file` - Helper function to check if a path is a file + - `get_file_size` - Helper function to get file size + +## Usage + +### From Kotlin/Java +The JNI functions are automatically exported and can be called from Kotlin: + +```kotlin +// In NativeOperations.kt +@JvmStatic +external fun isDirectory(path: String?): Boolean + +// Usage +val isDir = NativeOperations.isDirectory("/some/path") +``` + +### From Rust (Internal) +Functions can be called directly within the Rust codebase: + +```rust +use crate::rootoperations::check_is_directory; + +let is_dir = check_is_directory("/some/path")?; +``` + +### Through Re-exports +Functions can be accessed through the main library: + +```rust +use crate::check_is_directory; + +let is_dir = check_is_directory("/some/path")?; +``` + +## Adding New Functionality + +### 1. Create a New Module +Create a new `.rs` file (e.g., `network_ops.rs`): + +```rust +// network_ops.rs +pub fn download_file(url: &str, destination: &str) -> Result<(), Box> { + // Implementation + Ok(()) +} +``` + +### 2. Add Module Declaration +In `lib.rs`, add the module declaration: + +```rust +pub mod network_ops; +``` + +### 3. Re-export Functions (Optional) +In `lib.rs`, add re-exports for easy access: + +```rust +pub use network_ops::{ + download_file, + upload_file, +}; +``` + +### 4. Add JNI Functions (If Needed) +If you need to call the function from Kotlin: + +```rust +// In network_ops.rs +#[no_mangle] +pub extern "system" fn Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_downloadFile( + env: JNIEnv, + _class: JClass, + url: JString, + destination: JString, +) -> jboolean { + // Convert JString to Rust String + let url_str: String = env.get_string(url).unwrap().into(); + let dest_str: String = env.get_string(destination).unwrap().into(); + + // Call the actual function + match download_file(&url_str, &dest_str) { + Ok(_) => 1, // true + Err(_) => 0, // false + } +} +``` + +Then add the corresponding method in `NativeOperations.kt`: + +```kotlin +@JvmStatic +external fun downloadFile(url: String?, destination: String?): Boolean +``` + +## Testing + +Each module includes its own tests. Run tests with: + +```bash +cd file_operations +cargo test +``` + +To test a specific module: + +```bash +cargo test rootoperations::tests +``` + +## Best Practices + +1. **Separation of Concerns**: Keep related functions in the same module +2. **Error Handling**: Use `Result` for functions that can fail +3. **Documentation**: Add doc comments to all public functions +4. **Testing**: Include unit tests for all functions +5. **JNI Safety**: Always handle JNI string conversion errors +6. **Logging**: Use the `log` crate for debugging information + +## Example: Complete Function Implementation + +```rust +// In your_module.rs +use jni::objects::{JClass, JString}; +use jni::JNIEnv; +use jni::sys::jboolean; + +/// Check if a file is executable +pub fn is_executable(path: &str) -> Result> { + let path = std::path::Path::new(path); + let metadata = path.metadata()?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + Ok(metadata.permissions().mode() & 0o111 != 0) + } + + #[cfg(not(unix))] + { + // On non-Unix systems, check if it's a file with executable extension + Ok(path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd")) + .unwrap_or(false)) + } +} + +/// JNI function to check if a file is executable +#[no_mangle] +pub extern "system" fn Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_isExecutable( + env: JNIEnv, + _class: JClass, + path: JString, +) -> jboolean { + let path_str = match env.get_string(path) { + Ok(java_str) => java_str, + Err(_) => { + log::error!("Failed to get string from JString"); + return 0; + } + }; + + let path_string: String = path_str.into(); + + match is_executable(&path_string) { + Ok(is_exec) => if is_exec { 1 } else { 0 }, + Err(e) => { + log::error!("Error checking if file is executable: {}", e); + 0 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_executable() { + // Test with a known executable (on Unix systems) + #[cfg(unix)] + assert!(is_executable("/bin/ls").unwrap_or(false)); + + // Test with a non-executable file + assert!(!is_executable("/etc/passwd").unwrap_or(true)); + } +} +``` + +This structure makes the codebase highly maintainable and easy to extend with new functionality while keeping everything organized and well-tested. \ No newline at end of file diff --git a/file_operations/src/main/rust/lib.rs b/file_operations/src/main/rust/lib.rs new file mode 100644 index 000000000..d7d7fd8a5 --- /dev/null +++ b/file_operations/src/main/rust/lib.rs @@ -0,0 +1,29 @@ +// Main library file for the Rust file operations module +// This file organizes and re-exports functionality from various modules + +// Module declarations +pub mod rootoperations; + +// Re-export commonly used functions for easier access +pub use rootoperations::{ + check_is_directory, + check_path_exists, + check_is_file, + get_file_size, +}; + +// The JNI functions are automatically exported due to #[no_mangle] in their respective modules +// No need to re-export them here as they're called directly by the JVM + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_module_integration() { + // Test that we can access rootoperations functions through re-exports + assert!(check_path_exists("/").unwrap_or(false)); + assert!(check_is_directory("/").unwrap_or(false)); + assert!(!check_is_file("/").unwrap_or(true)); + } +} \ No newline at end of file diff --git a/file_operations/src/main/rust/rootoperations.rs b/file_operations/src/main/rust/rootoperations.rs new file mode 100644 index 000000000..d7bae5da5 --- /dev/null +++ b/file_operations/src/main/rust/rootoperations.rs @@ -0,0 +1,174 @@ +use jni::objects::{JClass, JString}; +use jni::JNIEnv; +use jni::sys::jboolean; +use std::path::Path; +use std::sync::Once; + +#[cfg(target_os = "android")] +use android_logger::Config; + +#[cfg(target_os = "android")] +use log; + +// Per Copilot review suggested, use Once::new to make Android logging init only once +static INIT: Once = Once::new(); + +/// Initialize Android logging (optional, but useful for debugging) +#[cfg(target_os = "android")] +fn init_logging() { + INIT.call_once(|| { + android_logger::init_once( + Config::default() + .with_min_level(log::Level::Debug) + .with_tag("RustRootOperations") + ); + }); +} + +/// Log error message (Android only) +#[cfg(target_os = "android")] +fn log_error(msg: &str) { + log::error!("{}", msg); +} + +/// Log error message (non-Android platforms) +#[cfg(not(target_os = "android"))] +fn log_error(msg: &str) { + eprintln!("ERROR: {}", msg); +} + +/// JNI function to check if a path is a directory +/// This replaces the C function: Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_isDirectory +#[unsafe(no_mangle)] +pub extern "system" fn Java_com_amaze_filemanager_fileoperations_filesystem_root_NativeOperations_isDirectory( + mut env: JNIEnv, + _class: JClass, + path: JString, +) -> jboolean { + #[cfg(target_os = "android")] + init_logging(); + + // Convert the JString to a Rust string + let path_str = match env.get_string(&path) { + Ok(java_str) => java_str, + Err(_) => { + log_error("Failed to get string from JString"); + return 0; // false + } + }; + + let path_string: String = path_str.into(); + + // Check if the path is a directory using Rust's standard library + match check_is_directory(&path_string) { + Ok(is_dir) => { + if is_dir { 1 } else { 0 } // true : false + } + Err(e) => { + log_error(&format!("Error checking if path is directory: {}", e)); + 0 // false on error + } + } +} + +/// Helper function to check if a path is a directory +/// This replicates the logic from the original C code +pub fn check_is_directory(path: &str) -> Result> { + let path = Path::new(path); + + match path.metadata() { + Ok(metadata) => { + Ok(metadata.is_dir()) + } + Err(e) => { + use std::io::ErrorKind; + match e.kind() { + ErrorKind::NotFound => Ok(false), + ErrorKind::PermissionDenied => Ok(false), + _ => { + // For other errors, we'll return false similar to the C code + // The original C code had special handling for ELOOP (too many symbolic links) + // which would return true, but Rust's std::fs handles this differently + Ok(false) + } + } + } + } +} + +/// Helper function to check if a path exists +/// This can be used for future extensions +pub fn check_path_exists(path: &str) -> Result> { + let path = Path::new(path); + Ok(path.exists()) +} + +/// Helper function to check if a path is a file +/// This can be used for future extensions +pub fn check_is_file(path: &str) -> Result> { + let path = Path::new(path); + + match path.metadata() { + Ok(metadata) => { + Ok(metadata.is_file()) + } + Err(e) => { + use std::io::ErrorKind; + match e.kind() { + ErrorKind::NotFound => Ok(false), + ErrorKind::PermissionDenied => Ok(false), + _ => Ok(false) + } + } + } +} + +/// Helper function to get file size +/// This can be used for future extensions +pub fn get_file_size(path: &str) -> Result> { + let path = Path::new(path); + + match path.metadata() { + Ok(metadata) => { + Ok(metadata.len()) + } + Err(e) => { + Err(Box::new(e)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_check_is_directory() { + // Test with a known directory + assert!(check_is_directory("/tmp").unwrap_or(false)); + + // Test with a non-existent path + assert!(!check_is_directory("/this/path/does/not/exist").unwrap_or(true)); + + // Test with root directory + assert!(check_is_directory("/").unwrap_or(false)); + } + + #[test] + fn test_check_path_exists() { + // Test with root directory (should exist) + assert!(check_path_exists("/").unwrap_or(false)); + + // Test with non-existent path + assert!(!check_path_exists("/this/path/does/not/exist").unwrap_or(true)); + } + + #[test] + fn test_check_is_file() { + // Test with a directory (should return false) + assert!(!check_is_file("/").unwrap_or(true)); + + // Test with non-existent path + assert!(!check_is_file("/this/path/does/not/exist").unwrap_or(true)); + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f17f76df..e475d17e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ compileSdk = "34" minSdk = "21" targetSdk = "35" kotlin = "1.9.25" -ndk = "28.2.13676358" +ndk = "28.2.13676358" #r28c jacocoAndroid = "0.2.1" gradle = "8.5.2" @@ -35,7 +35,7 @@ androidXTestRunner = "1.6.2" androidXTestExt = "1.2.1" androidXArchCoreTest = "2.2.0" androidXMultidex = "2.0.1" -autoService = "1.0-rc4" +autoService = "1.1.1" kotlinxCoroutineTest = "1.7.3" kotlinStdlibJdk8 = "1.9.20" uiAutomator = "2.3.0" @@ -73,11 +73,13 @@ mpAndroidChart = "v3.0.2" concurrentTrees = "2.6.1" aboutLibraries = "6.1.1" amazeTrashBin = "1.0.10" +rustAndroidGradle = "0.9.6" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacocoAndroid" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +rust-android-gradle = { module = "org.mozilla.rust-android-gradle.rust-android:org.mozilla.rust-android-gradle.rust-android.gradle.plugin", version.ref = "rustAndroidGradle" } androidX-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidXAppCompat" } androidX-constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidXConstraintLayout" }