Skip to content

Conversation

@PratimMallick
Copy link
Contributor

@PratimMallick PratimMallick commented Dec 24, 2025

Goal

This PR replaces Twilio's AudioSwitch library with a custom implementation built on native Android AudioManager APIs. The new implementation maintains full API compatibility while providing better control over audio device management and reducing external dependencies.

Implementation

Core Implementation

  • New StreamAudioSwitch class: Custom audio switch implementation using Android's AudioManager APIs
  • Audio device managers:
    • ModernAudioDeviceManager for Android API 23+ (uses AudioManager.setCommunicationDevice())
    • LegacyAudioDeviceManager for older Android versions (uses AudioManager.setMode() and routing APIs)
  • Audio handler system: Factory pattern with AudioHandlerFactory to create appropriate handlers based on Android version
  • Device enumeration: Native Android device discovery using AudioManager.getDevices()

API Compatibility

  • Maintained backward compatibility by keeping the deprecated audio property in StreamAudioDevice
  • Added audioDeviceInfo property for native Android device information
  • Updated dependency visibility: Changed Twilio AudioSwitch from implementation to api to support consumers still using the deprecated property

Migration Notes

  • No breaking changes - existing APIs remain functional
  • The audio property on StreamAudioDevice is deprecated in favor of audioDeviceInfo
  • Consumers can continue using existing code, but should migrate to audioDeviceInfo for future compatibility

🎨 UI Changes

None

Testing

Use BT devices, wired headsets to test android on devices android 12+ and pre android 12(Legacy)

Summary by CodeRabbit

  • New Features

    • Added configurable built-in audio switch functionality for enhanced audio device management.
    • Improved audio device selection mechanism with better Bluetooth headset and wired device detection.
  • Improvements

    • Optimized device enumeration across Android API levels (API 24–31+) for more reliable audio routing.
    • Updated Bluetooth permissions to support modern connectivity requirements.

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

@PratimMallick PratimMallick requested a review from a team as a code owner December 24, 2025 11:44
@github-actions
Copy link
Contributor

github-actions bot commented Dec 24, 2025

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@PratimMallick PratimMallick changed the title audio_switch_replacement_same_api Replace Twilio's AudioSwitch library with a custom implementation Dec 24, 2025
@github-actions
Copy link
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 11.94 MB 11.96 MB 0.02 MB 🟢
stream-video-android-ui-xml 5.68 MB 5.70 MB 0.02 MB 🟢
stream-video-android-ui-compose 6.27 MB 6.27 MB 0.00 MB 🟢

@coderabbitai
Copy link

coderabbitai bot commented Dec 24, 2025

Walkthrough

Implements a native built-in audio device switching feature for the Stream Video SDK. Replaces AudioSwitch-based selection with an AudioHandler architecture supporting both Twilio's AudioSwitch and a new StreamAudioSwitch implementation, controlled via useInBuiltAudioSwitch configuration flag.

Changes

Cohort / File(s) Summary
Configuration & Builder
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
Adds useInBuiltAudioSwitch: Boolean parameter to StreamVideoBuilder (public, default false), propagates through StreamVideoClient, and updates MediaManagerImpl constructor invocation with named parameters including the new flag.
Audio Handler Abstraction
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt
Extends AudioHandler interface with selectDevice(StreamAudioDevice?) method. Introduces AudioHandlerFactory that selects between native StreamAudioSwitchHandler and Twilio AudioSwitchHandler based on useInBuiltAudioSwitch flag, handling device preference list mapping.
Native Audio Switch Implementation
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
Introduces native Android audio routing using AudioManager APIs with dynamic strategy selection (ModernAudioDeviceManager for API 31+, LegacyAudioDeviceManager for API 24-30), audio focus management, device enumeration with priority-based fallback, and Bluetooth state handling.
Audio Device Management (API Level-Specific)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt
Defines AudioDeviceManager interface for device lifecycle. Implements LegacyAudioDeviceManager (API 24-30) using BluetoothHeadset profile proxy, SCO management, and broadcast receivers; ModernAudioDeviceManager (API 31+) using AudioDeviceInfo and communication device APIs.
Audio Device Types & Helpers
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
Expands StreamAudioDevice with audioDeviceInfo property and conversion utilities (toAudioDevice, fromAudio, fromAudioDeviceInfo, toAudioDeviceInfo) with reflection-based Twilio device factory methods. Introduces StreamAudioManager compatibility wrapper for audio device callbacks and communication device APIs (API-level conditional).
Media Manager Integration
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
Replaces AudioSwitchHandler initialization with AudioHandlerFactory.create, wiring preferred device lists, device change listeners, and useInBuiltAudioSwitch flag. Updates MicrophoneManager to use new StreamAudioDevice-based selectDevice.
Demo Application
demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt, demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
Updates device selection equality logic from name-based to ID-based comparison when audioDeviceInfo available. Enables built-in audio switch in TelecomConfig via useInBuiltAudioSwitch = true.
Dependencies & Permissions
stream-video-android-core/build.gradle.kts, stream-video-android-core/src/main/AndroidManifest.xml
Exposes AudioSwitch library as public API (implementation → api). Replaces BLUETOOTH_CONNECT with BLUETOOTH (maxSdkVersion="30") for API-level compatibility.
API Surface
stream-video-android-core/api/stream-video-android-core.api
Comprehensive signature updates across MediaManagerImpl, CameraManager, SpeakerManager, AudioHandler, and related classes adding useInBuiltAudioSwitch parameter and StreamAudioDevice type support.
Test Coverage
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt, stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt, stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt, stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
Updates existing tests for new StreamAudioDevice constructors and method signatures. Adds comprehensive test suites for LegacyAudioDeviceManager (Bluetooth SCO, receiver lifecycle, error handling) and ModernAudioDeviceManager (device enumeration, selection, AudioManager integration).

Sequence Diagrams

sequenceDiagram
    participant App as Application
    participant SVB as StreamVideoBuilder
    participant SVC as StreamVideoClient
    participant MgmtMgr as MediaManagerImpl
    participant Factory as AudioHandlerFactory
    participant HAnd as AudioHandler
    participant DevMgr as AudioDeviceManager
    participant StrDev as StreamAudioDevice

    App->>SVB: create with useInBuiltAudioSwitch=true
    SVB->>SVC: build(useInBuiltAudioSwitch)
    SVC->>MgmtMgr: init with useInBuiltAudioSwitch
    MgmtMgr->>Factory: create(useInBuiltAudioSwitch, listeners)
    
    alt useInBuiltAudioSwitch == true
        Factory->>DevMgr: create StreamAudioSwitch → ModernAudioDeviceManager (API 31+)
        Note over DevMgr: or LegacyAudioDeviceManager (API 24-30)
        Factory->>HAnd: StreamAudioSwitchHandler
    else useInBuiltAudioSwitch == false
        Factory->>HAnd: AudioSwitchHandler (Twilio)
    end
    
    HAnd->>DevMgr: start()
    DevMgr->>DevMgr: enumerate devices
    
    App->>HAnd: selectDevice(device: StreamAudioDevice)
    HAnd->>DevMgr: selectDevice(StreamAudioDevice)
    DevMgr->>StrDev: convert to platform-specific
    DevMgr-->>HAnd: success/failure
    HAnd-->>App: device selected
Loading
sequenceDiagram
    participant BT as Bluetooth Headset
    participant LegacyMgr as LegacyAudioDeviceManager
    participant SCO as Bluetooth SCO
    participant Listener as StreamAudioDeviceChangeListener
    participant Receiver as BroadcastReceiver

    LegacyMgr->>LegacyMgr: start() - register receivers
    BT->>Receiver: connection change
    Receiver->>LegacyMgr: onReceive(intent)
    LegacyMgr->>LegacyMgr: updateDevices()
    LegacyMgr->>Listener: onDeviceChanged(devices, selected)
    
    App->>LegacyMgr: selectDevice(BluetoothHeadset)
    LegacyMgr->>SCO: startBluetoothSco()
    
    alt SCO connection succeeds
        SCO-->>LegacyMgr: SCO started
        LegacyMgr->>Listener: onDeviceChanged(devices, BluetoothHeadset)
    else SCO connection fails
        SCO-->>LegacyMgr: timeout/error
        LegacyMgr->>LegacyMgr: retry with backoff
        LegacyMgr->>Listener: onBluetoothConnectionFailure()
        LegacyMgr->>LegacyMgr: fallback to previous device
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Rationale: Multiple new audio device management implementations with substantial logic density (LegacyAudioDeviceManager Bluetooth state machine, lifecycle management, API-level conditionals), complex error handling across 8+ new files, state flow management, and integration across both Twilio and native Android audio APIs. Heterogeneous changes across configuration propagation, factory patterns, and low-level audio device enumeration/selection logic require separate reasoning per component despite cohesive feature goal.

Suggested labels

pr:improvement, audio, android-api

Poem

🐰 Hops through audio streams with glee,
Bluetooth, wired, speakers three!
Native switching, built just right,
Devices dance from left to right. 🎧✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.27% 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 includes Goal, Implementation, and Testing sections. However, it is missing several critical checklist items from the template: CLA signature confirmation, Slack channel notification, changelog updates, unit test coverage, and reviewer checklist completion. Complete the contributor checklist (CLA, Slack notification, changelog, unit tests) and specify reviewer checklist status to ensure the PR meets all repository requirements before merging.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing Twilio's AudioSwitch library with a custom implementation. It is specific and directly reflects the primary objective of the pull request.
✨ Finishing touches
  • 📝 Docstrings were successfully generated.
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch audio_switch_replacement_same_api

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

🧹 Nitpick comments (10)
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt (2)

51-52: Unused callback tracking flags.

The deviceChangeCallbackInvoked and bluetoothFailureCallbackInvoked flags are initialized but never asserted in any test. Either add tests that verify these callbacks are invoked under the right conditions, or remove the unused flags.


94-97: Inconsistent annotation import style.

Line 94 uses the fully-qualified @org.junit.After while line 56 uses the imported @Before. Consider using consistent annotation style.

🔎 Proposed fix
-    @org.junit.After
+    @After
     fun tearDown() {

And add the import:

import org.junit.After
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt (1)

71-89: Remove unused private function.

The convertTwilioDeviceToStreamDevice function is defined but never called within this file. Since it's private, it cannot be used elsewhere. This appears to be dead code that should be removed.

🔎 Proposed fix
     }
-
-    /**
-     * Converts a Twilio AudioDevice to StreamAudioDevice.
-     */
-    private fun convertTwilioDeviceToStreamDevice(twilioDevice: AudioDevice): StreamAudioDevice {
-        return when (twilioDevice) {
-            is AudioDevice.BluetoothHeadset -> StreamAudioDevice.BluetoothHeadset(
-                audio = twilioDevice,
-            )
-            is AudioDevice.WiredHeadset -> StreamAudioDevice.WiredHeadset(
-                audio = twilioDevice,
-            )
-            is AudioDevice.Earpiece -> StreamAudioDevice.Earpiece(
-                audio = twilioDevice,
-            )
-            is AudioDevice.Speakerphone -> StreamAudioDevice.Speakerphone(
-                audio = twilioDevice,
-            )
-        }
-    }
 }
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt (1)

48-51: Minor: Use consistent annotation style.

Line 48 uses @org.junit.After while line 39 uses @Before. Consider using @After for consistency.

🔎 Proposed fix
-    @org.junit.After
+    @After
     fun tearDown() {
         unmockkObject(StreamAudioManager)
     }
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt (2)

158-167: Log swallowed exception for debugging.

The exception caught when checking isWiredHeadsetOn is silently swallowed. While the fallback to false is appropriate, logging the exception would help with debugging audio issues on problematic devices.

🔎 Proposed fix
             @Suppress("DEPRECATION")
             val isWiredHeadsetOn = try {
                 audioManager.isWiredHeadsetOn
             } catch (e: Exception) {
+                logger.w { "[enumerateDevices] Failed to check isWiredHeadsetOn: ${e.message}" }
                 false
             }

646-650: Log swallowed SecurityException for debugging.

The SecurityException when checking isAudioConnected is silently swallowed. Adding a log statement would help diagnose Bluetooth permission issues.

🔎 Proposed fix
                     val isAudioConnected = try {
                         bluetoothHeadset?.isAudioConnected(bluetoothDevice) == true
                     } catch (e: SecurityException) {
+                        logger.w { "[startBluetoothSco] SecurityException checking isAudioConnected: ${e.message}" }
                         false
                     }
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt (1)

179-185: Consider removing empty synchronized block or adding actual deactivation logic.

The deactivate() method acquires a lock but only logs. If deactivation is intentionally a no-op, the synchronized block is unnecessary overhead. If device clearing is expected here, consider calling deviceManager?.clearDevice().

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt (2)

39-46: Swallowed exceptions hinder debugging - consider logging at debug level.

While returning false on failure is appropriate for graceful degradation, completely swallowing exceptions makes troubleshooting audio issues difficult. Consider logging at debug level before returning.

🔎 Proposed fix
+    private val logger by taggedLogger("StreamAudioManager")
+
     fun registerAudioDeviceCallback(
         audioManager: AudioManager,
         callback: AudioDeviceCallback,
         handler: android.os.Handler? = null,
     ): Boolean {
         return try {
             audioManager.registerAudioDeviceCallback(callback, handler)
             true
         } catch (e: Exception) {
+            logger.d(e) { "[registerAudioDeviceCallback] Failed to register callback" }
             false
         }
     }

Apply similar logging to other catch blocks at lines 59, 74, 82, 96, 118, and 132.


77-85: Redundant return statement on line 78.

The return keyword at line 78 is unnecessary since line 71 already uses return if. This is a minor style issue.

🔎 Proposed fix
         } else {
-            return try {
+            try {
                 // Use GET_DEVICES_OUTPUTS instead of GET_DEVICES_ALL to avoid duplicates
                 // (Bluetooth devices appear as both input and output, causing duplicates)
                 audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).toList()
             } catch (e: Exception) {
                 emptyList()
             }
         }
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt (1)

188-195: Test assertion weakened - captured device not verified.

The deviceSlot is captured at line 174 but never asserted. The verification at line 190-194 uses any<StreamAudioDevice>() which doesn't confirm the expected device was selected. Consider asserting deviceSlot.captured equals speakerDevice1.

🔎 Proposed fix
         // Then
         verify { microphoneManager.enforceSetup(preferSpeaker = false, any()) }
         // Since we only have speakers available, verify we selected the first one
-        verify {
-            microphoneManager.select(
-                any<StreamAudioDevice>(),
-            )
-        } // Verify the select method was called
+        assertEquals(speakerDevice1, deviceSlot.captured)
         assertEquals(false, speakerManager.speakerPhoneEnabled.value)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 01b3abc and a57c887.

📒 Files selected for processing (22)
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • stream-video-android-core/api/stream-video-android-core.api
  • stream-video-android-core/build.gradle.kts
  • stream-video-android-core/src/main/AndroidManifest.xml
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{kt,java}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{kt,java}: Use Kotlin with JVM toolchain 17; Java is legacy-only
Use 4-space indentation with no trailing whitespace
Avoid wildcard imports

Files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt
**/*.{kt,kts}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{kt,kts}: Use PascalCase for types and Composables (e.g., StreamCallActivity, ParticipantGrid)
Use camelCase for functions and values
Use UPPER_SNAKE_CASE for constants only when truly constant
Prefer explicit visibility modifiers; limit internal leakage across modules
Keep critical RTC paths off the main thread; prefer coroutines with structured scopes
Monitor logging verbosity; rely on StreamVideoImpl.developmentMode for guardrails
Use KDoc (/** ... */) for public APIs and complex subsystems; link to Stream docs when relevant
Group large files with // region judiciously; keep commentary purposeful
Sanitize logs to avoid dumping JWTs, ICE tokens, or call IDs in verbose logs
Pause/resume capture on lifecycle changes; ensure background audio routing is intentional
Validate orientation, aspect ratio, and dynascale handling for both portrait/landscape phones and tablets
Keep concurrency deterministic—use structured coroutines and avoid global scope
Ensure cleanup/teardown paths handle cancellation and failure (important for sockets, queues, retries)

Files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
  • stream-video-android-core/build.gradle.kts
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt
**/*.{kt,java,kts,gradle.kts}

📄 CodeRabbit inference engine (AGENTS.md)

Follow Spotless formatting; ensure custom license headers are in spotless/ directory

Files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
  • stream-video-android-core/build.gradle.kts
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt
**/src/test/**/*.{kt,kts}

📄 CodeRabbit inference engine (AGENTS.md)

**/src/test/**/*.{kt,kts}: Use descriptive backtick test names (e.g., fun `joining a call publishes participant tracks`())
Use TestBase for fast unit tests and IntegrationTestBase for end-to-end call flows

Files:

  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
