Reintroduce preliminary telecom support for 1:1 calling.

This commit is contained in:
Cody Henthorne
2026-03-13 13:19:59 -04:00
committed by Michelle Tang
parent 1b6cfe9fc6
commit a62f07db11
24 changed files with 677 additions and 489 deletions

View File

@@ -16,6 +16,7 @@ import kotlinx.collections.immutable.toImmutableList
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil
import org.thoughtcrime.securesms.webrtc.audio.AudioDeviceMapping
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.signal.core.ui.R as CoreUiR
@@ -38,17 +39,40 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
WebRtcAudioOutput.WIRED_HEADSET -> SignalAudioManager.AudioDevice.WIRED_HEADSET
}
}
private fun getDeviceList(context: Context): Pair<List<AudioOutputOption>, Int>? {
val telecomDevices = AndroidTelecomUtil.getAvailableAudioOutputOptions()
if (telecomDevices != null) {
return telecomDevices to AndroidTelecomUtil.getCurrentEndpointDeviceId()
}
val am = AppDependencies.androidCallAudioManager
val devices = am.availableCommunicationDevices
.map { AudioOutputOption(it.toFriendlyName(context).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }
.distinctBy { it.deviceType.name }
.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
if (devices.isEmpty()) return null
return devices to (am.communicationDevice?.id ?: -1)
}
private fun AudioDeviceInfo.toFriendlyName(context: Context): CharSequence {
return when (this.type) {
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> context.getString(R.string.WebRtcAudioOutputToggle__phone_earpiece)
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> context.getString(R.string.WebRtcAudioOutputToggle__speaker)
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headphones)
AudioDeviceInfo.TYPE_WIRED_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset)
AudioDeviceInfo.TYPE_USB_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset_usb)
else -> this.productName
}
}
}
fun showPicker(fragmentActivity: FragmentActivity, threshold: Int, onDismiss: (DialogInterface) -> Unit): DialogInterface? {
val am = AppDependencies.androidCallAudioManager
if (am.availableCommunicationDevices.isEmpty()) {
val (devices, currentDeviceId) = getDeviceList(fragmentActivity) ?: run {
Toast.makeText(fragmentActivity, R.string.WebRtcAudioOutputToggleButton_no_eligible_audio_i_o_detected, Toast.LENGTH_LONG).show()
return null
}
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(fragmentActivity).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinctBy { it.deviceType.name }.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val currentDeviceId = am.communicationDevice?.id ?: -1
if (devices.size < threshold) {
Log.d(TAG, "Only found $devices devices, not showing picker.")
if (devices.isEmpty()) return null
@@ -68,8 +92,8 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
fun Picker(threshold: Int) {
val context = LocalContext.current
val am = AppDependencies.androidCallAudioManager
if (am.availableCommunicationDevices.isEmpty()) {
val deviceList = getDeviceList(context)
if (deviceList == null) {
LaunchedEffect(Unit) {
Toast.makeText(context, R.string.WebRtcAudioOutputToggleButton_no_eligible_audio_i_o_detected, Toast.LENGTH_LONG).show()
stateUpdater.hidePicker()
@@ -77,8 +101,7 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
return
}
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(context).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinctBy { it.deviceType.name }.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val currentDeviceId = am.communicationDevice?.id ?: -1
val (devices, currentDeviceId) = deviceList
if (devices.size < threshold) {
LaunchedEffect(Unit) {
Log.d(TAG, "Only found $devices devices, not showing picker.")
@@ -101,7 +124,12 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
@RequiresApi(31)
val onAudioDeviceSelected: (AudioOutputOption) -> Unit = {
Log.d(TAG, "User selected audio device of type ${it.deviceType}")
audioOutputChangedListener.audioOutputChanged(WebRtcAudioDevice(it.toWebRtcAudioOutput(), it.deviceId))
val webRtcDevice = if (AndroidTelecomUtil.hasActiveController()) {
WebRtcAudioDevice(it.toWebRtcAudioOutput(), null)
} else {
WebRtcAudioDevice(it.toWebRtcAudioOutput(), it.deviceId)
}
audioOutputChangedListener.audioOutputChanged(webRtcDevice)
when (it.deviceType) {
SignalAudioManager.AudioDevice.WIRED_HEADSET -> {
@@ -123,35 +151,22 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
}
}
private fun AudioDeviceInfo.toFriendlyName(context: Context): CharSequence {
return when (this.type) {
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> context.getString(R.string.WebRtcAudioOutputToggle__phone_earpiece)
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> context.getString(R.string.WebRtcAudioOutputToggle__speaker)
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headphones)
AudioDeviceInfo.TYPE_WIRED_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset)
AudioDeviceInfo.TYPE_USB_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset_usb)
else -> this.productName
}
}
/**
* Cycles to the next audio device without showing a picker.
* Uses the system device list to resolve the actual device ID, falling back to
* type-based lookup from app-tracked state when the current communication device is unknown.
*/
fun cycleToNextDevice() {
val am = AppDependencies.androidCallAudioManager
val devices: List<AudioOutputOption> = am.availableCommunicationDevices
.map { AudioOutputOption("", AudioDeviceMapping.fromPlatformType(it.type), it.id) }
.distinctBy { it.deviceType.name }
.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val (devices, currentDeviceId) = getDeviceList(AppDependencies.application) ?: run {
Log.w(TAG, "cycleToNextDevice: no available communication devices")
return
}
if (devices.isEmpty()) {
Log.w(TAG, "cycleToNextDevice: no available communication devices")
return
}
val currentDeviceId = am.communicationDevice?.id ?: -1
val index = devices.indexOfFirst { it.deviceId == currentDeviceId }
if (index != -1) {

View File

@@ -1285,7 +1285,16 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
@RequiresApi(31)
override fun onAudioOutputChanged31(audioOutput: WebRtcAudioDevice) {
maybeDisplaySpeakerphonePopup(audioOutput.webRtcAudioOutput)
AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(audioOutput.deviceId!!))
if (audioOutput.deviceId != null) {
AppDependencies.signalCallManager.selectAudioDevice(ChosenAudioDeviceIdentifier(audioOutput.deviceId))
} else {
when (audioOutput.webRtcAudioOutput) {
WebRtcAudioOutput.HANDSET -> handleSetAudioHandset()
WebRtcAudioOutput.SPEAKER -> handleSetAudioSpeaker()
WebRtcAudioOutput.BLUETOOTH_HEADSET -> handleSetAudioBluetooth()
WebRtcAudioOutput.WIRED_HEADSET -> handleSetAudioWiredHeadset()
}
}
}
override fun onVideoChanged(isVideoEnabled: Boolean) {