feat: add Bluetooth headset audio routing for calls
Audio route cycling: the speaker button now cycles through Earpiece → Bluetooth → Speaker (or Earpiece → Speaker if no Bluetooth device is connected). CallAudioManager: - Detects Bluetooth SCO/A2DP/BLE headset devices via AudioManager - Manages Bluetooth SCO connection (startBluetoothSco/stopBluetoothSco) - BroadcastReceiver monitors SCO state changes — auto-falls back to earpiece if Bluetooth disconnects during call - Exposes audioRoute and isBluetoothAvailable as StateFlows CallController: - Replaced toggleSpeaker() with cycleAudioRoute() which delegates to CallAudioManager.cycleAudioRoute() - Exposes audioRoute and isBluetoothAvailable flows to UI CallScreen: - Audio route button shows context-aware icon: VolumeOff (earpiece), VolumeUp (speaker), BluetoothAudio - Blue tint for Bluetooth, cyan for speaker, white for earpiece Manifest: BLUETOOTH_CONNECT permission added. Strings: call_bluetooth string resource added. https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
|
|
||||||
<!-- Phone calls -->
|
<!-- Phone calls -->
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
<!-- Keeps screen on while playing videos -->
|
<!-- Keeps screen on while playing videos -->
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|||||||
+147
-10
@@ -20,8 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.call
|
package com.vitorpamplona.amethyst.service.call
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioDeviceInfo
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.Ringtone
|
import android.media.Ringtone
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
@@ -31,6 +35,15 @@ import android.os.PowerManager
|
|||||||
import android.os.VibrationEffect
|
import android.os.VibrationEffect
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.os.VibratorManager
|
import android.os.VibratorManager
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
enum class AudioRoute {
|
||||||
|
EARPIECE,
|
||||||
|
SPEAKER,
|
||||||
|
BLUETOOTH,
|
||||||
|
}
|
||||||
|
|
||||||
class CallAudioManager(
|
class CallAudioManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -41,6 +54,13 @@ class CallAudioManager(
|
|||||||
private var ringbackTone: ToneGenerator? = null
|
private var ringbackTone: ToneGenerator? = null
|
||||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
private var previousAudioMode: Int = AudioManager.MODE_NORMAL
|
private var previousAudioMode: Int = AudioManager.MODE_NORMAL
|
||||||
|
private var scoReceiver: BroadcastReceiver? = null
|
||||||
|
|
||||||
|
private val _audioRoute = MutableStateFlow(AudioRoute.EARPIECE)
|
||||||
|
val audioRoute: StateFlow<AudioRoute> = _audioRoute.asStateFlow()
|
||||||
|
|
||||||
|
private val _isBluetoothAvailable = MutableStateFlow(false)
|
||||||
|
val isBluetoothAvailable: StateFlow<Boolean> = _isBluetoothAvailable.asStateFlow()
|
||||||
|
|
||||||
fun startRinging() {
|
fun startRinging() {
|
||||||
startRingtone()
|
startRingtone()
|
||||||
@@ -59,7 +79,6 @@ class CallAudioManager(
|
|||||||
it.startTone(ToneGenerator.TONE_SUP_RINGTONE)
|
it.startTone(ToneGenerator.TONE_SUP_RINGTONE)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// ToneGenerator may not be available
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +91,43 @@ class CallAudioManager(
|
|||||||
fun switchToCallAudioMode() {
|
fun switchToCallAudioMode() {
|
||||||
previousAudioMode = audioManager.mode
|
previousAudioMode = audioManager.mode
|
||||||
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||||
audioManager.isSpeakerphoneOn = false
|
|
||||||
|
_isBluetoothAvailable.value = hasBluetoothDevice()
|
||||||
|
if (_isBluetoothAvailable.value) {
|
||||||
|
startBluetoothSco()
|
||||||
|
} else {
|
||||||
|
routeToEarpiece()
|
||||||
|
}
|
||||||
|
registerBluetoothScoReceiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreAudioMode() {
|
fun restoreAudioMode() {
|
||||||
|
stopBluetoothSco()
|
||||||
|
unregisterBluetoothScoReceiver()
|
||||||
audioManager.mode = previousAudioMode
|
audioManager.mode = previousAudioMode
|
||||||
audioManager.isSpeakerphoneOn = false
|
audioManager.isSpeakerphoneOn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cycleAudioRoute() {
|
||||||
|
val hasBt = _isBluetoothAvailable.value
|
||||||
|
val next =
|
||||||
|
when (_audioRoute.value) {
|
||||||
|
AudioRoute.EARPIECE -> if (hasBt) AudioRoute.BLUETOOTH else AudioRoute.SPEAKER
|
||||||
|
AudioRoute.BLUETOOTH -> AudioRoute.SPEAKER
|
||||||
|
AudioRoute.SPEAKER -> AudioRoute.EARPIECE
|
||||||
|
}
|
||||||
|
setAudioRoute(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAudioRoute(route: AudioRoute) {
|
||||||
|
_audioRoute.value = route
|
||||||
|
when (route) {
|
||||||
|
AudioRoute.EARPIECE -> routeToEarpiece()
|
||||||
|
AudioRoute.SPEAKER -> routeToSpeaker()
|
||||||
|
AudioRoute.BLUETOOTH -> routeToBluetooth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun acquireProximityWakeLock() {
|
fun acquireProximityWakeLock() {
|
||||||
if (proximityWakeLock != null) return
|
if (proximityWakeLock != null) return
|
||||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
@@ -88,13 +136,11 @@ class CallAudioManager(
|
|||||||
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||||
"amethyst:call_proximity",
|
"amethyst:call_proximity",
|
||||||
)
|
)
|
||||||
proximityWakeLock?.acquire(60 * 60 * 1000L) // 1 hour max
|
proximityWakeLock?.acquire(60 * 60 * 1000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun releaseProximityWakeLock() {
|
fun releaseProximityWakeLock() {
|
||||||
proximityWakeLock?.let {
|
proximityWakeLock?.let { if (it.isHeld) it.release() }
|
||||||
if (it.isHeld) it.release()
|
|
||||||
}
|
|
||||||
proximityWakeLock = null
|
proximityWakeLock = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +151,100 @@ class CallAudioManager(
|
|||||||
releaseProximityWakeLock()
|
releaseProximityWakeLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun routeToEarpiece() {
|
||||||
|
stopBluetoothSco()
|
||||||
|
audioManager.isSpeakerphoneOn = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun routeToSpeaker() {
|
||||||
|
stopBluetoothSco()
|
||||||
|
audioManager.isSpeakerphoneOn = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun routeToBluetooth() {
|
||||||
|
audioManager.isSpeakerphoneOn = false
|
||||||
|
startBluetoothSco()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasBluetoothDevice(): Boolean =
|
||||||
|
try {
|
||||||
|
audioManager
|
||||||
|
.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||||
|
.any {
|
||||||
|
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
|
||||||
|
it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
|
||||||
|
it.type == AudioDeviceInfo.TYPE_BLE_HEADSET
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun startBluetoothSco() {
|
||||||
|
try {
|
||||||
|
if (!audioManager.isBluetoothScoOn) {
|
||||||
|
audioManager.startBluetoothSco()
|
||||||
|
audioManager.isBluetoothScoOn = true
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun stopBluetoothSco() {
|
||||||
|
try {
|
||||||
|
if (audioManager.isBluetoothScoOn) {
|
||||||
|
audioManager.isBluetoothScoOn = false
|
||||||
|
audioManager.stopBluetoothSco()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerBluetoothScoReceiver() {
|
||||||
|
if (scoReceiver != null) return
|
||||||
|
scoReceiver =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(
|
||||||
|
ctx: Context?,
|
||||||
|
intent: Intent?,
|
||||||
|
) {
|
||||||
|
val state = intent?.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
|
||||||
|
when (state) {
|
||||||
|
AudioManager.SCO_AUDIO_STATE_CONNECTED -> {
|
||||||
|
_isBluetoothAvailable.value = true
|
||||||
|
if (_audioRoute.value == AudioRoute.BLUETOOTH) {
|
||||||
|
audioManager.isBluetoothScoOn = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
|
||||||
|
_isBluetoothAvailable.value = hasBluetoothDevice()
|
||||||
|
if (_audioRoute.value == AudioRoute.BLUETOOTH) {
|
||||||
|
_audioRoute.value = AudioRoute.EARPIECE
|
||||||
|
routeToEarpiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.registerReceiver(
|
||||||
|
scoReceiver,
|
||||||
|
IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterBluetoothScoReceiver() {
|
||||||
|
scoReceiver?.let {
|
||||||
|
try {
|
||||||
|
context.unregisterReceiver(it)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scoReceiver = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun startRingtone() {
|
private fun startRingtone() {
|
||||||
try {
|
try {
|
||||||
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
||||||
@@ -120,7 +260,6 @@ class CallAudioManager(
|
|||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Ringtone may not be available
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +278,9 @@ class CallAudioManager(
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
}
|
}
|
||||||
|
val pattern = longArrayOf(0, 1000, 1000)
|
||||||
val pattern = longArrayOf(0, 1000, 1000) // vibrate 1s, pause 1s, repeat
|
|
||||||
vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
|
vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Vibrator may not be available
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.call
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.AudioManager
|
|
||||||
import com.vitorpamplona.amethyst.commons.call.CallManager
|
import com.vitorpamplona.amethyst.commons.call.CallManager
|
||||||
import com.vitorpamplona.amethyst.commons.call.CallState
|
import com.vitorpamplona.amethyst.commons.call.CallState
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils
|
||||||
@@ -75,8 +74,8 @@ class CallController(
|
|||||||
val isAudioMuted: StateFlow<Boolean> = _isAudioMuted.asStateFlow()
|
val isAudioMuted: StateFlow<Boolean> = _isAudioMuted.asStateFlow()
|
||||||
private val _isVideoEnabled = MutableStateFlow(true)
|
private val _isVideoEnabled = MutableStateFlow(true)
|
||||||
val isVideoEnabled: StateFlow<Boolean> = _isVideoEnabled.asStateFlow()
|
val isVideoEnabled: StateFlow<Boolean> = _isVideoEnabled.asStateFlow()
|
||||||
private val _isSpeakerOn = MutableStateFlow(false)
|
val audioRoute: StateFlow<AudioRoute> = audioManager.audioRoute
|
||||||
val isSpeakerOn: StateFlow<Boolean> = _isSpeakerOn.asStateFlow()
|
val isBluetoothAvailable: StateFlow<Boolean> = audioManager.isBluetoothAvailable
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -232,11 +231,8 @@ class CallController(
|
|||||||
webRtcSession?.setVideoEnabled(enabled)
|
webRtcSession?.setVideoEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSpeaker() {
|
fun cycleAudioRoute() {
|
||||||
val on = !_isSpeakerOn.value
|
audioManager.cycleAudioRoute()
|
||||||
_isSpeakerOn.value = on
|
|
||||||
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
||||||
am.isSpeakerphoneOn = on
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEglBase() = webRtcSession?.eglBase
|
fun getEglBase() = webRtcSession?.eglBase
|
||||||
@@ -258,7 +254,6 @@ class CallController(
|
|||||||
_localVideoTrack.value = null
|
_localVideoTrack.value = null
|
||||||
_isAudioMuted.value = false
|
_isAudioMuted.value = false
|
||||||
_isVideoEnabled.value = true
|
_isVideoEnabled.value = true
|
||||||
_isSpeakerOn.value = false
|
|
||||||
webRtcSession?.dispose()
|
webRtcSession?.dispose()
|
||||||
webRtcSession = null
|
webRtcSession = null
|
||||||
remoteDescriptionSet = false
|
remoteDescriptionSet = false
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BluetoothAudio
|
||||||
import androidx.compose.material.icons.filled.Call
|
import androidx.compose.material.icons.filled.Call
|
||||||
import androidx.compose.material.icons.filled.CallEnd
|
import androidx.compose.material.icons.filled.CallEnd
|
||||||
import androidx.compose.material.icons.filled.Mic
|
import androidx.compose.material.icons.filled.Mic
|
||||||
@@ -69,6 +70,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.call.CallManager
|
import com.vitorpamplona.amethyst.commons.call.CallManager
|
||||||
import com.vitorpamplona.amethyst.commons.call.CallState
|
import com.vitorpamplona.amethyst.commons.call.CallState
|
||||||
|
import com.vitorpamplona.amethyst.service.call.AudioRoute
|
||||||
import com.vitorpamplona.amethyst.service.call.CallController
|
import com.vitorpamplona.amethyst.service.call.CallController
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
@@ -148,7 +150,7 @@ fun CallScreen(
|
|||||||
onHangup = { scope.launch { callManager.hangup() } },
|
onHangup = { scope.launch { callManager.hangup() } },
|
||||||
onToggleMute = { callController?.toggleAudioMute() },
|
onToggleMute = { callController?.toggleAudioMute() },
|
||||||
onToggleVideo = { callController?.toggleVideo() },
|
onToggleVideo = { callController?.toggleVideo() },
|
||||||
onToggleSpeaker = { callController?.toggleSpeaker() },
|
onCycleAudioRoute = { callController?.cycleAudioRoute() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +329,7 @@ private fun ConnectedCallUI(
|
|||||||
onHangup: () -> Unit,
|
onHangup: () -> Unit,
|
||||||
onToggleMute: () -> Unit,
|
onToggleMute: () -> Unit,
|
||||||
onToggleVideo: () -> Unit,
|
onToggleVideo: () -> Unit,
|
||||||
onToggleSpeaker: () -> Unit,
|
onCycleAudioRoute: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var elapsed by remember { mutableLongStateOf(0L) }
|
var elapsed by remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
@@ -343,9 +345,10 @@ private fun ConnectedCallUI(
|
|||||||
val localVideoTrack by (callController?.localVideoTrack ?: emptyVideoFlow).collectAsState()
|
val localVideoTrack by (callController?.localVideoTrack ?: emptyVideoFlow).collectAsState()
|
||||||
val defaultFalse = remember { kotlinx.coroutines.flow.MutableStateFlow(false) }
|
val defaultFalse = remember { kotlinx.coroutines.flow.MutableStateFlow(false) }
|
||||||
val defaultTrue = remember { kotlinx.coroutines.flow.MutableStateFlow(true) }
|
val defaultTrue = remember { kotlinx.coroutines.flow.MutableStateFlow(true) }
|
||||||
|
val defaultRoute = remember { kotlinx.coroutines.flow.MutableStateFlow(AudioRoute.EARPIECE) }
|
||||||
val isAudioMuted by (callController?.isAudioMuted ?: defaultFalse).collectAsState()
|
val isAudioMuted by (callController?.isAudioMuted ?: defaultFalse).collectAsState()
|
||||||
val isVideoEnabled by (callController?.isVideoEnabled ?: defaultTrue).collectAsState()
|
val isVideoEnabled by (callController?.isVideoEnabled ?: defaultTrue).collectAsState()
|
||||||
val isSpeakerOn by (callController?.isSpeakerOn ?: defaultFalse).collectAsState()
|
val currentAudioRoute by (callController?.audioRoute ?: defaultRoute).collectAsState()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -455,13 +458,30 @@ private fun ConnectedCallUI(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleSpeaker,
|
onClick = onCycleAudioRoute,
|
||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
|
imageVector =
|
||||||
contentDescription = stringRes(if (isSpeakerOn) R.string.call_earpiece else R.string.call_speaker),
|
when (currentAudioRoute) {
|
||||||
tint = if (isSpeakerOn) Color.Cyan else Color.White,
|
AudioRoute.EARPIECE -> Icons.Default.VolumeOff
|
||||||
|
AudioRoute.SPEAKER -> Icons.Default.VolumeUp
|
||||||
|
AudioRoute.BLUETOOTH -> Icons.Default.BluetoothAudio
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
stringRes(
|
||||||
|
when (currentAudioRoute) {
|
||||||
|
AudioRoute.EARPIECE -> R.string.call_earpiece
|
||||||
|
AudioRoute.SPEAKER -> R.string.call_speaker
|
||||||
|
AudioRoute.BLUETOOTH -> R.string.call_bluetooth
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tint =
|
||||||
|
when (currentAudioRoute) {
|
||||||
|
AudioRoute.EARPIECE -> Color.White
|
||||||
|
AudioRoute.SPEAKER -> Color.Cyan
|
||||||
|
AudioRoute.BLUETOOTH -> Color(0xFF2196F3)
|
||||||
|
},
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -793,6 +793,7 @@
|
|||||||
<string name="call_camera_off">Camera off</string>
|
<string name="call_camera_off">Camera off</string>
|
||||||
<string name="call_speaker">Speaker</string>
|
<string name="call_speaker">Speaker</string>
|
||||||
<string name="call_earpiece">Earpiece</string>
|
<string name="call_earpiece">Earpiece</string>
|
||||||
|
<string name="call_bluetooth">Bluetooth</string>
|
||||||
<string name="call_voice">Voice call</string>
|
<string name="call_voice">Voice call</string>
|
||||||
<string name="call_video">Video call</string>
|
<string name="call_video">Video call</string>
|
||||||
<string name="call_failed_start">Failed to start call</string>
|
<string name="call_failed_start">Failed to start call</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user