Skip to content

Conversation

@xianshijing-lk
Copy link
Contributor

@xianshijing-lk xianshijing-lk commented Feb 1, 2026

Summary by CodeRabbit

  • New Features

    • Added microphone and speaker audio controls to the mobile app
    • Integrated LiveKit SDK for real-time audio streaming capabilities
    • Added audio recording permission request
  • Chores

    • Updated Android SDK, Gradle plugins, and Kotlin versions
    • Updated Java target compatibility to version 21

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

@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

This PR integrates LiveKit audio capabilities into a mobile example project by updating Gradle and build configurations to Java 17/Android API 34, adding audio I/O management via a new MediaManager Kotlin class, implementing Rust-side audio capture/playback with room connection logic, and exposing JNI and FFI interfaces to bridge native and application layers.

Changes

Cohort / File(s) Summary
Dependency and Cargo Configuration
examples/mobile/Cargo.toml
Added futures and parking_lot dependencies for async task management and synchronization primitives.
Android IDE Configuration
examples/mobile/android/.idea/*
Added and updated IDE project metadata files including compiler target (JDK 21), code style rules, Gradle settings, VCS mappings, and run configurations. No behavioral changes; configuration artifacts only.
Gradle Build Configuration
examples/mobile/android/build.gradle, examples/mobile/android/gradle/wrapper/gradle-wrapper.properties
Updated plugin versions (Android Gradle Plugin 8.0.2→8.2.2, Kotlin 1.7.20→1.9.22) and Gradle wrapper distribution (8.0→9.0-milestone-1). Added network timeout and distribution validation settings.
Gradle Wrapper Scripts
examples/mobile/android/gradlew, examples/mobile/android/gradlew.bat
Refactored for improved portability: Unix script switched to /bin/sh with POSIX-compliant symlink resolution, Java detection, and argument handling; Windows batch script updated with SPDX header and error handling improvements.
Android App Build and Manifest
examples/mobile/android/app/build.gradle, examples/mobile/android/app/src/main/AndroidManifest.xml
Upgraded compileSdk and targetSdk to 34, Java compatibility to 17, Kotlin extension to 1.5.8, and bumped compose/lifecycle/activity dependencies. Added RECORD_AUDIO and MODIFY_AUDIO_SETTINGS permissions.
Android Kotlin Application Logic
examples/mobile/android/app/src/main/java/io/livekit/rustexample/App.kt, examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt, examples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.kt
Refactored App.kt to add native availability checks, guard JNI calls, and expose audio I/O methods. Expanded MainActivity.kt with full UI composition including connection/audio controls and lifecycle management. Introduced new MediaManager class to coordinate microphone capture and speaker playback threads with permission handling and error resilience.
Rust Native Library
examples/mobile/src/lib.rs
Implemented LiveKit audio integration with AudioState for buffer management, connection logic (livekit_connect, disconnect), and bidirectional audio handlers (capture/playback). Exposed JNI interfaces for Android and extern "C" FFI for iOS, bridging Rust async runtime with native mobile audio paths.

Sequence Diagrams

sequenceDiagram
    participant Android as Android App
    participant MediaMgr as MediaManager
    participant Rust as Rust (lib.rs)
    participant LiveKit as LiveKit Room
    
    Android->>Rust: livekit_connect(url, token)
    activate Rust
    Rust->>LiveKit: Connect & Initialize Room
    activate LiveKit
    LiveKit-->>Rust: Room Connected
    deactivate LiveKit
    Rust->>Rust: Create AudioState (capture/playback buffers)
    Rust-->>Android: Connection Established
    deactivate Rust
    
    Android->>MediaMgr: startMicrophone()
    activate MediaMgr
    MediaMgr->>MediaMgr: Create AudioRecord, spawn capture thread
    MediaMgr-->>Android: Recording Started
    deactivate MediaMgr
    
    loop Audio Capture Loop
        MediaMgr->>MediaMgr: Read from microphone
        MediaMgr->>Rust: push_audio_capture(samples)
        activate Rust
        Rust->>Rust: Buffer and convert to AudioFrame
        Rust->>LiveKit: Publish audio track
        LiveKit-->>Rust: ACK
        Rust-->>MediaMgr: Bytes processed
        deactivate Rust
    end
    
    loop Audio Playback Loop
        LiveKit->>Rust: Remote audio frames (subscription)
        activate Rust
        Rust->>Rust: Decode and buffer samples
        deactivate Rust
        MediaMgr->>Rust: pull_audio_playback(buffer)
        activate Rust
        Rust->>Rust: Drain playback buffer (or silence)
        Rust-->>MediaMgr: Playback samples
        deactivate Rust
        MediaMgr->>MediaMgr: Write to speaker (AudioTrack)
    end
    
    Android->>MediaMgr: stopMicrophone()
    Android->>Rust: livekit_disconnect()
    activate Rust
    Rust->>LiveKit: Disconnect Room
    Rust-->>Android: Disconnected
    deactivate Rust
Loading
sequenceDiagram
    participant User as User
    participant MainActivity as MainActivity
    participant App as App
    participant MediaMgr as MediaManager
    participant Rust as Rust
    
    User->>MainActivity: Enter URL & Token
    User->>MainActivity: Tap Connect
    MainActivity->>App: connect(url, token)
    activate App
    App->>Rust: connectNative(url, token)
    activate Rust
    Rust->>Rust: Initialize LiveKit audio state
    Rust-->>App: Connection Success
    deactivate Rust
    App->>MainActivity: onConnected
    deactivate App
    MainActivity->>MainActivity: Update UI (show audio controls)
    
    User->>MainActivity: Tap Start Audio
    MainActivity->>MediaMgr: startAll()
    activate MediaMgr
    MediaMgr->>MediaMgr: Request RECORD_AUDIO permission
    alt Permission Granted
        MediaMgr->>MediaMgr: Start microphone & speaker threads
        MediaMgr-->>MainActivity: Audio Active
    else Permission Denied
        MediaMgr-->>MainActivity: Permission Error
    end
    deactivate MediaMgr
    
    MainActivity->>MainActivity: Update isAudioActive state
    User->>MainActivity: Tap Stop Audio
    MainActivity->>MediaMgr: stopAll()
    MediaMgr->>MediaMgr: Stop capture & playback threads
    MediaMgr-->>MainActivity: Audio Stopped
    MainActivity->>MainActivity: Update UI
    
    User->>MainActivity: Tap Disconnect
    MainActivity->>App: disconnect()
    activate App
    App->>Rust: disconnectNative()
    Rust->>Rust: Close LiveKit room
    Rust-->>App: Disconnected
    deactivate App
    MainActivity->>MainActivity: Reset UI to disconnected state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • davidliu
  • ladvoc
  • theomonnom

Poem

🐰 Hops with glee through mobile dreams,
Audio flows in streaming beams,
Rust and Kotlin, hand in paw,
LiveKit magic, without a flaw!
Buffers hop, threads align—
A mobile marvel, oh so fine! 🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly summarizes the main objective: making the Rust SDK Android example functional. This aligns with the comprehensive changes across Android configuration, Kotlin code, and Rust FFI implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sxian/CLT-2516/fix_rust_sdk_android_example

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.

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: 9

Caution

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

⚠️ Outside diff range comments (1)
examples/mobile/src/lib.rs (1)

401-406: ⚠️ Potential issue | 🟠 Major

Guard FFI inputs to avoid UB/panics.

Line 401-405 assumes non-null UTF‑8; invalid pointers/UTF‑8 will panic or cause UB.

🛠️ Suggested fix
 #[no_mangle]
 pub extern "C" fn livekit_connect(url: *const c_char, token: *const c_char) {
-    let (url, token) = unsafe {
-        let url = CStr::from_ptr(url).to_str().unwrap().to_owned();
-        let token = CStr::from_ptr(token).to_str().unwrap().to_owned();
-        (url, token)
-    };
+    if url.is_null() || token.is_null() {
+        return;
+    }
+    let url = unsafe { CStr::from_ptr(url) };
+    let token = unsafe { CStr::from_ptr(token) };
+    let (url, token) = match (url.to_str(), token.to_str()) {
+        (Ok(u), Ok(t)) => (u.to_owned(), t.to_owned()),
+        _ => return,
+    };
 
     super::livekit_connect(url, token);
 }
🤖 Fix all issues with AI agents
In `@examples/mobile/android/.idea/compiler.xml`:
- Line 4: The compiler.xml currently sets <bytecodeTargetLevel target="21" />
which mismatches build.gradle's Java 17 settings (sourceCompatibility,
targetCompatibility, jvmTarget); fix by making the two consistent—either change
the bytecodeTargetLevel value in compiler.xml to 17 to match build.gradle, or
update build.gradle's sourceCompatibility/targetCompatibility/jvmTarget to 21 if
Java 21 is intended—ensure you update the specific symbol <bytecodeTargetLevel
target="..."/> and the sourceCompatibility/targetCompatibility/jvmTarget entries
so IDE and Gradle targets align.

In `@examples/mobile/android/.idea/deploymentTargetSelector.xml`:
- Line 10: Remove the hardcoded physical device serial entry from the committed
config: delete or replace the <DeviceId ... identifier="serial=R3CW4047CGT" />
line (the DeviceId element containing the serial value "R3CW4047CGT") so no
personal device identifiers are tracked, and add
examples/mobile/android/.idea/deploymentTargetSelector.xml to .gitignore (or
remove the .idea file from repo) to prevent committing machine-specific
IDE/device configs going forward.

In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt`:
- Around line 109-115: The code currently sets isConnected.value = true and
statusMessage.value = "Connected to room" inside a
Handler(mainLooper).postDelayed(..., 1000), which marks the app connected after
a fixed delay; remove that fixed-delay logic and instead update isConnected and
statusMessage from the actual connection-success callback/event coming from the
native layer (or the LiveKit connection listener) — subscribe to the
room/connection success callback provided by your native binding, call
isConnected.value = true, statusMessage.value = "Connected to room" and
Log.i(TAG, ...) only when that callback fires, and handle connection failure by
setting isConnected false and updating statusMessage accordingly (use the same
identifiers: isConnected, statusMessage, TAG, and replace the
Handler(mainLooper).postDelayed block).
- Around line 35-41: The companion object in MainActivity currently hardcodes a
sensitive JWT in DEFAULT_TOKEN; remove the real token and replace it with a
non-secret placeholder (e.g., "REPLACE_WITH_TOKEN") and update code that reads
DEFAULT_TOKEN to instead load the token at runtime (from secure storage,
BuildConfig field, Intent extras, environment/config, or user input).
Specifically, edit the companion object constant DEFAULT_TOKEN and ensure any
code paths using DEFAULT_TOKEN (in MainActivity) can accept a runtime-supplied
value so secrets are not committed to source control.

In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.kt`:
- Around line 240-244: startAll() may leave one audio component running when the
other fails; make it atomic by rolling back any started component on failure.
Call startMicrophone() then startSpeaker(); if startSpeaker() fails but
startMicrophone() returned true, call stopMicrophone() before returning false
(and symmetrically if you call startSpeaker() first). Use startAll(),
startMicrophone(), startSpeaker(), stopMicrophone(), and stopSpeaker() to
implement this rollback and ensure the method returns true only when both starts
succeed.

In `@examples/mobile/android/gradle/wrapper/gradle-wrapper.properties`:
- Line 3: The example project is pinning a pre-release Gradle distribution
("distributionUrl" set to gradle-9.0-milestone-1) which can cause unstable
builds; update the distributionUrl value to use the latest stable Gradle release
(replace "gradle-9.0-milestone-1" with "gradle-9.3.1") so the wrapper points to
the stable bin distribution URL instead of the milestone build.

In `@examples/mobile/src/lib.rs`:
- Around line 112-135: The CI failed rustfmt; run cargo fmt to reformat the
affected lines around the LocalAudioTrack::create_audio_track call and the
room.local_participant().publish_track await chain (the block that constructs
TrackPublishOptions and calls publish_track on LocalTrack::Audio). Re-run
rustfmt/cargo fmt and commit the resulting formatting changes so the
create_audio_track, TrackPublishOptions { ..Default::default() }, and
publish_track(...).await lines conform to the project’s formatter.
- Around line 71-74: The log in function livekit_connect currently prints the
full token (log::info!("Connecting to {} with token {}", url, token)); change
this to avoid leaking credentials by redacting or masking the token before
logging — e.g., log only a safe substring or token length (mask middle
characters) or remove the token from the message entirely; update the log::info
call in livekit_connect to use the masked_token representation instead of the
raw token.
- Around line 466-474: The JNI string conversions in
Java_io_livekit_rustexample_App_connectNative currently call
env.get_string(...).unwrap(), which will panic on JNI errors and cross the FFI
boundary; change this to safely handle the Result by matching or using if let to
detect errors from env.get_string for both url and token, log or report the JNI
error (rather than panicking), and return early from
Java_io_livekit_rustexample_App_connectNative when conversion fails so no panic
crosses FFI; update references to the converted values only after successful
conversion.
🧹 Nitpick comments (2)
examples/mobile/android/.idea/AndroidProjectSystem.xml (1)

1-6: Consider whether .idea/ files should be committed.

IDE-specific configuration files under .idea/ can cause issues when team members use different IDE versions or configurations. Some files (like this one) are benign, but others in this PR contain local paths or machine-specific settings.

Common practice is to either:

  • Exclude most .idea/ files via .gitignore (except codeStyles/ if team shares formatting)
  • Or commit them all for IDE consistency across the team
examples/mobile/android/.idea/misc.xml (1)

1-10: Consider excluding .idea files from version control.

IDE configuration files like misc.xml contain local machine settings (JDK paths, project-specific IDE preferences) that may vary between developers. These files are typically added to .gitignore to avoid unnecessary merge conflicts and to prevent enforcing specific IDE configurations on contributors.

If the intent is to provide a ready-to-use example project, this is acceptable, but consider documenting the JDK 21 requirement in a README instead.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdbca12 and d965187.

⛔ Files ignored due to path filters (3)
  • examples/mobile/android/app/libs/libwebrtc.jar is excluded by !**/*.jar
  • examples/mobile/android/app/src/main/jniLibs/arm64-v8a/libmobile.so is excluded by !**/*.so
  • examples/mobile/android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (21)
  • examples/mobile/Cargo.toml
  • examples/mobile/android/.idea/AndroidProjectSystem.xml
  • examples/mobile/android/.idea/codeStyles/Project.xml
  • examples/mobile/android/.idea/codeStyles/codeStyleConfig.xml
  • examples/mobile/android/.idea/compiler.xml
  • examples/mobile/android/.idea/deploymentTargetSelector.xml
  • examples/mobile/android/.idea/gradle.xml
  • examples/mobile/android/.idea/migrations.xml
  • examples/mobile/android/.idea/misc.xml
  • examples/mobile/android/.idea/runConfigurations.xml
  • examples/mobile/android/.idea/vcs.xml
  • examples/mobile/android/app/build.gradle
  • examples/mobile/android/app/src/main/AndroidManifest.xml
  • examples/mobile/android/app/src/main/java/io/livekit/rustexample/App.kt
  • examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt
  • examples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.kt
  • examples/mobile/android/build.gradle
  • examples/mobile/android/gradle/wrapper/gradle-wrapper.properties
  • examples/mobile/android/gradlew
  • examples/mobile/android/gradlew.bat
  • examples/mobile/src/lib.rs
🧰 Additional context used
🧬 Code graph analysis (1)
examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt (1)
examples/mobile/android/app/src/main/java/io/livekit/rustexample/ui/theme/Theme.kt (1)
  • RustexampleTheme (40-70)
🪛 Blinter (1.0.102)
examples/mobile/android/gradlew.bat

[error] 88-88: Complex quote escaping error. Explanation: Complex quote patterns may not be handled correctly by the command interpreter. Recommendation: Use triple-quote pattern """text""" for quotes within quoted strings, or escape properly with variables. Context: Complex quote escaping detected

(E028)

🪛 GitHub Actions: Rust Formatting
examples/mobile/src/lib.rs

[error] 117-117: Rustfmt diff detected at line 117. Run 'cargo fmt' to apply formatting changes.


[error] 127-127: Rustfmt diff detected at line 127. Run 'cargo fmt' to apply formatting changes.

🪛 Gitleaks (8.30.0)
examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt

[high] 40-40: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test (x86_64-apple-darwin)
  • GitHub Check: Test (x86_64-unknown-linux-gnu)
  • GitHub Check: Test (x86_64-pc-windows-msvc)
🔇 Additional comments (16)
examples/mobile/android/gradle/wrapper/gradle-wrapper.properties (1)

4-5: LGTM!

Adding networkTimeout and validateDistributionUrl are sensible defaults for more robust wrapper behavior.

examples/mobile/android/.idea/migrations.xml (1)

1-10: LGTM!

Standard Android Studio migration configuration using portable $PROJECT_DIR$ reference.

examples/mobile/android/.idea/gradle.xml (1)

3-9: LGTM!

The Gradle configuration updates use portable references (#GRADLE_LOCAL_JAVA_HOME) and the CHOOSE_PER_TEST runner provides flexibility for test execution.

examples/mobile/android/.idea/vcs.xml (1)

5-6: LGTM!

VCS mappings for submodule directories using portable $PROJECT_DIR$ references.

examples/mobile/android/.idea/codeStyles/codeStyleConfig.xml (1)

1-5: LGTM!

Enabling per-project code style settings ensures consistent formatting across team members.

examples/mobile/Cargo.toml (1)

12-13: LGTM!

The futures and parking_lot dependencies are well-established crates appropriate for async audio I/O and efficient synchronization in the LiveKit integration.

examples/mobile/android/.idea/runConfigurations.xml (1)

1-17: Standard IDE configuration.

This suppresses unwanted run configuration suggestions. Same consideration applies as with other .idea files regarding version control inclusion.

examples/mobile/android/app/build.gradle (1)

8-8: LGTM!

The build configuration updates are well-aligned:

  • SDK 34 is the current stable target
  • Java 17 compatibility with matching jvmTarget
  • Kotlin compiler extension 1.5.8 is compatible with Kotlin 1.9.22
  • Dependency versions are recent and consistent

Also applies to: 13-13, 29-34, 40-40, 51-55, 63-63

examples/mobile/android/build.gradle (1)

3-5: LGTM!

Plugin version updates are consistent with the app-level build configuration and align with modern Android tooling.

examples/mobile/android/gradlew.bat (3)

16-17: LGTM on license header addition.

Adding the SPDX license identifier is a good practice for license clarity.


46-53: Improved error handling.

The changes from == to equ for numeric comparisons (ERRORLEVEL) and redirecting errors to stderr (1>&2) follow Windows batch best practices.

Also applies to: 62-66, 81-81


86-89: The original pattern on line 88 is correct; the suggested refactor would change behavior incorrectly.

The pattern if not ""=="%GRADLE_EXIT_CONSOLE%" is a standard, idiomatic Windows batch check for non-empty strings and is documented as a best practice. The suggested alternative if defined GRADLE_EXIT_CONSOLE is not equivalent: it checks whether the variable is defined (exists), whereas the original checks whether it is non-empty. Using if defined would execute the exit regardless of whether GRADLE_EXIT_CONSOLE is set to an empty value, changing the script's intended behavior. No changes needed on line 88.

Likely an incorrect or invalid review comment.

examples/mobile/android/.idea/codeStyles/Project.xml (1)

1-123: LGTM: Project-level code style config is consistent.

No issues spotted in this IDE-only configuration.

examples/mobile/android/gradlew (1)

1-251: LGTM: POSIX wrapper update looks standard.

No issues found.

examples/mobile/android/app/src/main/AndroidManifest.xml (1)

5-7: LGTM: audio permissions added.

Looks appropriate for microphone + audio output use.

examples/mobile/android/app/src/main/java/io/livekit/rustexample/App.kt (1)

1-90: LGTM: native availability guard and API surface look good.

No issues found.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f 'build.gradle' examples/mobile/android --exec grep -n -E '(sourceCompatibility|targetCompatibility|jvmTarget|JavaVersion)' {} \;

Repository: livekit/rust-sdks

Length of output: 196


Bytecode target level mismatches build.gradle Java version.

compiler.xml sets bytecodeTargetLevel to 21, but build.gradle configures Java 17 for sourceCompatibility, targetCompatibility, and jvmTarget. This inconsistency can cause the IDE to behave differently from command-line Gradle builds.

Align the bytecode target level in compiler.xml to match Java 17, or update build.gradle to use Java 21 if that's the intended target.

🤖 Prompt for AI Agents
In `@examples/mobile/android/.idea/compiler.xml` at line 4, The compiler.xml
currently sets <bytecodeTargetLevel target="21" /> which mismatches
build.gradle's Java 17 settings (sourceCompatibility, targetCompatibility,
jvmTarget); fix by making the two consistent—either change the
bytecodeTargetLevel value in compiler.xml to 17 to match build.gradle, or update
build.gradle's sourceCompatibility/targetCompatibility/jvmTarget to 21 if Java
21 is intended—ensure you update the specific symbol <bytecodeTargetLevel
target="..."/> and the sourceCompatibility/targetCompatibility/jvmTarget entries
so IDE and Gradle targets align.

<DropdownSelection timestamp="2026-01-31T06:23:52.591285Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=R3CW4047CGT" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded device serial number should not be committed.

This file contains a device-specific serial number (R3CW4047CGT) that is unique to your physical device. Other contributors will not have this device, making this configuration ineffective and potentially confusing. This file should be added to .gitignore.

🤖 Prompt for AI Agents
In `@examples/mobile/android/.idea/deploymentTargetSelector.xml` at line 10,
Remove the hardcoded physical device serial entry from the committed config:
delete or replace the <DeviceId ... identifier="serial=R3CW4047CGT" /> line (the
DeviceId element containing the serial value "R3CW4047CGT") so no personal
device identifiers are tracked, and add
examples/mobile/android/.idea/deploymentTargetSelector.xml to .gitignore (or
remove the .idea file from repo) to prevent committing machine-specific
IDE/device configs going forward.

Comment on lines +35 to +41
companion object {
private const val TAG = "MainActivity"

// Default connection values - change these for your setup
private const val DEFAULT_URL = "ws://192.168.1.3:7880"
private const val DEFAULT_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzQyMDQ4MzQsImlkZW50aXR5IjoiZGMiLCJpc3MiOiJkZXZrZXkiLCJuYW1lIjoiZGMiLCJuYmYiOjE3NjgyMDQ4MzQsInN1YiI6ImRjIiwidmlkZW8iOnsicm9vbSI6ImNwcCIsInJvb21Kb2luIjp0cnVlfX0.e5dGHaTnEw6kToAJ_Z4SbhVuTKQ-3lUrMYhRxhE3Mu4"
}
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

Remove hardcoded JWT from source.

Line 40 contains a real-looking token (also flagged by Gitleaks). Avoid committing secrets; use a placeholder or user input instead.

🛠️ Suggested fix
-        private const val DEFAULT_TOKEN = "<redacted>"
+        private const val DEFAULT_TOKEN = "your-token-here"
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 40-40: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt`
around lines 35 - 41, The companion object in MainActivity currently hardcodes a
sensitive JWT in DEFAULT_TOKEN; remove the real token and replace it with a
non-secret placeholder (e.g., "REPLACE_WITH_TOKEN") and update code that reads
DEFAULT_TOKEN to instead load the token at runtime (from secure storage,
BuildConfig field, Intent extras, environment/config, or user input).
Specifically, edit the companion object constant DEFAULT_TOKEN and ensure any
code paths using DEFAULT_TOKEN (in MainActivity) can accept a runtime-supplied
value so secrets are not committed to source control.

Comment on lines +109 to +115
// Update state after a short delay to allow connection to establish
// In a real app, you'd want callbacks from the native side
android.os.Handler(mainLooper).postDelayed({
isConnected.value = true
statusMessage.value = "Connected to room"
Log.i(TAG, "Connected to room")
}, 1000)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t mark connected based on a fixed delay.

Line 109-115 sets connected after 1s regardless of actual result; update based on real connection status.

🛠️ Suggested fix
-        android.os.Handler(mainLooper).postDelayed({
-            isConnected.value = true
-            statusMessage.value = "Connected to room"
-            Log.i(TAG, "Connected to room")
-        }, 1000)
+        android.os.Handler(mainLooper).postDelayed({
+            val connected = currentApp.isConnected()
+            isConnected.value = connected
+            statusMessage.value = if (connected) "Connected to room" else "Connection failed"
+            Log.i(TAG, "Connection result: $connected")
+        }, 1000)
🤖 Prompt for AI Agents
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MainActivity.kt`
around lines 109 - 115, The code currently sets isConnected.value = true and
statusMessage.value = "Connected to room" inside a
Handler(mainLooper).postDelayed(..., 1000), which marks the app connected after
a fixed delay; remove that fixed-delay logic and instead update isConnected and
statusMessage from the actual connection-success callback/event coming from the
native layer (or the LiveKit connection listener) — subscribe to the
room/connection success callback provided by your native binding, call
isConnected.value = true, statusMessage.value = "Connected to room" and
Log.i(TAG, ...) only when that callback fires, and handle connection failure by
setting isConnected false and updating statusMessage accordingly (use the same
identifiers: isConnected, statusMessage, TAG, and replace the
Handler(mainLooper).postDelayed block).

Comment on lines +240 to +244
fun startAll(): Boolean {
val micStarted = startMicrophone()
val speakerStarted = startSpeaker()
return micStarted && speakerStarted
}
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

Rollback partial audio startup on failure.

startAll() can return false while leaving mic or speaker running (Line 240-244). That’s a privacy/UX risk.

🛠️ Suggested fix
     fun startAll(): Boolean {
         val micStarted = startMicrophone()
         val speakerStarted = startSpeaker()
+        if (!micStarted || !speakerStarted) {
+            if (micStarted) stopMicrophone()
+            if (speakerStarted) stopSpeaker()
+        }
         return micStarted && speakerStarted
     }
📝 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 startAll(): Boolean {
val micStarted = startMicrophone()
val speakerStarted = startSpeaker()
return micStarted && speakerStarted
}
fun startAll(): Boolean {
val micStarted = startMicrophone()
val speakerStarted = startSpeaker()
if (!micStarted || !speakerStarted) {
if (micStarted) stopMicrophone()
if (speakerStarted) stopSpeaker()
}
return micStarted && speakerStarted
}
🤖 Prompt for AI Agents
In
`@examples/mobile/android/app/src/main/java/io/livekit/rustexample/MediaManager.kt`
around lines 240 - 244, startAll() may leave one audio component running when
the other fails; make it atomic by rolling back any started component on
failure. Call startMicrophone() then startSpeaker(); if startSpeaker() fails but
startMicrophone() returned true, call stopMicrophone() before returning false
(and symmetrically if you call startSpeaker() first). Use startAll(),
startMicrophone(), startSpeaker(), stopMicrophone(), and stopSpeaker() to
implement this rollback and ensure the method returns true only when both starts
succeed.

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
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

🌐 Web query:

latest stable Gradle version 2025 2026

💡 Result:

As of February 1, 2026, the latest stable Gradle release is 9.3.1, released January 29, 2026. [1]

For 2025 context: Gradle 9.1.0 was released September 19, 2025. [2]

Sources: Gradle release notes pages. [1] [2]


Avoid using Gradle pre-release/milestone versions in example projects.

gradle-9.0-milestone-1 is a pre-release version that may contain bugs, incomplete features, or breaking changes before the stable release. This can cause unexpected build failures for users following the example.

Use the latest stable Gradle version instead (currently 9.3.1).

🤖 Prompt for AI Agents
In `@examples/mobile/android/gradle/wrapper/gradle-wrapper.properties` at line 3,
The example project is pinning a pre-release Gradle distribution
("distributionUrl" set to gradle-9.0-milestone-1) which can cause unstable
builds; update the distributionUrl value to use the latest stable Gradle release
(replace "gradle-9.0-milestone-1" with "gradle-9.3.1") so the wrapper points to
the stable bin distribution URL instead of the milestone build.

Comment on lines +71 to 74
/// Connect to a LiveKit room and set up audio handling
pub fn livekit_connect(url: String, token: String) {
log::info!("Connecting to {} with token {}", url, token);

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

Redact access tokens from logs.

Line 73 logs the full token, which can leak credentials via logs.

🛠️ Suggested fix
-    log::info!("Connecting to {} with token {}", url, token);
+    log::info!("Connecting to {}", url);
+    log::debug!("LiveKit token length: {}", token.len());
🤖 Prompt for AI Agents
In `@examples/mobile/src/lib.rs` around lines 71 - 74, The log in function
livekit_connect currently prints the full token (log::info!("Connecting to {}
with token {}", url, token)); change this to avoid leaking credentials by
redacting or masking the token before logging — e.g., log only a safe substring
or token length (mask middle characters) or remove the token from the message
entirely; update the log::info call in livekit_connect to use the masked_token
representation instead of the raw token.

Comment on lines +112 to +135
// Store room reference
{
let mut state = audio_state.lock();
state.room = Some(room.clone());
state.is_connected = true;
}

// Create and publish local audio track
let track = LocalAudioTrack::create_audio_track(
"microphone",
RtcAudioSource::Native(audio_source),
);

if let Err(e) = room
.local_participant()
.publish_track(
LocalTrack::Audio(track),
TrackPublishOptions {
source: TrackSource::Microphone,
..Default::default()
},
)
.await
{
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Run rustfmt to satisfy CI.

Pipeline shows rustfmt diffs at Line 117 and Line 127; please run cargo fmt.

🧰 Tools
🪛 GitHub Actions: Rust Formatting

[error] 117-117: Rustfmt diff detected at line 117. Run 'cargo fmt' to apply formatting changes.


[error] 127-127: Rustfmt diff detected at line 127. Run 'cargo fmt' to apply formatting changes.

🤖 Prompt for AI Agents
In `@examples/mobile/src/lib.rs` around lines 112 - 135, The CI failed rustfmt;
run cargo fmt to reformat the affected lines around the
LocalAudioTrack::create_audio_track call and the
room.local_participant().publish_track await chain (the block that constructs
TrackPublishOptions and calls publish_track on LocalTrack::Audio). Re-run
rustfmt/cargo fmt and commit the resulting formatting changes so the
create_audio_track, TrackPublishOptions { ..Default::default() }, and
publish_track(...).await lines conform to the project’s formatter.

Comment on lines +466 to 474
pub extern "C" fn Java_io_livekit_rustexample_App_connectNative(
mut env: JNIEnv,
_: JClass,
url: JString,
token: JString,
) {
let url: String = env.get_string(&url).unwrap().into();
let token: String = env.get_string(&token).unwrap().into();

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

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and read the relevant section
if [ -f "examples/mobile/src/lib.rs" ]; then
  echo "=== File exists, reading lines 466-480 ==="
  sed -n '466,480p' examples/mobile/src/lib.rs
else
  echo "File not found, listing examples directory structure"
  find examples -type f -name "*.rs" | head -20
fi

Repository: livekit/rust-sdks

Length of output: 533


🏁 Script executed:

# Search for other JNI get_string usage patterns in the file
rg "get_string" examples/mobile/src/lib.rs -A 2 -B 2

Repository: livekit/rust-sdks

Length of output: 269


🏁 Script executed:

# Check if there's error handling pattern for JNI errors in the file
rg "unwrap|expect|match.*Err|log::error" examples/mobile/src/lib.rs | head -20

Repository: livekit/rust-sdks

Length of output: 933


Avoid panics on invalid JNI strings.

Lines 472-473 unwrap get_string(...) results; JNI errors will panic across the FFI boundary causing undefined behavior.

🛠️ Suggested fix
-    let url: String = env.get_string(&url).unwrap().into();
-    let token: String = env.get_string(&token).unwrap().into();
+    let url: String = match env.get_string(&url) {
+        Ok(s) => s.into(),
+        Err(e) => {
+            log::error!("Failed to read url JString: {}", e);
+            return;
+        }
+    };
+    let token: String = match env.get_string(&token) {
+        Ok(s) => s.into(),
+        Err(e) => {
+            log::error!("Failed to read token JString: {}", e);
+            return;
+        }
+    };
📝 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
pub extern "C" fn Java_io_livekit_rustexample_App_connectNative(
mut env: JNIEnv,
_: JClass,
url: JString,
token: JString,
) {
let url: String = env.get_string(&url).unwrap().into();
let token: String = env.get_string(&token).unwrap().into();
pub extern "C" fn Java_io_livekit_rustexample_App_connectNative(
mut env: JNIEnv,
_: JClass,
url: JString,
token: JString,
) {
let url: String = match env.get_string(&url) {
Ok(s) => s.into(),
Err(e) => {
log::error!("Failed to read url JString: {}", e);
return;
}
};
let token: String = match env.get_string(&token) {
Ok(s) => s.into(),
Err(e) => {
log::error!("Failed to read token JString: {}", e);
return;
}
};
🤖 Prompt for AI Agents
In `@examples/mobile/src/lib.rs` around lines 466 - 474, The JNI string
conversions in Java_io_livekit_rustexample_App_connectNative currently call
env.get_string(...).unwrap(), which will panic on JNI errors and cross the FFI
boundary; change this to safely handle the Result by matching or using if let to
detect errors from env.get_string for both url and token, log or report the JNI
error (rather than panicking), and return early from
Java_io_livekit_rustexample_App_connectNative when conversion fails so no panic
crosses FFI; update references to the converted values only after successful
conversion.

Copy link
Member

Choose a reason for hiding this comment

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

do we need this binary here? if so it should be git LFSed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, we don't need this libmobile.so. Let me clean them up.

Copy link
Member

Choose a reason for hiding this comment

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

should this jar be checked in? interesting it's only 22kb

Copy link
Member

Choose a reason for hiding this comment

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

it only contains java files

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it needs to be checked in.

@theomonnom
Copy link
Member

theomonnom commented Feb 1, 2026

would be worth discussing whether we should use libwebrtc native mediadevices vs rewriting them in our example

@xianshijing-lk
Copy link
Contributor Author

would be worth discussing whether we should use libwebrtc native mediadevices vs rewriting them in our example

@cloudwebrtc mentioned that before, I wonder how the native mediadevice modules can be integrated into Rust SDK, how large the binary will add, and will it cause conflict problems for users who don't want them.

I will sync with Duan to understand the details better.

And I think it won't be done in this PR.

@xianshijing-lk xianshijing-lk changed the title initial version that seems functional Get Rust SDK Android Example working Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants