-
Notifications
You must be signed in to change notification settings - Fork 56
Replace Twilio's AudioSwitch library with a custom implementation #1591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
…nto audio_switch_replacement # Conflicts: # stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
…nto audio_switch_replacement
…nto audio_switch_replacement_same_api
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
WalkthroughImplements 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 Changes
Sequence DiagramssequenceDiagram
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
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
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
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
deviceChangeCallbackInvokedandbluetoothFailureCallbackInvokedflags 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.Afterwhile 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.Afterstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt (1)
71-89: Remove unused private function.The
convertTwilioDeviceToStreamDevicefunction is defined but never called within this file. Since it'sprivate, 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.Afterwhile line 39 uses@Before. Consider using@Afterfor 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
isWiredHeadsetOnis silently swallowed. While the fallback tofalseis 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
SecurityExceptionwhen checkingisAudioConnectedis 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 callingdeviceManager?.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
falseon 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: Redundantreturnstatement on line 78.The
returnkeyword at line 78 is unnecessary since line 71 already usesreturn 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
deviceSlotis captured at line 174 but never asserted. The verification at line 190-194 usesany<StreamAudioDevice>()which doesn't confirm the expected device was selected. Consider assertingdeviceSlot.capturedequalsspeakerDevice1.🔎 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.
📒 Files selected for processing (22)
demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.ktdemo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.ktstream-video-android-core/api/stream-video-android-core.apistream-video-android-core/build.gradle.ktsstream-video-android-core/src/main/AndroidManifest.xmlstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.ktstream-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.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.ktdemo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.ktdemo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.ktstream-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; limitinternalleakage across modules
Keep critical RTC paths off the main thread; prefer coroutines with structured scopes
Monitor logging verbosity; rely onStreamVideoImpl.developmentModefor guardrails
Use KDoc (/** ... */) for public APIs and complex subsystems; link to Stream docs when relevant
Group large files with// regionjudiciously; 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.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.ktstream-video-android-core/build.gradle.ktsstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.ktdemo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.ktdemo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.ktstream-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.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.ktstream-video-android-core/build.gradle.ktsstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.ktdemo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.ktdemo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.ktstream-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`())
UseTestBasefor fast unit tests andIntegrationTestBasefor end-to-end call flows
Files:
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.kt
**/build.gradle.kts
📄 CodeRabbit inference engine (AGENTS.md)
Do not modify
minSdkortargetSdkvalues; followgradle.propertiesand modulebuild.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.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/MicrophoneManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.ktstream-video-android-core/build.gradle.ktsstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManagerTest.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.ktdemo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManagerTest.ktstream-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.ktdemo-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.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.ktdemo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.ktdemo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.ktstream-video-android-core/src/test/kotlin/io/getstream/video/android/core/SpeakerManagerTest.ktstream-video-android-core/api/stream-video-android-core.apistream-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
useInBuiltAudioSwitchflag 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 deprecatedaudioproperty exists. Consider tracking a follow-up task to revert this toimplementationonce the deprecation period ends andaudiois 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
MediaManagerImplconstructor. 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 = truein the demo app for testing the new audio switching implementation. The SDK defaults tofalsefor 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
useInBuiltAudioSwitchparameter is well-documented with clear KDoc explaining both behaviors. The default value offalsemaintains backward compatibility, and the parameter is correctly propagated toStreamVideoClient.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
AudioDeviceManagerinterface 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 nullaudioproperty handling in the Twilio path.When
StreamAudioDeviceis created without a backing TwilioAudioDevice(e.g.,StreamAudioDevice.Speakerphone()with noaudioparameter),convertStreamDeviceToTwilioDevicewill returnnull. The TwilioAudioSwitch.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
audiois null butstreamDeviceis non-null.stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt (1)
27-117: LGTM!The
ModernAudioDeviceManagerimplementation correctly uses the API 31+ communication device APIs viaStreamAudioManager. The fallback logic inselectDevice()(tryingaudioDeviceInfofirst, thentoAudioDeviceInfo) and the cache synchronization ingetSelectedDevice()are well-handled. The@RequiresApiannotation 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
ModernAudioDeviceManagerfunctionality including device enumeration across all supported types, selection with various scenarios (directaudioDeviceInfo, 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
LegacyAudioDeviceManagercomprehensively 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
StateFlowforavailableDevicesandselectedDeviceStateprovides 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
selectDevicefails andselectCustomDeviceByPriority()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
StreamAudioDevicevariants 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 theAudioHandlervia factory pattern with:
- Priority-based device list respecting
preferSpeakerflag- Separate listeners for Twilio and Stream audio device changes
- Proper
useInBuiltAudioSwitchflag propagationThe conditional priority list construction (lines 742-755) correctly prioritizes speakerphone over earpiece when
preferSpeakeris true.
760-780: Dual listener approach correctly handles both audio switch implementations.The
twilioAudioDeviceChangeListenerconverts TwilioAudioDevicetoStreamAudioDeviceviafromAudio(), whilestreamAudioDeviceChangeListenerusesStreamAudioDevicedirectly. 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 toAudioHandler.selectDevice(device)without requiring conversion. The follow-up logic correctly updates speaker status and tracks non-headset fallback devices.
1268-1271: NewuseInBuiltAudioSwitchflag correctly defaults tofalsefor backward compatibility.The new constructor parameter enables the in-house audio switch when set to
true, while defaulting tofalsepreserves 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 lazyfor defaultAudioDeviceinstances 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
audioproperty for Twilio compatibility- New
audioDeviceInfoproperty for native Android- Clear
@Deprecatedannotation withReplaceWithhint...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 theaudioproperty is marked@Deprecatedwith 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
useInBuiltAudioSwitchparameter. The synthetic constructor withDefaultConstructorMarkercorrectly 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
useInBuiltAudioSwitchflag 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 newselectDevice(StreamAudioDevice)overload enables gradual migration. Consumers can continue using the deprecated Twilio types while migrating to the newStreamAudioDeviceAPI.
8542-8544: LGTM on data class expansions.The
component3()and updatedcopy()methods properly expose the newaudioDeviceInfoproperty 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 -StreamAudioDeviceis a sealed class.The concern about breaking external implementations does not apply.
StreamAudioDeviceis 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 theaudioDeviceInfoproperty 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:
namedefaults to device-specific strings ("Bluetooth", "Wired Headset", "Earpiece", "Speakerphone")audiodefaults to lazy-initialized Twilio AudioDevice instances- Only
audioDeviceInfois nullable (= null), which is intentional and properly handled via safe navigation operators in consuming code (e.g.,ModernAudioDeviceManager.selectDevice()andtoAudioDeviceInfo())The no-arg constructors are intentional and widely used throughout the codebase without issue.
Likely an incorrect or invalid review comment.
| // 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, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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?).
| <!-- 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" />--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <!-- 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.
| @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() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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).
| @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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
+ }
}| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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).
|
|
Note Docstrings generation - SUCCESS |
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`


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
StreamAudioSwitchclass: Custom audio switch implementation using Android'sAudioManagerAPIsModernAudioDeviceManagerfor Android API 23+ (usesAudioManager.setCommunicationDevice())LegacyAudioDeviceManagerfor older Android versions (usesAudioManager.setMode()and routing APIs)AudioHandlerFactoryto create appropriate handlers based on Android versionAudioManager.getDevices()API Compatibility
audioproperty inStreamAudioDeviceaudioDeviceInfoproperty for native Android device informationimplementationtoapito support consumers still using the deprecated propertyMigration Notes
audioproperty onStreamAudioDeviceis deprecated in favor ofaudioDeviceInfoaudioDeviceInfofor 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
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.