Use Bluetooth headset mic to record voice notes.

This commit is contained in:
Nicholas
2023-05-04 15:58:24 -04:00
committed by Alex Hart
parent fc9a6b98d1
commit f1fd29a477
9 changed files with 281 additions and 66 deletions

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.audio
/**
* A listener for when audio devices are added or removed, for example if a wired headset is plugged/unplugged or Bluetooth connected/disconnected.
*/
interface AudioDeviceUpdatedListener {
fun onAudioDeviceUpdated()
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.audio
import android.content.Context
import android.media.AudioDeviceInfo
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.RequiresApi
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
internal const val TAG = "BluetoothVoiceNoteUtil"
sealed interface BluetoothVoiceNoteUtil {
fun connectBluetoothScoConnection()
fun disconnectBluetoothScoConnection()
fun destroy()
companion object {
fun create(context: Context, listener: () -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil {
return if (Build.VERSION.SDK_INT >= 31) BluetoothVoiceNoteUtil31(listener) else BluetoothVoiceNoteUtilLegacy(context, listener, bluetoothPermissionDeniedHandler)
}
}
}
@RequiresApi(31)
private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoiceNoteUtil {
override fun connectBluetoothScoConnection() {
val audioManager = ApplicationDependencies.getAndroidCallAudioManager()
val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice
if (device != null) {
val result: Boolean = audioManager.setCommunicationDevice(device)
if (result) {
Log.d(TAG, "Successfully set Bluetooth device as active communication device.")
} else {
Log.d(TAG, "Found Bluetooth device but failed to set it as active communication device.")
}
} else {
Log.d(TAG, "Could not find Bluetooth device in list of communications devices, falling back to current input.")
}
listener()
}
override fun disconnectBluetoothScoConnection() = Unit
override fun destroy() = Unit
}
/**
* Encapsulated logic for managing a Bluetooth connection withing the Fragment lifecycle for voice notes.
*
* @param context Context with reference to the main thread.
* @param listener This will be executed on the main thread after the Bluetooth connection connects, or if it doesn't.
* @param bluetoothPermissionDeniedHandler called when we detect the Bluetooth permission has been denied to our app.
*/
private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: () -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil {
private val commandAndControlThread: HandlerThread = SignalExecutors.getAndStartHandlerThread("voice-note-audio", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
private val uiThreadHandler = Handler(context.mainLooper)
private val audioHandler: SignalAudioHandler = SignalAudioHandler(commandAndControlThread.looper)
private val deviceUpdatedListener: AudioDeviceUpdatedListener = object : AudioDeviceUpdatedListener {
override fun onAudioDeviceUpdated() {
if (signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED) {
Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.")
uiThreadHandler.post { listener() }
}
}
}
private val signalBluetoothManager: SignalBluetoothManager = SignalBluetoothManager(context, deviceUpdatedListener, audioHandler)
private var hasWarnedAboutBluetooth = false
init {
if (Build.VERSION.SDK_INT < 31) {
audioHandler.post {
signalBluetoothManager.start()
Log.d(TAG, "Bluetooth manager started.")
}
}
}
override fun connectBluetoothScoConnection() {
if (Build.VERSION.SDK_INT >= 31) {
val audioManager = ApplicationDependencies.getAndroidCallAudioManager()
val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice
if (device != null) {
val result: Boolean = audioManager.setCommunicationDevice(device)
if (result) {
Log.d(TAG, "Successfully set Bluetooth device as active communication device.")
} else {
Log.d(TAG, "Found Bluetooth device but failed to set it as active communication device.")
}
} else {
Log.d(TAG, "Could not find Bluetooth device in list of communications devices, falling back to current input.")
}
listener()
} else {
audioHandler.post {
if (signalBluetoothManager.state.shouldUpdate()) {
signalBluetoothManager.updateDevice()
}
val currentState = signalBluetoothManager.state
if (currentState == SignalBluetoothManager.State.AVAILABLE) {
signalBluetoothManager.startScoAudio()
} else {
Log.d(TAG, "Recording from phone mic because bluetooth state was " + currentState + ", not " + SignalBluetoothManager.State.AVAILABLE)
uiThreadHandler.post {
if (currentState == SignalBluetoothManager.State.PERMISSION_DENIED && !hasWarnedAboutBluetooth) {
bluetoothPermissionDeniedHandler()
hasWarnedAboutBluetooth = true
}
listener()
}
}
}
}
}
override fun disconnectBluetoothScoConnection() {
audioHandler.post {
if (signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED) {
signalBluetoothManager.stopScoAudio()
}
}
}
override fun destroy() {
audioHandler.post {
signalBluetoothManager.stop()
}
}
}

View File

@@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.webrtc.audio
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.audio
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
@@ -13,18 +18,19 @@ import android.media.AudioManager
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
import java.util.concurrent.TimeUnit
/**
* Manages the bluetooth lifecycle with a headset. This class doesn't make any
* determination on if bluetooth should be used. It determines if a device is connected,
* reports that to the [SignalAudioManager], and then handles connecting/disconnecting
* to the device if requested by [SignalAudioManager].
* reports that to the [org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager], and then handles connecting/disconnecting
* to the device if requested by [org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager].
*/
@SuppressLint("MissingPermission") // targetSdkVersion is still 30 (https://issuetracker.google.com/issues/201454155)
class SignalBluetoothManager(
private val context: Context,
private val audioManager: FullSignalAudioManager,
private val audioDeviceUpdatedListener: AudioDeviceUpdatedListener,
private val handler: SignalAudioHandler
) {
@@ -139,11 +145,6 @@ class SignalBluetoothManager(
return false
}
if (androidAudioManager.isBluetoothScoOn) {
Log.i(TAG, "SCO connection already started")
return true
}
state = State.CONNECTING
androidAudioManager.startBluetoothSco()
androidAudioManager.isBluetoothScoOn = true
@@ -202,10 +203,6 @@ class SignalBluetoothManager(
}
}
private fun updateAudioDeviceState() {
audioManager.updateAudioDeviceState()
}
private fun startTimer() {
handler.postDelayed(bluetoothTimeout, SCO_TIMEOUT)
}
@@ -243,12 +240,12 @@ class SignalBluetoothManager(
stopScoAudio()
}
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
private fun onServiceConnected(proxy: BluetoothHeadset?) {
bluetoothHeadset = proxy
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
private fun onServiceDisconnected() {
@@ -256,7 +253,7 @@ class SignalBluetoothManager(
bluetoothHeadset = null
bluetoothDevice = null
state = State.UNAVAILABLE
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
private fun onHeadsetConnectionStateChanged(connectionState: Int) {
@@ -265,12 +262,12 @@ class SignalBluetoothManager(
when (connectionState) {
BluetoothHeadset.STATE_CONNECTED -> {
scoConnectionAttempts = 0
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
BluetoothHeadset.STATE_DISCONNECTED -> {
stopScoAudio()
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
}
}
@@ -284,7 +281,7 @@ class SignalBluetoothManager(
Log.d(TAG, "Bluetooth audio SCO is now connected")
state = State.CONNECTED
scoConnectionAttempts = 0
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
} else {
Log.w(TAG, "Unexpected state ${audioState.toStateString()}")
}
@@ -296,7 +293,7 @@ class SignalBluetoothManager(
Log.d(TAG, "Ignore ${audioState.toStateString()} initial sticky broadcast.")
return
}
updateAudioDeviceState()
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
}
@@ -347,7 +344,9 @@ class SignalBluetoothManager(
}
} else if (intent.action == AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) {
if (wasScoDisconnected(intent)) {
handler.post(::updateAudioDeviceState)
handler.post {
audioDeviceUpdatedListener.onAudioDeviceUpdated()
}
}
} else {
Log.d(TAG, "Received broadcast of ${intent.action}")

View File

@@ -1,18 +1,6 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation;
@@ -101,6 +89,7 @@ import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.PendingIntentFlags;
import org.signal.core.util.StringUtil;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
@@ -116,6 +105,8 @@ import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.BluetoothVoiceNoteUtil;
import org.thoughtcrime.securesms.audio.BluetoothVoiceNoteUtilKt;
import org.thoughtcrime.securesms.badges.gifts.thanks.GiftThanksSheet;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
@@ -286,11 +277,10 @@ import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.Material3OnScrollHelper;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
@@ -309,6 +299,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil;
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.io.IOException;
@@ -421,8 +412,10 @@ public class ConversationParentFragment extends Fragment
private Stub<FrameLayout> voiceNotePlayerViewStub;
private View navigationBarBackground;
private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
private AttachmentManager attachmentManager;
private BluetoothVoiceNoteUtil bluetoothVoiceNoteUtil;
private AudioRecorder audioRecorder;
private RecordingSession recordingSession;
private BroadcastReceiver securityUpdateReceiver;
private Stub<MediaKeyboard> emojiDrawerStub;
@@ -519,6 +512,7 @@ public class ConversationParentFragment extends Fragment
voiceNoteMediaController = new VoiceNoteMediaController(requireActivity(), true);
voiceRecorderWakeLock = new VoiceRecorderWakeLock(requireActivity());
bluetoothVoiceNoteUtil = BluetoothVoiceNoteUtil.Companion.create(requireContext(), this::beginRecording, this::onBluetoothPermissionDenied);
// TODO [alex] LargeScreenSupport -- Should be removed once we move to multi-pane layout.
new FullscreenHelper(requireActivity()).showSystemUI();
@@ -676,8 +670,9 @@ public class ConversationParentFragment extends Fragment
@Override
public void onDestroy() {
if (securityUpdateReceiver != null) requireActivity().unregisterReceiver(securityUpdateReceiver);
if (pinnedShortcutReceiver != null) requireActivity().unregisterReceiver(pinnedShortcutReceiver);
if (securityUpdateReceiver != null) requireActivity().unregisterReceiver(securityUpdateReceiver);
if (pinnedShortcutReceiver != null) requireActivity().unregisterReceiver(pinnedShortcutReceiver);
if (bluetoothVoiceNoteUtil != null) bluetoothVoiceNoteUtil.destroy();
super.onDestroy();
}
@@ -3251,6 +3246,25 @@ public class ConversationParentFragment extends Fragment
@Override
public void onRecorderStarted() {
final AudioManagerCompat audioManager = ApplicationDependencies.getAndroidCallAudioManager();
if (audioManager.isBluetoothAvailable()) {
connectToBluetoothAndBeginRecording();
} else {
Log.d(TAG, "Recording from phone mic because no bluetooth devices were available.");
beginRecording();
}
}
private void connectToBluetoothAndBeginRecording() {
if (bluetoothVoiceNoteUtil != null) {
Log.d(TAG, "Initiating Bluetooth SCO connection...");
bluetoothVoiceNoteUtil.connectBluetoothScoConnection();
} else {
Log.e(TAG, "Unable to instantiate BluetoothVoiceNoteUtil.");
}
}
private Unit beginRecording() {
Vibrator vibrator = ServiceUtil.getVibrator(requireContext());
vibrator.vibrate(20);
@@ -3260,6 +3274,18 @@ public class ConversationParentFragment extends Fragment
voiceNoteMediaController.pausePlayback();
recordingSession = new RecordingSession(audioRecorder.startRecording());
disposables.add(recordingSession);
return Unit.INSTANCE;
}
private Unit onBluetoothPermissionDenied() {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ConversationParentFragment__bluetooth_permission_denied)
.setMessage(R.string.ConversationParentFragment__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
.setPositiveButton(R.string.ConversationParentFragment__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(requireContext())))
.setNegativeButton(R.string.ConversationParentFragment__not_now, null)
.show();
return Unit.INSTANCE;
}
@Override
@@ -3271,6 +3297,7 @@ public class ConversationParentFragment extends Fragment
@Override
public void onRecorderFinished() {
bluetoothVoiceNoteUtil.disconnectBluetoothScoConnection();
voiceRecorderWakeLock.release();
updateToggleButtonState();
Vibrator vibrator = ServiceUtil.getVibrator(requireContext());
@@ -4092,7 +4119,7 @@ public class ConversationParentFragment extends Fragment
} else {
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
if (messageRecord.isMms() && ((MmsMessageRecord) messageRecord).isViewOnce()) {
if (messageRecord.isMms() && messageRecord.isViewOnce()) {
Attachment attachment = new TombstoneAttachment(MediaUtil.VIEW_ONCE, true);
slideDeck = new SlideDeck();
slideDeck.addSlide(MediaUtil.getSlideForAttachment(requireContext(), attachment));

View File

@@ -6,6 +6,7 @@ import android.app.AlarmManager;
import android.app.KeyguardManager;
import android.app.NotificationManager;
import android.app.job.JobScheduler;
import android.bluetooth.BluetoothManager;
import android.content.ClipboardManager;
import android.content.Context;
import android.hardware.SensorManager;
@@ -107,4 +108,8 @@ public class ServiceUtil {
public static KeyguardManager getKeyguardManager(@NotNull Context context) {
return ContextCompat.getSystemService(context, KeyguardManager.class);
}
public static BluetoothManager getBluetoothManager(@NotNull Context context) {
return ContextCompat.getSystemService(context, BluetoothManager.class);
}
}

View File

@@ -7,7 +7,7 @@ import androidx.annotation.RequiresApi
object AudioDeviceMapping {
private val systemDeviceTypeMap: Map<SignalAudioManager.AudioDevice, List<Int>> = mapOf(
SignalAudioManager.AudioDevice.BLUETOOTH to listOf(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_HEARING_AID),
SignalAudioManager.AudioDevice.BLUETOOTH to listOf(AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_HEARING_AID),
SignalAudioManager.AudioDevice.EARPIECE to listOf(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE),
SignalAudioManager.AudioDevice.SPEAKER_PHONE to listOf(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE),
SignalAudioManager.AudioDevice.WIRED_HEADSET to listOf(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, AudioDeviceInfo.TYPE_USB_HEADSET),

View File

@@ -52,6 +52,14 @@ public abstract class AudioManagerCompat {
audioManager.stopBluetoothSco();
}
public boolean isBluetoothAvailable() {
if (Build.VERSION.SDK_INT >= 31) {
return audioManager.getAvailableCommunicationDevices().stream().anyMatch(it -> AudioDeviceMapping.fromPlatformType(it.getType()) == SignalAudioManager.AudioDevice.BLUETOOTH);
} else {
return isBluetoothScoAvailableOffCall();
}
}
public boolean isBluetoothConnected() {
if (Build.VERSION.SDK_INT >= 31) {
final SignalAudioManager.AudioDevice audioDevice = AudioDeviceMapping.fromPlatformType(audioManager.getCommunicationDevice().getType());
@@ -97,6 +105,11 @@ public abstract class AudioManagerCompat {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
}
@RequiresApi(31)
public @Nullable AudioDeviceInfo getConnectedBluetoothDevice() {
return getAvailableCommunicationDevices().stream().filter(it -> AudioDeviceMapping.fromPlatformType(it.getType()) == SignalAudioManager.AudioDevice.BLUETOOTH).findAny().orElse(null);
}
@RequiresApi(31)
public List<AudioDeviceInfo> getAvailableCommunicationDevices() {
return audioManager.getAvailableCommunicationDevices();
@@ -163,7 +176,9 @@ public abstract class AudioManagerCompat {
}
abstract public SoundPool createSoundPool();
abstract public boolean requestCallAudioFocus();
abstract public void abandonCallAudioFocus();
public static AudioManagerCompat create(@NonNull Context context) {
@@ -178,9 +193,9 @@ public abstract class AudioManagerCompat {
private static class Api26AudioManagerCompat extends AudioManagerCompat {
private static AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build();
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build();
private AudioFocusRequest audioFocusRequest;
@@ -191,9 +206,9 @@ public abstract class AudioManagerCompat {
@Override
public SoundPool createSoundPool() {
return new SoundPool.Builder()
.setAudioAttributes(AUDIO_ATTRIBUTES)
.setMaxStreams(1)
.build();
.setAudioAttributes(AUDIO_ATTRIBUTES)
.setMaxStreams(1)
.build();
}
@Override
@@ -205,9 +220,9 @@ public abstract class AudioManagerCompat {
if (audioFocusRequest == null) {
audioFocusRequest = new AudioFocusRequest.Builder(AUDIOFOCUS_GAIN)
.setAudioAttributes(AUDIO_ATTRIBUTES)
.setOnAudioFocusChangeListener(onAudioFocusChangeListener)
.build();
.setAudioAttributes(AUDIO_ATTRIBUTES)
.setOnAudioFocusChangeListener(onAudioFocusChangeListener)
.build();
} else {
Log.w(TAG, "Trying again to request audio focus");
}
@@ -243,10 +258,10 @@ public abstract class AudioManagerCompat {
private static class Api21AudioManagerCompat extends Api19AudioManagerCompat {
private static AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
.build();
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
.build();
private Api21AudioManagerCompat(@NonNull Context context) {
super(context);
@@ -255,9 +270,9 @@ public abstract class AudioManagerCompat {
@Override
public SoundPool createSoundPool() {
return new SoundPool.Builder()
.setAudioAttributes(AUDIO_ATTRIBUTES)
.setMaxStreams(1)
.build();
.setAudioAttributes(AUDIO_ATTRIBUTES)
.setMaxStreams(1)
.build();
}
}

View File

@@ -1,3 +1,8 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.webrtc.audio
import android.content.BroadcastReceiver
@@ -12,6 +17,8 @@ import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.audio.AudioDeviceUpdatedListener
import org.thoughtcrime.securesms.audio.SignalBluetoothManager
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
@@ -138,7 +145,7 @@ sealed class SignalAudioManager(protected val context: Context, protected val ev
* bluetooth headset is then disconnected, and reconnected, the audio will again automatically switch to
* the bluetooth headset.
*/
class FullSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener) {
class FullSignalAudioManager(context: Context, eventListener: EventListener?) : SignalAudioManager(context, eventListener), AudioDeviceUpdatedListener {
private val signalBluetoothManager = SignalBluetoothManager(context, this, handler)
private var audioDevices: MutableSet<AudioDevice> = mutableSetOf()
@@ -175,7 +182,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
signalBluetoothManager.start()
updateAudioDeviceState()
onAudioDeviceUpdated()
wiredHeadsetReceiver = WiredHeadsetReceiver()
context.registerReceiver(wiredHeadsetReceiver, IntentFilter(AudioManager.ACTION_HEADSET_PLUG))
@@ -239,7 +246,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
Log.d(TAG, "Stopped")
}
fun updateAudioDeviceState() {
override fun onAudioDeviceUpdated() {
handler.assertHandlerThread()
Log.i(
@@ -356,7 +363,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
}
Log.d(TAG, "New default: $defaultAudioDevice userSelected: $userSelectedAudioDevice")
updateAudioDeviceState()
onAudioDeviceUpdated()
}
override fun selectAudioDevice(recipientId: RecipientId?, device: Int, isId: Boolean) {
@@ -371,7 +378,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
Log.w(TAG, "Can not select $actualDevice from available $audioDevices")
}
userSelectedAudioDevice = actualDevice
updateAudioDeviceState()
onAudioDeviceUpdated()
}
private fun setAudioDevice(device: AudioDevice) {
@@ -420,7 +427,7 @@ class FullSignalAudioManager(context: Context, eventListener: EventListener?) :
private fun onWiredHeadsetChange(pluggedIn: Boolean, hasMic: Boolean) {
Log.i(TAG, "onWiredHeadsetChange state: $state plug: $pluggedIn mic: $hasMic")
hasWiredHeadset = pluggedIn
updateAudioDeviceState()
onAudioDeviceUpdated()
}
private inner class WiredHeadsetReceiver : BroadcastReceiver() {

View File

@@ -1686,7 +1686,7 @@
<string name="WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call">Please enable the \"Nearby devices\" permission to use bluetooth during a call.</string>
<!-- Positive action for bluetooth warning dialog to open settings -->
<string name="WebRtcCallActivity__open_settings">Open settings</string>
<!-- Negative aciton for bluetooth warning dialog to dismiss dialog -->
<!-- Negative action for bluetooth warning dialog to dismiss dialog -->
<string name="WebRtcCallActivity__not_now">Not now</string>
<!-- WebRtcCallView -->
@@ -2437,6 +2437,16 @@
<!-- Label for quoted gift -->
<string name="QuoteView__donation_for_a_friend">Donation for a friend</string>
<!-- ConversationParentFragment -->
<!-- Title for dialog warning about lacking bluetooth permissions during a voice message -->
<string name="ConversationParentFragment__bluetooth_permission_denied">Bluetooth permission denied</string>
<!-- Message for dialog warning about lacking bluetooth permissions during a voice message and references the permission needed by name -->
<string name="ConversationParentFragment__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call">Please enable the \"Nearby devices\" permission to use bluetooth to record voice messages.</string>
<!-- Positive action for bluetooth warning dialog to open settings -->
<string name="ConversationParentFragment__open_settings">Open settings</string>
<!-- Negative action for bluetooth warning dialog to dismiss dialog -->
<string name="ConversationParentFragment__not_now">Not now</string>
<!-- conversation_fragment -->
<string name="conversation_fragment__scroll_to_the_bottom_content_description">Scroll to the bottom</string>