diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index ed9680a0af..7cd15b2547 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -72,6 +72,31 @@ import io.getstream.video.android.ui.menu.transcriptions.TranscriptionUiStateMan import io.getstream.video.android.util.filters.SampleAudioFilter import java.nio.ByteBuffer +/** + * Displays a settings popup for a Call with device, audio, video filter, transcription, and debug controls. + * + * Provides UI and actions to select audio/video devices and codecs, toggle audio usage and filters, + * control noise cancellation and incoming video visibility, manage recordings/transcriptions, and + * perform debug operations (ICE/SFU actions). The popup dismisses via onDismissed. + * + * @param call The active Call instance whose state and actions the menu manipulates. + * @param selectedVideoFilter Index of the currently selected video filter. + * @param showDebugOptions Whether debug-related menu items should be shown. + * @param noiseCancellationFeatureEnabled Whether the noise-cancellation feature is available. + * @param noiseCancellationEnabled Whether noise cancellation is currently enabled. + * @param onDismissed Callback invoked when the menu should be dismissed. + * @param onSelectVideoFilter Callback invoked with the selected filter index. + * @param onShowFeedback Callback invoked to open feedback UI. + * @param onNoiseCancellation Callback invoked to toggle or configure noise cancellation. + * @param selectedIncomingVideoResolution Currently selected preferred incoming video resolution, or null. + * @param onSelectIncomingVideoResolution Callback invoked with a new preferred incoming video resolution or null. + * @param isIncomingVideoEnabled Whether incoming video is enabled. + * @param onToggleIncomingVideoVisibility Callback invoked to show/hide incoming video. + * @param onShowCallStats Callback invoked to display call statistics. + * @param onSelectScaleType Callback invoked to change the remote video scaling type. + * @param closedCaptionUiState UI state for closed captions. + * @param onClosedCaptionsToggle Callback invoked to toggle closed captions. + */ @OptIn(ExperimentalPermissionsApi::class) @Composable internal fun SettingsMenu( @@ -429,4 +454,4 @@ private fun SettingsMenuPreview() { ), ) } -} +} \ No newline at end of file diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt index e5780a33b6..60cd8fa05d 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt @@ -216,7 +216,11 @@ object StreamVideoInitHelper { ).enqueue() } - /** Sets up and returns the [StreamVideo] required to connect to the API. */ + /** + * Builds a configured StreamVideo instance for the provided API key and user. + * + * @return A ready-to-use StreamVideo configured with the given user, token, logging level, call service registry, notification and telecom settings. + */ @OptIn(ExperimentalStreamVideoApi::class) private fun initializeStreamVideo( context: Context, @@ -338,4 +342,4 @@ object StreamVideoInitHelper { useInBuiltAudioSwitch = true, ).build() } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 6d40751fc1..db7f574170 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -1350,6 +1350,13 @@ public class Call( } } + /** + * Observes available microphone devices and ensures an appropriate audio input is selected. + * + * When a Bluetooth headset becomes available it is selected; otherwise a wired headset is selected + * if present. If no headset is available, the previously saved non-headset fallback device is + * restored when available. + */ private fun monitorHeadset() { microphone.devices.onEach { availableDevices -> logger.d { @@ -1780,4 +1787,4 @@ public data class CreateCallOptions( } return memberRequestList } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt index 2eef17b07a..1757482666 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt @@ -629,7 +629,11 @@ class MicrophoneManager( } /** - * Select a specific device + * Selects the given audio output device and updates related internal state. + * + * Requests the initialized audio handler to switch to `device`, sets the manager's selected device state, updates the speaker manager status when the speakerphone is (de)selected, and records a non-headset fallback device for future use. + * + * @param device The desired `StreamAudioDevice` to select, or `null` to clear selection. */ fun select(device: StreamAudioDevice?) { logger.i { "selecting device $device" } @@ -719,7 +723,17 @@ class MicrophoneManager( fun canHandleDeviceSwitch() = audioUsageProvider.invoke() != AudioAttributes.USAGE_MEDIA - // Internal logic + /** + * Initializes audio routing and device handling for the microphone manager. + * + * Sets up the Android AudioManager and, if device switching is supported and not already initialized, + * creates and starts an AudioHandler configured with a device priority that respects `preferSpeaker`. + * Once device information becomes available (or immediately if already set up), the optional + * `onAudioDevicesUpdate` callback is invoked and internal device/selection state is updated. + * + * @param preferSpeaker When true, prioritizes the speakerphone over the earpiece when building the preferred device list. + * @param onAudioDevicesUpdate Optional callback invoked once the available audio devices and current selection have been populated (or immediately if setup was already completed). + */ internal fun setup(preferSpeaker: Boolean = false, onAudioDevicesUpdate: (() -> Unit)? = null) { synchronized(this) { var capturedOnAudioDevicesUpdate = onAudioDevicesUpdate @@ -790,11 +804,25 @@ class MicrophoneManager( } } + /** + * Ensures audio handler setup (initializing it if necessary) and runs the provided action when audio devices update. + * + * @param preferSpeaker If true, prefer speakerphone in the initial device priority when setting up. + * @param actual Action to invoke when the audio devices are updated. + */ internal fun enforceSetup(preferSpeaker: Boolean = false, actual: () -> Unit) = setup( preferSpeaker, onAudioDevicesUpdate = actual, ) + /** + * Invokes the provided block with the initialized `AudioHandler` if available. + * + * If the audio handler has been initialized, the `then` lambda is called with it; + * otherwise the function logs an error indicating setup() must be called first. + * + * @param then Lambda to execute with the initialized `AudioHandler`. + */ private fun ifAudioHandlerInitialized(then: (audioHandler: AudioHandler) -> Unit) { if (this::audioHandler.isInitialized) { then(this.audioHandler) @@ -1393,4 +1421,4 @@ class MediaManagerImpl( } } -fun MediaStreamTrack.trySetEnabled(enabled: Boolean) = safeCall { setEnabled(enabled) } +fun MediaStreamTrack.trySetEnabled(enabled: Boolean) = safeCall { setEnabled(enabled) } \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt index ebcde7e6dc..8086069b65 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt @@ -179,15 +179,17 @@ public class StreamVideoBuilder @JvmOverloads constructor( } /** - * Builds the [StreamVideo] client. + * Constructs and initializes a configured StreamVideo client. * - * @return The [StreamVideo] client. + * Performs validation, sets up logging and time library, initializes notification and connection modules, + * attempts background coordinator connection and location loading, installs the client as the singleton, + * and returns the created client. * - * @throws RuntimeException If an instance of the client already exists and [ensureSingleInstance] is set to true. - * @throws IllegalArgumentException If [apiKey] is blank. - * @throws IllegalArgumentException If [user] type is [UserType.Authenticated] and the [user] id is blank. - * @throws IllegalArgumentException If [user] type is [UserType.Authenticated] and both [token] and [tokenProvider] are empty. - * @throws ConnectException If the WebSocket connection fails. + * @return The initialized StreamVideo client. + * @throws RuntimeException If an existing StreamVideo instance is present and `ensureSingleInstance` is true. + * @throws IllegalArgumentException If `apiKey` is blank. + * @throws IllegalArgumentException If `token` is blank. + * @throws IllegalArgumentException If `user` is of type `UserType.Authenticated` and the user id is blank. */ public fun build(): StreamVideo { val lifecycle = ProcessLifecycleOwner.get().lifecycle @@ -365,4 +367,4 @@ internal val tokenRepository = TokenRepository("") sealed class GEO { /** Run calls over our global edge network, this is the default and right for most applications */ object GlobalEdgeNetwork : GEO() -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt index 953c44b7f8..db1406fe75 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioDeviceManager.kt @@ -23,15 +23,18 @@ package io.getstream.video.android.core.audio */ internal interface AudioDeviceManager { /** - * Enumerates available audio devices. - */ + * Lists the currently available audio devices. + * + * @return A list of available StreamAudioDevice instances representing detected audio input/output devices. + */ fun enumerateDevices(): List /** - * Selects an audio device for routing. - * @param device The device to select - * @return true if selection was successful, false otherwise - */ + * Sets the active audio routing device. + * + * @param device The device to make active for audio routing. + * @return `true` if the device was successfully selected, `false` otherwise. + */ fun selectDevice(device: StreamAudioDevice): Boolean /** @@ -40,17 +43,21 @@ internal interface AudioDeviceManager { fun clearDevice() /** - * Gets the currently selected device. - */ + * Returns the currently selected audio device. + * + * @return The selected StreamAudioDevice, or `null` if no device is selected. + */ fun getSelectedDevice(): StreamAudioDevice? /** - * Starts the device manager (registers listeners, etc.) - */ + * Initializes the device manager and prepares it for use. + */ fun start() /** - * Stops the device manager (unregisters listeners, etc.) - */ + * Releases resources and tears down the device manager. + * + * Stops device monitoring, unregisters listeners or observers, and performs any necessary cleanup so the manager is no longer active. The manager must be started again before it can be used after this call. + */ fun stop() -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt index 92a88b056d..cd0255e658 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt @@ -33,11 +33,22 @@ public interface AudioHandler { public fun start() /** - * Called when a room is disconnected. - */ + * Stops audio device management and releases related resources. + * + * Cancels any pending initialization, stops any active audio switch on the main thread, + * and clears internal references so audio handling is fully stopped when a room is disconnected. + */ public fun stop() - public fun selectDevice(audioDevice: StreamAudioDevice?) + /** + * Selects the given StreamAudioDevice for audio output. + * + * Converts the provided StreamAudioDevice to its underlying Twilio AudioDevice and applies it. + * Passing `null` clears the current selection. + * + * @param audioDevice The StreamAudioDevice to select, or `null` to clear selection. + */ +public fun selectDevice(audioDevice: StreamAudioDevice?) } /** @@ -77,6 +88,12 @@ public class AudioSwitchHandler( } } + /** + * Stops and releases the audio switcher, cancelling any pending initialization. + * + * Cancels any pending main-thread runnables related to audio-switch initialization and schedules + * a main-thread task to stop the active AudioSwitch (if any) and clear its reference. + */ override fun stop() { logger.d { "[stop] no args" } mainThreadHandler.removeCallbacksAndMessages(null) @@ -86,11 +103,21 @@ public class AudioSwitchHandler( } } + /** + * Selects the given StreamAudioDevice for audio output by converting it to Twilio's AudioDevice and delegating to the existing selection logic. + * + * @param audioDevice The StreamAudioDevice to select; pass `null` to clear the current selection. + */ override fun selectDevice(audioDevice: StreamAudioDevice?) { val twilioDevice = convertStreamDeviceToTwilioDevice(audioDevice) selectDevice(twilioDevice) } + /** + * Selects the given Twilio audio device and applies the change. + * + * @param audioDevice The Twilio `AudioDevice` to select; pass `null` to clear the current selection. + */ public fun selectDevice(audioDevice: AudioDevice?) { logger.i { "[selectDevice] audioDevice: $audioDevice" } audioSwitch?.selectDevice(audioDevice) @@ -98,8 +125,10 @@ public class AudioSwitchHandler( } /** - * Converts a StreamAudioDevice to Twilio's AudioDevice. - * Returns null if the input is null. + * Extracts the underlying Twilio `AudioDevice` from a `StreamAudioDevice`. + * + * @param streamDevice The `StreamAudioDevice` to convert, or `null`. + * @return The contained `AudioDevice`, or `null` if `streamDevice` is `null`. */ private fun convertStreamDeviceToTwilioDevice(streamDevice: StreamAudioDevice?): AudioDevice? { return streamDevice?.audio @@ -127,4 +156,4 @@ public class AudioSwitchHandler( } } } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt index ba4c8c2c55..3762f4343c 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandlerFactory.kt @@ -26,13 +26,18 @@ import com.twilio.audioswitch.AudioDeviceChangeListener as TwilioAudioDeviceChan */ internal object AudioHandlerFactory { /** - * Creates an AudioHandler instance based on the useInBuiltAudioSwitch flag. + * Create an AudioHandler configured either with the in-built StreamAudioSwitch or the Twilio AudioSwitch. * - * @param context Android context - * @param preferredStreamAudioDeviceList List of preferred audio device types in priority order - * @param twilioAudioDeviceChangeListener Callback for device changes - * @param useInBuiltAudioSwitch If true, uses custom StreamAudioSwitch; if false, uses Twilio AudioSwitch - * @return An AudioHandler instance + * When `useInBuiltAudioSwitch` is true, returns a StreamAudioSwitchHandler using `preferredStreamAudioDeviceList` + * and `streamAudioDeviceChangeListener`. When false, returns an AudioSwitchHandler with `preferredStreamAudioDeviceList` + * mapped to Twilio `AudioDevice` types (unmapped types fall back to speakerphone) and `twilioAudioDeviceChangeListener`. + * + * @param context Android context used to initialize the audio handler. + * @param preferredStreamAudioDeviceList Preferred StreamAudioDevice classes in priority order. + * @param twilioAudioDeviceChangeListener Callback invoked on Twilio audio device changes (used when not using in-built switch). + * @param streamAudioDeviceChangeListener Callback invoked on Stream audio device changes (used when using in-built switch). + * @param useInBuiltAudioSwitch If true, use the in-built StreamAudioSwitch; otherwise use the Twilio AudioSwitch. + * @return An AudioHandler instance configured according to `useInBuiltAudioSwitch` and the provided preferences/listeners. */ fun create( context: Context, @@ -69,7 +74,10 @@ internal object AudioHandlerFactory { } /** - * Converts a Twilio AudioDevice to StreamAudioDevice. + * Map a Twilio `AudioDevice` to the corresponding `StreamAudioDevice`. + * + * @param twilioDevice The Twilio `AudioDevice` to convert. + * @return A `StreamAudioDevice` instance representing the same physical device as the provided Twilio `AudioDevice`. */ private fun convertTwilioDeviceToStreamDevice(twilioDevice: AudioDevice): StreamAudioDevice { return when (twilioDevice) { @@ -87,4 +95,4 @@ internal object AudioHandlerFactory { ) } } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt index 5af931b52e..c470d5b344 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/LegacyAudioDeviceManager.kt @@ -69,6 +69,15 @@ internal class LegacyAudioDeviceManager( private val bluetoothScoTimeoutHandler = Handler(Looper.getMainLooper()) private var bluetoothScoTimeoutRunnable: Runnable? = null + /** + * Build a deduplicated list of available audio devices on legacy Android (API 24–30). + * + * Detects Bluetooth headsets using the Bluetooth HEADSET profile proxy when available or falls back + * to AudioDeviceInfo enumeration; deduplicates SCO vs A2DP devices by Bluetooth address (preferring + * SCO), includes wired headset (with a fallback check), always includes speakerphone, and adds + * earpiece only if the device has telephony capability. + * + * @return A list of detected StreamAudioDevice instances representing available audio routes. */ override fun enumerateDevices(): List { val devices = mutableListOf() @@ -192,6 +201,16 @@ internal class LegacyAudioDeviceManager( return devices } + /** + * Selects the specified audio device and configures system audio routing accordingly. + * + * For speakerphone and wired headset selection this enables/disables the speakerphone as appropriate. + * For earpiece selection this disables the speakerphone. For Bluetooth headset selection this + * disables the speakerphone and initiates a Bluetooth SCO connection. + * + * @param device The audio device to select. + * @return `true` if the selection and the corresponding system audio configuration were initiated, `false` otherwise. + */ override fun selectDevice(device: StreamAudioDevice): Boolean { when (device) { is StreamAudioDevice.Speakerphone -> { @@ -228,6 +247,9 @@ internal class LegacyAudioDeviceManager( } } + /** + * Resets audio routing to a neutral state by stopping Bluetooth SCO, turning off the speakerphone, and clearing the selected device. + */ override fun clearDevice() { stopBluetoothSco() @Suppress("DEPRECATION") @@ -235,12 +257,29 @@ internal class LegacyAudioDeviceManager( selectedDevice = null } - override fun getSelectedDevice(): StreamAudioDevice? = selectedDevice + /** + * Retrieve the currently selected audio device. + * + * @return The currently selected [StreamAudioDevice], or `null` if no device is selected. + */ +override fun getSelectedDevice(): StreamAudioDevice? = selectedDevice + /** + * Begin monitoring legacy audio device changes. + * + * Registers the legacy listeners required to detect wired headset plug events, + * Bluetooth headset/profile state and SCO/audio state updates so device + * availability and connection changes are reported to the manager's callbacks. + */ override fun start() { registerLegacyListeners() } + /** + * Stops the legacy audio device manager and releases audio-related resources. + * + * Unregisters legacy listeners, stops any ongoing Bluetooth SCO connection, and clears the currently selected device. + */ override fun stop() { unregisterLegacyListeners() stopBluetoothSco() @@ -248,13 +287,23 @@ internal class LegacyAudioDeviceManager( } /** - * Registers legacy listeners for API 24-30: - * - BroadcastReceiver for ACTION_HEADSET_PLUG (wired headset) - * - BluetoothManager for Bluetooth device detection + * Register legacy audio listeners and initialize Bluetooth HEADSET profile proxy for API 24–30. + * + * Registers a BroadcastReceiver for wired headset plug/unplug events and a BroadcastReceiver for + * Bluetooth headset connection, audio, and SCO state updates. Initializes BluetoothManager and + * BluetoothAdapter and requests a HEADSET profile proxy; if the device does not support Bluetooth, + * SCO is unavailable, or a SecurityException occurs while requesting the profile proxy, the manager + * records that the profile proxy is unavailable and continues using the fallback enumeration path. */ private fun registerLegacyListeners() { // Register BroadcastReceiver for wired headset plug/unplug headsetPlugReceiver = object : BroadcastReceiver() { + /** + * Receives headset plug/unplug broadcasts and triggers device re-enumeration. + * + * @param context The context in which the receiver is running. + * @param intent The broadcast intent; expects AudioManager.ACTION_HEADSET_PLUG and reads the `state` and `microphone` extras. + */ override fun onReceive(context: Context, intent: Intent) { if (intent.action == AudioManager.ACTION_HEADSET_PLUG) { val state = intent.getIntExtra("state", -1) @@ -319,6 +368,16 @@ internal class LegacyAudioDeviceManager( // Register Bluetooth state change receiver bluetoothStateReceiver = object : BroadcastReceiver() { + /** + * Handles incoming broadcast intents related to Bluetooth headset connection, Bluetooth audio state, and SCO audio state, + * extracting the relevant state values and dispatching them to the manager's internal handlers. + * + * @param context The Context in which the receiver is running. + * @param intent The received Intent; expected to contain one of: + * - BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED (contains BluetoothHeadset.EXTRA_STATE) + * - BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED (contains BluetoothHeadset.EXTRA_STATE) + * - AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED (contains AudioManager.EXTRA_SCO_AUDIO_STATE) + */ override fun onReceive(context: Context, intent: Intent) { val action = intent.action when { @@ -349,7 +408,11 @@ internal class LegacyAudioDeviceManager( } /** - * Unregisters legacy listeners. + * Stops and cleans up legacy audio listeners and Bluetooth resources. + * + * Unregisters the wired-headset and Bluetooth broadcast receivers if registered, + * closes the Bluetooth HEADSET profile proxy when available, and clears related + * Bluetooth manager, adapter, headset proxy, and device references. */ private fun unregisterLegacyListeners() { headsetPlugReceiver?.let { receiver -> @@ -386,8 +449,11 @@ internal class LegacyAudioDeviceManager( } /** - * Detects Bluetooth devices using BluetoothHeadset profile - * This only returns devices that support SCO and are actually connected. + * Detects connected Bluetooth headsets that support SCO. + * + * Returns a list of BluetoothHeadset devices that are currently connected via the HEADSET profile and suitable for SCO audio. + * + * @return A list of StreamAudioDevice.BluetoothHeadset representing connected SCO-capable Bluetooth headsets; empty if none are available or the HEADSET profile proxy is not connected. */ private fun detectBluetoothDevices(): List { if (bluetoothHeadset == null) { @@ -455,6 +521,19 @@ internal class LegacyAudioDeviceManager( * BluetoothProfile.ServiceListener for BluetoothHeadset profile proxy. */ private val bluetoothProfileServiceListener = object : BluetoothProfile.ServiceListener { + /** + * Handles Bluetooth profile connection events for the HEADSET profile. + * + * When the HEADSET profile is connected, stores the provided proxy as the internal + * `BluetoothHeadset` proxy, marks the profile proxy as available, inspects any + * currently connected Bluetooth devices (logging their name, address, and connection state), + * and triggers device enumeration via `onDeviceChange()`. SecurityExceptions thrown + * while querying device state are caught and logged. Logs a warning for unexpected profiles. + * + * @param profile The Bluetooth profile constant (from `BluetoothProfile`) that was connected. + * @param proxy The profile proxy instance; expected to be castable to `BluetoothHeadset` when `profile` + * equals `BluetoothProfile.HEADSET`. May be null. + */ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) { if (profile == BluetoothProfile.HEADSET) { mainHandler.post { @@ -494,6 +573,14 @@ internal class LegacyAudioDeviceManager( } } + /** + * Handles disconnection of a Bluetooth profile service and updates internal Bluetooth state. + * + * When the HEADSET profile is disconnected, posts a task to the main handler that stops + * Bluetooth SCO, clears the HEADSET profile proxy and targeted Bluetooth device, marks + * the profile proxy as unavailable, and triggers `onDeviceChange()`. For other profiles, + * logs a warning indicating an unexpected profile disconnection. + */ override fun onServiceDisconnected(profile: Int) { if (profile == BluetoothProfile.HEADSET) { mainHandler.post { @@ -511,7 +598,13 @@ internal class LegacyAudioDeviceManager( } /** - * Handles Bluetooth headset connection state changes. + * Update internal state and notify listeners when the Bluetooth HEADSET profile connection state changes. + * + * For `BluetoothHeadset.STATE_CONNECTED` this resets SCO connection attempts and signals device availability changes. + * For `BluetoothHeadset.STATE_DISCONNECTED` this stops any active Bluetooth SCO connection and signals device availability changes. + * + * @param connectionState The Bluetooth HEADSET profile connection state (one of `BluetoothHeadset.STATE_CONNECTED`, + * `BluetoothHeadset.STATE_DISCONNECTED`, etc.). */ private fun onHeadsetConnectionStateChanged(connectionState: Int) { logger.d { "[onHeadsetConnectionStateChanged] Connection state: $connectionState" } @@ -528,7 +621,15 @@ internal class LegacyAudioDeviceManager( } /** - * Handles Bluetooth audio state changes. + * Process Bluetooth headset audio connection state updates and adjust internal SCO state. + * + * Cancels any pending SCO connection timeout when audio becomes connected; if the manager + * was attempting to connect, marks SCO as connected and resets retry attempts. When audio + * becomes disconnected, ignores the initial sticky broadcast and otherwise triggers a device + * availability update. + * + * @param audioState One of the BluetoothHeadset audio state constants (e.g. `STATE_AUDIO_CONNECTED`, `STATE_AUDIO_DISCONNECTED`). + * @param isInitialStateChange `true` when the received broadcast represents an initial sticky state delivered on registration, otherwise `false`. */ private fun onAudioStateChanged(audioState: Int, isInitialStateChange: Boolean) { logger.d { "[onAudioStateChanged] Audio state: $audioState, isInitial: $isInitialStateChange" } @@ -551,7 +652,13 @@ internal class LegacyAudioDeviceManager( } /** - * Handles Bluetooth SCO audio state updates. + * Updates internal Bluetooth SCO state and reacts to SCO audio state changes. + * + * Updates the manager's internal SCO state to CONNECTING, CONNECTED, or DISCONNECTED based on the provided + * AudioManager SCO state, clears any pending SCO connection timeout when connected, resets connection attempts + * after a successful connection, and invokes Bluetooth connection failure handling on errors or failed connect attempts. + * + * @param state One of the AudioManager.SCO_AUDIO_STATE_* constants indicating the current SCO audio state. */ private fun handleBluetoothScoStateUpdate(state: Int) { logger.d { "[handleBluetoothScoStateUpdate] SCO state: $state" } @@ -594,8 +701,13 @@ internal class LegacyAudioDeviceManager( } /** - * Starts Bluetooth SCO connection with robust error handling and retry logic. - * checks if SCO is available and device is connected. + * Initiates a Bluetooth SCO audio connection with attempt limits and failure handling. + * + * Verifies that a headset profile proxy and target device are available and connected, enforces + * the maximum retry limit, updates internal SCO state and attempt counters, and schedules a + * timeout to detect and handle connection failures. + * + * @return `true` if the SCO start was initiated or SCO is already connecting/connected, `false` otherwise. */ private fun startBluetoothSco(): Boolean { if (bluetoothScoConnectionAttempts >= maxBluetoothScoAttempts) { @@ -676,7 +788,10 @@ internal class LegacyAudioDeviceManager( } /** - * Stops Bluetooth SCO connection. + * Stops an active or pending Bluetooth SCO connection. + * + * Takes no action if SCO is not currently connecting or connected. + * Resets the connection attempt counter for future reconnection attempts. */ private fun stopBluetoothSco() { if (bluetoothScoState != BluetoothScoState.CONNECTING && bluetoothScoState != BluetoothScoState.CONNECTED) { @@ -730,8 +845,9 @@ internal class LegacyAudioDeviceManager( } /** - * Checks if the device has an earpiece (i.e., is a phone). - * checks for telephony feature. + * Determines whether the device includes a built-in earpiece (has telephony capability). + * + * @return `true` if the device reports the telephony feature, `false` otherwise. */ private fun hasEarpiece(context: Context): Boolean { return context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) @@ -741,4 +857,4 @@ internal class LegacyAudioDeviceManager( private const val TAG = "LegacyAudioDeviceManager" private const val BLUETOOTH_SCO_CONNECTION_TIMEOUT_MS = 5000L } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt index f7379e0c76..12c2f87be8 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/ModernAudioDeviceManager.kt @@ -32,6 +32,14 @@ internal class ModernAudioDeviceManager( private val logger by taggedLogger(TAG) private var selectedDevice: StreamAudioDevice? = null + /** + * Enumerates available communication audio devices and returns them as StreamAudioDevice instances. + * + * Converts Android AudioDeviceInfo entries reported by StreamAudioManager into StreamAudioDevice objects, + * omitting any devices that cannot be converted. + * + * @return A list of converted StreamAudioDevice objects representing available communication devices. + */ override fun enumerateDevices(): List { val androidDevices = StreamAudioManager.getAvailableCommunicationDevices(audioManager) logger.d { "[enumerateDevices] Found ${androidDevices.size} available communication devices" } @@ -70,6 +78,14 @@ internal class ModernAudioDeviceManager( return streamAudioDevices } + /** + * Selects the given StreamAudioDevice as the system communication audio device. + * + * If the platform AudioDeviceInfo for the device can be resolved and setting it succeeds, the manager's selected device is updated. + * + * @param device The StreamAudioDevice to select. + * @return `true` if the device was successfully set as the communication device, `false` otherwise. + */ override fun selectDevice(device: StreamAudioDevice): Boolean { val androidDevice = device.audioDeviceInfo ?: StreamAudioDevice.toAudioDeviceInfo(device, audioManager) @@ -85,11 +101,24 @@ internal class ModernAudioDeviceManager( } } + /** + * Clears the current communication audio device and resets the cached selection. + * + * This removes any device previously set as the communication device from the system + * and sets the manager's selectedDevice to null. + */ override fun clearDevice() { StreamAudioManager.clearCommunicationDevice(audioManager) selectedDevice = null } + /** + * Retrieve the currently active communication audio device. + * + * Attempts to read the active communication device from the system AudioManager and convert it to a StreamAudioDevice; if conversion succeeds, caches and returns that device. If no system device is available or conversion fails, returns the locally cached selection. + * + * @return The current StreamAudioDevice if available and convertible, otherwise the cached selected device, or `null` if none is set. + */ override fun getSelectedDevice(): StreamAudioDevice? { // Try to get from AudioManager first val currentDevice = StreamAudioManager.getCommunicationDevice(audioManager) @@ -103,10 +132,20 @@ internal class ModernAudioDeviceManager( return selectedDevice } + /** + * Initialize audio device management resources if required. + * + * This implementation is a no-op for Android API 31+ because no startup work is necessary. + */ override fun start() { // No special setup needed for modern API } + /** + * Stops the audio device manager and clears any selected communication device. + * + * Clears the manager's selected device state so no communication device remains set. + */ override fun stop() { clearDevice() } @@ -114,4 +153,4 @@ internal class ModernAudioDeviceManager( public companion object { private const val TAG = "ModernAudioDeviceManager" } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt index 2133e56d90..b88d30861d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioDevice.kt @@ -94,11 +94,21 @@ public sealed class StreamAudioDevice { ) : StreamAudioDevice() public companion object { + /** + * Obtain the Twilio AudioDevice backing this StreamAudioDevice. + * + * @return The underlying `AudioDevice` instance. + */ @JvmStatic public fun StreamAudioDevice.toAudioDevice(): AudioDevice { return this.audio } + /** + * Map a Twilio AudioDevice to the corresponding StreamAudioDevice. + * + * @return A StreamAudioDevice corresponding to the receiver audio device. + */ @JvmStatic public fun AudioDevice.fromAudio(): StreamAudioDevice { return when (this) { @@ -110,9 +120,14 @@ public sealed class StreamAudioDevice { } /** - * Converts an Android AudioDeviceInfo to a StreamAudioDevice. - * Returns null if the device type is not supported. - * Available from API 23+ (always available since minSdk is 24). + * Map an Android AudioDeviceInfo to the corresponding StreamAudioDevice variant. + * + * Maps Bluetooth SCO/A2DP to BluetoothHeadset, wired and USB headsets to WiredHeadset, + * built-in earpiece to Earpiece, and built-in speaker to Speakerphone. Returns `null` + * for device types that have no corresponding StreamAudioDevice. + * + * @param deviceInfo The Android AudioDeviceInfo to convert. + * @return The matching StreamAudioDevice, or `null` if the device type is unsupported. */ @JvmStatic public fun fromAudioDeviceInfo(deviceInfo: AudioDeviceInfo): StreamAudioDevice? { @@ -147,9 +162,13 @@ public sealed class StreamAudioDevice { } /** - * Converts a StreamAudioDevice to an AudioDeviceInfo by finding a matching device - * from the available communication devices. - * Returns null if no matching device is found. + * Finds an Android AudioDeviceInfo that corresponds to the provided StreamAudioDevice. + * + * Prefers the device referenced by streamDevice.audioDeviceInfo when present; on API 31+ searches available communication devices and on older APIs falls back to output devices. + * + * @param streamDevice The StreamAudioDevice to match. + * @param audioManager AudioManager used to query available audio devices. + * @return The matching `AudioDeviceInfo` for the given streamDevice, or `null` if none is found. */ @RequiresApi(Build.VERSION_CODES.S) @JvmStatic @@ -220,36 +239,51 @@ public sealed class StreamAudioDevice { } /** - * Creates a Twilio AudioDevice.BluetoothHeadset instance using reflection. + * Create a Twilio `AudioDevice.BluetoothHeadset` instance via reflection. + * + * @return A newly constructed `AudioDevice.BluetoothHeadset`. + * @throws IllegalStateException If a suitable constructor cannot be invoked to create the instance. */ internal fun createTwilioBluetoothHeadset(): AudioDevice { return createTwilioAudioDevice(AudioDevice.BluetoothHeadset::class.java) } /** - * Creates a Twilio AudioDevice.WiredHeadset instance using reflection. + * Create a Twilio `AudioDevice.WiredHeadset` instance. + * + * @return A new `AudioDevice.WiredHeadset` instance. */ internal fun createTwilioWiredHeadset(): AudioDevice { return createTwilioAudioDevice(AudioDevice.WiredHeadset::class.java) } /** - * Creates a Twilio AudioDevice.Earpiece instance using reflection. + * Create a Twilio `AudioDevice.Earpiece` instance. + * + * @return An `AudioDevice.Earpiece` instance. */ internal fun createTwilioEarpiece(): AudioDevice { return createTwilioAudioDevice(AudioDevice.Earpiece::class.java) } /** - * Creates a Twilio AudioDevice.Speakerphone instance using reflection. + * Create a Twilio `AudioDevice.Speakerphone` instance. + * + * @return A new `AudioDevice` representing the speakerphone. + * @throws IllegalStateException If a `AudioDevice.Speakerphone` instance cannot be instantiated. */ internal fun createTwilioSpeakerphone(): AudioDevice { return createTwilioAudioDevice(AudioDevice.Speakerphone::class.java) } /** - * Generic method to create Twilio AudioDevice instances using reflection. - * Accesses the private constructor and creates an instance. + * Instantiate a Twilio `AudioDevice` implementation by attempting to invoke its constructors via reflection. + * + * Tries each declared constructor with reasonable default argument values (empty string for `String`, zero/false for primitives, `null` otherwise) and returns the first successfully created instance. + * + * @param deviceClass The concrete `AudioDevice` class to instantiate. + * @return A new instance of the requested `AudioDevice` subclass. + * @throws IllegalStateException If the class has no constructors or none of its constructors could be invoked to create an instance. */ @Suppress("UNCHECKED_CAST") private fun createTwilioAudioDevice(deviceClass: Class): T { @@ -296,4 +330,4 @@ public sealed class StreamAudioDevice { ) } } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt index fcf4d1cc24..cbb6523cee 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioManager.kt @@ -29,8 +29,12 @@ import androidx.annotation.RequiresApi internal object StreamAudioManager { /** - * Registers an audio device callback to monitor device changes. - * always available since minSdk is 24 (Available from API 23+) + * Register an AudioDeviceCallback to receive audio device change events. + * + * @param audioManager The AudioManager used to register the callback. + * @param callback The AudioDeviceCallback to register. + * @param handler Optional Handler on which callback methods will be invoked; if null the system selects the caller's thread. + * @return `true` if registration succeeded, `false` if an exception occurred. */ fun registerAudioDeviceCallback( audioManager: AudioManager, @@ -46,8 +50,10 @@ internal object StreamAudioManager { } /** - * Unregisters an audio device callback. - * always available since minSdk is 24 (Available from API 23+) + * Unregisters an AudioDeviceCallback from the given AudioManager. + * + * @param callback The AudioDeviceCallback to unregister. + * @return `true` if the callback was unregistered successfully, `false` if an exception occurred. */ fun unregisterAudioDeviceCallback( audioManager: AudioManager, @@ -62,10 +68,12 @@ internal object StreamAudioManager { } /** - * Gets the list of available communication devices. - * For API 31+: Uses getAvailableCommunicationDevices() - * For API 24-30: Uses getDevices(AudioManager.GET_DEVICES_OUTPUTS) to get only output devices - * (using GET_DEVICES_ALL would include both input and output, causing duplicates for Bluetooth devices) + * Retrieve the list of available communication devices. + * + * On API 31+ this reflects the platform communication devices; on earlier API levels it returns available output devices. + * + * @param audioManager The AudioManager to query. + * @return A list of AudioDeviceInfo representing available communication devices; an empty list is returned if querying fails. */ fun getAvailableCommunicationDevices(audioManager: AudioManager): List { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -86,8 +94,9 @@ internal object StreamAudioManager { } /** - * Gets the currently selected communication device. - * On API < 31, returns null. + * Retrieves the currently selected communication device. + * + * @return The selected communication `AudioDeviceInfo`, or `null` if no device is selected or an error occurs. */ @RequiresApi(Build.VERSION_CODES.S) fun getCommunicationDevice(audioManager: AudioManager): AudioDeviceInfo? { @@ -99,9 +108,10 @@ internal object StreamAudioManager { } /** - * Sets the communication device - * For API 31+: Uses setCommunicationDevice() - * For API < 31: Returns false (use setDeviceLegacy in StreamAudioSwitch instead) + * Sets or clears the current communication audio device. + * + * @param device The AudioDeviceInfo to select for communication; pass `null` to clear the selection. + * @return `true` if the device was set or cleared successfully, `false` otherwise. */ @RequiresApi(Build.VERSION_CODES.S) fun setCommunicationDevice( @@ -121,8 +131,9 @@ internal object StreamAudioManager { } /** - * Clears the communication device selection. - * On API < 31, uses legacy AudioManager APIs. + * Clears the currently selected communication device. + * + * @return `true` if the communication device was cleared successfully, `false` otherwise. */ @RequiresApi(Build.VERSION_CODES.S) fun clearCommunicationDevice(audioManager: AudioManager): Boolean { @@ -133,4 +144,4 @@ internal object StreamAudioManager { false } } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt index 34e1c9044c..8c17f566e9 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitch.kt @@ -90,8 +90,11 @@ internal class StreamAudioSwitch( private var previousDeviceBeforeBluetooth: StreamAudioDevice? = null /** - * Starts monitoring audio devices and begins device enumeration. - * @param listener Callback that receives device updates + * Begins monitoring system audio devices, requests audio focus, registers callbacks, and performs an initial device enumeration. + * + * If the switch is already started, this method is a no-op. + * + * @param listener Optional callback invoked when available devices or the selected device change. */ public fun start(listener: StreamAudioDeviceChangeListener? = null) { synchronized(this) { @@ -134,7 +137,11 @@ internal class StreamAudioSwitch( } /** - * Stops monitoring audio devices and releases resources. + * Stop monitoring audio devices and release internal resources. + * + * Deactivates audio routing, abandons any held audio focus, unregisters the system + * audio device callback, stops and clears the underlying device manager, clears the + * device-change listener and resets available/selected device state. */ public fun stop() { synchronized(this) { @@ -167,15 +174,18 @@ internal class StreamAudioSwitch( } /** - * Selects an audio device for routing. - * @param device The device to select, or null for automatic selection + * Sets the active audio device used for routing. + * + * @param device The device to select; pass `null` to let the switch choose a device automatically based on the configured priority list. */ public fun selectDevice(device: StreamAudioDevice?) { selectStreamAudioDevice(device) } /** - * Deactivates audio routing. + * Deactivates audio routing and logs the action. + * + * This does not change or abandon audio focus; audio focus is managed by WebRTC. */ public fun deactivate() { synchronized(this) { @@ -185,18 +195,29 @@ internal class StreamAudioSwitch( } /** - * Gets the currently selected device. - */ + * Retrieve the currently selected audio device. + * + * @return The currently selected StreamAudioDevice, or `null` if no device is selected. + */ public fun getSelectedDevice(): StreamAudioDevice? = _selectedDeviceState.value /** - * Gets the list of available devices. - */ + * Return the current list of available audio devices. + * + * @return The list of currently available `StreamAudioDevice` instances. + */ public fun getAvailableDevices(): List = _availableDevices.value /** - * Selects a native audio device for routing. - * @param device The device to select, or null for automatic selection + * Selects and routes audio to the given StreamAudioDevice or, when `device` is null, + * chooses a device automatically using the configured priority list. + * + * Attempts to route through the active device manager and updates the internal selected-device state. + * When switching to a Bluetooth headset, preserves the previous device for potential fallback. + * If the requested routing fails, attempts an automatic re-selection by priority. If no device is available, + * clears the current selection and underlying routing. + * + * @param device The device to select, or `null` to select the highest-priority available device automatically. */ public fun selectStreamAudioDevice(device: StreamAudioDevice?) { synchronized(this) { @@ -248,7 +269,13 @@ internal class StreamAudioSwitch( } /** - * Selects a device by priority from available native devices. + * Selects the highest-priority available audio device from the current available devices. + * + * Checks the resolved preferred device type order and returns the first available device + * that matches the priority list; if no preferred types are present, returns the first + * available device. Returns `null` when no devices are available. + * + * @return The selected StreamAudioDevice according to priority, or `null` if none are available. */ private fun selectCustomDeviceByPriority(): StreamAudioDevice? { val availableDevices = _availableDevices.value @@ -268,6 +295,13 @@ internal class StreamAudioSwitch( return availableDevices.firstOrNull() } + /** + * Registers and starts an AudioDeviceCallback to monitor system audio device changes. + * + * If a callback is already registered this method is a no-op. When audio devices are added + * or removed the callback triggers a device enumeration via `enumerateDevices()`. The callback + * is registered with StreamAudioManager using this class's `audioManager` and `mainHandler`. + */ private fun registerDeviceCallback() { if (audioDeviceCallback != null) { return @@ -292,6 +326,11 @@ internal class StreamAudioSwitch( ) } + /** + * Unregisters the current AudioDeviceCallback from the AudioManager and clears the stored reference. + * + * If no callback is registered, this function does nothing. + */ private fun unregisterDeviceCallback() { audioDeviceCallback?.let { callback -> StreamAudioManager.unregisterAudioDeviceCallback(audioManager, callback) @@ -299,6 +338,15 @@ internal class StreamAudioSwitch( } } + /** + * Refreshes the list of available native audio devices and reconciles the current selection. + * + * Updates the internal available-devices state from the device manager, clears the selected device + * if it is no longer present, otherwise updates the selection from the manager's current routing. + * If the previously selected device is gone, requests the device manager to clear its routing. + * Finally invokes the external device-change listener with the current available devices and + * selected device. + */ private fun enumerateDevices() { // Enumerate devices using the device manager (which works with NativeStreamAudioDevice) val manager = deviceManager @@ -325,7 +373,9 @@ internal class StreamAudioSwitch( } /** - * Handles Bluetooth connection failure by reverting to the previously selected device. + * Restore audio routing after a Bluetooth connection failure. + * + * Attempts to reselect the device that was active before Bluetooth was chosen; if that device is no longer available, clears the stored previous-device reference and falls back to the automatic priority-based selection. */ private fun handleBluetoothConnectionFailure() { logger.w { @@ -359,8 +409,11 @@ internal class StreamAudioSwitch( } /** - * Requests audio focus for audio routing. - * Uses AudioFocusRequest on API 26+ and legacy requestAudioFocus on older versions. + * Requests transient audio focus suitable for voice communication and updates internal focus state. + * + * Attempts to obtain audio focus and, when an AudioFocusRequest is created, stores it in the + * `audioFocusRequest` property. The result of the request is logged; any exceptions encountered + * during the request are caught and logged. */ private fun requestAudioFocus() { try { @@ -407,7 +460,10 @@ internal class StreamAudioSwitch( } /** - * Abandons audio focus. + * Releases any previously requested audio focus and clears internal focus state. + * + * Uses an `AudioFocusRequest` on API 26+ and the legacy `abandonAudioFocus` on older APIs. + * Logs the abandonment result and suppresses any exceptions so callers are not thrown. */ private fun abandonAudioFocus() { try { @@ -431,8 +487,9 @@ internal class StreamAudioSwitch( private const val TAG = "StreamAudioSwitch" /** - * Returns the default preferred device list matching Twilio's priority: - * BluetoothHeadset -> WiredHeadset -> Earpiece -> Speakerphone + * Provides the default preferred device priority list. + * + * @return List of `StreamAudioDevice` classes ordered by preference: BluetoothHeadset, WiredHeadset, Earpiece, Speakerphone. */ @JvmStatic fun getDefaultPreferredDeviceList(): List> { @@ -444,4 +501,4 @@ internal class StreamAudioSwitch( ) } } -} +} \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt index 913cbebaae..7ae786ae47 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/StreamAudioSwitchHandler.kt @@ -37,6 +37,13 @@ internal class StreamAudioSwitchHandler( private val mainThreadHandler = Handler(Looper.getMainLooper()) private var isAudioSwitchInitScheduled = false + /** + * Initializes and starts the underlying StreamAudioSwitch on the main thread if not already scheduled. + * + * Schedules creation of a StreamAudioSwitch using the handler's context and preferred device list, stores the + * created instance, and starts it with the configured audio device change listener. If initialization has already + * been scheduled, this method is a no-op. + */ override fun start() { synchronized(this) { if (!isAudioSwitchInitScheduled) { @@ -59,6 +66,12 @@ internal class StreamAudioSwitchHandler( } } + /** + * Stops and releases the active StreamAudioSwitch. + * + * Removes any pending main-thread callbacks, stops the active StreamAudioSwitch if present, + * and clears the stored reference. All teardown is performed on the main thread. + */ override fun stop() { logger.d { "[stop] no args" } mainThreadHandler.removeCallbacksAndMessages(null) @@ -68,6 +81,13 @@ internal class StreamAudioSwitchHandler( } } + /** + * Selects an audio device for use. + * + * If the internal audio switch is initialized, forwards the selection to it; otherwise no-op. + * + * @param audioDevice The device to select, or `null` to clear the current selection. + */ override fun selectDevice(audioDevice: StreamAudioDevice?) { streamAudioSwitch?.selectDevice(audioDevice) } @@ -75,4 +95,4 @@ internal class StreamAudioSwitchHandler( public companion object { private const val TAG = "StreamAudioSwitchHandler" } -} +} \ No newline at end of file