From 4cfd510a68f2a1f554e0acf6d1c83b3f2bce8182 Mon Sep 17 00:00:00 2001 From: Siddhesh Date: Sun, 22 Feb 2026 04:36:50 +0530 Subject: [PATCH 1/2] Added Lora + Fixed Build-All-Work-Flow (#389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(lora): add LoRA adapter support across SDK + demo app Implement LoRA (Low-Rank Adaptation) adapter hot-swapping for llama.cpp backend across all 6 SDK layers (C++ -> C API -> Component -> JNI -> Kotlin Bridge -> Kotlin Public API). - Add load/remove/clear/query LoRA adapter operations - Use vtable dispatch in component layer to decouple librac_commons from librac_backend_llamacpp (fixes linker errors) - Add LoRA vtable entries to rac_llm_service_ops_t - Fix AttachCurrentThread cast for Android NDK C++ JNI build - Add RunAnyWhereLora Android demo app with Material 3 Q&A UI - Add comprehensive implementation docs with C/C++ API reference * feat(ci): add selectable build targets to Build All workflow + fix Swift concurrency errors Rewrite build-all-test.yml with 9 boolean checkbox inputs so each build target can be toggled independently from the GitHub Actions UI: - C++ Android Backends (arm64-v8a, armeabi-v7a, x86_64 matrix) - C++ iOS Backends (XCFramework) - Kotlin SDK (JVM + Android) - Swift SDK (iOS/macOS) - Web SDK (TypeScript) - Flutter SDK (Dart analyze via Melos) - React Native SDK (TypeScript via Lerna) - Android Example Apps (RunAnywhereAI + RunAnyWhereLora) - IntelliJ Plugin Fix two Swift strict-concurrency errors that fail the Swift SDK build: - LiveTranscriptionSession: add @unchecked Sendable (safe because class is @MainActor, all access serialized) - RunAnywhere+VisionLanguage: add Sendable conformance to rac_vlm_image_t so the C struct can cross the Task boundary in the streaming builder; simplify StreamingCollector to start timing at init * fix(swift): resolve strict concurrency errors in LiveTranscriptionSession and VLM streaming LiveTranscriptionSession.swift: - Replace [weak self] captures with strong `let session = self` before closures to avoid captured var in @Sendable/@Task contexts (class is @MainActor @unchecked Sendable so strong ref is safe, bounded by stream lifecycle) - Wrap deprecated startStreamingTranscription call in @available helper to silence deprecation warning until migration to transcribeStream API RunAnywhere+VisionLanguage.swift: - Add `let capturedCImage = cImage` before AsyncThrowingStream closure so the Task captures an immutable let instead of a mutable var - Add `extension rac_vlm_image_t: @unchecked Sendable {}` for the C struct to cross Task concurrency boundaries safely - Simplify StreamingCollector to initialize startTime at init instead of requiring a separate async start() call * fix(jni): address CodeRabbit review findings in LoRA JNI functions - Replace raw -1 returns with RAC_ERROR_INVALID_HANDLE/RAC_ERROR_INVALID_ARGUMENT to match codebase error handling conventions - Use getCString() helper instead of raw GetStringUTFChars/ReleaseStringUTFChars - Add missing result logging to racLlmComponentRemoveLora and racLlmComponentClearLora - Use rac_free() instead of free() in racLlmComponentGetLoraInfo for consistency - Clarify LoRA adapter memory ownership comments (adapters freed automatically with model per llama.cpp b8011 API — llama_adapter_lora_free is deprecated) --- .github/workflows/build-all-test.yml | 433 ++++++++++- .idea/vcs.xml | 4 + docs/impl/lora_adapter_support.md | 700 ++++++++++++++++++ examples/android/RunAnyWhereLora/.gitignore | 15 + .../android/RunAnyWhereLora/.idea/.gitignore | 3 + examples/android/RunAnyWhereLora/.idea/.name | 1 + .../.idea/AndroidProjectSystem.xml | 6 + .../android/RunAnyWhereLora/.idea/gradle.xml | 11 + .../android/RunAnyWhereLora/.idea/misc.xml | 10 + .../.idea/runConfigurations.xml | 17 + .../android/RunAnyWhereLora/app/.gitignore | 1 + .../RunAnyWhereLora/app/build.gradle.kts | 120 +++ .../RunAnyWhereLora/app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.kt | 24 + .../app/src/main/AndroidManifest.xml | 36 + .../run_anywhere_lora/LoraApplication.kt | 72 ++ .../run_anywhere_lora/LoraScreen.kt | 530 +++++++++++++ .../run_anywhere_lora/LoraViewModel.kt | 235 ++++++ .../run_anywhere_lora/MainActivity.kt | 85 +++ .../run_anywhere_lora/ui/theme/Color.kt | 11 + .../run_anywhere_lora/ui/theme/Theme.kt | 58 ++ .../run_anywhere_lora/ui/theme/Type.kt | 34 + .../res/drawable/ic_launcher_background.xml | 170 +++++ .../res/drawable/ic_launcher_foreground.xml | 30 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 5 + .../app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../run_anywhere_lora/ExampleUnitTest.kt | 17 + .../android/RunAnyWhereLora/build.gradle.kts | 8 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + examples/android/RunAnyWhereLora/gradlew | 251 +++++++ examples/android/RunAnyWhereLora/gradlew.bat | 94 +++ .../RunAnyWhereLora/settings.gradle.kts | 40 + .../include/rac/backends/rac_llm_llamacpp.h | 52 ++ .../rac/features/llm/rac_llm_component.h | 53 ++ .../rac/features/llm/rac_llm_service.h | 12 + .../backends/llamacpp/llamacpp_backend.cpp | 182 +++++ .../src/backends/llamacpp/llamacpp_backend.h | 25 + .../rac_backend_llamacpp_register.cpp | 21 + .../backends/llamacpp/rac_llm_llamacpp.cpp | 73 ++ .../src/features/llm/llm_component.cpp | 90 +++ .../src/jni/runanywhere_commons_jni.cpp | 72 ++ .../sdk/public/extensions/LLM/LLMTypes.kt | 30 + .../sdk/public/extensions/RunAnywhere+LoRA.kt | 49 ++ .../bridge/extensions/CppBridgeLLM.kt | 115 +++ .../sdk/native/bridge/RunAnywhereBridge.kt | 16 + .../extensions/RunAnywhere+LoRA.jvmAndroid.kt | 83 +++ .../VLM/RunAnywhere+VisionLanguage.swift | 15 +- .../Sessions/LiveTranscriptionSession.swift | 29 +- settings.gradle.kts | 1 + 65 files changed, 3984 insertions(+), 40 deletions(-) create mode 100644 docs/impl/lora_adapter_support.md create mode 100644 examples/android/RunAnyWhereLora/.gitignore create mode 100644 examples/android/RunAnyWhereLora/.idea/.gitignore create mode 100644 examples/android/RunAnyWhereLora/.idea/.name create mode 100644 examples/android/RunAnyWhereLora/.idea/AndroidProjectSystem.xml create mode 100644 examples/android/RunAnyWhereLora/.idea/gradle.xml create mode 100644 examples/android/RunAnyWhereLora/.idea/misc.xml create mode 100644 examples/android/RunAnyWhereLora/.idea/runConfigurations.xml create mode 100644 examples/android/RunAnyWhereLora/app/.gitignore create mode 100644 examples/android/RunAnyWhereLora/app/build.gradle.kts create mode 100644 examples/android/RunAnyWhereLora/app/proguard-rules.pro create mode 100644 examples/android/RunAnyWhereLora/app/src/androidTest/java/com/runanywhere/run_anywhere_lora/ExampleInstrumentedTest.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/AndroidManifest.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraApplication.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/MainActivity.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Color.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Theme.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Type.kt create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/values/colors.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/values/strings.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/values/themes.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/xml/backup_rules.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 examples/android/RunAnyWhereLora/app/src/test/java/com/runanywhere/run_anywhere_lora/ExampleUnitTest.kt create mode 100644 examples/android/RunAnyWhereLora/build.gradle.kts create mode 100644 examples/android/RunAnyWhereLora/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/android/RunAnyWhereLora/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/android/RunAnyWhereLora/gradlew create mode 100644 examples/android/RunAnyWhereLora/gradlew.bat create mode 100644 examples/android/RunAnyWhereLora/settings.gradle.kts create mode 100644 sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.kt create mode 100644 sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/public/extensions/RunAnywhere+LoRA.jvmAndroid.kt diff --git a/.github/workflows/build-all-test.yml b/.github/workflows/build-all-test.yml index d3b855e58..3c110b902 100644 --- a/.github/workflows/build-all-test.yml +++ b/.github/workflows/build-all-test.yml @@ -1,17 +1,277 @@ name: Build All (Test) -# Manually-triggered workflow that builds all SDKs and example projects. -# Use this to verify everything compiles after changes. +# ============================================================================= +# Comprehensive Build Verification Workflow +# +# Manually-triggered workflow with checkboxes for each build target. +# Select what you want to build and hit "Run workflow". +# +# Targets: +# - C++ Android Backends (arm64-v8a, armeabi-v7a, x86_64) +# - C++ iOS Backends (XCFramework) +# - Kotlin SDK (JVM + Android) +# - Swift SDK (iOS/macOS) +# - Web SDK (TypeScript) +# - Flutter SDK (Dart analyze) +# - React Native SDK (TypeScript) +# - Android Example Apps +# - IntelliJ Plugin +# ============================================================================= on: - workflow_dispatch: {} + workflow_dispatch: + inputs: + build_cpp_android: + description: 'C++ Android Backends (arm64-v8a, armeabi-v7a, x86_64)' + required: false + default: true + type: boolean + build_cpp_ios: + description: 'C++ iOS Backends (XCFramework)' + required: false + default: true + type: boolean + build_kotlin_sdk: + description: 'Kotlin SDK (JVM + Android)' + required: false + default: true + type: boolean + build_swift_sdk: + description: 'Swift SDK (iOS/macOS)' + required: false + default: true + type: boolean + build_web_sdk: + description: 'Web SDK (TypeScript)' + required: false + default: true + type: boolean + build_flutter_sdk: + description: 'Flutter SDK (Dart)' + required: false + default: true + type: boolean + build_react_native_sdk: + description: 'React Native SDK (TypeScript)' + required: false + default: true + type: boolean + build_android_apps: + description: 'Android Example Apps' + required: false + default: true + type: boolean + build_intellij_plugin: + description: 'IntelliJ Plugin' + required: false + default: true + type: boolean permissions: contents: read +env: + COMMONS_DIR: sdk/runanywhere-commons + jobs: + # =========================================================================== + # C++ Android Backends (Matrix: arm64-v8a, armeabi-v7a, x86_64) + # =========================================================================== + cpp-android: + name: C++ Android (${{ matrix.abi }}) + if: ${{ inputs.build_cpp_android }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + abi: [arm64-v8a, armeabi-v7a, x86_64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Setup NDK + run: | + NDK_VERSION="27.0.12077973" + echo "y" | sdkmanager --install "ndk;${NDK_VERSION}" --sdk_root=${ANDROID_SDK_ROOT} + echo "ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" >> $GITHUB_ENV + + - name: Setup Development Config + working-directory: ${{ env.COMMONS_DIR }} + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + CONFIG_FILE="src/infrastructure/network/development_config.cpp" + CLEAN_URL=$(printf '%s' "${SUPABASE_URL:-https://placeholder.supabase.co}" | tr -d '\n\r') + CLEAN_KEY=$(printf '%s' "${SUPABASE_ANON_KEY:-placeholder_key}" | tr -d '\n\r') + CLEAN_TOKEN=$(printf '%s' "${BUILD_TOKEN:-bt_test_build}" | tr -d '\n\r') + + cat > "$CONFIG_FILE" << CONFIGEOF + #include + #include "rac/core/rac_logger.h" + #include "rac/infrastructure/network/rac_dev_config.h" + + namespace { + constexpr const char* SUPABASE_URL = "${CLEAN_URL}"; + constexpr const char* SUPABASE_ANON_KEY = "${CLEAN_KEY}"; + constexpr const char* BUILD_TOKEN = "${CLEAN_TOKEN}"; + constexpr const char* SENTRY_DSN = nullptr; + } + + extern "C" { + bool rac_dev_config_is_available(void) { + if (SUPABASE_URL == nullptr || SUPABASE_ANON_KEY == nullptr) return false; + if (std::strlen(SUPABASE_URL) == 0 || std::strlen(SUPABASE_ANON_KEY) == 0) return false; + if (std::strstr(SUPABASE_URL, "YOUR_") != nullptr) return false; + if (std::strstr(SUPABASE_ANON_KEY, "YOUR_") != nullptr) return false; + return true; + } + const char* rac_dev_config_get_supabase_url(void) { return SUPABASE_URL; } + const char* rac_dev_config_get_supabase_key(void) { return SUPABASE_ANON_KEY; } + const char* rac_dev_config_get_build_token(void) { return BUILD_TOKEN; } + const char* rac_dev_config_get_sentry_dsn(void) { return SENTRY_DSN; } + bool rac_dev_config_has_supabase(void) { return rac_dev_config_is_available(); } + bool rac_dev_config_has_build_token(void) { return BUILD_TOKEN != nullptr && std::strlen(BUILD_TOKEN) > 0; } + } + CONFIGEOF + + - name: Download Dependencies + working-directory: ${{ env.COMMONS_DIR }} + run: | + chmod +x scripts/android/download-sherpa-onnx.sh + rm -rf third_party/sherpa-onnx-android + ./scripts/android/download-sherpa-onnx.sh + + - name: Build C++ Android ${{ matrix.abi }} + working-directory: ${{ env.COMMONS_DIR }} + run: | + chmod +x scripts/build-android.sh + ./scripts/build-android.sh all ${{ matrix.abi }} + + - name: List Build Output + working-directory: ${{ env.COMMONS_DIR }} + run: | + echo "=== Build Artifacts ===" + find dist/android -name "*.so" -type f 2>/dev/null | head -30 || echo "No .so files found" + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: cpp-android-${{ matrix.abi }} + path: ${{ env.COMMONS_DIR }}/dist/android/ + retention-days: 7 + + # =========================================================================== + # C++ iOS Backends (XCFramework) + # =========================================================================== + cpp-ios: + name: C++ iOS (XCFramework) + if: ${{ inputs.build_cpp_ios }} + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.4' + + - name: Install Dependencies + run: brew install cmake ninja + + - name: Setup Development Config + working-directory: ${{ env.COMMONS_DIR }} + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }} + run: | + CONFIG_FILE="src/infrastructure/network/development_config.cpp" + CLEAN_URL=$(printf '%s' "${SUPABASE_URL:-https://placeholder.supabase.co}" | tr -d '\n\r') + CLEAN_KEY=$(printf '%s' "${SUPABASE_ANON_KEY:-placeholder_key}" | tr -d '\n\r') + CLEAN_TOKEN=$(printf '%s' "${BUILD_TOKEN:-bt_test_build}" | tr -d '\n\r') + + cat > "$CONFIG_FILE" << CONFIGEOF + #include + #include "rac/core/rac_logger.h" + #include "rac/infrastructure/network/rac_dev_config.h" + + namespace { + constexpr const char* SUPABASE_URL = "${CLEAN_URL}"; + constexpr const char* SUPABASE_ANON_KEY = "${CLEAN_KEY}"; + constexpr const char* BUILD_TOKEN = "${CLEAN_TOKEN}"; + constexpr const char* SENTRY_DSN = nullptr; + } + + extern "C" { + bool rac_dev_config_is_available(void) { + if (SUPABASE_URL == nullptr || SUPABASE_ANON_KEY == nullptr) return false; + if (std::strlen(SUPABASE_URL) == 0 || std::strlen(SUPABASE_ANON_KEY) == 0) return false; + if (std::strstr(SUPABASE_URL, "YOUR_") != nullptr) return false; + if (std::strstr(SUPABASE_ANON_KEY, "YOUR_") != nullptr) return false; + return true; + } + const char* rac_dev_config_get_supabase_url(void) { return SUPABASE_URL; } + const char* rac_dev_config_get_supabase_key(void) { return SUPABASE_ANON_KEY; } + const char* rac_dev_config_get_build_token(void) { return BUILD_TOKEN; } + const char* rac_dev_config_get_sentry_dsn(void) { return SENTRY_DSN; } + bool rac_dev_config_has_supabase(void) { return rac_dev_config_is_available(); } + bool rac_dev_config_has_build_token(void) { return BUILD_TOKEN != nullptr && std::strlen(BUILD_TOKEN) > 0; } + } + CONFIGEOF + + - name: Download iOS Dependencies + working-directory: ${{ env.COMMONS_DIR }} + run: | + chmod +x scripts/ios/download-sherpa-onnx.sh + chmod +x scripts/ios/download-onnx.sh + ./scripts/ios/download-sherpa-onnx.sh + ./scripts/ios/download-onnx.sh + + - name: Build RACommons + All Backends (iOS) + working-directory: ${{ env.COMMONS_DIR }} + run: | + chmod +x scripts/build-ios.sh + ./scripts/build-ios.sh --backend all --release --package + + - name: List Build Output + working-directory: ${{ env.COMMONS_DIR }} + run: | + echo "=== Build Artifacts ===" + ls -la dist/ + [ -d dist/RACommons.xcframework ] && echo "RACommons.xcframework" || echo "RACommons.xcframework MISSING" + [ -d dist/RABackendLLAMACPP.xcframework ] && echo "RABackendLLAMACPP.xcframework" || echo "RABackendLLAMACPP.xcframework MISSING" + [ -d dist/RABackendONNX.xcframework ] && echo "RABackendONNX.xcframework" || echo "RABackendONNX.xcframework MISSING" + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: cpp-ios-xcframeworks + path: | + ${{ env.COMMONS_DIR }}/dist/RACommons.xcframework + ${{ env.COMMONS_DIR }}/dist/RABackendLLAMACPP.xcframework + ${{ env.COMMONS_DIR }}/dist/RABackendONNX.xcframework + retention-days: 7 + + # =========================================================================== + # Kotlin SDK (JVM + Android) + # =========================================================================== kotlin-sdk: name: Kotlin SDK (JVM + Android) + if: ${{ inputs.build_kotlin_sdk }} runs-on: ubuntu-latest steps: @@ -29,8 +289,7 @@ jobs: - name: Setup local.properties working-directory: sdk/runanywhere-kotlin - run: | - echo "sdk.dir=${ANDROID_SDK_ROOT}" > local.properties + run: echo "sdk.dir=${ANDROID_SDK_ROOT}" > local.properties - name: Build JVM JAR working-directory: sdk/runanywhere-kotlin @@ -55,8 +314,31 @@ jobs: run: ./gradlew publishToMavenLocal -Prunanywhere.testLocal=false continue-on-error: true + # =========================================================================== + # Swift SDK (iOS/macOS) + # =========================================================================== + swift-sdk: + name: Swift SDK + if: ${{ inputs.build_swift_sdk }} + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Swift Build (remote XCFrameworks) + run: swift build + + - name: Swift Test + run: swift test + continue-on-error: true + + # =========================================================================== + # Web SDK (TypeScript) + # =========================================================================== web-sdk: name: Web SDK (TypeScript) + if: ${{ inputs.build_web_sdk }} runs-on: ubuntu-latest steps: @@ -80,23 +362,78 @@ jobs: working-directory: sdk/runanywhere-web/packages/core run: npx tsc - swift-sdk: - name: Swift SDK - runs-on: macos-14 + # =========================================================================== + # Flutter SDK (Dart) + # =========================================================================== + flutter-sdk: + name: Flutter SDK (Dart) + if: ${{ inputs.build_flutter_sdk }} + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Swift Build (remote XCFrameworks) - run: swift build + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.27.x' + channel: 'stable' - - name: Swift Test - run: swift test + - name: Install Melos + run: dart pub global activate melos + + - name: Bootstrap Packages + working-directory: sdk/runanywhere-flutter + run: melos bootstrap + + - name: Analyze All Packages + working-directory: sdk/runanywhere-flutter + run: melos run analyze || dart analyze packages/runanywhere packages/runanywhere_llamacpp packages/runanywhere_onnx + continue-on-error: true + + - name: Run Tests + working-directory: sdk/runanywhere-flutter + run: melos run test || true + continue-on-error: true + + # =========================================================================== + # React Native SDK (TypeScript) + # =========================================================================== + react-native-sdk: + name: React Native SDK (TypeScript) + if: ${{ inputs.build_react_native_sdk }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Dependencies + working-directory: sdk/runanywhere-react-native + run: yarn install --frozen-lockfile || npm install + + - name: TypeScript Typecheck + working-directory: sdk/runanywhere-react-native + run: npx tsc --noEmit -p tsconfig.base.json + continue-on-error: true + + - name: Build Packages + working-directory: sdk/runanywhere-react-native + run: npx lerna run build || true continue-on-error: true - android-app: - name: Android Example App + # =========================================================================== + # Android Example Apps + # =========================================================================== + android-apps: + name: Android Example Apps + if: ${{ inputs.build_android_apps }} runs-on: ubuntu-latest steps: @@ -115,17 +452,29 @@ jobs: - name: Setup local.properties run: | echo "sdk.dir=${ANDROID_SDK_ROOT}" > examples/android/RunAnywhereAI/local.properties + echo "sdk.dir=${ANDROID_SDK_ROOT}" > examples/android/RunAnyWhereLora/local.properties echo "sdk.dir=${ANDROID_SDK_ROOT}" > sdk/runanywhere-kotlin/local.properties - - name: Build Android App + - name: Build RunAnywhereAI working-directory: examples/android/RunAnywhereAI run: | chmod +x gradlew ./gradlew assembleDebug -Prunanywhere.testLocal=false continue-on-error: true + - name: Build RunAnyWhereLora + working-directory: examples/android/RunAnyWhereLora + run: | + chmod +x gradlew + ./gradlew assembleDebug -Prunanywhere.testLocal=false + continue-on-error: true + + # =========================================================================== + # IntelliJ Plugin + # =========================================================================== intellij-plugin: name: IntelliJ Plugin + if: ${{ inputs.build_intellij_plugin }} runs-on: ubuntu-latest steps: @@ -142,8 +491,7 @@ jobs: uses: android-actions/setup-android@v3 - name: Setup local.properties - run: | - echo "sdk.dir=${ANDROID_SDK_ROOT}" > sdk/runanywhere-kotlin/local.properties + run: echo "sdk.dir=${ANDROID_SDK_ROOT}" > sdk/runanywhere-kotlin/local.properties - name: Publish SDK to Maven Local working-directory: sdk/runanywhere-kotlin @@ -157,9 +505,21 @@ jobs: chmod +x gradlew ./gradlew buildPlugin + # =========================================================================== + # Build Summary + # =========================================================================== summary: name: Build Summary - needs: [kotlin-sdk, web-sdk, swift-sdk, android-app, intellij-plugin] + needs: + - cpp-android + - cpp-ios + - kotlin-sdk + - swift-sdk + - web-sdk + - flutter-sdk + - react-native-sdk + - android-apps + - intellij-plugin if: always() runs-on: ubuntu-latest @@ -168,13 +528,32 @@ jobs: run: | echo "## Build All (Test) Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "| Project | Status |" >> $GITHUB_STEP_SUMMARY - echo "|---------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Kotlin SDK (JVM + Android) | ${{ needs.kotlin-sdk.result == 'success' && 'Pass' || needs.kotlin-sdk.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Web SDK (TypeScript) | ${{ needs.web-sdk.result == 'success' && 'Pass' || needs.web-sdk.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Swift SDK | ${{ needs.swift-sdk.result == 'success' && 'Pass' || needs.swift-sdk.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| Android Example App | ${{ needs.android-app.result == 'success' && 'Pass' || needs.android-app.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| IntelliJ Plugin | ${{ needs.intellij-plugin.result == 'success' && 'Pass' || needs.intellij-plugin.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Target | Status |" >> $GITHUB_STEP_SUMMARY + echo "|--------|--------|" >> $GITHUB_STEP_SUMMARY + + # Helper: map job result to display string + status() { + case "$1" in + success) echo "Pass" ;; + skipped) echo "Skipped (unchecked)" ;; + failure) echo "FAILED" ;; + cancelled) echo "Cancelled" ;; + *) echo "$1" ;; + esac + } + + echo "| C++ Android Backends | $(status '${{ needs.cpp-android.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| C++ iOS Backends | $(status '${{ needs.cpp-ios.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| Kotlin SDK (JVM + Android) | $(status '${{ needs.kotlin-sdk.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| Swift SDK | $(status '${{ needs.swift-sdk.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| Web SDK (TypeScript) | $(status '${{ needs.web-sdk.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| Flutter SDK (Dart) | $(status '${{ needs.flutter-sdk.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| React Native SDK | $(status '${{ needs.react-native-sdk.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| Android Example Apps | $(status '${{ needs.android-apps.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "| IntelliJ Plugin | $(status '${{ needs.intellij-plugin.result }}') |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY - echo "Triggered by: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY - echo "Commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "Triggered by: **${{ github.actor }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "Branch: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 69b837be9..544725c32 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,10 @@ + + + + \ No newline at end of file diff --git a/docs/impl/lora_adapter_support.md b/docs/impl/lora_adapter_support.md new file mode 100644 index 000000000..5c3c0a54f --- /dev/null +++ b/docs/impl/lora_adapter_support.md @@ -0,0 +1,700 @@ +# LoRA Adapter Support - Implementation Documentation + +## Table of Contents + +- [Overview](#overview) +- [Kotlin SDK Usage Guide](#kotlin-sdk-usage-guide) + - [Prerequisites](#prerequisites) + - [Data Types](#data-types) + - [Loading a LoRA Adapter](#loading-a-lora-adapter) + - [Stacking Multiple Adapters](#stacking-multiple-adapters) + - [Removing Adapters](#removing-adapters) + - [Querying Loaded Adapters](#querying-loaded-adapters) + - [Error Handling](#error-handling) + - [Android ViewModel Example](#android-viewmodel-example) +- [C/C++ API Reference](#cc-api-reference-for-other-sdk-implementations) + - [Component API (Recommended)](#api-level-1-component-api-recommended) + - [Backend API (LlamaCPP-specific)](#api-level-2-backend-api-llamacpp-specific) + - [Vtable Integration](#vtable-integration-for-new-backends) + - [C Usage Example](#usage-example-c) + - [Swift Usage Example](#usage-example-swift----ios-sdk-pattern) + - [Return Codes Reference](#return-codes-reference) +- [Architecture](#architecture) + - [Layer Diagram](#layer-diagram) + - [Vtable Dispatch](#vtable-dispatch) +- [llama.cpp LoRA API (b8011)](#llamacpp-lora-api-b8011) +- [Optimizations and Design Decisions](#optimizations-and-design-decisions) + - [Context Recreation](#context-recreation) + - [KV Cache Invalidation](#kv-cache-invalidation) + - [Thread Safety](#thread-safety) + - [Duplicate Detection](#duplicate-detection) + - [Rollback on Failure](#rollback-on-failure) + - [Adapter Memory Lifecycle](#adapter-memory-lifecycle) +- [Files Changed](#files-changed) +- [How to Extend](#how-to-extend) +- [Build Verification](#build-verification) +- [Changelog](#changelog) + +--- + +## Overview + +LoRA (Low-Rank Adaptation) adapter support was added to the RunAnywhere SDK across +two modules: `sdk/runanywhere-commons` (C/C++) and `sdk/runanywhere-kotlin` (Kotlin +Multiplatform). This enables users to load fine-tuned LoRA adapters (GGUF format) +alongside a base model, hot-swap adapters without reloading the base model, stack +multiple adapters with individual scales, and remove adapters at runtime. + +The implementation spans 6 layers, bottom-up: C++ internal, C API, component, +JNI bridge, Kotlin bridge, and Kotlin public API. + +--- + +## Kotlin SDK Usage Guide + +### Prerequisites + +Before using LoRA adapters: + +1. The RunAnywhere SDK must be initialized +2. The LlamaCPP backend must be registered +3. A base model must be loaded via `RunAnywhere.loadLLMModel()` +4. LoRA adapter files must be in GGUF format + +```kotlin +import com.runanywhere.sdk.public.RunAnywhere +import com.runanywhere.sdk.public.extensions.loadLoraAdapter +import com.runanywhere.sdk.public.extensions.removeLoraAdapter +import com.runanywhere.sdk.public.extensions.clearLoraAdapters +import com.runanywhere.sdk.public.extensions.getLoadedLoraAdapters +import com.runanywhere.sdk.public.extensions.LLM.LoRAAdapterConfig +import com.runanywhere.sdk.public.extensions.LLM.LoRAAdapterInfo +``` + +### Data Types + +**LoRAAdapterConfig** -- Configuration passed when loading an adapter. + +```kotlin +data class LoRAAdapterConfig( + val path: String, // Path to the LoRA GGUF file (must not be blank) + val scale: Float = 1.0f, // Scale factor: 0.0 = no effect, 1.0 = full effect, >1.0 = amplified +) +``` + +**LoRAAdapterInfo** -- Read-only info returned when querying loaded adapters. + +```kotlin +data class LoRAAdapterInfo( + val path: String, // Path used when loading + val scale: Float, // Active scale factor + val applied: Boolean, // Whether the adapter is currently applied to the context +) +``` + +### Loading a LoRA Adapter + +Load a GGUF LoRA file and apply it to the current model. The SDK recreates the +llama.cpp context internally and clears the KV cache. + +```kotlin +// Load with default scale (1.0) +RunAnywhere.loadLoraAdapter(LoRAAdapterConfig(path = "/path/to/adapter.gguf")) + +// Load with custom scale (0.5 = half strength) +RunAnywhere.loadLoraAdapter( + LoRAAdapterConfig(path = "/path/to/adapter.gguf", scale = 0.5f) +) +``` + +All functions are `suspend` -- call them from a coroutine scope. + +### Stacking Multiple Adapters + +Multiple adapters can be applied simultaneously. Each adapter has its own scale. +The effects combine additively at the weight level. + +```kotlin +// Load base writing style adapter +RunAnywhere.loadLoraAdapter( + LoRAAdapterConfig(path = "/path/to/style.gguf", scale = 1.0f) +) + +// Stack a domain knowledge adapter on top +RunAnywhere.loadLoraAdapter( + LoRAAdapterConfig(path = "/path/to/domain.gguf", scale = 0.7f) +) + +// Check what's loaded +val adapters = RunAnywhere.getLoadedLoraAdapters() +// adapters.size == 2 +``` + +### Removing Adapters + +```kotlin +// Remove a specific adapter by path +RunAnywhere.removeLoraAdapter("/path/to/style.gguf") + +// Remove all adapters at once +RunAnywhere.clearLoraAdapters() +``` + +After removal, the context is recreated and KV cache is cleared. Any remaining +adapters are re-applied automatically. + +### Querying Loaded Adapters + +```kotlin +val adapters: List = RunAnywhere.getLoadedLoraAdapters() + +for (adapter in adapters) { + println("Path: ${adapter.path}") + println("Scale: ${adapter.scale}") + println("Applied: ${adapter.applied}") +} +``` + +Returns an empty list if no adapters are loaded or if no model is loaded. + +### Error Handling + +All LoRA functions throw `SDKError` on failure: + +```kotlin +try { + RunAnywhere.loadLoraAdapter(LoRAAdapterConfig(path = "/invalid/path.gguf")) +} catch (e: SDKError) { + // SDKError.notInitialized -- SDK not initialized + // SDKError.llm -- C++ operation failed (bad path, incompatible adapter, etc.) + println("LoRA error: ${e.message}") +} +``` + +Common failure causes: +- SDK not initialized (`SDKError.notInitialized`) +- No model loaded (`SDKError.llm` with "no model loaded") +- Invalid adapter file or path (`SDKError.llm`) +- Adapter already loaded with same path (`SDKError.llm` with duplicate detection) +- Adapter incompatible with base model (`SDKError.llm`) + +### Android ViewModel Example + +A typical Android integration pattern using ViewModel and Compose: + +```kotlin +class LlmViewModel : ViewModel() { + + data class UiState( + val modelLoaded: Boolean = false, + val loraAdapters: List = emptyList(), + val error: String? = null, + ) + + private val _state = MutableStateFlow(UiState()) + val state = _state.asStateFlow() + + fun loadLoraAdapter(path: String, scale: Float = 1.0f) { + viewModelScope.launch { + try { + RunAnywhere.loadLoraAdapter(LoRAAdapterConfig(path, scale)) + refreshAdapterList() + } catch (e: SDKError) { + _state.update { it.copy(error = e.message) } + } + } + } + + fun clearAdapters() { + viewModelScope.launch { + RunAnywhere.clearLoraAdapters() + refreshAdapterList() + } + } + + private suspend fun refreshAdapterList() { + val adapters = RunAnywhere.getLoadedLoraAdapters() + _state.update { it.copy(loraAdapters = adapters) } + } +} +``` + +For a full working Android app, see `examples/android/RunAnyWhereLora/`. + +--- + +## C/C++ API Reference (for other SDK implementations) + +This section documents the C functions that back the JNI layer. Any language +that can call C functions (Swift, Python, Dart, Rust, C#, etc.) can use these +directly to implement LoRA support without going through JNI/Kotlin. + +There are two API levels to choose from: + +### API Level 1: Component API (Recommended) + +Header: `include/rac/features/llm/rac_llm_component.h` +Library: `librac_commons.so` / `RACommons.xcframework` + +These are the **high-level** functions. They handle mutex locking, service +lookup, and vtable dispatch internally. Use these unless you have a reason +to call the backend directly. + +```c +#include "rac/features/llm/rac_llm_component.h" + +// handle = the rac_handle_t returned by rac_llm_component_create() + +// ---- Load a LoRA adapter ---- +// Loads a GGUF LoRA file and applies it to the current model. +// Context is recreated internally. KV cache is cleared. +// Duplicate paths are rejected. +// +// Returns: RAC_SUCCESS, RAC_ERROR_INVALID_HANDLE, RAC_ERROR_INVALID_ARGUMENT, +// RAC_ERROR_COMPONENT_NOT_READY, RAC_ERROR_NOT_SUPPORTED, +// or backend-specific error code +rac_result_t rac_llm_component_load_lora( + rac_handle_t handle, // Component handle + const char* adapter_path, // Absolute path to LoRA .gguf file + float scale // 0.0 = no effect, 1.0 = full, >1.0 = amplified +); + +// ---- Remove a specific adapter ---- +// Removes the adapter that was loaded from the given path. +// Context is recreated and KV cache is cleared. +// +// Returns: RAC_SUCCESS, RAC_ERROR_NOT_FOUND, RAC_ERROR_COMPONENT_NOT_READY +rac_result_t rac_llm_component_remove_lora( + rac_handle_t handle, + const char* adapter_path // Must match the path used in load_lora +); + +// ---- Clear all adapters ---- +// Removes every loaded adapter. Safe to call with no adapters loaded. +// +// Returns: RAC_SUCCESS +rac_result_t rac_llm_component_clear_lora( + rac_handle_t handle +); + +// ---- Query loaded adapters ---- +// Returns a JSON array string describing all loaded adapters. +// Format: [{"path":"/path/to/file.gguf","scale":1.0,"applied":true}, ...] +// Caller MUST free the returned string with free(). +// +// Returns: RAC_SUCCESS, RAC_ERROR_COMPONENT_NOT_READY +rac_result_t rac_llm_component_get_lora_info( + rac_handle_t handle, + char** out_json // Output: heap-allocated JSON string +); +``` + +**JNI mapping** (for reference -- how the Kotlin bridge calls these): + +| JNI Function | C Function | Notes | +|---|---|---| +| `racLlmComponentLoadLora(long handle, String path, float scale)` | `rac_llm_component_load_lora(handle, path, scale)` | Returns `int` (0 = success) | +| `racLlmComponentRemoveLora(long handle, String path)` | `rac_llm_component_remove_lora(handle, path)` | Returns `int` | +| `racLlmComponentClearLora(long handle)` | `rac_llm_component_clear_lora(handle)` | Returns `int` | +| `racLlmComponentGetLoraInfo(long handle)` | `rac_llm_component_get_lora_info(handle, &json)` | Returns `String?` (JSON) | + +### API Level 2: Backend API (LlamaCPP-specific) + +Header: `include/rac/backends/rac_llm_llamacpp.h` +Library: `librac_backend_llamacpp.so` / `RABackendLLAMACPP.xcframework` + +These are **low-level** functions that talk directly to the LlamaCPP backend. +Use these if you want to bypass the component layer (e.g., building a custom +pipeline without the lifecycle manager). You must handle your own locking. + +```c +#include "rac/backends/rac_llm_llamacpp.h" + +// handle = the backend impl pointer (NOT the component handle). +// Obtained from rac_llm_service_t.impl after creating a service. + +// Load and apply a LoRA adapter. Context is recreated internally. +rac_result_t rac_llm_llamacpp_load_lora( + rac_handle_t handle, + const char* adapter_path, + float scale +); + +// Remove a specific adapter by path. +rac_result_t rac_llm_llamacpp_remove_lora( + rac_handle_t handle, + const char* adapter_path +); + +// Clear all adapters. +rac_result_t rac_llm_llamacpp_clear_lora( + rac_handle_t handle +); + +// Get adapter info as JSON. Caller must free(*out_json). +rac_result_t rac_llm_llamacpp_get_lora_info( + rac_handle_t handle, + char** out_json +); +``` + +### Vtable Integration (for new backends) + +If you are adding LoRA support to a different backend (not LlamaCPP), implement +these 4 function pointers in your `rac_llm_service_ops_t` vtable: + +```c +#include "rac/features/llm/rac_llm_service.h" + +typedef struct rac_llm_service_ops { + // ... existing ops (initialize, generate, generate_stream, etc.) ... + + // LoRA ops -- set to NULL if your backend doesn't support LoRA + rac_result_t (*load_lora)(void* impl, const char* adapter_path, float scale); + rac_result_t (*remove_lora)(void* impl, const char* adapter_path); + rac_result_t (*clear_lora)(void* impl); + rac_result_t (*get_lora_info)(void* impl, char** out_json); +} rac_llm_service_ops_t; +``` + +The component layer checks for NULL before calling. If your backend sets +these to NULL, calls return `RAC_ERROR_NOT_SUPPORTED`. + +### Usage Example (C) + +Complete example of loading a model and applying a LoRA adapter using the +component API: + +```c +#include "rac/core/rac_core.h" +#include "rac/backends/rac_llm_llamacpp.h" +#include "rac/features/llm/rac_llm_component.h" + +int main() { + // 1. Initialize SDK + rac_init(NULL); + rac_backend_llamacpp_register(); + + // 2. Create and load model via component + rac_handle_t component = 0; + rac_llm_component_create(&component); + rac_llm_component_load_model(component, "/path/to/model.gguf", + "my-model", "My Model", NULL); + + // 3. Load LoRA adapter (scale = 0.8) + rac_result_t r = rac_llm_component_load_lora( + component, "/path/to/adapter.gguf", 0.8f); + if (r != RAC_SUCCESS) { + printf("Failed to load LoRA: %s\n", rac_error_message(r)); + return 1; + } + + // 4. Stack a second adapter + rac_llm_component_load_lora(component, "/path/to/adapter2.gguf", 0.5f); + + // 5. Query what's loaded + char* json = NULL; + rac_llm_component_get_lora_info(component, &json); + if (json) { + printf("Adapters: %s\n", json); + // Output: [{"path":"/path/to/adapter.gguf","scale":0.8,"applied":true}, + // {"path":"/path/to/adapter2.gguf","scale":0.5,"applied":true}] + free(json); + } + + // 6. Generate text (adapters are applied automatically) + rac_llm_options_t opts = RAC_LLM_OPTIONS_DEFAULT; + rac_llm_result_t result = {0}; + rac_llm_component_generate(component, "Hello, world!", &opts, &result); + printf("Response: %s\n", result.text); + rac_llm_result_free(&result); + + // 7. Remove one adapter + rac_llm_component_remove_lora(component, "/path/to/adapter.gguf"); + + // 8. Clear all adapters + rac_llm_component_clear_lora(component); + + // 9. Cleanup + rac_llm_component_destroy(component); + rac_shutdown(); + return 0; +} +``` + +### Usage Example (Swift -- iOS SDK pattern) + +For Swift SDK implementers, the pattern would be: + +```swift +// The C functions are imported via CRACommons module +import CRACommons + +// Load adapter +let result = rac_llm_component_load_lora(componentHandle, path, scale) +guard result == RAC_SUCCESS else { + throw SDKError.llm("LoRA load failed: \(rac_error_message(result))") +} + +// Query adapters +var jsonPtr: UnsafeMutablePointer? = nil +rac_llm_component_get_lora_info(componentHandle, &jsonPtr) +if let json = jsonPtr { + let jsonString = String(cString: json) + free(json) + // Parse JSON string into Swift structs +} +``` + +### Return Codes Reference + +| Code | Constant | Meaning | +|------|----------|---------| +| 0 | `RAC_SUCCESS` | Operation succeeded | +| -1 | `RAC_ERROR_INVALID_HANDLE` | NULL or invalid component handle | +| -2 | `RAC_ERROR_INVALID_ARGUMENT` | NULL adapter_path | +| -236 | `RAC_ERROR_NOT_SUPPORTED` | Backend does not implement LoRA (vtable entry is NULL) | +| -230 | `RAC_ERROR_COMPONENT_NOT_READY` | No model loaded | +| -110 | `RAC_ERROR_MODEL_NOT_FOUND` | Adapter file path doesn't exist | +| -600+ | Backend-specific | Duplicate path, incompatible adapter, context recreation failure | + +--- + +## Architecture + +### Layer Diagram + +``` +Kotlin Public API (RunAnywhere.loadLoraAdapter) + | + v +Kotlin Bridge (CppBridgeLLM.loadLoraAdapter) + | + v +JNI Native (RunAnywhereBridge.racLlmComponentLoadLora) + | + v +Component C API (rac_llm_component_load_lora) + | + v [vtable dispatch: llm_service->ops->load_lora()] +Service Vtable (rac_llm_service_ops_t) + | + v +Backend C API (rac_llm_llamacpp_load_lora) + | + v +C++ Internal (LlamaCppTextGeneration::load_lora_adapter) + | + v +llama.cpp API (llama_adapter_lora_init + llama_set_adapter_lora) +``` + +Each layer only talks to the one directly below it. No layer skips. + +### Vtable Dispatch + +The component layer (`llm_component.cpp`) does NOT directly call backend-specific +functions. Instead, it dispatches through the `rac_llm_service_ops_t` vtable: + +```c +// Component dispatches through vtable (backend-agnostic) +auto* llm_service = reinterpret_cast(service); +if (!llm_service->ops || !llm_service->ops->load_lora) + return RAC_ERROR_NOT_SUPPORTED; +return llm_service->ops->load_lora(llm_service->impl, adapter_path, scale); +``` + +The llamacpp backend registers its LoRA vtable entries during service creation +in `rac_backend_llamacpp_register.cpp`. Backends that do not support LoRA leave +these pointers as NULL, and the component returns `RAC_ERROR_NOT_SUPPORTED`. + +This keeps `librac_commons.so` decoupled from `librac_backend_llamacpp.so`. + +--- + +## llama.cpp LoRA API (b8011) + +The implementation uses these llama.cpp functions: + +| Function | Purpose | +|----------|---------| +| `llama_adapter_lora_init(model, path)` | Load adapter tensors from GGUF file | +| `llama_set_adapter_lora(ctx, adapter, scale)` | Apply adapter to context with scale | +| `llama_rm_adapter_lora(ctx, adapter)` | Remove specific adapter from context | +| `llama_clear_adapter_lora(ctx)` | Remove all adapters from context | +| `llama_memory_clear(memory, true)` | Clear KV cache after adapter changes | + +Note: `llama_adapter_lora_free()` is deprecated. Adapters are freed automatically +when the model is freed. + +--- + +## Optimizations and Design Decisions + +### Context Recreation + +llama.cpp requires all adapters to be loaded before context creation. When a new +adapter is loaded after the model is already running (context exists), the +implementation recreates the context: + +1. Free old context and sampler +2. Create new context with same parameters (context_size, num_threads) +3. Rebuild sampler chain (temperature, top_p, top_k, repetition penalty) +4. Re-apply ALL loaded adapters to the new context +5. Clear KV cache + +This is handled by `recreate_context()` + `apply_lora_adapters()` in +`llamacpp_backend.cpp`. The approach keeps things simple while ensuring +correctness -- adapter memory overhead is typically 1-5% of the base model, +so the cost of re-applying all adapters is negligible. + +### KV Cache Invalidation + +After any adapter change (load, remove, clear), the KV cache is always +cleared via `llama_memory_clear(llama_get_memory(context_), true)`. This is +mandatory because cached key-value pairs were computed with the previous +adapter configuration and would produce incorrect results. + +### Thread Safety + +All LoRA operations acquire the same mutex (`mtx_`) used by the text generation +inference loop. This guarantees that adapters are never modified while inference +is in progress. The lock hierarchy is: + +- C++ layer: `std::lock_guard` on `mtx_` (already used by generate) +- Component layer: `std::lock_guard` on `component->mtx` +- Kotlin bridge layer: `synchronized(lock)` on the CppBridgeLLM lock object + +### Duplicate Detection + +`load_lora_adapter()` checks for duplicate adapter paths before loading. If the +same path is already loaded, it returns an error instead of loading twice. + +### Rollback on Failure + +If context recreation fails after an adapter is loaded, the adapter entry is +popped from the `lora_adapters_` vector. Same if `apply_lora_adapters()` fails. +This prevents the tracking vector from going out of sync with actual context +state. + +### Adapter Memory Lifecycle + +Adapters are stored in a `std::vector` on the +`LlamaCppTextGeneration` instance. When `unload_model_internal()` is called, +adapters are cleared from the context first, then the vector is cleared, then +the context and model are freed. This ordering prevents use-after-free. + +--- + +## Files Changed + +### Layer 1: C++ Internal + +| File | Changes | +|------|---------| +| `sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.h` | Added `LoraAdapterEntry` struct, 4 public methods (`load_lora_adapter`, `remove_lora_adapter`, `clear_lora_adapters`, `get_lora_info`), 2 private helpers (`recreate_context`, `apply_lora_adapters`), `lora_adapters_` vector member | +| `sdk/runanywhere-commons/src/backends/llamacpp/llamacpp_backend.cpp` | Implemented 6 new methods. Modified `unload_model_internal()` to clear adapters before freeing context/model | + +### Layer 2: Backend C API + +| File | Changes | +|------|---------| +| `sdk/runanywhere-commons/include/rac/backends/rac_llm_llamacpp.h` | Added 4 C function declarations: `rac_llm_llamacpp_load_lora`, `rac_llm_llamacpp_remove_lora`, `rac_llm_llamacpp_clear_lora`, `rac_llm_llamacpp_get_lora_info` | +| `sdk/runanywhere-commons/src/backends/llamacpp/rac_llm_llamacpp.cpp` | Implemented 4 C functions. Pattern: validate handle, cast to impl, call C++ method, return result | + +### Layer 3: Vtable + Component Wrappers + +| File | Changes | +|------|---------| +| `sdk/runanywhere-commons/include/rac/features/llm/rac_llm_service.h` | Added 4 optional LoRA function pointers to `rac_llm_service_ops_t` vtable: `load_lora`, `remove_lora`, `clear_lora`, `get_lora_info` | +| `sdk/runanywhere-commons/include/rac/features/llm/rac_llm_component.h` | Added 4 component-level function declarations | +| `sdk/runanywhere-commons/src/features/llm/llm_component.cpp` | Implemented 4 component functions. Dispatches through vtable with NULL checks (returns `RAC_ERROR_NOT_SUPPORTED` if backend doesn't implement LoRA) | +| `sdk/runanywhere-commons/src/backends/llamacpp/rac_backend_llamacpp_register.cpp` | Added 4 vtable wrapper functions and wired them into `g_llamacpp_ops` | + +### Layer 4: JNI Bridge + +| File | Changes | +|------|---------| +| `sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` | Added 4 JNI functions: `racLlmComponentLoadLora`, `racLlmComponentRemoveLora`, `racLlmComponentClearLora`, `racLlmComponentGetLoraInfo` | + +### Layer 5: Kotlin Bridge + +| File | Changes | +|------|---------| +| `sdk/runanywhere-kotlin/src/jvmAndroidMain/.../RunAnywhereBridge.kt` | Added 4 `external` JNI method declarations | +| `sdk/runanywhere-kotlin/src/jvmAndroidMain/.../CppBridgeLLM.kt` | Added 4 bridge methods with synchronized access, state validation, and logging | + +### Layer 6: Kotlin Public API + +| File | Changes | +|------|---------| +| `sdk/runanywhere-kotlin/src/commonMain/.../LLMTypes.kt` | Added `LoRAAdapterConfig` and `LoRAAdapterInfo` data classes | +| `sdk/runanywhere-kotlin/src/commonMain/.../RunAnywhere+LoRA.kt` | NEW file. `expect` declarations for 4 public API functions | +| `sdk/runanywhere-kotlin/src/jvmAndroidMain/.../RunAnywhere+LoRA.jvmAndroid.kt` | NEW file. `actual` implementations with init checks, CppBridgeLLM delegation, JSON parsing for adapter info | + +--- + +## How to Extend + +### Adding a new LoRA operation + +Follow the same 6-layer pattern: + +1. Add C++ method to `LlamaCppTextGeneration` in `llamacpp_backend.h/.cpp` +2. Add C function to `rac_llm_llamacpp.h/.cpp` +3. Add vtable entry to `rac_llm_service_ops_t` in `rac_llm_service.h` +4. Wire vtable entry in `rac_backend_llamacpp_register.cpp` +5. Add component wrapper to `rac_llm_component.h` / `llm_component.cpp` (dispatch through vtable) +6. Add JNI function to `runanywhere_commons_jni.cpp` +7. Add external declaration to `RunAnywhereBridge.kt`, bridge method to `CppBridgeLLM.kt` +8. Add expect/actual declarations to `RunAnywhere+LoRA.kt` / `RunAnywhere+LoRA.jvmAndroid.kt` + +### Adding scale adjustment without reload + +Could be done by calling `llama_set_adapter_lora(ctx, adapter, new_scale)` +directly without context recreation. Would need a new method at each layer. + +--- + +## Build Verification + +Android native build (confirmed passing): +```bash +cd sdk/runanywhere-commons +./scripts/build-android.sh +``` + +C++ desktop build (confirmed passing): +```bash +cd sdk/runanywhere-commons +cmake -B build/dev -DRAC_BUILD_BACKENDS=ON -DRAC_BUILD_JNI=ON +cmake --build build/dev +``` + +After Android build, copy `.so` files to jniLibs: +```bash +DIST=sdk/runanywhere-commons/dist/android +JNILIBS=sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/src/androidMain/jniLibs/arm64-v8a +/usr/bin/cp $DIST/llamacpp/arm64-v8a/librac_backend_llamacpp.so $JNILIBS/ +/usr/bin/cp $DIST/llamacpp/arm64-v8a/librac_backend_llamacpp_jni.so $JNILIBS/ +/usr/bin/cp $DIST/llamacpp/arm64-v8a/librac_commons.so $JNILIBS/ +/usr/bin/cp $DIST/llamacpp/arm64-v8a/libc++_shared.so $JNILIBS/ +/usr/bin/cp $DIST/llamacpp/arm64-v8a/libomp.so $JNILIBS/ +/usr/bin/cp $DIST/jni/arm64-v8a/librunanywhere_jni.so $JNILIBS/ +``` + +Kotlin build: +```bash +cd sdk/runanywhere-kotlin +./scripts/sdk.sh build +``` + +--- + +## Changelog + +| Date | Author | Description | +|------|--------|-------------| +| 2026-02-19 | Claude | Initial implementation of LoRA adapter support across all 6 layers (C++ through Kotlin public API). C++ desktop build verified. | +| 2026-02-19 | Claude | Fixed architecture: Component layer now dispatches LoRA ops through vtable (`rac_llm_service_ops_t`) instead of calling backend directly. This decouples `librac_commons.so` from `librac_backend_llamacpp.so`. Added 4 vtable entries and wrapper functions. Fixed `AttachCurrentThread` cast for Android NDK C++ build. Android native build verified. | +| 2026-02-19 | Claude | Added detailed Kotlin SDK usage guide with data types, code examples, error handling, Android ViewModel pattern, and table of contents with section links. Updated "How to Extend" to include vtable step. | diff --git a/examples/android/RunAnyWhereLora/.gitignore b/examples/android/RunAnyWhereLora/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/examples/android/RunAnyWhereLora/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/examples/android/RunAnyWhereLora/.idea/.gitignore b/examples/android/RunAnyWhereLora/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/examples/android/RunAnyWhereLora/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/examples/android/RunAnyWhereLora/.idea/.name b/examples/android/RunAnyWhereLora/.idea/.name new file mode 100644 index 000000000..16e1c7c63 --- /dev/null +++ b/examples/android/RunAnyWhereLora/.idea/.name @@ -0,0 +1 @@ +RunAnyWhere-Lora \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/.idea/AndroidProjectSystem.xml b/examples/android/RunAnyWhereLora/.idea/AndroidProjectSystem.xml new file mode 100644 index 000000000..4a53bee8c --- /dev/null +++ b/examples/android/RunAnyWhereLora/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/.idea/gradle.xml b/examples/android/RunAnyWhereLora/.idea/gradle.xml new file mode 100644 index 000000000..7505d8d39 --- /dev/null +++ b/examples/android/RunAnyWhereLora/.idea/gradle.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/.idea/misc.xml b/examples/android/RunAnyWhereLora/.idea/misc.xml new file mode 100644 index 000000000..c2b3ddced --- /dev/null +++ b/examples/android/RunAnyWhereLora/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/.idea/runConfigurations.xml b/examples/android/RunAnyWhereLora/.idea/runConfigurations.xml new file mode 100644 index 000000000..16660f1d8 --- /dev/null +++ b/examples/android/RunAnyWhereLora/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/.gitignore b/examples/android/RunAnyWhereLora/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/build.gradle.kts b/examples/android/RunAnyWhereLora/app/build.gradle.kts new file mode 100644 index 000000000..2a4100bb1 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/build.gradle.kts @@ -0,0 +1,120 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.runanywhere.run_anywhere_lora" + compileSdk = 36 + + defaultConfig { + applicationId = "com.runanywhere.run_anywhere_lora" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters += listOf("arm64-v8a", "x86_64") + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + packaging { + resources { + excludes += listOf( + "/META-INF/{AL2.0,LGPL2.1}", + "/META-INF/DEPENDENCIES", + "/META-INF/LICENSE", + "/META-INF/LICENSE.txt", + "/META-INF/NOTICE", + "/META-INF/NOTICE.txt", + "/META-INF/licenses/**", + "**/kotlin/**", + "kotlin/**", + "META-INF/kotlin/**", + "META-INF/*.kotlin_module", + "META-INF/INDEX.LIST", + ) + } + jniLibs { + useLegacyPackaging = true + pickFirsts += listOf("lib/**/*.so") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs += listOf( + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + ) + } + + buildFeatures { + compose = true + } +} + +dependencies { + // RunAnywhere SDK + LlamaCPP backend + implementation(project(":runanywhere-kotlin")) + implementation(project(":runanywhere-core-llamacpp")) + + // AndroidX Core & Lifecycle + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + + // Compose + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + // Testing + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + // Kotlin version constraints + constraints { + implementation("org.jetbrains.kotlin:kotlin-stdlib") { + version { strictly(libs.versions.kotlin.get()) } + } + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7") { + version { strictly(libs.versions.kotlin.get()) } + } + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") { + version { strictly(libs.versions.kotlin.get()) } + } + } +} diff --git a/examples/android/RunAnyWhereLora/app/proguard-rules.pro b/examples/android/RunAnyWhereLora/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/androidTest/java/com/runanywhere/run_anywhere_lora/ExampleInstrumentedTest.kt b/examples/android/RunAnyWhereLora/app/src/androidTest/java/com/runanywhere/run_anywhere_lora/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..089fdfb76 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/androidTest/java/com/runanywhere/run_anywhere_lora/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.runanywhere.run_anywhere_lora + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.runanywhere.run_anywhere_lora", appContext.packageName) + } +} \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/AndroidManifest.xml b/examples/android/RunAnyWhereLora/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7b264f3b7 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraApplication.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraApplication.kt new file mode 100644 index 000000000..705017e38 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraApplication.kt @@ -0,0 +1,72 @@ +package com.runanywhere.run_anywhere_lora + +import android.app.Application +import android.util.Log +import com.runanywhere.sdk.llm.llamacpp.LlamaCPP +import com.runanywhere.sdk.public.RunAnywhere +import com.runanywhere.sdk.public.SDKEnvironment +import com.runanywhere.sdk.storage.AndroidPlatformContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +sealed class SDKInitState { + data object Loading : SDKInitState() + data object Ready : SDKInitState() + data class Error(val error: Throwable) : SDKInitState() +} + +class LoraApplication : Application() { + + companion object { + private const val TAG = "LoraApp" + } + + private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + private val _initializationState = MutableStateFlow(SDKInitState.Loading) + val initializationState: StateFlow = _initializationState.asStateFlow() + + override fun onCreate() { + super.onCreate() + Log.i(TAG, "App launched, initializing SDK...") + applicationScope.launch(Dispatchers.IO) { + delay(200) + initializeSDK() + } + } + + override fun onTerminate() { + applicationScope.cancel() + super.onTerminate() + } + + private suspend fun initializeSDK() { + try { + AndroidPlatformContext.initialize(this@LoraApplication) + + RunAnywhere.initialize(environment = SDKEnvironment.DEVELOPMENT) + Log.i(TAG, "SDK initialized in DEVELOPMENT mode") + + kotlinx.coroutines.runBlocking { + RunAnywhere.completeServicesInitialization() + } + Log.i(TAG, "SDK services initialization complete") + + LlamaCPP.register(priority = 100) + Log.i(TAG, "LlamaCPP backend registered") + + _initializationState.value = SDKInitState.Ready + Log.i(TAG, "SDK ready") + } catch (e: Exception) { + Log.e(TAG, "SDK initialization failed: ${e.message}", e) + _initializationState.value = SDKInitState.Error(e) + } + } +} diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt new file mode 100644 index 000000000..b9f76c556 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraScreen.kt @@ -0,0 +1,530 @@ +package com.runanywhere.run_anywhere_lora + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun LoraScreen(viewModel: LoraViewModel = viewModel()) { + val state by viewModel.uiState.collectAsState() + val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + // Track pending LoRA file path for scale dialog + var pendingLoraPath by remember { mutableStateOf(null) } + var loraScale by remember { mutableFloatStateOf(1.0f) } + + // Storage permission state + var hasStoragePermission by remember { + mutableStateOf( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + true + } + ) + } + + // Permission launcher for Android 11+ + val storagePermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { + hasStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + true + } + } + + // Legacy permission launcher for Android < 11 + val legacyPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + hasStoragePermission = granted + } + + // File picker for model + val modelFilePicker = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri: Uri? -> + uri?.let { resolveFilePath(context, it) }?.let { path -> + viewModel.loadModel(path) + } + } + + // File picker for LoRA adapter + val loraFilePicker = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri: Uri? -> + uri?.let { resolveFilePath(context, it) }?.let { path -> + pendingLoraPath = path + } + } + + // Show errors as snackbar + LaunchedEffect(state.error) { + state.error?.let { + snackbarHostState.showSnackbar(it) + viewModel.clearError() + } + } + + // Auto-scroll when answer updates + LaunchedEffect(state.answer) { + scrollState.animateScrollTo(scrollState.maxValue) + } + + // LoRA scale dialog + if (pendingLoraPath != null) { + LoraScaleDialog( + filename = pendingLoraPath!!.substringAfterLast('/'), + scale = loraScale, + onScaleChange = { loraScale = it }, + onConfirm = { + viewModel.loadLoraAdapter(pendingLoraPath!!, loraScale) + pendingLoraPath = null + loraScale = 1.0f + }, + onDismiss = { + pendingLoraPath = null + loraScale = 1.0f + }, + ) + } + + fun requestStoragePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { + data = Uri.parse("package:${context.packageName}") + } + storagePermissionLauncher.launch(intent) + } else { + legacyPermissionLauncher.launch(android.Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("RunAnywhere LoRA") }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .imePadding(), + ) { + // Status section + StatusSection(state) + + // Response card (fills available space) + Card( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 12.dp, vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(12.dp), + ) { + if (state.answer.isEmpty() && !state.isGenerating) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = if (state.modelLoaded) { + "Ask a question below" + } else { + "Load a model to get started" + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + ) + } + } else { + SelectionContainer( + modifier = Modifier + .weight(1f) + .verticalScroll(scrollState), + ) { + Text( + text = state.answer, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + // Metrics row + state.metrics?.let { metrics -> + HorizontalDivider( + modifier = Modifier.padding(vertical = 6.dp), + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "%.1f tok/s".format(metrics.tokensPerSecond), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + Text( + text = "${metrics.totalTokens} tokens", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + Text( + text = "%.1fs".format(metrics.latencyMs / 1000.0), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + } + } + + // Loading indicator + if (state.isGenerating && state.answer.isEmpty()) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(modifier = Modifier.size(32.dp)) + } + } + } + } + } + + // Bottom section: action chips + input + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 6.dp), + ) { + // Action chips row + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + AssistChip( + onClick = { + if (!hasStoragePermission) { + requestStoragePermission() + } else { + modelFilePicker.launch(arrayOf("*/*")) + } + }, + label = { Text("Model", maxLines = 1) }, + leadingIcon = { + if (state.modelLoading) { + CircularProgressIndicator( + modifier = Modifier.size(AssistChipDefaults.IconSize), + strokeWidth = 2.dp, + ) + } else { + Icon( + Icons.Default.FolderOpen, + contentDescription = "Load model", + modifier = Modifier.size(AssistChipDefaults.IconSize), + ) + } + }, + enabled = !state.modelLoading, + ) + + AssistChip( + onClick = { + if (!hasStoragePermission) { + requestStoragePermission() + } else { + loraFilePicker.launch(arrayOf("*/*")) + } + }, + label = { Text("LoRA", maxLines = 1) }, + leadingIcon = { + Icon( + Icons.Default.Add, + contentDescription = "Load LoRA", + modifier = Modifier.size(AssistChipDefaults.IconSize), + ) + }, + enabled = state.modelLoaded && !state.isGenerating, + ) + + AnimatedVisibility(visible = state.loraAdapters.isNotEmpty()) { + AssistChip( + onClick = { viewModel.clearLoraAdapters() }, + label = { Text("Clear", maxLines = 1) }, + leadingIcon = { + Icon( + Icons.Default.Clear, + contentDescription = "Clear LoRA", + modifier = Modifier.size(AssistChipDefaults.IconSize), + ) + }, + enabled = !state.isGenerating, + ) + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Input row + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Bottom, + ) { + OutlinedTextField( + value = state.question, + onValueChange = { viewModel.updateQuestion(it) }, + modifier = Modifier.weight(1f), + placeholder = { Text("Ask a question...") }, + maxLines = 3, + shape = MaterialTheme.shapes.medium, + ) + Spacer(modifier = Modifier.width(8.dp)) + IconButton( + onClick = { + if (state.isGenerating) { + viewModel.cancelGeneration() + } else { + viewModel.askQuestion() + } + }, + enabled = state.modelLoaded, + ) { + Icon( + imageVector = if (state.isGenerating) Icons.Default.Stop else Icons.Default.Send, + contentDescription = if (state.isGenerating) "Stop" else "Send", + tint = if (state.modelLoaded) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + }, + ) + } + } + } + } + } +} + +@Composable +private fun StatusSection(state: LoraUiState) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + ) { + // Model status + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Model: ", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + if (state.modelLoading) { + Text( + text = "Loading...", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.tertiary, + ) + } else if (state.modelPath != null) { + Text( + text = state.modelPath.substringAfterLast('/'), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + Text( + text = "None", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + ) + } + } + + // LoRA adapters status + if (state.loraAdapters.isNotEmpty()) { + for (adapter in state.loraAdapters) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "LoRA: ", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = "${adapter.path.substringAfterLast('/')} x${adapter.scale}", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@Composable +private fun LoraScaleDialog( + filename: String, + scale: Float, + onScaleChange: (Float) -> Unit, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Load LoRA Adapter") }, + text = { + Column { + Text( + text = filename, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Scale: %.2f".format(scale), + style = MaterialTheme.typography.labelMedium, + ) + Slider( + value = scale, + onValueChange = onScaleChange, + valueRange = 0f..2f, + steps = 19, + ) + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { Text("Load") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Cancel") } + }, + ) +} + +/** + * Resolve a content URI to a real file path. + * With MANAGE_EXTERNAL_STORAGE, we can access files directly. + */ +private fun resolveFilePath(context: android.content.Context, uri: Uri): String? { + // Try to get the file path from the URI directly + if (uri.scheme == "file") { + return uri.path + } + + // For content:// URIs, try to resolve via cursor + try { + context.contentResolver.query(uri, arrayOf("_data"), null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val idx = cursor.getColumnIndex("_data") + if (idx >= 0) { + val path = cursor.getString(idx) + if (path != null) return path + } + } + } + } catch (_: Exception) { + // Fall through to copy approach + } + + // Fallback: copy to app cache and return that path + try { + val filename = uri.lastPathSegment?.substringAfterLast('/') ?: "model.gguf" + val cacheFile = java.io.File(context.cacheDir, filename) + context.contentResolver.openInputStream(uri)?.use { input -> + cacheFile.outputStream().use { output -> + input.copyTo(output) + } + } + return cacheFile.absolutePath + } catch (e: Exception) { + android.util.Log.e("LoraScreen", "Failed to resolve file path: ${e.message}", e) + return null + } +} diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt new file mode 100644 index 000000000..7950460a5 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/LoraViewModel.kt @@ -0,0 +1,235 @@ +package com.runanywhere.run_anywhere_lora + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.runanywhere.sdk.core.types.InferenceFramework +import com.runanywhere.sdk.foundation.bridge.extensions.CppBridgeModelRegistry +import com.runanywhere.sdk.public.RunAnywhere +import com.runanywhere.sdk.public.extensions.LLM.LLMGenerationOptions +import com.runanywhere.sdk.public.extensions.LLM.LoRAAdapterConfig +import com.runanywhere.sdk.public.extensions.LLM.LoRAAdapterInfo +import com.runanywhere.sdk.public.extensions.cancelGeneration +import com.runanywhere.sdk.public.extensions.clearLoraAdapters +import com.runanywhere.sdk.public.extensions.generateStreamWithMetrics +import com.runanywhere.sdk.public.extensions.getLoadedLoraAdapters +import com.runanywhere.sdk.public.extensions.isLLMModelLoaded +import com.runanywhere.sdk.public.extensions.loadLLMModel +import com.runanywhere.sdk.public.extensions.loadLoraAdapter +import com.runanywhere.sdk.public.extensions.registerModel +import com.runanywhere.sdk.public.extensions.removeLoraAdapter +import com.runanywhere.sdk.public.extensions.unloadLLMModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class GenerationMetrics( + val tokensPerSecond: Double, + val totalTokens: Int, + val latencyMs: Double, +) + +data class LoraUiState( + val modelPath: String? = null, + val modelLoaded: Boolean = false, + val modelLoading: Boolean = false, + val loraAdapters: List = emptyList(), + val question: String = "", + val answer: String = "", + val isGenerating: Boolean = false, + val metrics: GenerationMetrics? = null, + val error: String? = null, +) + +class LoraViewModel : ViewModel() { + + companion object { + private const val TAG = "LoraVM" + } + + private val _uiState = MutableStateFlow(LoraUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var generationJob: Job? = null + + fun updateQuestion(text: String) { + _uiState.update { it.copy(question = text) } + } + + fun clearError() { + _uiState.update { it.copy(error = null) } + } + + fun loadModel(path: String) { + viewModelScope.launch { + _uiState.update { it.copy(modelLoading = true, error = null) } + try { + // Unload existing model if loaded + if (RunAnywhere.isLLMModelLoaded()) { + RunAnywhere.unloadLLMModel() + } + + // Generate a model ID from filename + val filename = path.substringAfterLast('/') + val modelId = filename.removeSuffix(".gguf") + + // Register the model in the SDK registry + RunAnywhere.registerModel( + id = modelId, + name = filename, + url = "file://$path", + framework = InferenceFramework.LLAMA_CPP, + ) + + // Tell the C++ registry the file is already local at this path + CppBridgeModelRegistry.updateDownloadStatus(modelId, path) + + // Load the model + withContext(Dispatchers.IO) { + RunAnywhere.loadLLMModel(modelId) + } + + _uiState.update { + it.copy( + modelPath = path, + modelLoaded = true, + modelLoading = false, + loraAdapters = emptyList(), + ) + } + Log.i(TAG, "Model loaded: $filename") + } catch (e: Exception) { + Log.e(TAG, "Failed to load model: ${e.message}", e) + _uiState.update { + it.copy( + modelLoading = false, + error = "Failed to load model: ${e.message}", + ) + } + } + } + } + + fun loadLoraAdapter(path: String, scale: Float) { + viewModelScope.launch { + _uiState.update { it.copy(error = null) } + try { + withContext(Dispatchers.IO) { + RunAnywhere.loadLoraAdapter(LoRAAdapterConfig(path = path, scale = scale)) + } + refreshAdapters() + Log.i(TAG, "LoRA adapter loaded: ${path.substringAfterLast('/')} (scale=$scale)") + } catch (e: Exception) { + Log.e(TAG, "Failed to load LoRA adapter: ${e.message}", e) + _uiState.update { it.copy(error = "Failed to load LoRA: ${e.message}") } + } + } + } + + fun removeLoraAdapter(path: String) { + viewModelScope.launch { + try { + withContext(Dispatchers.IO) { + RunAnywhere.removeLoraAdapter(path) + } + refreshAdapters() + } catch (e: Exception) { + _uiState.update { it.copy(error = "Failed to remove LoRA: ${e.message}") } + } + } + } + + fun clearLoraAdapters() { + viewModelScope.launch { + try { + withContext(Dispatchers.IO) { + RunAnywhere.clearLoraAdapters() + } + _uiState.update { it.copy(loraAdapters = emptyList()) } + } catch (e: Exception) { + _uiState.update { it.copy(error = "Failed to clear LoRA: ${e.message}") } + } + } + } + + fun askQuestion() { + val question = _uiState.value.question.trim() + if (question.isEmpty()) return + if (!_uiState.value.modelLoaded) { + _uiState.update { it.copy(error = "Load a model first") } + return + } + + generationJob?.cancel() + generationJob = viewModelScope.launch { + _uiState.update { + it.copy( + answer = "", + isGenerating = true, + metrics = null, + error = null, + ) + } + + try { + val result = withContext(Dispatchers.IO) { + RunAnywhere.generateStreamWithMetrics( + prompt = question, + options = LLMGenerationOptions( + maxTokens = 1024, + temperature = 0.7f, + ), + ) + } + + // Collect streaming tokens + result.stream.collect { token -> + _uiState.update { it.copy(answer = it.answer + token) } + } + + // Get final metrics + val finalResult = result.result.await() + _uiState.update { + it.copy( + isGenerating = false, + metrics = GenerationMetrics( + tokensPerSecond = finalResult.tokensPerSecond, + totalTokens = finalResult.tokensUsed, + latencyMs = finalResult.latencyMs, + ), + ) + } + } catch (e: Exception) { + Log.e(TAG, "Generation failed: ${e.message}", e) + _uiState.update { + it.copy( + isGenerating = false, + error = "Generation failed: ${e.message}", + ) + } + } + } + } + + fun cancelGeneration() { + generationJob?.cancel() + RunAnywhere.cancelGeneration() + _uiState.update { it.copy(isGenerating = false) } + } + + private suspend fun refreshAdapters() { + try { + val adapters = withContext(Dispatchers.IO) { + RunAnywhere.getLoadedLoraAdapters() + } + _uiState.update { it.copy(loraAdapters = adapters) } + } catch (e: Exception) { + Log.e(TAG, "Failed to refresh adapters: ${e.message}", e) + } + } +} diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/MainActivity.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/MainActivity.kt new file mode 100644 index 000000000..d06422964 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/MainActivity.kt @@ -0,0 +1,85 @@ +package com.runanywhere.run_anywhere_lora + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.runanywhere.run_anywhere_lora.ui.theme.RunAnyWhereLoraTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + RunAnyWhereLoraTheme { + val app = application as LoraApplication + val sdkState by app.initializationState.collectAsState() + when (sdkState) { + is SDKInitState.Loading -> LoadingIntroScreen() + is SDKInitState.Error -> ErrorScreen( + error = (sdkState as SDKInitState.Error).error, + ) + is SDKInitState.Ready -> LoraScreen() + } + } + } + } +} + +@Composable +private fun LoadingIntroScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Initializing SDK...", + style = MaterialTheme.typography.bodyLarge, + ) + } + } +} +@Composable +private fun ErrorScreen(error: Throwable) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "SDK initialization failed", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error.message ?: "Unknown error", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + ) + Spacer(modifier = Modifier.height(16.dp)) + TextButton(onClick = { /* Would need app reference to retry */ }) { + Text("Retry") + } + } + } +} diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Color.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Color.kt new file mode 100644 index 000000000..7ccee9fa0 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.runanywhere.run_anywhere_lora.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Theme.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Theme.kt new file mode 100644 index 000000000..14830842d --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.runanywhere.run_anywhere_lora.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun RunAnyWhereLoraTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Type.kt b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Type.kt new file mode 100644 index 000000000..ef93ea6e2 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/java/com/runanywhere/run_anywhere_lora/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.runanywhere.run_anywhere_lora.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_background.xml b/examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_foreground.xml b/examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/android/RunAnyWhereLora/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/values/colors.xml b/examples/android/RunAnyWhereLora/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/values/strings.xml b/examples/android/RunAnyWhereLora/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..af52595ad --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RunAnyWhere-Lora + \ No newline at end of file diff --git a/examples/android/RunAnyWhereLora/app/src/main/res/values/themes.xml b/examples/android/RunAnyWhereLora/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..5d82f6825 --- /dev/null +++ b/examples/android/RunAnyWhereLora/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +