Skip to content

Conversation

@sanchitmonga22
Copy link
Contributor

@sanchitmonga22 sanchitmonga22 commented Dec 19, 2025

  • Refactored event handling to replace EventBus with EventPublisher for improved event tracking and analytics integration.
  • Deleted obsolete files and streamlined the SDK structure for better maintainability.

Description

Brief description of the changes made.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring

Testing

  • Tests pass locally
  • Tested on Macbook if swift changes
  • Tested on Tablet/iPad if swift changes
  • Added/updated tests for changes

Labels

Please add the appropriate label(s):

  • iOS SDK - Changes to iOS/Swift SDK
  • Android SDK - Changes to Android/Kotlin SDK
  • iOS Sample - Changes to iOS example app
  • Android Sample - Changes to Android example app

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Documentation updated (if needed)

Screenshots - Attach all the relevant UI changes screenshots for iOS/Android and MacOS/Tablet/large screen sizes

Summary by CodeRabbit

  • Removed Features

    • Removed MLC-LLM on-device language model support.
    • Removed voice agent pipeline (integrated VAD, STT, LLM, TTS processing).
    • Removed wake word detection and vision language model capabilities.
    • Removed direct audio capture and file management APIs from public interface.
  • Refactored Architecture

    • Reorganized STT, TTS, VAD modules for improved modularity.
    • Enhanced code quality with Detekt and Ktlint analysis tools.

✏️ Tip: You can customize this high-level summary in your review settings.

Greptile Summary

This PR refactors event handling by introducing EventPublisher as a centralized event router that replaces direct EventBus.publish() calls throughout the SDK. The new architecture provides intelligent event routing based on destination:

Key Changes:

  • EventPublisher acts as the single entry point for all SDK events via EventPublisher.track()
  • EventDestination enum controls routing: PUBLIC_ONLY → app developers via EventBus, ANALYTICS_ONLY → backend telemetry, ALL → both destinations (default)
  • EventBus remains the public API for app developers but now receives events through EventPublisher's routing logic
  • SDKEventAnalyticsAdapter bridges SDKEvent to AnalyticsEvent for the analytics queue
  • All SDK components (STT, TTS, LLM, VAD) updated to use EventPublisher.track() instead of EventBus.publish()

Architecture Improvements:

  • Unified event tracking with consistent routing across all components
  • Better separation of concerns: EventPublisher handles routing, EventBus handles pub/sub
  • Analytics integration is now centralized and configured during SDK initialization
  • Event destination control allows fine-grained event filtering (e.g., high-frequency events can be analytics-only)

Documentation Added:

  • Comprehensive task notes documenting the refactor rationale and implementation
  • Clear guidance on EventPublisher usage patterns

The refactor maintains backward compatibility with existing EventBus subscriptions while providing the foundation for improved analytics tracking and event management.

Confidence Score: 4/5

  • This PR is safe to merge with low risk - the refactor is well-architected and maintains backward compatibility
  • The refactor follows clean architecture principles and maintains backward compatibility. EventBus public API remains unchanged. The routing logic is straightforward with clear destination-based control. However, given the scope of changes across 498 files and the critical nature of event handling, thorough integration testing is recommended before merging to production.
  • Verify that all example applications properly initialize EventPublisher and that analytics events flow correctly in production environments

Important Files Changed

Filename Overview
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/infrastructure/events/EventPublisher.kt New event router that unifies event distribution to both public EventBus and analytics backend based on event destination
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/infrastructure/events/EventBus.kt Refactored to use internal publish methods called by EventPublisher rather than direct component usage
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/infrastructure/events/SDKEvent.kt Added EventDestination enum and unified SDKEvent interface with routing capabilities for analytics integration
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt Updated to initialize EventPublisher with analytics routing during SDK bootstrap phase
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt Refactored SDK initialization flow and simplified application setup with cleaner state management
SHARED_TASK_NOTES.md Added comprehensive documentation for event handling refactor and implementation guidance

Sequence Diagram

sequenceDiagram
    participant Component as SDK Component<br/>(LLM, STT, TTS, VAD)
    participant EventPublisher as EventPublisher<br/>(Router)
    participant EventBus as EventBus<br/>(Public)
    participant Adapter as SDKEventAnalyticsAdapter
    participant AnalyticsQueue as AnalyticsQueueManager
    participant TelemetryRepo as TelemetryRepository<br/>(Backend)

    Note over Component,TelemetryRepo: SDK Initialization Phase
    Component->>EventPublisher: EventPublisher.initializeWithSDKEventRouting(telemetryRepo)
    EventPublisher->>EventPublisher: Create AnalyticsEnqueuer lambda
    EventPublisher-->>Component: Initialized

    Note over Component,TelemetryRepo: Event Tracking Flow
    Component->>EventPublisher: EventPublisher.track(LLMEvent.GenerationCompleted)
    
    alt destination == PUBLIC_ONLY or ALL
        EventPublisher->>EventBus: EventBus.publishSDKEvent(event)
        EventBus->>EventBus: Route to typed publishers<br/>(generationEvents, modelEvents, etc.)
        EventBus-->>Component: Event published to app
    end

    alt destination == ANALYTICS_ONLY or ALL
        EventPublisher->>Adapter: Wrap SDKEvent as AnalyticsEvent
        Adapter->>AnalyticsQueue: AnalyticsQueueManager.enqueue(analyticsEvent)
        AnalyticsQueue->>AnalyticsQueue: Add to queue<br/>(batch when size >= 50)
        
        alt Queue threshold reached or flush timer
            AnalyticsQueue->>TelemetryRepo: Send batch to backend
            TelemetryRepo-->>AnalyticsQueue: Success/Failure
        end
    end

    Note over Component,TelemetryRepo: EventDestination Controls Routing:<br/>• PUBLIC_ONLY → EventBus only<br/>• ANALYTICS_ONLY → Analytics only<br/>• ALL → Both destinations (default)
Loading

Context used:

  • Context from dashboard - CLAUDE.md (source)

- Refactored event handling to replace EventBus with EventPublisher for improved event tracking and analytics integration.
- Deleted obsolete files and streamlined the SDK structure for better maintainability.
@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Extensive refactoring consolidating Android implementations, reorganizing package structures from components.* to features.*, replacing LLMFramework with InferenceFramework, refactoring ModuleRegistry to factory-based registration, removing the MLC module entirely, deleting numerous platform-specific classes, and eliminating several SDK components.

Changes

Cohort / File(s) Summary
Module-Level Removals
sdk/runanywhere-kotlin/modules/runanywhere-llm-mlc/
Entire MLC module removed: build.gradle.kts, README.md, download_libs.sh, proguard-rules.pro, source files (MLCEngine.kt, MLCService.kt, MLCModule.kt, MLCProvider.kt) and associated Android implementations deleted.
Framework Type Migration
sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/src/commonMain/kotlin/com/runanywhere/sdk/llm/llamacpp/*, sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/src/commonMain/kotlin/com/runanywhere/sdk/core/onnx/*, sdk/runanywhere-kotlin/modules/runanywhere-whisperkit/src/commonMain/kotlin/com/runanywhere/whisperkit/*
Updated LLMFramework to InferenceFramework across adapter and provider classes; method signatures and framework properties updated to use new enum type.
Package Reorganization (Components → Features)
sdk/runanywhere-kotlin/modules/runanywhere-whisperkit/src/*, sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/src/jvmAndroidMain/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/*
Moved imports and implementations from com.runanywhere.sdk.components.stt, .tts, .vad to com.runanywhere.sdk.features.stt, .tts, .vad; updated public method signatures and type references.
Android Platform Implementation Removals
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/audio/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/files/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/memory/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/network/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/storage/*
Deleted Android-specific classes: AndroidAudioCapture, AndroidAudioSession, FileManager, MemoryMonitor, AndroidNetworkChecker, AndroidSecureStorage; removed public APIs for audio capture, file I/O, memory queries, and network checks.
Repository & Database Updates
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/repositories/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/database/*
Refactored ConfigurationRepositoryImpl to remove NetworkService dependency; updated DAOs (ModelInfoDao, etc.) to use InferenceFramework instead of LLMFramework; added explicit DAO accessors in RunAnywhereDatabase; reformatted DAO queries with trailing commas.
Android Actual Implementations
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/services/deviceinfo/*, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/services/download/*
Removed entire Android-specific RunAnywhere singleton and device info collection logic; deleted download implementation files (AndroidDownloadImpl, AndroidSimpleDownloader from services/download).
Device Capability Models Removal
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/capabilities/device/*
Deleted: BatteryInfo, DeviceCapabilities, ProcessorInfo, HardwareConfiguration, DeviceInfo, related enums (BatteryState, ProcessorType, MemoryPressureLevel, ThermalState), and service interfaces (BatteryMonitorService, CapabilityAnalyzer, GPUDetector, HardwareCapabilityManager, NeuralEngineDetector, ProcessorDetector, ThermalMonitorService).
Component Framework Removals
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/components/*, sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/ComponentAdapter.kt
Deleted entire component implementations: VADComponent, VADModels, VLMComponent, VoiceAgentComponent, WakeWordComponent, WakeWordModels; removed ComponentAdapter, ComponentState, component-related error types; deleted AnalyticsTracker.
Build & Configuration
gradle/libs.versions.toml, sdk/runanywhere-kotlin/build.gradle.kts, sdk/runanywhere-kotlin/.editorconfig, sdk/runanywhere-kotlin/detekt.yml
Added ktlint and commons-compress dependencies; integrated Detekt and Ktlint plugins; created EditorConfig with ktlint rules and 250-char line limit; relaxed Detekt rule enforcement across multiple categories.
ModuleRegistry Refactor
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/ModuleRegistry.kt
Major restructure: replaced provider lists with PrioritizedProvider<T> wrappers; added factory-based registration (typealias factories for STT/LLM/TTS/VAD/SpeakerDiarization); introduced DEFAULT_PRIORITY constant; updated framework adapter management to use InferenceFramework; added createX factory methods; removed VLM and WakeWord provider concepts; added SpeakerDiarization provider support.
LLM Service Type Updates
sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/src/*/kotlin/com/runanywhere/sdk/llm/llamacpp/LlamaCppService.kt
Updated public method signatures: generate and streamGenerate now accept LLMGenerationOptions instead of RunAnywhereGenerationOptions; moved imports from com.runanywhere.sdk.components.llm to com.runanywhere.sdk.features.llm.
ONNX & WhisperKit Provider Updates
sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/src/commonMain/kotlin/com/runanywhere/sdk/core/onnx/*, sdk/runanywhere-kotlin/modules/runanywhere-whisperkit/src/commonMain/kotlin/com/runanywhere/whisperkit/provider/*
Updated ONNXAdapter, ONNXServiceProvider, WhisperKitProvider framework types from LLMFramework to InferenceFramework; expanded LlamaCppServiceProvider.canHandle() with robust GGUF/GGML detection.
STT Service Additions
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/stt/AndroidAudioCaptureManager.kt
New Android implementation of AudioCaptureManager with permission handling, audio recording via AudioRecord, real-time audio level computation, and Flow<AudioChunk> emission; includes lifecycle methods (startRecording, stopRecording, cleanup).
TTS Service Updates
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/tts/AndroidTTSService.kt
Moved from components to features package; simplified Whisper parameter strategy to always use GREEDY; removed sensitivity-based strategy selection; updated default parameters (suppressBlank=true, suppressNonSpeechTokens=true, temperature=0.0f).
VAD Service Enhancements
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/vad/WebRTCVADService.kt, sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/core/onnx/ONNXServiceProviderImpl.kt
Added TTS feedback prevention for VAD: new isTTSActive flag, notifyTTSWillStart/DidFinish lifecycle hooks, setTTSThresholdMultiplier, startCalibration, getStatistics methods; moved WebRTC VAD to features package; lowered default energyThreshold from 0.022f to 0.015f.
Download Implementation
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/infrastructure/download/AndroidDownloadImpl.kt, sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/infrastructure/download/AndroidSimpleDownloader.kt
New Android download implementations: AndroidDownloadImpl delegates to AndroidSimpleDownloader with progress tracking; AndroidSimpleDownloader uses HttpURLConnection to download files with 8KB buffering and periodic progress callbacks.
Minor Formatting & Expression Bodies
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/**, sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/**
Widespread refactoring to expression-bodied functions, multi-line parameter lists with trailing commas, and reformatted constructors across repositories, services, and platform files; no behavioral changes.
Documentation & Task Notes
docs/KMP_REWRITE_TASK_NOTES.md
New documentation file detailing KMP SDK rewrite task status, completed phases (P1–P5, P7–P16, P18), build status, final integration points (EventPublisher to AnalyticsQueueManager wiring), and code snippets.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120–180 minutes

This PR involves extensive refactoring across multiple dimensions with high heterogeneity:

  • Massive deletions: Entire module (MLC), multiple Android platform implementations, component framework classes, device capability models, and analytics logic—each requires context-aware review to ensure nothing essential is lost.
  • Type system migration: LLMFrameworkInferenceFramework spread across service providers, adapters, database converters, and DAOs—requires tracing impact on all dependent code.
  • Package reorganization: Components → Features migration affects imports, public API signatures, and STT/TTS/VAD integration points—each location requires verification of consistency.
  • Registry pattern overhaul: ModuleRegistry refactored with factory-based registration, PrioritizedProvider wrappers, and new service lifecycle—complex control flow requiring careful validation of priority selection and factory creation paths.
  • New implementations: AndroidAudioCaptureManager, AndroidDownloadImpl, VAD TTS feedback logic, and WebRTC VAD enhancements—each introduces new public APIs and control flows.
  • Gradle/build changes: New plugins (Detekt, Ktlint), dependencies, and relaxed rule configuration—impact on CI/CD and code quality enforcement.

Areas requiring extra attention:

  • ModuleRegistry factory registration and provider selection logic (src/commonMain/kotlin/com/runanywhere/sdk/core/ModuleRegistry.kt): Verify priority-based provider selection, canHandle predicate evaluation, and factory instantiation paths for STT/LLM/TTS/VAD/SpeakerDiarization.
  • Framework type migration across LlamaCppServiceProvider, ONNXServiceProvider, ONNXAdapter, WhisperKitProvider: Ensure all references to InferenceFramework are correct and no stale LLMFramework usage remains.
  • Package reorganization for STT/TTS/VAD: Verify import paths and public API consistency across features modules, especially in AndroidAudioCaptureManager, service interfaces, and provider implementations.
  • AndroidAudioCaptureManager implementation: Review permission handling, AudioRecord lifecycle, audio level computation, and Flow emission correctness.
  • VAD TTS feedback mechanism in WebRTCVADService and ONNXServiceProviderImpl: Verify isTTSActive flag synchronization, threshold multiplier clamping, and statistics calculation logic.
  • Database and repository refactoring: Ensure InferenceFramework usage in DAOs and type converters aligns with model entity definitions; verify ConfigurationRepositoryImpl removal of NetworkService dependency does not break expected behavior.
  • Gradle build configuration: Confirm Detekt and Ktlint integration does not introduce CI failures; validate rule relaxation is intentional.
  • Deleted public APIs: FileManager, MemoryMonitor, NetworkChecker, AndroidAudioCapture, device capability models—ensure no SDK consumers depend on removed APIs.

Possibly related issues

Possibly related PRs

Suggested labels

kotlin-sdk, refactoring, breaking-change, multiplatform, arch-improvement

Poem

🐰 Hoppy refactoring times are here!
MLC module vanished without fear,
Frameworks realigned, packages rearranged,
Providers with factories—patterns changed!
Kotlin SDK springs into shape,
Cleaner APIs help us escape. 🌱

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.15% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description is mostly complete with clear sections for Description, Type of Change, Testing, Labels, and Checklist. However, several checklist items are unchecked despite the author claiming completion, and no screenshots are provided. Verify which checklist items should be marked as complete, particularly 'Tests pass locally', 'Added/updated tests', and 'Testing on Macbook/Tablet'. Clarify why these remain unchecked if the PR is ready for merge.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[Kotlin-SDK] [Kotlin-Example] KMP SDK refactor and aligning to iOS' is directly related to the main changes which involve KMP SDK restructuring, refactoring event handling, and aligning with iOS architecture.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Deleted the runanywhere-llm-mlc module and its associated files to streamline the SDK.
- Updated various service and model files to enhance maintainability and performance.
- Introduced new components for LLM, STT, TTS, and VAD functionalities, ensuring better integration and support for analytics.
- Improved event handling and resource management across the SDK.
…tainability

- Streamlined various service and model files, enhancing overall performance and maintainability.
- Updated event handling mechanisms to improve integration and analytics support.
- Refactored configuration and repository files to align with new architectural standards.
- Enhanced network services and data handling for better efficiency and reliability.
- Improved documentation and code structure across multiple components.
- Removed obsolete NetworkChecker implementation and refactored related files for improved maintainability.
- Introduced new DeviceIdentity and DeviceRegistration services for better device management.
- Enhanced event handling by replacing EventBus with EventPublisher across various components.
- Streamlined download services and added support for multipart data handling.
- Updated network configuration and HTTP client implementations for better performance and reliability.
…nator

- Deleted MemoryMonitor, MemoryService, AllocationManager, CacheEviction, and related memory management files to streamline the SDK.
- Added SyncCoordinator to manage synchronization operations across services, enhancing data consistency and performance.
- Updated ConfigurationLoader documentation for clarity on environment-specific config loading.
- Refactored ServiceContainer to integrate SyncCoordinator and improve service management.
- Updated LlamaCppServiceProvider to improve model handling with explicit checks for GGUF/GGML formats and common LLM patterns.
- Introduced structured output generation capabilities in LLM, including new classes for structured output handling, generation hints, and stream accumulation.
- Enhanced VAD services with TTS feedback prevention and debug statistics tracking, aligning with iOS implementations.
- Added new audio capture management components for both Android and JVM, improving audio processing capabilities.
- Refactored SDK error handling to include structured error codes for better consistency across platforms.
…cy and performance

- Updated InMemoryTelemetryDao to enhance event deletion methods with consistent locking mechanisms.
- Refactored AndroidAudioCaptureManager to improve audio recording flow and error handling.
- Streamlined audio capture management for both Android and JVM platforms, ensuring better integration and performance.
- Removed obsolete components and improved code structure across various files for maintainability.
…vements

- Introduced factory type aliases for STT, LLM, TTS, VAD, and Speaker Diarization services to streamline service creation.
- Added a new ServiceRegistration data class to manage service factory registrations with priority and handling capabilities.
- Enhanced ModuleRegistry with factory-based registration methods for STT, LLM, TTS, VAD, and Speaker Diarization services, aligning with iOS patterns.
- Updated SDKError to include a new ProviderNotFound error type for improved error handling.
- Removed the obsolete MockNetworkService class to clean up the codebase and improve maintainability.
- Refactored APIEndpoint to ensure consistency with iOS endpoint definitions and added environment-specific endpoint handling.
- Enhanced VAD and STT error handling to include new error types for better clarity and debugging.
@sanchitmonga22
Copy link
Contributor Author

@coderabbitai please do a detailed review

@sanchitmonga22
Copy link
Contributor Author

@greptile please do a detailed review

@coderabbitai
Copy link

coderabbitai bot commented Dec 21, 2025

@sanchitmonga22: I'll conduct a detailed review of the changes in this PR, focusing on:

  • The refactoring from EventBus to EventPublisher for event handling
  • The deletion of obsolete files and SDK structure improvements
  • Code quality, potential issues, and best practices
  • Documentation completeness for the KMP SDK

I'll analyze all the changes thoroughly and provide comprehensive feedback.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps
Copy link

greptile-apps bot commented Dec 21, 2025

Greptile's behavior is changing!

From now on, if a review finishes with no comments, we will not post an additional "statistics" comment to confirm that our review found nothing to comment on. However, you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

@sanchitmonga22
Copy link
Contributor Author

@greptile please do a detailed review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/platform/StoragePlatform.android.kt (2)

42-53: Add error handling for directory creation failures.

The mkdirs() call doesn't check whether directory creation succeeded. If it fails (e.g., due to insufficient permissions or disk space), the function still returns a path to a non-existent directory, which will cause IOException or FileNotFoundException when file operations are attempted later.

🔎 Proposed fix to add error handling
 actual fun getPlatformBaseDirectory(): String {
     val context = AndroidPlatformContext.applicationContext
 
     // Match iOS pattern: app-specific directory for SDK files
     // iOS: .applicationSupportDirectory
     // Android: filesDir/runanywhere
     val baseDir = context.filesDir.resolve("runanywhere")
     if (!baseDir.exists()) {
-        baseDir.mkdirs()
+        if (!baseDir.mkdirs()) {
+            throw IllegalStateException("Failed to create base directory: ${baseDir.absolutePath}")
+        }
     }
     return baseDir.absolutePath
 }

55-66: Add error handling for directory creation failures.

Same issue as getPlatformBaseDirectory(): the mkdirs() call doesn't verify success, risking later file operation failures.

🔎 Proposed fix to add error handling
 actual fun getPlatformTempDirectory(): String {
     val context = AndroidPlatformContext.applicationContext
 
     // Match iOS pattern: temporary directory
     // iOS: .temporaryDirectory
     // Android: cacheDir/runanywhere-temp
     val tempDir = context.cacheDir.resolve("runanywhere-temp")
     if (!tempDir.exists()) {
-        tempDir.mkdirs()
+        if (!tempDir.mkdirs()) {
+            throw IllegalStateException("Failed to create temp directory: ${tempDir.absolutePath}")
+        }
     }
     return tempDir.absolutePath
 }
sdk/runanywhere-kotlin/src/androidUnitTest/kotlin/test/kotlin/com/runanywhere/sdk/RunAnywhereSTTTest.kt (1)

16-134: Move business logic tests to commonTest/ for cross-platform coverage.

This test file is in androidUnitTest/ but tests common business logic (initialization, transcription, streaming, cleanup) without any Android-specific APIs. Platform-specific test directories should only test platform-specific implementations.

Based on coding guidelines, move this test file to commonTest/ to ensure the business logic is tested across all supported platforms. Reserve androidUnitTest/ for testing Android-specific implementations like AndroidSTTService or Android-specific optimizations.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/security/SecureStorage.kt (1)

22-47: Caching strategy may cause unexpected behavior with multiple identifiers.

The companion object caches only one AndroidSecureStorage instance. When create(identifier) is called with a different identifier, the previous cached instance is silently replaced. This could lead to unexpected behavior if the SDK uses multiple storage identifiers concurrently.

🔎 Proposed fix: Use a map to cache multiple instances
     companion object {
-        private var cachedStorage: AndroidSecureStorage? = null
+        private val cachedStorages = mutableMapOf<String, AndroidSecureStorage>()
         private var context: Context? = null
 
         /**
          * Initialize Android secure storage with application context
          * This should be called during SDK initialization
          */
         fun initialize(applicationContext: Context) {
             context = applicationContext.applicationContext
         }
 
         /**
          * Create secure storage instance for Android
          */
         fun create(identifier: String): AndroidSecureStorage {
             val appContext =
                 context
                     ?: throw SDKError.SecurityError("AndroidSecureStorage not initialized. Call initialize(context) first.")
 
             // Return cached instance if available for the same identifier
-            cachedStorage?.let { cached ->
-                if (cached.identifier == identifier) {
-                    return cached
-                }
-            }
+            cachedStorages[identifier]?.let { return it }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/security/KeychainManager.kt (1)

68-181: Standardize error handling: All methods should throw exceptions, not return null/false.

The error handling is inconsistent and violates KMP best practices. Mutating operations (saveTokens, deleteTokens, clearAll) throw exceptions, while query operations (getTokens, hasStoredTokens) return null/false. This violates the KMP principle that expect/actual declarations must have identical return types across platforms.

Since Swift best practice recommends avoiding try? and returning optional when errors should be thrown, and your guidelines mandate iOS as the source of truth, all operations should throw exceptions consistently. Returning false from hasStoredTokens() on encryption failure conflates "no tokens stored" with "operation failed," hiding real security issues. Similarly, returning null from getTokens() masks encryption errors.

gradle/libs.versions.toml (1)

19-20: Typo in section comment.

The comment reads "# Jetpack Composeios h" — appears to be a typo. Should likely be "# Jetpack Compose".

🔎 Proposed fix
 # ============================================================================
-# Jetpack Composeios h
+# Jetpack Compose
 # ============================================================================
sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/llm/llamacpp/LlamaCppService.kt (1)

288-329: streamProcess has a limitation with callback-based streaming.

The comment on lines 325-327 correctly notes that tokens can't be emitted from inside the callback. The current implementation calls the callback but doesn't actually emit tokens to the Flow. This means streamProcess returns an empty Flow.

The streamProcess method creates a Flow but the coreService.generateStream callback doesn't emit tokens to the Flow collector. Consider using callbackFlow or channelFlow to bridge the callback-based API:

Proposed fix using callbackFlow
-    actual fun streamProcess(input: LLMInput): Flow<LLMGenerationChunk> = flow {
+    actual fun streamProcess(input: LLMInput): Flow<LLMGenerationChunk> = callbackFlow {
         if (!isServiceInitialized) {
             throw IllegalStateException("LlamaCppService not initialized")
         }
         // ... setup code ...
         
         coreService.generateStream(
             prompt = prompt,
             systemPrompt = systemPrompt,
             maxTokens = maxTokens,
             temperature = options.temperature
         ) { token ->
-            val currentChunk = chunkIndex++
-            val currentTokens = tokenCount++
-            val isComplete = currentTokens >= maxTokens
-
-            logger.info("Stream token #$currentTokens: '$token'")
-
-            // Note: We can't emit from inside a callback, so we collect tokens
-            // This is a limitation - for true streaming, use generateStream directly
-            true // continue
+            trySend(LLMGenerationChunk(
+                index = chunkIndex++,
+                text = token,
+                isComplete = false
+            ))
+            true
         }
+        channel.close()
+        awaitClose()
     }
sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/src/commonMain/kotlin/com/runanywhere/sdk/llm/llamacpp/LlamaCppAdapter.kt (1)

64-86: Add framework compatibility check for consistency.

The canHandle method checks format and quantization compatibility but doesn't validate framework compatibility via model.compatibleFrameworks. ONNXAdapter explicitly checks this with if (model.compatibleFrameworks.isNotEmpty()) { return model.compatibleFrameworks.contains(InferenceFramework.ONNX) }. Add the same check here for consistency:

// Check framework compatibility
if (model.compatibleFrameworks.isNotEmpty()) {
    return model.compatibleFrameworks.contains(InferenceFramework.LLAMA_CPP)
}

This ensures the method properly validates that the model is compatible with the llama.cpp framework.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/tts/AndroidTTSService.kt (2)

123-150: Race condition in state management.

Setting _isSynthesizing = true at line 132 without acquiring synthesisLock creates a race condition. The nested synthesize() call at line 140 will acquire the lock and set the flag again, leading to redundant state updates. If two threads call synthesizeStream concurrently, both can set the flag before either acquires the lock.

Consider either:

  1. Acquiring the lock before setting _isSynthesizing, or
  2. Removing the flag management from this method entirely and relying on the nested synthesize() call
🔎 Option 1: Acquire lock before setting state
     ) {
-        if (!isInitialized) {
+        synthesisLock.withLock {
+            if (!isInitialized) {
+                throw SDKError.ComponentNotReady("TTS service not initialized")
+            }
+
+            _isSynthesizing = true
+            try {
+                logger.debug("Starting streaming synthesis")
+
+                // For streaming, split text into sentences and synthesize each
+                val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
+
+                for (sentence in sentences) {
+                    val audioData = synthesize(sentence.trim(), options)
+                    if (audioData.isNotEmpty()) {
+                        onChunk(audioData)
+                    }
+                    // Small delay between sentences for natural speech rhythm
+                    delay(100)
+                }
+            } finally {
+                _isSynthesizing = false
+            }
-            throw SDKError.ComponentNotReady("TTS service not initialized")
         }
-
-        _isSynthesizing = true
-        try {
-            // existing code
-        } finally {
-            _isSynthesizing = false
-        }

Wait, that won't work because synthesize() also tries to acquire the lock. Mutex is not reentrant in Kotlin. So Option 1 would deadlock.

Better solution: Remove state management from this method since synthesize() handles it.

     ) {
         if (!isInitialized) {
             throw SDKError.ComponentNotReady("TTS service not initialized")
         }
 
-        _isSynthesizing = true
         try {
             logger.debug("Starting streaming synthesis")
 
             // For streaming, split text into sentences and synthesize each
             val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
 
             for (sentence in sentences) {
                 val audioData = synthesize(sentence.trim(), options)
                 if (audioData.isNotEmpty()) {
                     onChunk(audioData)
                 }
                 // Small delay between sentences for natural speech rhythm
                 delay(100)
             }
-        } finally {
-            _isSynthesizing = false
         }

Note: Kotlin's Mutex is non-reentrant, so nested lock acquisition would deadlock. The cleanest fix is to remove redundant state management.


226-236: Cleanup should coordinate with ongoing synthesis.

The cleanup() method modifies shared state (isInitialized, _isSynthesizing) and nullifies textToSpeech without acquiring synthesisLock. If cleanup is called while synthesize() is running, it can lead to:

  1. NPE if textToSpeech is accessed after being nullified
  2. Inconsistent state if flags are modified while the synthesis finally block is executing

Consider acquiring the lock in cleanup to ensure safe coordination:

🔎 Suggested fix
     override suspend fun cleanup() {
+        synthesisLock.withLock {
             withContext(Dispatchers.Main) {
                 textToSpeech?.stop()
                 textToSpeech?.shutdown()
                 textToSpeech = null
                 isInitialized = false
                 _isSynthesizing = false
                 availableTTSVoices.clear()
                 logger.info("Android TTS service cleaned up")
             }
+        }
     }
🟡 Minor comments (9)
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/models/DeviceInfo.kt-44-50 (1)

44-50: Remove redundant API level check.

Both branches of the conditional return identical values (packageInfo?.versionName), making the API level check and suppression annotation unnecessary. The versionName property has remained consistent across Android API levels.

🔎 Proposed fix
         appVersion =
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
-                packageInfo?.versionName
-            } else {
-                @Suppress("DEPRECATION")
-                packageInfo?.versionName
-            },
+            packageInfo?.versionName,
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/utils/PlatformUtils.kt-38-46 (1)

38-46: Log the swallowed exception for debugging.

The exception is caught but not logged, which could hide permission issues, security exceptions, or platform bugs during development and debugging.

🔎 Proposed fix to add logging
             deviceId =
                 try {
                     Settings.Secure.getString(
                         applicationContext.contentResolver,
                         Settings.Secure.ANDROID_ID,
                     )
                 } catch (e: Exception) {
+                    // Log for debugging - Android ID retrieval failed
+                    android.util.Log.w("PlatformUtils", "Failed to retrieve Android ID", e)
                     null
                 }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/stt/AndroidAudioCaptureManager.kt-62-68 (1)

62-68: Misleading method name: requestPermission() only checks permission status.

The method name implies it will request permission, but it only checks if permission is already granted. While the comment clarifies this, the API surface is confusing. Consider renaming to checkPermission() or documenting more prominently that the actual request must be handled by the calling Activity.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/security/SecureStorage.kt-78-83 (1)

78-83: Swallowed exception loses diagnostic information.

The exception is caught but not logged, making it difficult to diagnose why secure storage is unsupported. Consider logging the error before returning false.

Based on static analysis hints.

🔎 Proposed fix: Log the exception
         fun isSupported(): Boolean =
             try {
                 context != null
             } catch (e: Exception) {
+                SDKLogger("AndroidSecureStorage").error("Secure storage support check failed", e)
                 false
             }

Committable suggestion skipped: line range outside the PR's diff.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/infrastructure/download/AndroidSimpleDownloader.kt-65-72 (1)

65-72: Fix duplicate logging at 10% intervals.

The condition if (percent % 10 == 0) will log multiple times at each 10% threshold. For example, when progress is between 10.0% and 10.9%, percent will be 10, and the condition will be true repeatedly. This is similar logic to what's in AndroidDownloadImpl.kt (lines 50-57), which correctly tracks lastLoggedPercent to avoid duplicates.

🔎 Proposed fix
+var lastLoggedPercent = -1
 while (input.read(buffer).also { bytesRead = it } != -1) {
     output.write(buffer, 0, bytesRead)
     bytesDownloaded += bytesRead

     // Report progress every 100ms
     val currentTime = System.currentTimeMillis()
     if (currentTime - lastReportTime >= 100) {
         progressCallback?.invoke(bytesDownloaded, totalBytes)
         lastReportTime = currentTime

         // Log every 10%
         if (totalBytes > 0) {
             val percent = (bytesDownloaded.toDouble() / totalBytes * 100).toInt()
-            if (percent % 10 == 0) {
+            if (percent >= lastLoggedPercent + 10) {
                 logger.debug("Download progress: $percent% ($bytesDownloaded / $totalBytes bytes)")
+                lastLoggedPercent = percent
             }
         }
     }
 }
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/AudioTypes.kt-28-29 (1)

28-29: Verify fileExtension logic for PCM_16BIT.

The fileExtension property returns rawValue directly, which means PCM_16BIT will have a file extension of "pcm_16bit". Typically, file extensions don't contain underscores, and both PCM variants should likely use "pcm" as the extension. Please verify this matches the iOS implementation and the intended file naming convention.

sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/AudioTypes.kt-11-14 (1)

11-14: Fix AudioFormat enum serialization to match iOS behavior.

The @Serializable enum currently serializes by enum name (e.g., "PCM", "WAV"), but the iOS version uses a String-backed enum that serializes to its raw value (e.g., "pcm", "wav"). This creates a mismatch if AudioFormat is serialized to JSON.

Use @SerialName annotations on each enum case with its rawValue to ensure serialized output matches iOS:

 @Serializable
 enum class AudioFormat(
     val rawValue: String,
 ) {
+    @SerialName("pcm")
     PCM("pcm"),
+    @SerialName("wav")
     WAV("wav"),
+    @SerialName("mp3")
     MP3("mp3"),
+    @SerialName("opus")
     OPUS("opus"),
+    @SerialName("aac")
     AAC("aac"),
+    @SerialName("flac")
     FLAC("flac"),
+    @SerialName("ogg")
     OGG("ogg"),
+    @SerialName("pcm_16bit")
     PCM_16BIT("pcm_16bit"),
     ;

This ensures AudioFormat serializes to its rawValue, matching the iOS pattern exactly.

sdk/runanywhere-kotlin/.editorconfig-5-6 (1)

5-6: Contradictory rule: wildcard imports disabled while DAOs use explicit imports.

The PR updates AuthTokenDao.kt and ConfigurationDao.kt to use explicit imports, yet this config disables the no-wildcard-imports rule. Consider enabling this rule to enforce the explicit import pattern consistently, or document a clear rationale for the exception.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/repositories/TelemetryRepositoryImpl.kt-24-25 (1)

24-25: Remove the unused networkService parameter from the constructor.

The networkService parameter is not referenced anywhere in the implementation. The JVM variant of TelemetryRepositoryImpl demonstrates this dependency is unnecessary, and the actual implementation uses remoteTelemetryDataSource for telemetry submission. Remove this parameter to simplify the constructor and reduce unnecessary dependency injection.

🧹 Nitpick comments (30)
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/foundation/HostAppInfo.kt (1)

8-27: Consider type-safe error handling with sealed classes.

The current implementation catches all exceptions and returns a HostAppInfo with null values, which silently swallows errors. Consider using a sealed class result type for more explicit error handling, which aligns with the project's coding guidelines.

As per coding guidelines, sealed classes should be used for type-safe error handling in Kotlin code.

🔎 Example refactor using sealed class result type

Define a result type in commonMain:

sealed class HostAppInfoResult {
    data class Success(val info: HostAppInfo) : HostAppInfoResult()
    data class Error(val exception: Exception) : HostAppInfoResult()
}

Then refactor the function:

-actual fun getHostAppInfo(): HostAppInfo =
+actual fun getHostAppInfoResult(): HostAppInfoResult =
     try {
         val context = AndroidPlatformContext.getContext()
         val packageName = context.packageName
         val packageManager = context.packageManager
         val appInfo = context.applicationInfo
         val packageInfo = packageManager.getPackageInfo(packageName, 0)
 
         val appName = packageManager.getApplicationLabel(appInfo).toString()
         val versionName = packageInfo.versionName
 
-        HostAppInfo(
+        HostAppInfoResult.Success(HostAppInfo(
             identifier = packageName,
             name = appName,
             version = versionName,
-        )
+        ))
     } catch (e: Exception) {
-        // Return nulls if unable to get app info
-        HostAppInfo(null, null, null)
+        HostAppInfoResult.Error(e)
     }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/models/DeviceInfo.kt (1)

28-33: Consider logging the caught exception for debugging.

The exception is swallowed and null is returned for graceful degradation, which is acceptable. However, logging the exception would improve observability and help diagnose issues during development or troubleshooting.

🔎 Proposed enhancement
     val packageInfo =
         try {
             packageManager.getPackageInfo(context.packageName, 0)
         } catch (e: Exception) {
+            // Log for debugging while gracefully degrading
+            android.util.Log.w("DeviceInfo", "Failed to get package info", e)
             null
         }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/utils/PlatformUtils.kt (1)

60-60: LGTM! Clean expression body conversions.

The conversion of single-expression functions to expression bodies is idiomatic Kotlin and improves readability.

Also applies to: 78-78, 80-80

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/storage/AndroidPlatformStorage.kt (1)

15-70: Consider wrapping SharedPreferences operations in withContext(Dispatchers.IO).

The suspend functions perform synchronous SharedPreferences operations, which could block the calling coroutine and potentially cause ANR issues if called from the main thread. While apply() is asynchronous for writes, the function itself doesn't suspend.

Recommended approach to make operations truly non-blocking
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
 override suspend fun putString(
     key: String,
     value: String,
-) {
+) = withContext(Dispatchers.IO) {
     sharedPreferences.edit().putString(key, value).apply()
 }

-override suspend fun getString(key: String): String? = sharedPreferences.getString(key, null)
+override suspend fun getString(key: String): String? = withContext(Dispatchers.IO) {
+    sharedPreferences.getString(key, null)
+}

Apply similar wrapping to all other suspend functions.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/foundation/device/DeviceInfoService.kt (1)

83-94: Expression body refactoring for getArchitecture is safe and idiomatic.

The refactoring mirrors the getChipName pattern and preserves exception handling. However, both methods contain duplicate ABI-extraction logic.

Consider extracting the common ABI-retrieval logic into a private helper method to eliminate duplication:

🔎 Proposed refactor for ABI extraction
+    private fun getPrimaryAbi(): String? =
+        try {
+            val abis = Build.SUPPORTED_ABIS
+            abis.takeIf { it.isNotEmpty() }?.get(0)
+        } catch (e: Exception) {
+            null
+        }
+
-    actual fun getChipName(): String? =
-        try {
-            // Get primary ABI (architecture)
-            val abis = Build.SUPPORTED_ABIS
-            if (abis.isNotEmpty()) {
-                abis[0]
-            } else {
-                null
-            }
-        } catch (e: Exception) {
-            null
-        }
+    actual fun getChipName(): String? =
+        getPrimaryAbi()

-    actual fun getArchitecture(): String? =
-        try {
-            // Get primary ABI (architecture) - same as chip name for Android
-            val abis = Build.SUPPORTED_ABIS
-            if (abis.isNotEmpty()) {
-                abis[0]
-            } else {
-                null
-            }
-        } catch (e: Exception) {
-            null
-        }
+    actual fun getArchitecture(): String? =
+        getPrimaryAbi()
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/vad/WebRTCVADService.kt (2)

34-39: Unused variable: ttsThresholdMultiplier is set but never read.

The ttsThresholdMultiplier field is assigned in setTTSThresholdMultiplier() (line 308) but is never used anywhere in the class. Either integrate it into the threshold adjustment logic or remove it to avoid dead code.

Additionally, recentConfidenceValues is a non-synchronized MutableList that could encounter concurrent modification if processAudioChunk is called from multiple threads. Consider using a thread-safe collection or synchronization.

🔎 Proposed fix
-    private val recentConfidenceValues = mutableListOf<Float>()
+    private val recentConfidenceValues = java.util.Collections.synchronizedList(mutableListOf<Float>())

Or remove ttsThresholdMultiplier if it's not needed:

-    private var ttsThresholdMultiplier: Float = 3.0f

131-135: Consider using ArrayDeque for efficient sliding window.

removeAt(0) on an ArrayList is O(n) as it shifts all elements. For a frequently-called method like processAudioChunk, consider using ArrayDeque which provides O(1) removal from the front.

🔎 Proposed fix
-    private val recentConfidenceValues = mutableListOf<Float>()
+    private val recentConfidenceValues = ArrayDeque<Float>(maxRecentValues + 1)

And update the tracking logic:

             // Track for statistics
             recentConfidenceValues.add(confidence)
             if (recentConfidenceValues.size > maxRecentValues) {
-                recentConfidenceValues.removeAt(0)
+                recentConfidenceValues.removeFirst()
             }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/config/ConfigurationLoader.kt (1)

30-38: Swallowed IOException hides asset loading failures.

The IOException at line 35 is caught but not logged, making it difficult to diagnose when configuration files are missing or inaccessible. This is inconsistent with the outer exception handler (line 45) which does log.

Consider adding debug logging here for consistency:

🔎 Proposed fix
             if (context != null) {
                 try {
                     context.assets
                         .open(fileName)
                         .bufferedReader()
                         .use { it.readText() }
                 } catch (e: IOException) {
-                    // File not found in assets - return empty string
+                    SDKLogger("ConfigurationLoader").debug("Asset file not found: $fileName")
                     ""
                 }
             } else {
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/stt/AndroidAudioCaptureManager.kt (2)

159-176: Consider adding synchronization for thread-safe AudioRecord access.

The audioRecord field is accessed from the recording coroutine (on Dispatchers.IO) and from stopRecordingInternal() which may be called from different threads. While the _isRecording flag provides some coordination, explicit synchronization could prevent edge-case race conditions during concurrent stop/cleanup operations.

💡 Optional: Add synchronized block
 private fun stopRecordingInternal() {
+    synchronized(this) {
         try {
             audioRecord?.stop()
             audioRecord?.release()
             audioRecord = null
         } catch (e: Exception) {
             logger.error("Error stopping recording: ${e.message}")
         }

         _isRecording.value = false
         _audioLevel.value = 0.0f
         logger.info("Recording stopped")
+    }
 }

182-198: Extract audio level calculation to commonMain as shared utility.

This audio level calculation logic is duplicated between AndroidAudioCaptureManager and JvmAudioCaptureManager (see relevant snippet). Since this is pure business logic with no platform-specific dependencies, it should be extracted to a shared utility in commonMain/ to eliminate duplication and improve maintainability.

As per coding guidelines, business logic should be defined in commonMain/ for KMP projects.

💡 Example: Shared utility in commonMain

Create a shared utility in commonMain/:

// commonMain/.../AudioLevelCalculator.kt
internal object AudioLevelCalculator {
    fun calculateNormalizedLevel(samples: ShortArray, count: Int): Float {
        if (count <= 0) return 0.0f
        
        var sum = 0.0
        for (i in 0 until count) {
            val sample = samples[i].toDouble() / 32768.0
            sum += sample * sample
        }
        
        val rms = sqrt(sum / count).toFloat()
        val dbLevel = 20 * log10((rms + 0.0001).toDouble())
        return ((dbLevel + 60) / 60).coerceIn(0.0, 1.0).toFloat()
    }
}

Then simplify the platform implementations:

 private fun updateAudioLevel(buffer: ShortArray, count: Int) {
-    if (count <= 0) return
-    // ... calculation logic ...
-    _audioLevel.value = normalizedLevel
+    _audioLevel.value = AudioLevelCalculator.calculateNormalizedLevel(buffer, count)
 }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/security/SecureStorage.kt (2)

166-174: Consider whether returning false on exception is the intended behavior.

Unlike other methods that rethrow exceptions as SDKError, containsKey swallows exceptions and returns false. While this may be intentional (treating errors as "key not found"), it could mask underlying storage issues.


187-198: Consider whether returning emptySet() on exception is the intended behavior.

Similar to containsKey, getAllKeys swallows exceptions and returns an empty set instead of rethrowing. This could mask underlying storage issues during diagnostics.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/infrastructure/device/services/DeviceIdentityAndroid.kt (1)

50-67: Consider more readable UUID formatting.

Expanding all 16 byte arguments across separate lines makes the UUID structure harder to parse visually. Consider keeping the format call more compact, or grouping logically (e.g., 4-2-2-2-6 pattern matching UUID structure).

Alternative formatting options

Option 1: Keep compact (original style)

-            "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".format(
-                bytes[0],
-                bytes[1],
-                bytes[2],
-                bytes[3],
-                bytes[4],
-                bytes[5],
-                bytes[6],
-                bytes[7],
-                bytes[8],
-                bytes[9],
-                bytes[10],
-                bytes[11],
-                bytes[12],
-                bytes[13],
-                bytes[14],
-                bytes[15],
-            )
+            "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".format(
+                bytes[0], bytes[1], bytes[2], bytes[3],
+                bytes[4], bytes[5], bytes[6], bytes[7],
+                bytes[8], bytes[9], bytes[10], bytes[11],
+                bytes[12], bytes[13], bytes[14], bytes[15]
+            )

Option 2: Use UUID structure grouping

-            "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".format(
-                bytes[0],
-                bytes[1],
-                bytes[2],
-                bytes[3],
-                bytes[4],
-                bytes[5],
-                bytes[6],
-                bytes[7],
-                bytes[8],
-                bytes[9],
-                bytes[10],
-                bytes[11],
-                bytes[12],
-                bytes[13],
-                bytes[14],
-                bytes[15],
-            )
+            "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x".format(
+                // time-low (4 bytes)
+                bytes[0], bytes[1], bytes[2], bytes[3],
+                // time-mid (2 bytes)
+                bytes[4], bytes[5],
+                // time-high-and-version (2 bytes)
+                bytes[6], bytes[7],
+                // clock-seq (2 bytes)
+                bytes[8], bytes[9],
+                // node (6 bytes)
+                bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
+            )
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/ModuleRegistry.kt (1)

765-786: Provider summary utilities only reflect provider registrations, not factory registrations.

The registeredModules and providerSummary properties are correctly implemented but only reflect provider-based registrations:

  • registeredModules uses hasSTT, hasLLM, etc., which check _xxxProviders
  • providerSummary counts entries in _xxxProviders lists

Factory-only registrations (via registerXXXFactory()) won't be reflected in these summaries. Consider adding factory counts to provide complete visibility, or clarify the naming to indicate these are provider-specific summaries.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/platform/Checksum.kt (1)

37-40: Consider using require() for precondition checks.

The manual file existence check followed by throwing IllegalArgumentException can be simplified using Kotlin's require() function for more idiomatic code.

🔎 Proposed refactor using require()
-    val file = File(filePath)
-    if (!file.exists()) {
-        throw IllegalArgumentException("File does not exist: $filePath")
-    }
+    val file = File(filePath)
+    require(file.exists()) { "File does not exist: $filePath" }
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/infrastructure/download/AndroidSimpleDownloader.kt (1)

21-26: Add cancellation support for long-running downloads.

The download operation lacks cancellation support, which can be problematic for large files or slow connections. Consider checking for cancellation periodically in the download loop using ensureActive() or checking the coroutine context.

🔎 Suggested enhancement
 suspend fun download(
     url: String,
     destinationPath: String,
     progressCallback: ((bytesDownloaded: Long, totalBytes: Long) -> Unit)? = null,
 ): Long =
     withContext(Dispatchers.IO) {
+        // Check for cancellation before starting
+        ensureActive()
         logger.info("Starting simple download - url: $url, destination: $destinationPath")

And within the download loop (around line 55):

         while (input.read(buffer).also { bytesRead = it } != -1) {
+            ensureActive() // Check for cancellation
             output.write(buffer, 0, bytesRead)
sdk/runanywhere-kotlin/.editorconfig (2)

21-22: Consider reducing max_line_length to a more conventional limit.

250 characters is considerably higher than typical limits (100–140). Very long lines can impair readability and code reviews. If there are specific patterns requiring this length, consider refactoring those patterns instead.


7-18: Consider a plan to gradually re-enable naming rules.

Disabling multiple naming conventions (backing-property-naming, property-naming, enum-entry-name-case, class-naming) creates technical debt. Consider tracking these as issues to address incrementally, enabling one rule at a time as violations are fixed.

SHARED_TASK_NOTES.md (2)

145-157: Consider adding language specifier to fenced code blocks.

The directory tree code blocks should specify a language (e.g., text or plaintext) for better rendering and to satisfy linting rules.

🔎 Proposed fix
-```
+```text
 data/
 ├── sync/              # Sync services (matches iOS)

161-182: Same suggestion: add language specifier.

🔎 Proposed fix
-```
+```text
 infrastructure/
 ├── analytics/           # Analytics services (matches iOS)
sdk/runanywhere-kotlin/detekt.yml (1)

154-155: Consider re-enabling naming rules after the refactor stabilizes.

Disabling the entire naming rule set removes enforcement of naming conventions. Consider re-enabling this after the major refactoring is complete to maintain consistency.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/repositories/TelemetryRepositoryImpl.kt (1)

185-199: Helper methods are Android-specific and not in the shared interface.

getEventCount() and getUnsentEventCount() exist only in the Android implementation and are not defined in the TelemetryRepository interface. The JVM implementation uses sentEventIds map tracking instead. If these methods need to be available across platforms, consider adding them to the interface; otherwise, document them as Android-specific helpers to clarify the design intent.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt (1)

31-36: Consider logging the caught exception for diagnostics.

While catching UninitializedPropertyAccessException is appropriate for lazy initialization, logging the exception would improve diagnostics without affecting the fallback behavior.

🔎 Proposed enhancement
 val remoteTelemetryDataSource =
     try {
         com.runanywhere.sdk.foundation.ServiceContainer.shared.remoteTelemetryDataSource
     } catch (e: UninitializedPropertyAccessException) {
+        logger.debug("RemoteTelemetryDataSource not yet initialized (expected in dev mode)")
         null // Not available yet or not in production mode
     }

Based on learnings, lazy initialization for service dependencies is preferred to avoid memory pressure.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/database/RunAnywhereDatabase.kt (1)

88-112: DatabaseManager singleton with lazy initialization follows best practices.

The shared singleton with lazy database initialization aligns with the coding guideline to use the Service Container pattern and lazy initialization. However, context is lateinit which could throw if getDatabase() is called before initialize().

Consider adding a guard in getDatabase() to provide a clearer error message:

Suggested improvement
 suspend fun getDatabase(): RunAnywhereDatabase {
+    if (!::context.isInitialized) {
+        throw IllegalStateException("DatabaseManager not initialized. Call DatabaseManager.initialize(context) first.")
+    }
     return database ?: RunAnywhereDatabase.getDatabase(context).also { database = it }
 }
sdk/runanywhere-kotlin/modules/runanywhere-core-llamacpp/src/commonMain/kotlin/com/runanywhere/sdk/llm/llamacpp/LlamaCppServiceProvider.kt (2)

54-103: Enhanced canHandle logic is comprehensive but should be documented.

The new pattern-matching logic for model identification is thorough:

  • Checks GGUF/GGML extensions and references
  • Detects llamacpp framework markers
  • Uses regex for quantization patterns
  • Excludes models with other framework markers (ONNX, CoreML, etc.)

Consider adding a KDoc comment explaining the detection strategy for maintainability.

Suggested documentation
+    /**
+     * Determines if this provider can handle the given model.
+     * 
+     * Detection strategy (in order):
+     * 1. GGUF/GGML file extensions or name references
+     * 2. Explicit llamacpp framework markers
+     * 3. GGUF quantization patterns (q2_k, q4_0, q5_1, q8_0, etc.)
+     * 4. Known LLM model family names (unless marked for other frameworks)
+     */
     override fun canHandle(modelId: String?): Boolean {

80-85: Quantization regex pattern is mostly correct but could miss some variants.

The pattern q[2-8]([_-][kK])?([_-][mMsS0])? handles common cases but may not match:

  • q4_0 correctly matches via [_-][mMsS0] (the 0 is included)
  • f16/f32 quantizations are not matched

Since F16/F32 are handled in LlamaCppAdapter.supportedQuantizations, consider adding them here if they appear in model names.

sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/tts/AndroidTTSService.kt (4)

40-41: Inconsistent naming convention for state fields.

The isInitialized field lacks an underscore prefix while _isSynthesizing has one. Since _isSynthesizing is a backing field for the public isSynthesizing property (line 206-207), consider either:

  • Adding an underscore to isInitialized if you plan to expose it publicly later, or
  • Removing the underscore from _isSynthesizing if it's meant to remain private

For consistency, align the naming pattern across similar state fields.


48-93: Consider adding initialization timeout.

The initialize() method uses suspendCancellableCoroutine without a timeout. If the TextToSpeech engine fails to invoke the callback, the coroutine will hang indefinitely. Consider wrapping this in withTimeout or withTimeoutOrNull to handle unresponsive TTS initialization gracefully.

🔎 Suggested improvement
override suspend fun initialize() = withTimeout(5000L) { // 5 second timeout
    suspendCancellableCoroutine { cont ->
        // ... existing code
    }
}

186-190: State management in stop() should be consistent with synthesis lock.

Setting _isSynthesizing = false at line 188 without acquiring synthesisLock can race with the finally blocks in synthesize() and streaming methods. Consider whether stop() should acquire the lock or rely on the synthesis methods' finally blocks to reset the state.

However, since stop() is meant to interrupt ongoing synthesis and textToSpeech?.stop() is the actual interrupt mechanism, the current approach may be acceptable if the state flag is advisory rather than critical for correctness.


338-416: Consider adding synthesis timeout.

Similar to the initialization method, synthesizeToByteArray can hang indefinitely if the TTS engine fails to invoke the onDone or onError callbacks. Consider wrapping the operation in withTimeout to handle unresponsive synthesis gracefully.

🔎 Suggested improvement
private suspend fun synthesizeToByteArray(
    text: String,
    options: TTSOptions,
): ByteArray = withTimeout(30000L) { // 30 second timeout for synthesis
    suspendCancellableCoroutine { cont ->
        // ... existing code
    }
}

Otherwise, the implementation is solid: proper file cleanup on all paths, good error handling, and correct use of cancellation.

Comment on lines 635 to 643
// TTS feedback prevention (matching iOS)
override var isTTSActive: Boolean = false
private set
private var baseEnergyThreshold: Float = 0.0f
private var ttsThresholdMultiplier: Float = 3.0f

// Debug statistics tracking
private val recentConfidenceValues = mutableListOf<Float>()
private val maxRecentValues = 20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider thread safety for concurrent VAD processing.

The isTTSActive flag and recentConfidenceValues list are accessed and modified in processAudioChunk (lines 665-680), which may be called from multiple threads during concurrent audio processing. Consider adding:

  • @Volatile annotation to isTTSActive
  • Synchronization mechanism for recentConfidenceValues (e.g., Collections.synchronizedList() or a mutex)
🔎 Proposed fix for thread safety
-    override var isTTSActive: Boolean = false
+    @Volatile
+    override var isTTSActive: Boolean = false
         private set
     private var baseEnergyThreshold: Float = 0.0f
     private var ttsThresholdMultiplier: Float = 3.0f
 
     // Debug statistics tracking
-    private val recentConfidenceValues = mutableListOf<Float>()
+    private val recentConfidenceValues = Collections.synchronizedList(mutableListOf<Float>())
     private val maxRecentValues = 20

Based on learnings, this aligns with proper concurrency handling for service components.

🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/modules/runanywhere-core-onnx/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/core/onnx/ONNXServiceProviderImpl.kt
around lines 635-643, the isTTSActive flag and recentConfidenceValues list are
not safe for concurrent access from processAudioChunk; mark isTTSActive as
@Volatile and replace recentConfidenceValues with a thread-safe structure (e.g.,
Collections.synchronizedList(mutableListOf<Float>()) or use a dedicated mutex)
and ensure all reads/writes/iterations over recentConfidenceValues are performed
while holding that synchronization (or via synchronizedList methods) to avoid
race conditions.

Comment on lines 19 to 27
val context =
try {
// Access Android context if available
val contextField = ServiceContainer::class.java.getDeclaredField("androidContext")
contextField.isAccessible = true
contextField.get(ServiceContainer.shared) as? Context
} catch (e: Exception) {
null
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find sdk/runanywhere-kotlin -name "ServiceContainer.kt" -o -name "ConfigurationLoader.kt" | head -20

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 753


🏁 Script executed:

cat -n sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt | head -80

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4778


🏁 Script executed:

cat -n sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/config/ConfigurationLoader.kt | head -50

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2234


🏁 Script executed:

cat -n sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt | head -50

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2212


🏁 Script executed:

wc -l sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 290


🏁 Script executed:

cat -n sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt | tail -100

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4197


🏁 Script executed:

cat -n sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2212


Replace reflection-based context access with a proper public API method.

Per KMP guidelines, shared logic should depend on abstractions, not on concrete platform details. Using reflection to access a private androidContext field violates the Service Container pattern by bypassing its encapsulation. Reflection involves additional runtime checks and lookups and is generally recommended to be used sparingly and only when necessary.

Fix by either:

  1. Adding a public method on ServiceContainer to retrieve the Android context, or
  2. Injecting the context as a parameter to the loadResourceFile function during initialization.

Additionally, exceptions from reflection operations should be handled gracefully with try-catch blocks rather than being swallowed. Add logging when context resolution fails at line 25:

try {
    // Access Android context if available
    val contextField = ServiceContainer::class.java.getDeclaredField("androidContext")
    contextField.isAccessible = true
    contextField.get(ServiceContainer.shared) as? Context
} catch (e: Exception) {
    SDKLogger("ConfigurationLoader").debug("Android context not available: ${e.message}")
    null
}
🧰 Tools
🪛 detekt (1.23.8)

[warning] 25-25: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/data/config/ConfigurationLoader.kt
around lines 19 to 27, the code uses reflection to read a private androidContext
field from ServiceContainer and swallows any exception; replace that reflection
with a proper public API or dependency injection and add logging on failures:
either (A) add a public accessor on ServiceContainer (e.g., getAndroidContext()
or androidContext property) and call that here, or (B) change loadResourceFile
to accept an Android Context parameter provided at initialization, then remove
the reflection block entirely; if you must keep a fallback, catch exceptions and
log the failure via SDKLogger("ConfigurationLoader").debug(...) before returning
null rather than silently swallowing them.

Comment on lines +200 to +209
private fun getApplicationContext(): Context? {
// Try to get context through reflection (common pattern in KMP)
return try {
val activityThread = Class.forName("android.app.ActivityThread")
val currentApplication = activityThread.getMethod("currentApplication")
currentApplication.invoke(null) as? Context
} catch (e: Exception) {
null
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace reflection-based context retrieval with proper dependency injection.

Using reflection to obtain the application context via ActivityThread.currentApplication() is fragile and unreliable. This approach can fail due to:

  • Android version changes or internal API modifications
  • ProGuard/R8 obfuscation
  • Security manager restrictions
  • Non-standard Android environments

When getApplicationContext() returns null, hasPermission() always returns false, potentially blocking legitimate recording attempts.

As per coding guidelines, use the Service Container pattern for dependency injection. Inject the Context through the constructor or a proper DI mechanism.

🔎 Recommended: Constructor injection approach

Modify the class to accept Context as a parameter:

-class AndroidAudioCaptureManager : AudioCaptureManager {
+class AndroidAudioCaptureManager(
+    private val context: Context
+) : AudioCaptureManager {
     private val logger = SDKLogger("AudioCapture")
     // ...
 
     override suspend fun hasPermission(): Boolean {
         return try {
-            val context = getApplicationContext()
-            if (context != null) {
                 ContextCompat.checkSelfPermission(
                     context,
                     Manifest.permission.RECORD_AUDIO,
                 ) == PackageManager.PERMISSION_GRANTED
-            } else {
-                logger.warning("Cannot check permission - no application context")
-                false
-            }
         } catch (e: Exception) {
             logger.error("Error checking permission: ${e.message}")
             false
         }
     }
-
-    private fun getApplicationContext(): Context? {
-        // ...
-    }
 }

Update the factory function:

-actual fun createAudioCaptureManager(): AudioCaptureManager = AndroidAudioCaptureManager()
+actual fun createAudioCaptureManager(): AudioCaptureManager {
+    // Context should be provided via Service Container or initialization
+    val context = ServiceContainer.get<Context>()
+    return AndroidAudioCaptureManager(context)
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/stt/AndroidAudioCaptureManager.kt
around lines 200-209, the current reflection-based getApplicationContext() is
fragile; replace it by accepting a non-null Context via constructor injection
(or the project's Service Container/DI), remove the reflection method, and use
the injected context wherever hasPermission() or other context-dependent calls
run; update the class factory/constructors and all call sites (and any tests) to
pass a valid applicationContext (or applicationContext ?: throw
IllegalArgumentException) so permission checks never silently fail due to null.

Comment on lines 155 to 181
override fun synthesizeStream(
text: String,
options: TTSOptions,
): Flow<ByteArray> =
flow {
if (!isInitialized) {
throw SDKError.ComponentNotReady("TTS service not initialized")
}

_isSynthesizing = true
try {
logger.debug("Starting Flow-based streaming synthesis")
_isSynthesizing = true
try {
logger.debug("Starting Flow-based streaming synthesis")

// Split text into sentences and synthesize each
val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
// Split text into sentences and synthesize each
val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }

for (sentence in sentences) {
val audioData = synthesize(sentence.trim(), options)
if (audioData.isNotEmpty()) {
emit(audioData)
for (sentence in sentences) {
val audioData = synthesize(sentence.trim(), options)
if (audioData.isNotEmpty()) {
emit(audioData)
}
delay(100)
}
delay(100)
} finally {
_isSynthesizing = false
}
} finally {
_isSynthesizing = false
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same race condition as callback-based streaming.

This method has the identical concurrency issue as the callback-based synthesizeStream (lines 123-150): _isSynthesizing is set at line 164 without acquiring the lock, then synthesize() is called at line 172, which acquires the lock and sets the flag again.

Apply the same fix: remove the redundant state management and rely on the nested synthesize() call to handle the flag correctly.

🔎 Suggested fix
         flow {
             if (!isInitialized) {
                 throw SDKError.ComponentNotReady("TTS service not initialized")
             }
 
-            _isSynthesizing = true
             try {
                 logger.debug("Starting Flow-based streaming synthesis")
 
                 // Split text into sentences and synthesize each
                 val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
 
                 for (sentence in sentences) {
                     val audioData = synthesize(sentence.trim(), options)
                     if (audioData.isNotEmpty()) {
                         emit(audioData)
                     }
                     delay(100)
                 }
-            } finally {
-                _isSynthesizing = false
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun synthesizeStream(
text: String,
options: TTSOptions,
): Flow<ByteArray> =
flow {
if (!isInitialized) {
throw SDKError.ComponentNotReady("TTS service not initialized")
}
_isSynthesizing = true
try {
logger.debug("Starting Flow-based streaming synthesis")
_isSynthesizing = true
try {
logger.debug("Starting Flow-based streaming synthesis")
// Split text into sentences and synthesize each
val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
// Split text into sentences and synthesize each
val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
for (sentence in sentences) {
val audioData = synthesize(sentence.trim(), options)
if (audioData.isNotEmpty()) {
emit(audioData)
for (sentence in sentences) {
val audioData = synthesize(sentence.trim(), options)
if (audioData.isNotEmpty()) {
emit(audioData)
}
delay(100)
}
delay(100)
} finally {
_isSynthesizing = false
}
} finally {
_isSynthesizing = false
}
override fun synthesizeStream(
text: String,
options: TTSOptions,
): Flow<ByteArray> =
flow {
if (!isInitialized) {
throw SDKError.ComponentNotReady("TTS service not initialized")
}
try {
logger.debug("Starting Flow-based streaming synthesis")
// Split text into sentences and synthesize each
val sentences = text.split(Regex("[.!?]+")).filter { it.trim().isNotEmpty() }
for (sentence in sentences) {
val audioData = synthesize(sentence.trim(), options)
if (audioData.isNotEmpty()) {
emit(audioData)
}
delay(100)
}
}
}
🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/tts/AndroidTTSService.kt
around lines 155 to 181, remove the redundant local state management that causes
a race: delete the assignment of `_isSynthesizing = true` before calling
`synthesize()` and remove the corresponding `finally` block that sets
`_isSynthesizing = false`; rely on the existing `synthesize()` implementation
(which acquires the lock and manages `_isSynthesizing`) to set and clear the
flag so the flow-based stream no longer races with the nested synthesize call.

Comment on lines 439 to 447
override suspend fun synthesize(
text: String,
options: TTSOptions,
): ByteArray {
if (!service.isSynthesizing) {
service.initialize()
}
return service.synthesize(text = text, options = options)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialization logic is incorrect.

Lines 443-445 check !service.isSynthesizing to decide whether to initialize, but this is wrong:

  • isSynthesizing indicates whether synthesis is currently running, not whether the service is initialized
  • If the service is initialized but idle, this will call initialize() again unnecessarily
  • Each call to initialize() creates a new TextToSpeech instance without cleaning up the old one, potentially causing resource leaks

The service's isInitialized property is private, so the provider cannot check it directly. Consider either:

  1. Exposing isInitialized as a public property in AndroidTTSService, or
  2. Tracking initialization state separately in the provider, or
  3. Making initialization idempotent by checking textToSpeech != null in the initialize() method
🔎 Suggested fix (Option 1)

In AndroidTTSService, expose the initialization state:

     override val isSynthesizing: Boolean
         get() = _isSynthesizing
+    
+    val isInitialized: Boolean
+        get() = this.isInitialized

Then in the provider:

     override suspend fun synthesize(
         text: String,
         options: TTSOptions,
     ): ByteArray {
-        if (!service.isSynthesizing) {
+        if (!service.isInitialized) {
             service.initialize()
         }
         return service.synthesize(text = text, options = options)
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/androidMain/kotlin/com/runanywhere/sdk/features/tts/AndroidTTSService.kt
around lines 439-447, the provider currently checks service.isSynthesizing to
decide initialization which is incorrect and can recreate TextToSpeech instances
causing leaks; make initialization idempotent by modifying
AndroidTTSService.initialize() to return immediately if its internal
textToSpeech instance is non-null (or expose a public isInitialized boolean and
use that in the provider), then update the provider call to simply call
service.initialize() unconditionally (or check the new public isInitialized) so
initialize() won’t create a new TextToSpeech if one already exists.

Comment on lines +89 to +91
} finally {
urlConnection.disconnect()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add cleanup for temporary file on download failure.

The temporary file created on line 46 ($destinationPath.tmp) is not deleted if an exception occurs during download. This can lead to accumulation of incomplete downloads on disk.

🔎 Suggested enhancement
+val tempPath = "$destinationPath.tmp"
+val tempFile = java.io.File(tempPath)
+
 try {
     urlConnection.connect()
     // ... download logic ...
+} catch (e: Exception) {
+    // Clean up temp file on failure
+    tempFile.delete()
+    throw e
 } finally {
     urlConnection.disconnect()
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +5 to +10
/**
* Audio format enum matching iOS AudioFormat pattern exactly
* This is the single source of truth for audio formats across STT, TTS, and VAD
*
* iOS reference: Core/Types/AudioTypes.swift
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for iOS AudioTypes or AudioFormat implementations
find . -type f -name "*.swift" | grep -i audio | head -20

# Search for AudioTypes.swift in SDK directories
fd "AudioTypes.swift" -e swift

# Search for AudioFormat enum/struct definitions in iOS or Core directories
rg -n "enum AudioFormat|struct AudioFormat" --type swift -A 15

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2340


🏁 Script executed:

#!/bin/bash
# Read full iOS AudioTypes.swift
echo "=== iOS AudioTypes.swift ==="
cat -n ./sdk/runanywhere-swift/Sources/RunAnywhere/Core/Types/AudioTypes.swift

echo ""
echo "=== Kotlin AudioTypes.kt ==="
# Find and read the Kotlin AudioTypes file
fd "AudioTypes.kt" -e kt | head -5 | xargs -I {} cat -n {}

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2338


Correct documentation: Kotlin AudioFormat does not match iOS exactly.

The iOS AudioFormat enum contains only 6 formats (pcm, wav, mp3, opus, aac, flac), while the Kotlin version adds OGG and PCM_16BIT. Update lines 6-9 to accurately reflect that:

  • OGG and PCM_16BIT are Android-only extensions
  • The base formats are shared with iOS
  • Only platform-specific formats should be added outside the iOS parity set

Per KMP guidelines, copy iOS logic exactly; extensions must be explicitly marked as platform-specific, not claimed as iOS-compatible.

🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/AudioTypes.kt
around lines 5 to 10, the file header incorrectly states the Kotlin AudioFormat
matches iOS exactly; update the comment to say the Kotlin enum includes
Android-only extensions (OGG and PCM_16BIT) while the base formats (pcm, wav,
mp3, opus, aac, flac) are shared with iOS, and explicitly mark any additional
entries as platform-specific so iOS parity is preserved and platform extensions
are clearly documented.

/**
* Get AudioFormat from raw value string
*/
fun fromRawValue(value: String): AudioFormat? = entries.find { it.rawValue == value.lowercase() }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the case-insensitive comparison logic.

The comparison compares the lowercased input with it.rawValue directly, which is not lowercased. This breaks case-insensitive matching.

🔎 Proposed fix
-        fun fromRawValue(value: String): AudioFormat? = entries.find { it.rawValue == value.lowercase() }
+        fun fromRawValue(value: String): AudioFormat? = entries.find { it.rawValue.lowercase() == value.lowercase() }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun fromRawValue(value: String): AudioFormat? = entries.find { it.rawValue == value.lowercase() }
fun fromRawValue(value: String): AudioFormat? = entries.find { it.rawValue.lowercase() == value.lowercase() }
🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/AudioTypes.kt
around line 35, the fromRawValue function lowercases only the input but compares
it to it.rawValue unchanged, breaking case-insensitive matching; update the
comparison to perform a case-insensitive match by either lowercasing both sides
(compare value.lowercase() to it.rawValue.lowercase()) or use a case-insensitive
comparison API (value.equals(it.rawValue, ignoreCase = true)) so entries are
found regardless of input case.

Comment on lines 96 to 121
private data class PrioritizedProvider<T>(
val provider: T,
val priority: Int,
val registrationTime: Long = currentTimeMillis(),
)

// Provider lists - protected by synchronized blocks for thread safety
// Matches iOS @MainActor pattern but using Kotlin's synchronized for cross-platform support
private val _sttProviders = mutableListOf<STTServiceProvider>()
private val _vadProviders = mutableListOf<VADServiceProvider>()
private val _llmProviders = mutableListOf<LLMServiceProvider>()
private val _ttsProviders = mutableListOf<TTSServiceProvider>()
private val _vlmProviders = mutableListOf<VLMServiceProvider>()
private val _wakeWordProviders = mutableListOf<WakeWordServiceProvider>()
private val _speakerDiarizationProviders = mutableListOf<SpeakerDiarizationServiceProvider>()
// Now using PrioritizedProvider for priority-based selection
private val _sttProviders = mutableListOf<PrioritizedProvider<STTServiceProvider>>()
private val _vadProviders = mutableListOf<PrioritizedProvider<VADServiceProvider>>()
private val _llmProviders = mutableListOf<PrioritizedProvider<LLMServiceProvider>>()
private val _ttsProviders = mutableListOf<PrioritizedProvider<TTSServiceProvider>>()
private val _speakerDiarizationProviders = mutableListOf<PrioritizedProvider<SpeakerDiarizationServiceProvider>>()

// MARK: - Factory-Based Registrations (iOS Pattern - ServiceRegistry.swift lines 77-90)

/**
* Factory-based service registrations matching iOS ServiceRegistry pattern
* These allow for closure-based service creation with direct createXXX methods
*/
private val _sttRegistrations = mutableListOf<ServiceRegistration<STTServiceFactory>>()
private val _llmRegistrations = mutableListOf<ServiceRegistration<LLMServiceFactory>>()
private val _ttsRegistrations = mutableListOf<ServiceRegistration<TTSServiceFactory>>()
private val _vadRegistrations = mutableListOf<ServiceRegistration<VADServiceFactory>>()
private val _speakerDiarizationRegistrations = mutableListOf<ServiceRegistration<SpeakerDiarizationServiceFactory>>()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical architectural issue: Dual independent registration systems create confusion and runtime errors.

The introduction of factory-based registrations (_sttRegistrations, _llmRegistrations, etc.) alongside the existing provider-based registrations (_sttProviders, _vadProviders, etc.) creates two independent, non-interoperating systems:

  • createSTT() only searches _sttRegistrations (factory-based)
  • sttProvider() only searches _sttProviders (provider-based)

This means:

  1. If a user registers via registerSTT(provider), calling createSTT() will throw ProviderNotFound
  2. If a user registers via registerSTTFactory(), calling sttProvider() will return null

This creates significant confusion about which registration pattern to use and can cause unexpected runtime failures.

Recommendation: Bridge the two systems by either:

  1. Making createXXX() methods also check provider registrations and call createXXXService() on providers
  2. Making xxxProvider() methods also check factory registrations and create wrapper providers
  3. Deprecating one pattern in favor of the other with clear migration guidance
🔎 Example bridge implementation for createSTT
 suspend fun createSTT(
     modelId: String? = null,
     config: STTConfiguration,
 ): STTService {
-    val registration = synchronized(_sttRegistrations) {
-        _sttRegistrations.firstOrNull { it.canHandle(modelId) }
-    } ?: throw SDKError.ProviderNotFound("STT provider for model: ${modelId ?: "default"}")
+    // Try factory registrations first
+    val registration = synchronized(_sttRegistrations) {
+        _sttRegistrations.firstOrNull { it.canHandle(modelId) }
+    }
+    
+    if (registration != null) {
+        logger.info("Creating STT service: ${registration.name} for model: ${modelId ?: "default"}")
+        return registration.factory(config)
+    }
+    
+    // Fall back to provider registrations
+    val provider = sttProvider(modelId)
+        ?: throw SDKError.ProviderNotFound("STT provider for model: ${modelId ?: "default"}")
     
-    logger.info("Creating STT service: ${registration.name} for model: ${modelId ?: "default"}")
-    return registration.factory(config)
+    logger.info("Creating STT service: ${provider.name} for model: ${modelId ?: "default"}")
+    return provider.createSTTService(config)
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 799 to 854
interface STTServiceProvider {
suspend fun createSTTService(configuration: STTConfiguration): STTService

fun canHandle(modelId: String?): Boolean

val name: String
val framework: LLMFramework
val framework: InferenceFramework
}

/**
* Provider for Voice Activity Detection services
*/
interface VADServiceProvider {
suspend fun createVADService(configuration: VADConfiguration): VADService

fun canHandle(modelId: String): Boolean

val name: String
}

// LLMServiceProvider is now imported from com.runanywhere.sdk.components.llm.LLMServiceProvider
// LLMServiceProvider is now imported from com.runanywhere.sdk.features.llm.LLMServiceProvider

/**
* Provider for Text-to-Speech services
*/
interface TTSServiceProvider {
suspend fun synthesize(text: String, options: com.runanywhere.sdk.components.TTSOptions): ByteArray
fun synthesizeStream(text: String, options: com.runanywhere.sdk.components.TTSOptions): kotlinx.coroutines.flow.Flow<ByteArray>
fun canHandle(modelId: String): Boolean = true
val name: String
/** Framework this provider supports */
val framework: LLMFramework
}
suspend fun synthesize(
text: String,
options: com.runanywhere.sdk.features.tts.TTSOptions,
): ByteArray

fun synthesizeStream(
text: String,
options: com.runanywhere.sdk.features.tts.TTSOptions,
): kotlinx.coroutines.flow.Flow<ByteArray>

/**
* Provider for Vision Language Model services
*/
interface VLMServiceProvider {
suspend fun analyze(image: ByteArray, prompt: String?): com.runanywhere.sdk.components.VLMOutput
suspend fun generateFromImage(image: ByteArray, prompt: String, options: com.runanywhere.sdk.generation.GenerationOptions): String
fun canHandle(modelId: String): Boolean = true
val name: String
}

/**
* Provider for Wake Word Detection services
*/
interface WakeWordServiceProvider {
suspend fun createWakeWordService(configuration: com.runanywhere.sdk.components.wakeword.WakeWordConfiguration): com.runanywhere.sdk.components.wakeword.WakeWordService
fun canHandle(modelId: String?): Boolean
val name: String

/** Framework this provider supports */
val framework: InferenceFramework
}

/**
* Provider for Speaker Diarization services
*/
interface SpeakerDiarizationServiceProvider {
suspend fun createSpeakerDiarizationService(configuration: com.runanywhere.sdk.components.speakerdiarization.SpeakerDiarizationConfiguration): com.runanywhere.sdk.components.speakerdiarization.SpeakerDiarizationService
suspend fun createSpeakerDiarizationService(
configuration: com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationConfiguration,
): com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationService

fun canHandle(modelId: String?): Boolean

val name: String
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major inconsistency: Framework property missing from some provider interfaces.

The provider interfaces have inconsistent framework properties:

Has framework property:

  • STTServiceProvider (line 805)
  • TTSServiceProvider (line 840)

Missing framework property:

  • VADServiceProvider (lines 811-817)
  • SpeakerDiarizationServiceProvider (lines 846-854)

This inconsistency makes the API confusing and prevents uniform framework-based queries across all provider types. Given that framework information is important for adapter management and the coding guidelines recommend structured types over strings, all provider interfaces should consistently declare the framework property.

🔎 Recommended fix to add framework property to all providers
 interface VADServiceProvider {
     suspend fun createVADService(configuration: VADConfiguration): VADService
 
     fun canHandle(modelId: String): Boolean
 
     val name: String
+    val framework: InferenceFramework
 }
 interface SpeakerDiarizationServiceProvider {
     suspend fun createSpeakerDiarizationService(
         configuration: com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationConfiguration,
     ): com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationService
 
     fun canHandle(modelId: String?): Boolean
 
     val name: String
+    val framework: InferenceFramework
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface STTServiceProvider {
suspend fun createSTTService(configuration: STTConfiguration): STTService
fun canHandle(modelId: String?): Boolean
val name: String
val framework: LLMFramework
val framework: InferenceFramework
}
/**
* Provider for Voice Activity Detection services
*/
interface VADServiceProvider {
suspend fun createVADService(configuration: VADConfiguration): VADService
fun canHandle(modelId: String): Boolean
val name: String
}
// LLMServiceProvider is now imported from com.runanywhere.sdk.components.llm.LLMServiceProvider
// LLMServiceProvider is now imported from com.runanywhere.sdk.features.llm.LLMServiceProvider
/**
* Provider for Text-to-Speech services
*/
interface TTSServiceProvider {
suspend fun synthesize(text: String, options: com.runanywhere.sdk.components.TTSOptions): ByteArray
fun synthesizeStream(text: String, options: com.runanywhere.sdk.components.TTSOptions): kotlinx.coroutines.flow.Flow<ByteArray>
fun canHandle(modelId: String): Boolean = true
val name: String
/** Framework this provider supports */
val framework: LLMFramework
}
suspend fun synthesize(
text: String,
options: com.runanywhere.sdk.features.tts.TTSOptions,
): ByteArray
fun synthesizeStream(
text: String,
options: com.runanywhere.sdk.features.tts.TTSOptions,
): kotlinx.coroutines.flow.Flow<ByteArray>
/**
* Provider for Vision Language Model services
*/
interface VLMServiceProvider {
suspend fun analyze(image: ByteArray, prompt: String?): com.runanywhere.sdk.components.VLMOutput
suspend fun generateFromImage(image: ByteArray, prompt: String, options: com.runanywhere.sdk.generation.GenerationOptions): String
fun canHandle(modelId: String): Boolean = true
val name: String
}
/**
* Provider for Wake Word Detection services
*/
interface WakeWordServiceProvider {
suspend fun createWakeWordService(configuration: com.runanywhere.sdk.components.wakeword.WakeWordConfiguration): com.runanywhere.sdk.components.wakeword.WakeWordService
fun canHandle(modelId: String?): Boolean
val name: String
/** Framework this provider supports */
val framework: InferenceFramework
}
/**
* Provider for Speaker Diarization services
*/
interface SpeakerDiarizationServiceProvider {
suspend fun createSpeakerDiarizationService(configuration: com.runanywhere.sdk.components.speakerdiarization.SpeakerDiarizationConfiguration): com.runanywhere.sdk.components.speakerdiarization.SpeakerDiarizationService
suspend fun createSpeakerDiarizationService(
configuration: com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationConfiguration,
): com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationService
fun canHandle(modelId: String?): Boolean
val name: String
}
interface STTServiceProvider {
suspend fun createSTTService(configuration: STTConfiguration): STTService
fun canHandle(modelId: String?): Boolean
val name: String
val framework: InferenceFramework
}
/**
* Provider for Voice Activity Detection services
*/
interface VADServiceProvider {
suspend fun createVADService(configuration: VADConfiguration): VADService
fun canHandle(modelId: String): Boolean
val name: String
val framework: InferenceFramework
}
// LLMServiceProvider is now imported from com.runanywhere.sdk.features.llm.LLMServiceProvider
/**
* Provider for Text-to-Speech services
*/
interface TTSServiceProvider {
suspend fun synthesize(
text: String,
options: com.runanywhere.sdk.features.tts.TTSOptions,
): ByteArray
fun synthesizeStream(
text: String,
options: com.runanywhere.sdk.features.tts.TTSOptions,
): kotlinx.coroutines.flow.Flow<ByteArray>
fun canHandle(modelId: String): Boolean = true
val name: String
/** Framework this provider supports */
val framework: InferenceFramework
}
/**
* Provider for Speaker Diarization services
*/
interface SpeakerDiarizationServiceProvider {
suspend fun createSpeakerDiarizationService(
configuration: com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationConfiguration,
): com.runanywhere.sdk.features.speakerdiarization.SpeakerDiarizationService
fun canHandle(modelId: String?): Boolean
val name: String
val framework: InferenceFramework
}
🤖 Prompt for AI Agents
In
sdk/runanywhere-kotlin/src/commonMain/kotlin/com/runanywhere/sdk/core/ModuleRegistry.kt
around lines 799 to 854, the VADServiceProvider and
SpeakerDiarizationServiceProvider interfaces are missing the framework property
present on STTServiceProvider and TTSServiceProvider; add a val framework:
InferenceFramework to both interfaces, keeping it non-nullable and named exactly
"framework" to match the other providers so consumers can uniformly query
supported frameworks and adapter selection logic remains consistent.

- Updated service registration methods in ModuleRegistry for STT, LLM, TTS, VAD, and Speaker Diarization to enhance readability.
- Improved error handling in VADError by adding new error types for better debugging.
- Refactored APIEndpoint to ensure consistent environment handling.
- Streamlined model discovery logic in ModelDiscoveryService for better maintainability.
- Enhanced ErrorContext logging for clearer error reporting.
@sanchitmonga22
Copy link
Contributor Author

@greptile please review the PR

- Introduced PlatformAudioProcessor for speaker embedding creation and audio feature extraction.
- Added SimpleEnergyVADService for energy-based voice activity detection, replacing the deprecated WebRTCVADService.
- Enhanced VADComponent error handling and service registration for improved clarity.
- Updated model discovery service to streamline framework inference and model format handling.
- Refactored various components for better maintainability and performance across the SDK.
- Introduced .editorconfig for Ktlint to customize rules for Kotlin files, particularly for Compose functions and imports.
- Updated build.gradle.kts to include Ktlint plugin configuration for better code quality checks.
- Removed outdated detekt-config.yml and adjusted detekt.yml settings to enhance code analysis and maintainability.
- Deleted unused model-related files and cleaned up the project structure for improved clarity and performance.
…ntegration

- Removed outdated models_info.json file to streamline model management.
- Implemented registerModulesAndModels() method in RunAnywhereApplication for explicit model registration, aligning with iOS patterns.
- Enhanced SettingsViewModel to utilize SDK's getStorageInfo(), clearCache(), and cleanTempFiles() APIs for better storage management.
- Introduced LlamaCPP and ONNX modules for LLM and STT/TTS capabilities, respectively, with detailed model registration and usage instructions.
- Added public storage extensions for improved model handling and storage operations, matching iOS functionality.
…y and consistency

- Updated RunAnywhereApplication to utilize new artifact type methods for model registration, enhancing readability.
- Renamed downloadModel() to startDownload() in ModelSelectionViewModel for better clarity in function purpose.
- Improved logging during model download processes to provide clearer feedback on operations.
- Streamlined device info collection in Android and JVM implementations to match iOS patterns.
- Removed obsolete ModelIntegrityVerifier and related checks to simplify model management.
@sanchitmonga22
Copy link
Contributor Author

@greptile please do a detailed review

- Updated SpeechToTextScreen to set the selected model name in the ViewModel for improved user feedback.
- Refactored SpeechToTextViewModel to include a method for setting the selected model name, enhancing clarity in model selection.
- Improved logging in TextToSpeechViewModel and VoiceAssistantViewModel to use modelId consistently, ensuring better traceability during model load events.
- Removed obsolete LlamaCppAdapter and LlamaCppServiceProvider files to streamline the codebase and improve maintainability.
- Updated ONNX module to enhance service creation and registration processes, aligning with recent architectural changes.
… management

- Deleted SecureStorage.kt and SettingsDataStore.kt files to simplify the data handling architecture.
- Updated related components to remove dependencies on these classes, enhancing maintainability and clarity in the codebase.
sanchitmonga22 and others added 16 commits January 4, 2026 16:14
- Bumped coreVersion to 0.2.6 and commonsVersion to 0.1.2 in build.gradle.kts and gradle.properties files.
- Updated checksums in Package.swift for the new versions of XCFrameworks.
- Removed outdated unit test files for RunAnywhere STT and AnalyticsNetworkService.
- Introduced new JNI functions to convert Float32 and Int16 PCM audio data to WAV format.
- Added a function to retrieve the WAV header size.
- Updated CppBridgeTTS to utilize the new audio conversion functions for TTS output.
- Enhanced build scripts to ensure proper handling of JNI libraries across modules.
- Added context-specific key for ViewModel to prevent caching issues across different contexts in ModelSelectionBottomSheet.
- Enhanced ModelSelectionViewModel to sync currently loaded models with the UI state, ensuring accurate representation of loaded models.
- Updated VoiceAssistantViewModel to initialize audio capture service and refresh model states upon screen appearance, improving responsiveness.
- Implemented error handling for model loading failures in the SDK, ensuring robust operation during voice processing.
- Enhanced storage metrics retrieval for downloaded models, providing better insights into storage usage.
- Set runanywhere.testLocal to true in gradle.properties for both Android and Kotlin SDKs to facilitate local development.
- Added device manager and telemetry manager functionalities in the Kotlin SDK, including device registration and telemetry event handling.
- Improved analytics event registration to route events through the telemetry manager for better tracking and reporting.
- Updated CppBridge to initialize telemetry with device information and handle HTTP callbacks for telemetry events.
- Deleted the .gitmodules file as the associated submodule is no longer needed.
- Updated build.gradle.kts to ensure the buildLocalJniLibs task always cleans old JNI libraries before building fresh ones.
- Modified build-local.sh to improve backend selection logic and ensure proper handling of JNI libraries, including support for sherpa-onnx libraries.
- Complete telemetry implementation for LLM, STT, TTS events
- Device registration with UPSERT and persistent device ID
- JWT authentication flow for production/staging environments
- Token refresh mechanism
- Proper model ID/name tracking (not file paths)
- STT streaming telemetry with is_streaming flag
- Device info collection (architecture, chip, memory, GPU family)
- Production mode single registration, dev mode UPSERT
* chore: Enable local testing and enhance telemetry integration

- Set runanywhere.testLocal to true in gradle.properties for both Android and Kotlin SDKs to facilitate local development.
- Added device manager and telemetry manager functionalities in the Kotlin SDK, including device registration and telemetry event handling.
- Improved analytics event registration to route events through the telemetry manager for better tracking and reporting.
- Updated CppBridge to initialize telemetry with device information and handle HTTP callbacks for telemetry events.

* clenaup

* feat: Add Kotlin SDK telemetry aligned with Swift SDK

- Complete telemetry implementation for LLM, STT, TTS events
- Device registration with UPSERT and persistent device ID
- JWT authentication flow for production/staging environments
- Token refresh mechanism
- Proper model ID/name tracking (not file paths)
- STT streaming telemetry with is_streaming flag
- Device info collection (architecture, chip, memory, GPU family)
- Production mode single registration, dev mode UPSERT

* updates

* resolving comments

* async update

* fixing the final issue identified - telemetry looks good for dev/prod

* minor fixes

* fix merge conflict
@greptile-apps
Copy link

greptile-apps bot commented Jan 7, 2026

Too many files changed for review.

@greptile-apps
Copy link

greptile-apps bot commented Jan 7, 2026

Too many files changed for review.

- Update workflow to inject SUPABASE_URL, SUPABASE_ANON_KEY, BUILD_TOKEN from GitHub secrets
- Bump VERSION to 0.1.4
- Fixes telemetry not working due to placeholder credentials in 0.1.3
- Remove indentation from heredocs
- Use printf instead of echo to avoid newline issues
- Strip newlines/carriage returns from secret values
- Fixes build failure due to broken string literals
@greptile-apps
Copy link

greptile-apps bot commented Jan 7, 2026

Too many files changed for review.

- commonsVersion: 0.1.3 → 0.1.4 (with Supabase telemetry)
- coreVersion: 0.1.3 → 0.1.4 (with llama.cpp b7658)
- Updated RACommons checksum for iOS

Note: Backend checksums will be updated once core-v0.1.4 release completes
- RABackendLlamaCPP checksum updated for core-v0.1.4
- RABackendONNX checksum updated for core-v0.1.4
- Kotlin SDK version: 0.1.3 → 0.1.4
- runanywhere.coreVersion: 0.1.3 → 0.1.4
- runanywhere.commonsVersion: 0.1.3 → 0.1.4

This file was overriding the defaults in build.gradle.kts
@sanchitmonga22 sanchitmonga22 merged commit 6341482 into dev Jan 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants