From 125c4f43cf51e69c5cd7cac4b1af6230a54d2b5e Mon Sep 17 00:00:00 2001 From: Nicholas Date: Mon, 24 Apr 2023 11:56:58 -0400 Subject: [PATCH] Make audio device button directly toggle when only two devices are present. --- .../securesms/WebRtcCallActivity.java | 2 +- .../components/webrtc/AudioOutputAdapter.java | 2 +- .../components/webrtc/AudioStateUpdater.kt | 9 + .../webrtc/OnAudioOutputChangedListener.java | 2 +- .../webrtc/ToggleButtonOutputState.kt | 82 +++++++ .../components/webrtc/WebRtcAudioDevice.kt | 9 + .../webrtc/WebRtcAudioOutputToggleButton.kt | 204 +++--------------- .../components/webrtc/WebRtcAudioPicker31.kt | 85 ++++++++ .../webrtc/WebRtcAudioPickerLegacy.kt | 35 +++ .../components/webrtc/WebRtcCallView.java | 30 ++- .../components/webrtc/WebRtcControls.java | 2 +- 11 files changed, 274 insertions(+), 188 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioStateUpdater.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/ToggleButtonOutputState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioDevice.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPicker31.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPickerLegacy.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 0a7b575388..95ebfbafd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -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)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioOutputAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioOutputAdapter.java index 1e0b8700da..f4ff690fcb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioOutputAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioOutputAdapter.java @@ -57,7 +57,7 @@ final class AudioOutputAdapter extends RecyclerView.Adapter = 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 { + return availableOutputs.toList() + } + + fun peekNext(): WebRtcAudioOutput { + val peekIndex = (selectedDevice + 1) % availableOutputs.size + return getOutputs()[peekIndex] + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioDevice.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioDevice.kt new file mode 100644 index 0000000000..2b39917d6f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioDevice.kt @@ -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?) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt index 8582bf506e..db8b389f32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt @@ -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) { - 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 = 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 = 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 { - 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" diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPicker31.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPicker31.kt new file mode 100644 index 0000000000..d8028859fc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPicker31.kt @@ -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 = 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 + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPickerLegacy.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPickerLegacy.kt new file mode 100644 index 0000000000..eb47fb0ef4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioPickerLegacy.kt @@ -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): 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() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index ad4ec813b5..50b3413312 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index f5bef150bd..a3b3ec68a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -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() {