**/build.gradle.kts

📄 CodeRabbit inference engine (AGENTS.md)

Do not modify minSdk or targetSdk values; follow gradle.properties and module build.gradle.kts

Files:

  • stream-video-android-core/build.gradle.kts
🧠 Learnings (8)
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to stream-video-android-previewdata/**/*.{kt,kts} : Keep test fixtures in `stream-video-android-previewdata`; avoid duplicating builder logic

Applied to files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
  • stream-video-android-core/build.gradle.kts
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Prefer Jetpack Compose for UI (`stream-video-android-ui-compose`); XML views supported via `stream-video-android-ui-xml`

Applied to files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to **/*.{kt,kts} : Pause/resume capture on lifecycle changes; ensure background audio routing is intentional

Applied to files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
  • stream-video-android-core/api/stream-video-android-core.api
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to **/*.{kt,kts} : Prefer explicit visibility modifiers; limit `internal` leakage across modules

Applied to files:

  • stream-video-android-core/build.gradle.kts
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to gradle/libs.versions.toml : Add new third-party dependencies through `gradle/libs.versions.toml` with reviewer buy-in

Applied to files:

  • stream-video-android-core/build.gradle.kts
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to **/src/test/**/*.{kt,kts} : Use `TestBase` for fast unit tests and `IntegrationTestBase` for end-to-end call flows

Applied to files:

  • stream-video-android-core/build.gradle.kts
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to **/src/androidTest/**/*.{kt,kts} : For Compose UI tests, add `get:Rule val composeRule = createComposeRule()` and use snapshot assertions where feasible

Applied to files:

  • stream-video-android-core/build.gradle.kts
📚 Learning: 2025-12-19T09:15:37.269Z
Learnt from: CR
Repo: GetStream/stream-video-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-19T09:15:37.269Z
Learning: Applies to **/ui-compose/**/*.{kt,kts} : Handle runtime camera/microphone permissions gracefully; surface rationale via Compose components when denied

Applied to files:

  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
🧬 Code graph analysis (3)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt (3)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt (2)
  • enumerateDevices (24-56)
  • enumerateDevices (28-28)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt (2)
  • enumerateDevices (72-193)
  • handleBluetoothConnectionFailure (713-730)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt (1)
  • enumerateDevices (35-71)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt (5)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt (1)
  • selectDevice (35-35)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt (1)
  • selectDevice (195-229)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt (1)
  • selectDevice (73-86)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt (1)
  • selectDevice (173-175)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt (1)
  • selectDevice (71-73)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt (2)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt (1)
  • clearDevice (40-40)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt (1)
  • clearDevice (231-236)
🪛 detekt (1.23.8)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt

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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt

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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt

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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)

⏰ 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). (5)
  • GitHub Check: compare-sdk-sizes / Compare SDK sizes
  • GitHub Check: base-android-ci / Run unit tests
  • GitHub Check: base-android-ci / Build
  • GitHub Check: base-android-ci / Run static checks
  • GitHub Check: Build / compose apks
🔇 Additional comments (31)
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.kt (1)

81-84: LGTM!

Good fix aligning the comment with the actual assertion count.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt (1)

183-183: LGTM!

Clean addition of the useInBuiltAudioSwitch flag with appropriate internal visibility and backward-compatible default value.

stream-video-android-core/build.gradle.kts (1)

192-193: Intentional API exposure for backward compatibility.

The api() configuration exposes Twilio AudioSwitch transitively to consumers. This is necessary for backward compatibility while the deprecated audio property exists. Consider tracking a follow-up task to revert this to implementation once the deprecation period ends and audio is removed.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt (2)

344-352: LGTM!

Good use of named parameters for the MediaManagerImpl constructor. This improves readability and makes the code more resilient to parameter ordering changes.


1356-1358: LGTM!

Minor formatting improvement for log readability.

stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.kt (1)

1-497: Good test coverage for LegacyAudioDeviceManager.

Comprehensive test suite covering device enumeration (speakerphone, earpiece, wired headset, Bluetooth), device selection with SCO management, lifecycle methods, and edge cases (null Bluetooth adapter, exceptions, missing telephony feature). Well-structured with proper mocking.

demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt (1)

338-338: LGTM!

Appropriate to enable useInBuiltAudioSwitch = true in the demo app for testing the new audio switching implementation. The SDK defaults to false for backward compatibility, while the demo app opts into the new behavior.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt (1)

149-153: LGTM!

The new useInBuiltAudioSwitch parameter is well-documented with clear KDoc explaining both behaviors. The default value of false maintains backward compatibility, and the parameter is correctly propagated to StreamVideoClient.

Also applies to: 283-283

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt (1)

24-56: LGTM!

The AudioDeviceManager interface is well-designed with clear separation of concerns: lifecycle management (start/stop), device enumeration, and selection. The KDoc for each method adequately describes its purpose, and the internal visibility is appropriate for SDK internals.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt (1)

89-106: Verify null audio property handling in the Twilio path.

When StreamAudioDevice is created without a backing Twilio AudioDevice (e.g., StreamAudioDevice.Speakerphone() with no audio parameter), convertStreamDeviceToTwilioDevice will return null. The Twilio AudioSwitch.selectDevice(null) call typically clears selection rather than selecting a specific device.

Ensure this behavior is intentional when mixing in-built devices with the Twilio path, or consider logging a warning when audio is null but streamDevice is non-null.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt (1)

27-117: LGTM!

The ModernAudioDeviceManager implementation correctly uses the API 31+ communication device APIs via StreamAudioManager. The fallback logic in selectDevice() (trying audioDeviceInfo first, then toAudioDeviceInfo) and the cache synchronization in getSelectedDevice() are well-handled. The @RequiresApi annotation ensures compile-time safety.

stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.kt (1)

32-527: Comprehensive test coverage approved.

The test suite thoroughly covers ModernAudioDeviceManager functionality including device enumeration across all supported types, selection with various scenarios (direct audioDeviceInfo, fallback resolution, failures), clearing, state retrieval, and lifecycle methods. The use of descriptive backtick test names follows the coding guidelines.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt (1)

38-743: Robust legacy audio device management implementation.

The LegacyAudioDeviceManager comprehensively handles the complexities of audio device management for API 24-30, including:

  • Bluetooth SCO connection with retry logic and timeouts
  • Proper deduplication of SCO vs A2DP Bluetooth devices
  • Fallback from BluetoothHeadset profile proxy to AudioDeviceInfo enumeration
  • Careful lifecycle management with proper receiver registration/unregistration

The implementation appropriately uses deprecated APIs (with @Suppress("DEPRECATION")) since these are the correct APIs for the target API level.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt (3)

46-88: Well-structured audio switch implementation with proper state management.

The class design is clean with proper encapsulation of audio device management. The use of StateFlow for availableDevices and selectedDeviceState provides reactive state observation. The API-level branching for device manager selection (API 31+ vs legacy) is appropriate.


96-134: LGTM - Start sequence is well-ordered.

The startup sequence correctly: (1) guards against double-start, (2) creates the appropriate device manager per API level, (3) requests audio focus, (4) starts the manager, (5) registers callbacks, and (6) enumerates devices. The synchronized block ensures thread-safe state transitions.


231-238: Potential infinite recursion if fallback selection fails repeatedly.

If selectDevice fails and selectCustomDeviceByPriority() returns the same device (e.g., when only one device type is available), the recursive call at line 236 could loop indefinitely within the synchronized block, causing a deadlock.

🔎 Proposed fix to prevent recursion
                     if (success) {
                         _selectedDeviceState.value = deviceToSelect
                         logger.d { "[selectNativeDevice] Native device selected: $deviceToSelect" }
                     } else {
                         logger.w { "[selectNativeDevice] Failed to select native device: $deviceToSelect" }
-                        // Fallback to automatic selection if the requested device is not available
-                        val autoSelected = selectCustomDeviceByPriority()
-                        if (autoSelected != null && autoSelected != deviceToSelect) {
-                            selectStreamAudioDevice(autoSelected)
-                        }
+                        // Clear selection on failure - avoid recursive fallback that could loop
+                        deviceManager?.clearDevice()
+                        _selectedDeviceState.value = null
+                        logger.w { "[selectNativeDevice] Cleared device selection after failure" }
                     }

Likely an incorrect or invalid review comment.

stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt (1)

39-40: Test device instantiation correctly updated to name-based constructors.

The test correctly instantiates StreamAudioDevice variants using the new name-based constructors (e.g., StreamAudioDevice.Speakerphone("test-speaker")), aligning with the new API surface. As per coding guidelines, test fixtures use descriptive names.

Also applies to: 70-71, 102-103, 134-136, 165-166

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt (4)

740-782: Well-structured factory-based audio handler creation with dual listener support.

The refactored setup() method cleanly creates the AudioHandler via factory pattern with:

  • Priority-based device list respecting preferSpeaker flag
  • Separate listeners for Twilio and Stream audio device changes
  • Proper useInBuiltAudioSwitch flag propagation

The conditional priority list construction (lines 742-755) correctly prioritizes speakerphone over earpiece when preferSpeaker is true.


760-780: Dual listener approach correctly handles both audio switch implementations.

The twilioAudioDeviceChangeListener converts Twilio AudioDevice to StreamAudioDevice via fromAudio(), while streamAudioDeviceChangeListener uses StreamAudioDevice directly. Both paths correctly update internal state and mark setup complete. This design cleanly supports the migration path.


634-650: Device selection logic properly updated for new abstraction.

The select() method now directly delegates to AudioHandler.selectDevice(device) without requiring conversion. The follow-up logic correctly updates speaker status and tracks non-headset fallback devices.


1268-1271: New useInBuiltAudioSwitch flag correctly defaults to false for backward compatibility.

The new constructor parameter enables the in-house audio switch when set to true, while defaulting to false preserves existing Twilio AudioSwitch behavior for consumers who don't opt-in.

stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt (3)

25-37: Lazy initialization of default Twilio devices is appropriate.

Using by lazy for default AudioDevice instances defers reflection-based creation until actually needed. This avoids startup overhead and potential issues if reflection fails on certain devices.


55-94: Well-designed backward-compatible device model with deprecation guidance.

The sealed class structure with:

  • Deprecated audio property for Twilio compatibility
  • New audioDeviceInfo property for native Android
  • Clear @Deprecated annotation with ReplaceWith hint

...provides a clean migration path for consumers. The data class defaults ensure both paths work independently.


254-297: Reflection-based Twilio AudioDevice creation is already documented as legacy. The fragility concern is valid but already addressed: KDoc at lines 250–252 explicitly describes the reflection approach, and the audio property is marked @Deprecated with guidance to use native Android device management instead. StreamAudioSwitch provides a modern alternative that avoids reflection. No additional comment is needed.

stream-video-android-core/api/stream-video-android-core.api (7)

8022-8023: LGTM!

The constructor overloads properly maintain binary compatibility while adding the new useInBuiltAudioSwitch parameter. The synthetic constructor with DefaultConstructorMarker correctly handles Kotlin's default parameter mechanism.


8035-8035: LGTM!

The getter properly exposes the new configuration flag added to the constructor.


8469-8470: LGTM!

The new constructor overloads properly propagate the useInBuiltAudioSwitch flag through the builder pattern while maintaining backward compatibility with existing constructors.


8516-8517: Good backward compatibility design.

Retaining the Twilio-based selectDevice(AudioDevice) while adding the new selectDevice(StreamAudioDevice) overload enables gradual migration. Consumers can continue using the deprecated Twilio types while migrating to the new StreamAudioDevice API.


8542-8544: LGTM on data class expansions.

The component3() and updated copy() methods properly expose the new audioDeviceInfo property following Kotlin data class conventions. This enables destructuring and copying with the new property.

Also applies to: 8566-8568, 8583-8585, 8600-8602


8528-8533: No action required - StreamAudioDevice is a sealed class.

The concern about breaking external implementations does not apply. StreamAudioDevice is sealed (line 50 of StreamAudioDevice.kt), which restricts subclasses to the four nested data classes defined in the same file (BluetoothHeadset, WiredHeadset, Earpiece, Speakerphone). All of these already implement the audioDeviceInfo property with appropriate default values, and the factory/conversion methods are already defined in the companion object. This is a safe change.


8537-8539: Review comment is incorrect regarding no-arg constructor behavior.

The no-arg constructors do NOT produce instances with null properties. All three parameters have non-null defaults:

  • name defaults to device-specific strings ("Bluetooth", "Wired Headset", "Earpiece", "Speakerphone")
  • audio defaults to lazy-initialized Twilio AudioDevice instances
  • Only audioDeviceInfo is nullable (= null), which is intentional and properly handled via safe navigation operators in consuming code (e.g., ModernAudioDeviceManager.selectDevice() and toAudioDeviceInfo())

The no-arg constructors are intentional and widely used throughout the codebase without issue.

Likely an incorrect or invalid review comment.

Comment on lines +219 to 258
// Compare devices by type and audioDeviceInfo ID (if available) since audio can be null when using custom audio switch
val selected = selectedMicroPhoneDevice
val isSelected = when {
selected == null -> false
else -> {
// First try to compare by audioDeviceInfo ID if both have it
val itInfoId = when (it) {
is StreamAudioDevice.BluetoothHeadset -> it.audioDeviceInfo?.id
is StreamAudioDevice.WiredHeadset -> it.audioDeviceInfo?.id
is StreamAudioDevice.Earpiece -> it.audioDeviceInfo?.id
is StreamAudioDevice.Speakerphone -> it.audioDeviceInfo?.id
}
val selectedInfoId = when (selected) {
is StreamAudioDevice.BluetoothHeadset -> selected.audioDeviceInfo?.id
is StreamAudioDevice.WiredHeadset -> selected.audioDeviceInfo?.id
is StreamAudioDevice.Earpiece -> selected.audioDeviceInfo?.id
is StreamAudioDevice.Speakerphone -> selected.audioDeviceInfo?.id
}

if (itInfoId != null && selectedInfoId != null) {
// Both have audioDeviceInfo, compare by ID
itInfoId == selectedInfoId
} else {
// Fall back to type comparison
when {
it is StreamAudioDevice.BluetoothHeadset && selected is StreamAudioDevice.BluetoothHeadset -> true
it is StreamAudioDevice.WiredHeadset && selected is StreamAudioDevice.WiredHeadset -> true
it is StreamAudioDevice.Earpiece && selected is StreamAudioDevice.Earpiece -> true
it is StreamAudioDevice.Speakerphone && selected is StreamAudioDevice.Speakerphone -> true
else -> false
}
}
}
}
AudioDeviceUiState(
it,
it.name,
icon,
it.audio.name == selectedMicroPhoneDevice?.audio?.name,
it.audioDeviceInfo?.id == selectedMicroPhoneDevice?.audioDeviceInfo?.id,
)
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

Unused isSelected variable creates selection bug when audioDeviceInfo is null.

The isSelected variable (lines 221-252) implements proper fallback logic for when audioDeviceInfo is null, but it's never used. Line 257 uses a direct ID comparison that returns false when either device lacks an audioDeviceInfo, breaking selection highlighting for the Twilio AudioSwitch path where audioDeviceInfo can be null.

🔎 Proposed fix - use the computed isSelected
         AudioDeviceUiState(
             it,
             it.name,
             icon,
-            it.audioDeviceInfo?.id == selectedMicroPhoneDevice?.audioDeviceInfo?.id,
+            isSelected,
         )

}

