mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-28 13:48:12 +00:00
Make audio device button directly toggle when only two devices are present.
This commit is contained in:
@@ -832,7 +832,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@RequiresApi(31)
|
||||
@Override
|
||||
public void onAudioOutputChanged31(@NonNull int audioDeviceInfo) {
|
||||
public void onAudioOutputChanged31(@NonNull Integer audioDeviceInfo) {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioDeviceInfo));
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.V
|
||||
|
||||
if (mode != selected) {
|
||||
setSelectedOutput(mode);
|
||||
onAudioOutputChangedListener.audioOutputChanged(selected);
|
||||
onAudioOutputChangedListener.audioOutputChanged(new WebRtcAudioDevice(selected, null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
/**
|
||||
* This is an interface for [WebRtcAudioPicker31] and [WebRtcAudioPickerLegacy] to reference methods in [WebRtcAudioOutputToggleButton] without actually depending on it.
|
||||
*/
|
||||
interface AudioStateUpdater {
|
||||
fun updateAudioOutputState(audioOutput: WebRtcAudioOutput)
|
||||
fun hidePicker()
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
public interface OnAudioOutputChangedListener {
|
||||
void audioOutputChanged(WebRtcAudioOutput audioOutput);
|
||||
void audioOutputChanged(WebRtcAudioDevice device);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* This holds UI state for [WebRtcAudioOutputToggleButton]
|
||||
*/
|
||||
class ToggleButtonOutputState {
|
||||
private val availableOutputs: LinkedHashSet<WebRtcAudioOutput> = linkedSetOf(WebRtcAudioOutput.SPEAKER)
|
||||
private var selectedDevice = 0
|
||||
set(value) {
|
||||
if (value >= availableOutputs.size) {
|
||||
throw IndexOutOfBoundsException("Index: $value, size: ${availableOutputs.size}")
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
var isEarpieceAvailable: Boolean
|
||||
get() = availableOutputs.contains(WebRtcAudioOutput.HANDSET)
|
||||
set(value) {
|
||||
if (value) {
|
||||
availableOutputs.add(WebRtcAudioOutput.HANDSET)
|
||||
} else {
|
||||
availableOutputs.remove(WebRtcAudioOutput.HANDSET)
|
||||
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
var isBluetoothHeadsetAvailable: Boolean
|
||||
get() = availableOutputs.contains(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
set(value) {
|
||||
if (value) {
|
||||
availableOutputs.add(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
} else {
|
||||
availableOutputs.remove(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
|
||||
}
|
||||
}
|
||||
var isWiredHeadsetAvailable: Boolean
|
||||
get() = availableOutputs.contains(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
set(value) {
|
||||
if (value) {
|
||||
availableOutputs.add(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
} else {
|
||||
availableOutputs.remove(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Used only for onSaveInstanceState.")
|
||||
fun getBackingIndexForBackup(): Int {
|
||||
return selectedDevice
|
||||
}
|
||||
|
||||
@Deprecated("Used only for onRestoreInstanceState.")
|
||||
fun setBackingIndexForRestore(index: Int) {
|
||||
selectedDevice = 0
|
||||
}
|
||||
|
||||
fun getCurrentOutput(): WebRtcAudioOutput {
|
||||
return getOutputs()[selectedDevice]
|
||||
}
|
||||
|
||||
fun setCurrentOutput(outputType: WebRtcAudioOutput): Boolean {
|
||||
val newIndex = getOutputs().indexOf(outputType)
|
||||
return if (newIndex < 0) {
|
||||
false
|
||||
} else {
|
||||
selectedDevice = newIndex
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun getOutputs(): List<WebRtcAudioOutput> {
|
||||
return availableOutputs.toList()
|
||||
}
|
||||
|
||||
fun peekNext(): WebRtcAudioOutput {
|
||||
val peekIndex = (selectedDevice + 1) % availableOutputs.size
|
||||
return getOutputs()[peekIndex]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
/**
|
||||
* Holder class to smooth over the pre/post API 31 calls.
|
||||
*
|
||||
* @property webRtcAudioOutput audio device type, used by API 30 and below.
|
||||
* @property deviceId specific ID for a specific device. Used only by API 31+.
|
||||
*/
|
||||
data class WebRtcAudioDevice(val webRtcAudioOutput: WebRtcAudioOutput, val deviceId: Int?)
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.webrtc
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.DialogInterface
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
@@ -13,35 +12,28 @@ import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.webrtc.audio.AudioDeviceMapping
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* A UI button that triggers a picker dialog/bottom sheet allowing the user to select the audio output for the ongoing call.
|
||||
*/
|
||||
class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr) {
|
||||
class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), AudioStateUpdater {
|
||||
private val TAG = Log.tag(WebRtcAudioOutputToggleButton::class.java)
|
||||
|
||||
private var outputState: OutputState = OutputState()
|
||||
private var outputState: ToggleButtonOutputState = ToggleButtonOutputState()
|
||||
|
||||
private var audioOutputChangedListenerLegacy: OnAudioOutputChangedListener? = null
|
||||
private var audioOutputChangedListener31: OnAudioOutputChangedListener31? = null
|
||||
private var audioOutputChangedListener: OnAudioOutputChangedListener = OnAudioOutputChangedListener { Log.e(TAG, "Attempted to call audioOutputChangedListenerLegacy without initializing!") }
|
||||
private var picker: DialogInterface? = null
|
||||
|
||||
private val clickListenerLegacy: OnClickListener = OnClickListener {
|
||||
val outputs = outputState.getOutputs()
|
||||
if (outputs.size >= SHOW_PICKER_THRESHOLD || !outputState.isEarpieceAvailable) {
|
||||
showPickerLegacy(outputs)
|
||||
picker = WebRtcAudioPickerLegacy(audioOutputChangedListener, outputState, this).showPicker(context, outputs)
|
||||
} else {
|
||||
setAudioOutput(outputState.peekNext(), true)
|
||||
val audioOutput = outputState.peekNext()
|
||||
audioOutputChangedListener.audioOutputChanged(WebRtcAudioDevice(audioOutput, null))
|
||||
updateAudioOutputState(audioOutput)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +41,7 @@ class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context,
|
||||
private val clickListener31 = OnClickListener {
|
||||
val fragmentActivity = context.fragmentActivity()
|
||||
if (fragmentActivity != null) {
|
||||
showPicker31(fragmentActivity.supportFragmentManager)
|
||||
picker = WebRtcAudioPicker31(audioOutputChangedListener, outputState, this).showPicker(fragmentActivity, SHOW_PICKER_THRESHOLD)
|
||||
} else {
|
||||
Log.e(TAG, "WebRtcAudioOutputToggleButton instantiated from a context that does not inherit from FragmentActivity.")
|
||||
Toast.makeText(context, R.string.WebRtcAudioOutputToggleButton_fragment_activity_error, Toast.LENGTH_LONG).show()
|
||||
@@ -83,11 +75,22 @@ class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
|
||||
val currentOutput = outputState.getCurrentOutput()
|
||||
val extra = when (currentOutput) {
|
||||
WebRtcAudioOutput.HANDSET -> intArrayOf(R.attr.state_handset_selected)
|
||||
WebRtcAudioOutput.SPEAKER -> intArrayOf(R.attr.state_speaker_selected)
|
||||
WebRtcAudioOutput.BLUETOOTH_HEADSET -> intArrayOf(R.attr.state_bt_headset_selected)
|
||||
WebRtcAudioOutput.WIRED_HEADSET -> intArrayOf(R.attr.state_wired_headset_selected)
|
||||
|
||||
val numberOfOutputs = outputState.getOutputs().size
|
||||
val extra = if (numberOfOutputs < SHOW_PICKER_THRESHOLD) {
|
||||
when (currentOutput) {
|
||||
WebRtcAudioOutput.HANDSET -> intArrayOf(R.attr.state_speaker_off)
|
||||
WebRtcAudioOutput.SPEAKER -> intArrayOf(R.attr.state_speaker_on)
|
||||
WebRtcAudioOutput.BLUETOOTH_HEADSET -> intArrayOf(R.attr.state_bt_headset_selected) // should never be seen in practice.
|
||||
WebRtcAudioOutput.WIRED_HEADSET -> intArrayOf(R.attr.state_wired_headset_selected) // should never be seen in practice.
|
||||
}
|
||||
} else {
|
||||
when (currentOutput) {
|
||||
WebRtcAudioOutput.HANDSET -> intArrayOf(R.attr.state_handset_selected)
|
||||
WebRtcAudioOutput.SPEAKER -> intArrayOf(R.attr.state_speaker_selected)
|
||||
WebRtcAudioOutput.BLUETOOTH_HEADSET -> intArrayOf(R.attr.state_bt_headset_selected)
|
||||
WebRtcAudioOutput.WIRED_HEADSET -> intArrayOf(R.attr.state_wired_headset_selected)
|
||||
}
|
||||
}
|
||||
|
||||
val label = context.getString(currentOutput.labelRes)
|
||||
@@ -106,89 +109,19 @@ class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context,
|
||||
outputState.isEarpieceAvailable = isEarpieceAvailable
|
||||
outputState.isBluetoothHeadsetAvailable = isBluetoothHeadsetAvailable
|
||||
outputState.isWiredHeadsetAvailable = isHeadsetAvailable
|
||||
refreshDrawableState()
|
||||
}
|
||||
|
||||
fun setAudioOutput(audioOutput: WebRtcAudioOutput, notifyListener: Boolean) {
|
||||
override fun updateAudioOutputState(audioOutput: WebRtcAudioOutput) {
|
||||
val oldOutput = outputState.getCurrentOutput()
|
||||
if (oldOutput != audioOutput) {
|
||||
outputState.setCurrentOutput(audioOutput)
|
||||
refreshDrawableState()
|
||||
if (notifyListener) {
|
||||
audioOutputChangedListenerLegacy?.audioOutputChanged(audioOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnAudioOutputChangedListenerLegacy(listener: OnAudioOutputChangedListener?) {
|
||||
audioOutputChangedListenerLegacy = listener
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
fun setOnAudioOutputChangedListener31(listener: OnAudioOutputChangedListener31?) {
|
||||
audioOutputChangedListener31 = listener
|
||||
}
|
||||
|
||||
private fun showPickerLegacy(availableModes: List<WebRtcAudioOutput?>) {
|
||||
val rv = RecyclerView(context)
|
||||
val adapter = AudioOutputAdapter(
|
||||
{ audioOutput: WebRtcAudioOutput ->
|
||||
setAudioOutput(audioOutput, true)
|
||||
hidePicker()
|
||||
},
|
||||
availableModes
|
||||
)
|
||||
adapter.setSelectedOutput(outputState.getCurrentOutput())
|
||||
rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
rv.adapter = adapter
|
||||
picker = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.WebRtcAudioOutputToggle__audio_output)
|
||||
.setView(rv)
|
||||
.setCancelable(true)
|
||||
.show()
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private fun showPicker31(fragmentManager: FragmentManager) {
|
||||
val am = ApplicationDependencies.getAndroidCallAudioManager()
|
||||
if (am.availableCommunicationDevices.isEmpty()) {
|
||||
Toast.makeText(context, R.string.WebRtcAudioOutputToggleButton_no_eligible_audio_i_o_detected, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(context).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }
|
||||
picker = WebRtcAudioOutputBottomSheet.show(fragmentManager, devices, am.communicationDevice?.id ?: -1) {
|
||||
audioOutputChangedListener31?.audioOutputChanged(it.deviceId)
|
||||
|
||||
when (it.deviceType) {
|
||||
SignalAudioManager.AudioDevice.WIRED_HEADSET -> {
|
||||
outputState.isWiredHeadsetAvailable = true
|
||||
setAudioOutput(WebRtcAudioOutput.WIRED_HEADSET, true)
|
||||
}
|
||||
|
||||
SignalAudioManager.AudioDevice.EARPIECE -> {
|
||||
outputState.isEarpieceAvailable = true
|
||||
setAudioOutput(WebRtcAudioOutput.HANDSET, true)
|
||||
}
|
||||
|
||||
SignalAudioManager.AudioDevice.BLUETOOTH -> {
|
||||
outputState.isBluetoothHeadsetAvailable = true
|
||||
setAudioOutput(WebRtcAudioOutput.BLUETOOTH_HEADSET, true)
|
||||
}
|
||||
|
||||
SignalAudioManager.AudioDevice.SPEAKER_PHONE, SignalAudioManager.AudioDevice.NONE -> setAudioOutput(WebRtcAudioOutput.SPEAKER, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(23)
|
||||
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_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset)
|
||||
AudioDeviceInfo.TYPE_USB_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset_usb)
|
||||
else -> this.productName
|
||||
}
|
||||
fun setOnAudioOutputChangedListener(listener: OnAudioOutputChangedListener) {
|
||||
audioOutputChangedListener = listener
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
@@ -213,7 +146,7 @@ class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
private fun hidePicker() {
|
||||
override fun hidePicker() {
|
||||
try {
|
||||
picker?.dismiss()
|
||||
} catch (e: IllegalStateException) {
|
||||
@@ -223,84 +156,9 @@ class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context,
|
||||
picker = null
|
||||
}
|
||||
|
||||
inner class OutputState {
|
||||
private val availableOutputs: LinkedHashSet<WebRtcAudioOutput> = linkedSetOf(WebRtcAudioOutput.SPEAKER)
|
||||
private var selectedDevice = 0
|
||||
set(value) {
|
||||
if (value >= availableOutputs.size) {
|
||||
throw IndexOutOfBoundsException("Index: $value, size: ${availableOutputs.size}")
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
@Deprecated("Used only for onSaveInstanceState.")
|
||||
fun getBackingIndexForBackup(): Int {
|
||||
return selectedDevice
|
||||
}
|
||||
|
||||
@Deprecated("Used only for onRestoreInstanceState.")
|
||||
fun setBackingIndexForRestore(index: Int) {
|
||||
selectedDevice = 0
|
||||
}
|
||||
|
||||
fun getCurrentOutput(): WebRtcAudioOutput {
|
||||
return getOutputs()[selectedDevice]
|
||||
}
|
||||
|
||||
fun setCurrentOutput(outputType: WebRtcAudioOutput): Boolean {
|
||||
val newIndex = getOutputs().indexOf(outputType)
|
||||
return if (newIndex < 0) {
|
||||
false
|
||||
} else {
|
||||
selectedDevice = newIndex
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun getOutputs(): List<WebRtcAudioOutput> {
|
||||
return availableOutputs.toList()
|
||||
}
|
||||
|
||||
fun peekNext(): WebRtcAudioOutput {
|
||||
val peekIndex = (selectedDevice + 1) % availableOutputs.size
|
||||
return getOutputs()[peekIndex]
|
||||
}
|
||||
|
||||
var isEarpieceAvailable: Boolean
|
||||
get() = availableOutputs.contains(WebRtcAudioOutput.HANDSET)
|
||||
set(value) {
|
||||
if (value) {
|
||||
availableOutputs.add(WebRtcAudioOutput.HANDSET)
|
||||
} else {
|
||||
availableOutputs.remove(WebRtcAudioOutput.HANDSET)
|
||||
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
var isBluetoothHeadsetAvailable: Boolean
|
||||
get() = availableOutputs.contains(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
set(value) {
|
||||
if (value) {
|
||||
availableOutputs.add(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
} else {
|
||||
availableOutputs.remove(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
|
||||
}
|
||||
}
|
||||
var isWiredHeadsetAvailable: Boolean
|
||||
get() = availableOutputs.contains(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
set(value) {
|
||||
if (value) {
|
||||
availableOutputs.add(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
} else {
|
||||
availableOutputs.remove(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
selectedDevice = min(selectedDevice, availableOutputs.size - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHOW_PICKER_THRESHOLD = 3
|
||||
const val SHOW_PICKER_THRESHOLD = 3
|
||||
|
||||
private const val STATE_OUTPUT_INDEX = "audio.output.toggle.state.output.index"
|
||||
private const val STATE_HEADSET_ENABLED = "audio.output.toggle.state.headset.enabled"
|
||||
private const val STATE_HANDSET_ENABLED = "audio.output.toggle.state.handset.enabled"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.webrtc.audio.AudioDeviceMapping
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
/**
|
||||
* This launches the bottom sheet on Android 12+ devices for selecting which audio device to use during a call.
|
||||
* In cases where there are fewer than the provided threshold number of devices, it will cycle through them without presenting a bottom sheet.
|
||||
*/
|
||||
@RequiresApi(31)
|
||||
class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputChangedListener, private val outputState: ToggleButtonOutputState, private val stateUpdater: AudioStateUpdater) {
|
||||
|
||||
fun showPicker(fragmentActivity: FragmentActivity, threshold: Int): DialogInterface? {
|
||||
val am = ApplicationDependencies.getAndroidCallAudioManager()
|
||||
if (am.availableCommunicationDevices.isEmpty()) {
|
||||
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) }
|
||||
val currentDeviceId = am.communicationDevice?.id ?: -1
|
||||
if (devices.size < threshold) {
|
||||
if (devices.isEmpty()) return null
|
||||
|
||||
val index = devices.indexOfFirst { it.deviceId == currentDeviceId }
|
||||
if (index == -1) return null
|
||||
|
||||
onAudioDeviceSelected(devices[(index + 1) % devices.size])
|
||||
return null
|
||||
} else {
|
||||
return WebRtcAudioOutputBottomSheet.show(fragmentActivity.supportFragmentManager, devices, currentDeviceId, onAudioDeviceSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
val onAudioDeviceSelected: (AudioOutputOption) -> Unit = {
|
||||
audioOutputChangedListener.audioOutputChanged(WebRtcAudioDevice(it.toWebRtcAudioOutput(), it.deviceId))
|
||||
|
||||
when (it.deviceType) {
|
||||
SignalAudioManager.AudioDevice.WIRED_HEADSET -> {
|
||||
outputState.isWiredHeadsetAvailable = true
|
||||
stateUpdater.updateAudioOutputState(WebRtcAudioOutput.WIRED_HEADSET)
|
||||
}
|
||||
|
||||
SignalAudioManager.AudioDevice.EARPIECE -> {
|
||||
outputState.isEarpieceAvailable = true
|
||||
stateUpdater.updateAudioOutputState(WebRtcAudioOutput.HANDSET)
|
||||
}
|
||||
|
||||
SignalAudioManager.AudioDevice.BLUETOOTH -> {
|
||||
outputState.isBluetoothHeadsetAvailable = true
|
||||
stateUpdater.updateAudioOutputState(WebRtcAudioOutput.BLUETOOTH_HEADSET)
|
||||
}
|
||||
|
||||
SignalAudioManager.AudioDevice.SPEAKER_PHONE, SignalAudioManager.AudioDevice.NONE -> stateUpdater.updateAudioOutputState(WebRtcAudioOutput.SPEAKER)
|
||||
}
|
||||
}
|
||||
|
||||
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_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset)
|
||||
AudioDeviceInfo.TYPE_USB_HEADSET -> context.getString(R.string.WebRtcAudioOutputToggle__wired_headset_usb)
|
||||
else -> this.productName
|
||||
}
|
||||
}
|
||||
|
||||
private fun AudioOutputOption.toWebRtcAudioOutput(): WebRtcAudioOutput {
|
||||
return when (this.deviceType) {
|
||||
SignalAudioManager.AudioDevice.WIRED_HEADSET -> WebRtcAudioOutput.WIRED_HEADSET
|
||||
SignalAudioManager.AudioDevice.EARPIECE -> WebRtcAudioOutput.HANDSET
|
||||
SignalAudioManager.AudioDevice.BLUETOOTH -> WebRtcAudioOutput.BLUETOOTH_HEADSET
|
||||
SignalAudioManager.AudioDevice.SPEAKER_PHONE, SignalAudioManager.AudioDevice.NONE -> WebRtcAudioOutput.SPEAKER
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* This launches the bottom sheet on Android 11 and below devices for selecting which audio device to use during a call.
|
||||
* In cases where there are only [SHOW_PICKER_THRESHOLD] devices, it will cycle through them without presenting a bottom sheet.
|
||||
*/
|
||||
class WebRtcAudioPickerLegacy(private val audioOutputChangedListener: OnAudioOutputChangedListener, private val outputState: ToggleButtonOutputState, private val stateUpdater: AudioStateUpdater) {
|
||||
|
||||
fun showPicker(context: Context, availableModes: List<WebRtcAudioOutput?>): DialogInterface? {
|
||||
val rv = RecyclerView(context)
|
||||
val adapter = AudioOutputAdapter(
|
||||
fun(audioDevice: WebRtcAudioDevice) {
|
||||
audioOutputChangedListener.audioOutputChanged(audioDevice)
|
||||
stateUpdater.updateAudioOutputState(audioDevice.webRtcAudioOutput)
|
||||
stateUpdater.hidePicker()
|
||||
},
|
||||
availableModes
|
||||
)
|
||||
adapter.setSelectedOutput(outputState.getCurrentOutput())
|
||||
rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
rv.adapter = adapter
|
||||
return MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.WebRtcAudioOutputToggle__audio_output)
|
||||
.setView(rv)
|
||||
.setCancelable(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import com.google.common.collect.Sets;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
@@ -71,6 +72,8 @@ import java.util.Set;
|
||||
|
||||
public class WebRtcCallView extends ConstraintLayout {
|
||||
|
||||
private static final String TAG = Log.tag(WebRtcCallView.class);
|
||||
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
|
||||
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
|
||||
@@ -241,16 +244,21 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
adjustableMarginsSet.add(videoToggle);
|
||||
adjustableMarginsSet.add(audioToggle);
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
audioToggle.setOnAudioOutputChangedListener31(deviceId -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onAudioOutputChanged31(deviceId));
|
||||
audioToggle.setOnAudioOutputChangedListener(webRtcAudioDevice -> {
|
||||
runIfNonNull(controlsListener, listener ->
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
final Integer deviceId = webRtcAudioDevice.getDeviceId();
|
||||
if (deviceId != null) {
|
||||
listener.onAudioOutputChanged31(deviceId);
|
||||
} else {
|
||||
Log.e(TAG, "Attempted to change audio output to null device ID.");
|
||||
}
|
||||
} else {
|
||||
listener.onAudioOutputChanged(webRtcAudioDevice.getWebRtcAudioOutput());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
audioToggle.setOnAudioOutputChangedListenerLegacy(outputMode -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onAudioOutputChanged(outputMode));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
videoToggle.setOnCheckedChangeListener((v, isOn) -> {
|
||||
runIfNonNull(controlsListener, listener -> listener.onVideoChanged(isOn));
|
||||
@@ -673,7 +681,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
webRtcControls.isBluetoothHeadsetAvailableForAudioToggle(),
|
||||
webRtcControls.isWiredHeadsetAvailableForAudioToggle());
|
||||
|
||||
audioToggle.setAudioOutput(webRtcControls.getAudioOutput(), false);
|
||||
audioToggle.updateAudioOutputState(webRtcControls.getAudioOutput());
|
||||
}
|
||||
|
||||
if (webRtcControls.displayCameraToggle()) {
|
||||
@@ -1084,7 +1092,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
void hideSystemUI();
|
||||
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
|
||||
@RequiresApi(31)
|
||||
void onAudioOutputChanged31(@NonNull int audioOutputAddress);
|
||||
void onAudioOutputChanged31(@NonNull Integer audioOutputAddress);
|
||||
void onVideoChanged(boolean isVideoEnabled);
|
||||
void onMicChanged(boolean isMicEnabled);
|
||||
void onCameraDirectionChanged();
|
||||
|
||||
@@ -181,7 +181,7 @@ public final class WebRtcControls {
|
||||
}
|
||||
|
||||
boolean isWiredHeadsetAvailableForAudioToggle() {
|
||||
return availableDevices.contains(SignalAudioManager.AudioDevice.BLUETOOTH);
|
||||
return availableDevices.contains(SignalAudioManager.AudioDevice.WIRED_HEADSET);
|
||||
}
|
||||
|
||||
boolean isFadeOutEnabled() {
|
||||
|
||||
Reference in New Issue
Block a user