mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Use Bluetooth headset mic to record voice notes.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user