public abstract interface class io/getstream/video/android/core/audio/AudioHandler {
public abstract fun selectDevice (Lio/getstream/video/android/core/audio/StreamAudioDevice;)V
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
# Check git history for AudioHandler
echo "=== Checking git history for AudioHandler.kt ==="
git log --oneline --all -- "**/AudioHandler.kt" 2>/dev/null | head -20 || echo "No git history found"

echo ""
echo "=== Searching for AudioHandler implementations ==="
rg "implements AudioHandler|: AudioHandler" -t kotlin -t java 2>/dev/null || echo "No implementations found"

echo ""
echo "=== Checking for AudioHandler interface definition ==="
fd "AudioHandler.kt" -x cat {} \;

Repository: GetStream/stream-video-android

Length of output: 6657


Correct API signature for selectDevice method—parameter should be nullable.

The selectDevice method in the Kotlin source (selectDevice(audioDevice: StreamAudioDevice?)) accepts a nullable parameter, but the API file shows it as non-nullable (Lio/getstream/video/android/core/audio/StreamAudioDevice;). Update the API file to match the actual implementation signature with the nullable marker.

🤖 Prompt for AI Agents
In stream-video-android-core/api/stream-video-android-core.api around line 8508,
the abstract method signature shows the parameter as non-nullable; update the
method parameter type to match the Kotlin implementation's nullable type by
changing the parameter descriptor to the nullable form (add the nullable marker
to the StreamAudioDevice type) so the API line reflects the actual signature
selectDevice(StreamAudioDevice?).

Comment on lines +29 to +31
<!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />-->
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

Missing BLUETOOTH_CONNECT permission for API 31+.

The comment states that BLUETOOTH_CONNECT is required for API 31+, but the permission is commented out. On Android 12+ (API 31+), Bluetooth audio device enumeration and connection require the BLUETOOTH_CONNECT runtime permission. Without declaring it, the app cannot request this permission at runtime, effectively breaking Bluetooth audio functionality on modern Android versions.

🔎 Proposed fix
     <!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
     <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
-<!--    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />-->
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
📝 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
<!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<!-- <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />-->
<!-- Bluetooth permissions: BLUETOOTH for API < 31, BLUETOOTH_CONNECT for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
🤖 Prompt for AI Agents
In stream-video-android-core/src/main/AndroidManifest.xml around lines 29 to 31
the BLUETOOTH_CONNECT permission is commented out which prevents
declaring/requesting the runtime permission required on Android 12+ (API 31+)
for Bluetooth enumeration/connection; uncomment or add a uses-permission entry
for android.permission.BLUETOOTH_CONNECT (no maxSdkVersion) so the app can
request this runtime permission on API 31+ and retain the existing BLUETOOTH
entry with android:maxSdkVersion="30" for older APIs.

Comment on lines +154 to +178
@RequiresApi(Build.VERSION_CODES.S)
@JvmStatic
public fun toAudioDeviceInfo(
streamDevice: StreamAudioDevice,
audioManager: AudioManager,
): AudioDeviceInfo? {
// If the device already has an AudioDeviceInfo, use it
val existingInfo = streamDevice.audioDeviceInfo?.id

// Otherwise, try to find a matching device from available devices
// For API 31+: use communication devices, for API 24-30: use all output devices
val availableDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val commDevices = StreamAudioManager.getAvailableCommunicationDevices(audioManager)
if (existingInfo != null) {
return commDevices.find { it.id == existingInfo }
}
commDevices
} else {
// For API < 31, use getDevices() to get all output devices
try {
audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).toList()
} catch (e: Exception) {
emptyList()
}
}
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

@RequiresApi(S) annotation contradicts internal API-level branching.

The method is annotated with @RequiresApi(Build.VERSION_CODES.S) (line 154), but lines 171-178 contain code for API < 31 that will never be reached. Either remove the annotation if the method should support older APIs, or remove the dead code branch.

🔎 Proposed fix - remove dead code branch
         @RequiresApi(Build.VERSION_CODES.S)
         @JvmStatic
         public fun toAudioDeviceInfo(
             streamDevice: StreamAudioDevice,
             audioManager: AudioManager,
         ): AudioDeviceInfo? {
             // If the device already has an AudioDeviceInfo, use it
             val existingInfo = streamDevice.audioDeviceInfo?.id
 
-            // Otherwise, try to find a matching device from available devices
-            // For API 31+: use communication devices, for API 24-30: use all output devices
-            val availableDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                val commDevices = StreamAudioManager.getAvailableCommunicationDevices(audioManager)
-                if (existingInfo != null) {
-                    return commDevices.find { it.id == existingInfo }
-                }
-                commDevices
-            } else {
-                // For API < 31, use getDevices() to get all output devices
-                try {
-                    audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).toList()
-                } catch (e: Exception) {
-                    emptyList()
-                }
+            val commDevices = StreamAudioManager.getAvailableCommunicationDevices(audioManager)
+            if (existingInfo != null) {
+                return commDevices.find { it.id == existingInfo }
             }
+            val availableDevices = commDevices
🧰 Tools
🪛 detekt (1.23.8)

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

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt
around lines 154-178, the method is annotated with
@RequiresApi(Build.VERSION_CODES.S) while the method body contains an else
branch for API < S that will never be executed; remove the contradiction by
choosing one approach: either remove the @RequiresApi(Build.VERSION_CODES.S)
annotation so the method supports older API levels (keep and keep/adjust the
else branch and ensure runtime checks are correct), or delete the else branch
and any references to getDevices(...) and keep the @RequiresApi annotation;
apply the change consistently (update imports/annotations and adjust
nullability/return handling as needed).

Comment on lines +106 to +121
@RequiresApi(Build.VERSION_CODES.S)
fun setCommunicationDevice(
audioManager: AudioManager,
device: AudioDeviceInfo?,
): Boolean {
return try {
if (device != null) {
audioManager.setCommunicationDevice(device)
} else {
audioManager.clearCommunicationDevice()
}
true
} catch (e: Exception) {
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 | 🟡 Minor

Return value of setCommunicationDevice is discarded.

AudioManager.setCommunicationDevice() returns a boolean indicating success, but line 113 ignores it and always returns true. This could mask failures.

🔎 Proposed fix
     @RequiresApi(Build.VERSION_CODES.S)
     fun setCommunicationDevice(
         audioManager: AudioManager,
         device: AudioDeviceInfo?,
     ): Boolean {
         return try {
             if (device != null) {
-                audioManager.setCommunicationDevice(device)
-            } else {
-                audioManager.clearCommunicationDevice()
+                return audioManager.setCommunicationDevice(device)
             }
+            audioManager.clearCommunicationDevice()
             true
         } catch (e: Exception) {
             false
         }
     }
🧰 Tools
🪛 detekt (1.23.8)

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

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt
around lines 106 to 121, the method always returns true even though
AudioManager.setCommunicationDevice(device) returns a boolean that indicates
success; update the method to capture and return that boolean instead of
unconditionally returning true, e.g. call setCommunicationDevice when device !=
null and assign its return to a local result variable, call
clearCommunicationDevice when device == null and set result = true (or obtain an
appropriate success indicator), then return result; retain the existing
try/catch but ensure the catch returns false.

Comment on lines +302 to +325
private fun enumerateDevices() {
// Enumerate devices using the device manager (which works with NativeStreamAudioDevice)
val manager = deviceManager
val nativeDevices = manager?.enumerateDevices() ?: emptyList()

_availableDevices.value = nativeDevices

// Update selected device
val currentSelected = _selectedDeviceState.value
if (currentSelected != null && !nativeDevices.contains(currentSelected)) {
logger.w { "[enumerateDevices] Selected device no longer available: $currentSelected" }
deviceManager?.clearDevice()
_selectedDeviceState.value = null
} else {
// Update from manager
val managerSelected = manager?.getSelectedDevice()
if (managerSelected != null) {
_selectedDeviceState.value = managerSelected
}
}

// Notify listener
audioDeviceChangeListener?.invoke(nativeDevices, _selectedDeviceState.value)
}
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

Missing synchronization in enumerateDevices may cause race conditions.

This method modifies _availableDevices and _selectedDeviceState but is not synchronized, while selectStreamAudioDevice is. Since enumerateDevices is called from AudioDeviceCallback which can fire on any thread, concurrent access with selectStreamAudioDevice could lead to inconsistent state.

🔎 Proposed fix
     private fun enumerateDevices() {
+        synchronized(this) {
             // Enumerate devices using the device manager (which works with NativeStreamAudioDevice)
             val manager = deviceManager
             val nativeDevices = manager?.enumerateDevices() ?: emptyList()
 
             _availableDevices.value = nativeDevices
 
             // Update selected device
             val currentSelected = _selectedDeviceState.value
             if (currentSelected != null && !nativeDevices.contains(currentSelected)) {
                 logger.w { "[enumerateDevices] Selected device no longer available: $currentSelected" }
                 deviceManager?.clearDevice()
                 _selectedDeviceState.value = null
             } else {
                 // Update from manager
                 val managerSelected = manager?.getSelectedDevice()
                 if (managerSelected != null) {
                     _selectedDeviceState.value = managerSelected
                 }
             }
 
             // Notify listener
             audioDeviceChangeListener?.invoke(nativeDevices, _selectedDeviceState.value)
+        }
     }

Comment on lines +38 to +69
private var isAudioSwitchInitScheduled = false

override fun start() {
synchronized(this) {
if (!isAudioSwitchInitScheduled) {
isAudioSwitchInitScheduled = true
mainThreadHandler.removeCallbacksAndMessages(null)

logger.d { "[start] Posting on main" }

mainThreadHandler.post {
logger.d { "[start] Running on main" }

val switch = StreamAudioSwitch(
context = context,
preferredDeviceList = preferredDeviceList,
)
streamAudioSwitch = switch
switch.start(audioDeviceChangeListener)
}
}
}
}

override fun stop() {
logger.d { "[stop] no args" }
mainThreadHandler.removeCallbacksAndMessages(null)
mainThreadHandler.post {
streamAudioSwitch?.stop()
streamAudioSwitch = 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

State flag not reset on stop prevents restart.

The isAudioSwitchInitScheduled flag is set to true in start() but never reset in stop(). If start()stop()start() is called, the second start() will be skipped because the flag remains true, while streamAudioSwitch is already null.

🔎 Proposed fix
     override fun stop() {
         logger.d { "[stop] no args" }
         mainThreadHandler.removeCallbacksAndMessages(null)
         mainThreadHandler.post {
             streamAudioSwitch?.stop()
             streamAudioSwitch = null
         }
+        synchronized(this) {
+            isAudioSwitchInitScheduled = false
+        }
     }

Note: The same pattern exists in AudioSwitchHandler (lines 55-87), which may also need the same fix.

📝 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
private var isAudioSwitchInitScheduled = false
override fun start() {
synchronized(this) {
if (!isAudioSwitchInitScheduled) {
isAudioSwitchInitScheduled = true
mainThreadHandler.removeCallbacksAndMessages(null)
logger.d { "[start] Posting on main" }
mainThreadHandler.post {
logger.d { "[start] Running on main" }
val switch = StreamAudioSwitch(
context = context,
preferredDeviceList = preferredDeviceList,
)
streamAudioSwitch = switch
switch.start(audioDeviceChangeListener)
}
}
}
}
override fun stop() {
logger.d { "[stop] no args" }
mainThreadHandler.removeCallbacksAndMessages(null)
mainThreadHandler.post {
streamAudioSwitch?.stop()
streamAudioSwitch = null
}
}
override fun stop() {
logger.d { "[stop] no args" }
mainThreadHandler.removeCallbacksAndMessages(null)
mainThreadHandler.post {
streamAudioSwitch?.stop()
streamAudioSwitch = null
}
synchronized(this) {
isAudioSwitchInitScheduled = false
}
}
🤖 Prompt for AI Agents
In
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt
around lines 38 to 69, the isAudioSwitchInitScheduled flag is set true in
start() but never reset in stop(), preventing restart after stop(); modify
stop() to clear isAudioSwitchInitScheduled (set it to false) inside the
mainThreadHandler.post block after stopping and nulling streamAudioSwitch so
future start() calls can proceed, and apply the same fix to the corresponding
pattern in AudioSwitchHandler (lines ~55-87).

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots
31.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@PratimMallick PratimMallick added the pr:improvement Enhances an existing feature or code label Dec 24, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #1592

coderabbitai bot added a commit that referenced this pull request Dec 29, 2025
Docstrings generation was requested by @PratimMallick.

* #1591 (comment)

The following files were modified:

* `demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt`
* `demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt`
* `stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:improvement Enhances an existing feature or code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants