Switch to BT mic if available for voice memo recording.
Addresses #12016.
This commit is contained in:
parent
0e08b4ee26
commit
9f6eb142d2
2 changed files with 123 additions and 5 deletions
|
@ -26,15 +26,17 @@ public class AudioRecorder {
|
|||
|
||||
private static final ExecutorService executor = SignalExecutors.newCachedSingleThreadExecutor("signal-AudioRecorder");
|
||||
|
||||
private final Context context;
|
||||
private final AudioRecorderFocusManager audioFocusManager;
|
||||
private final Context context;
|
||||
private final AudioRecorderFocusManager audioFocusManager;
|
||||
private final BluetoothScoSessionManager bluetoothScoSessionManager;
|
||||
|
||||
private Recorder recorder;
|
||||
private Uri captureUri;
|
||||
|
||||
public AudioRecorder(@NonNull Context context) {
|
||||
this.context = context;
|
||||
audioFocusManager = AudioRecorderFocusManager.create(context, focusChange -> stopRecording());
|
||||
audioFocusManager = AudioRecorderFocusManager.create(context, focusChange -> stopRecording());
|
||||
bluetoothScoSessionManager = new BluetoothScoSessionManager(context);
|
||||
}
|
||||
|
||||
public void startRecording() {
|
||||
|
@ -53,13 +55,18 @@ public class AudioRecorder {
|
|||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
|
||||
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
int focusResult = audioFocusManager.requestAudioFocus();
|
||||
if (focusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.w(TAG, "Could not gain audio focus. Received result code " + focusResult);
|
||||
}
|
||||
recorder.start(fds[1]);
|
||||
if (bluetoothScoSessionManager.isBluetoothScoCapable()) {
|
||||
Log.i(TAG, "Starting voice memo recording with Bluetooth mic.");
|
||||
bluetoothScoSessionManager.startBluetooth(recorder, fds[1]);
|
||||
} else {
|
||||
Log.i(TAG, "Starting voice memo recording with built-in mic.");
|
||||
recorder.start(fds[1]);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
@ -79,6 +86,7 @@ public class AudioRecorder {
|
|||
|
||||
audioFocusManager.abandonAudioFocus();
|
||||
recorder.stop();
|
||||
bluetoothScoSessionManager.stopBluetooth();
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package org.thoughtcrime.securesms.audio
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothHeadset
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* This class manages the SCO (Synchronous Connection Oriented) Bluetooth link for voice memos.
|
||||
* A consumer of this class should first check if the hardware is prepared to receive input from a bluetooth device using [isBluetoothScoCapable]
|
||||
* Then they can use [startBluetooth] to initiate a session.
|
||||
* We send initialize an SCO link and receive its state updates as a system Broadcast.
|
||||
* Once the connection is established, we start storing audio via the provided [Recorder]
|
||||
* It is the responsibility of the owner of this object to close the Bluetooth link when recording is finished.
|
||||
*
|
||||
* Note: in testing, closing the SCO link does not interrupt an in-progress recording, and a user is free to continue recording on the device's mic.
|
||||
*/
|
||||
class BluetoothScoSessionManager(val context: Context) : BroadcastReceiver() {
|
||||
private val audioManager: AudioManager = ServiceUtil.getAudioManager(context)
|
||||
private var bluetoothSessionAlive: Boolean = false
|
||||
private var callback: Recorder? = null
|
||||
private var fileDescriptor: ParcelFileDescriptor? = null
|
||||
|
||||
private fun register() {
|
||||
Log.d(TAG, "Registering Bluetooth SCO broadcast receiver.")
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
|
||||
context.registerReceiver(this, filter)
|
||||
}
|
||||
|
||||
private fun unregister() {
|
||||
Log.d(TAG, "Unregistering Bluetooth SCO broadcast receiver.")
|
||||
context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
fun isBluetoothScoCapable(): Boolean {
|
||||
if (!audioManager.isBluetoothScoAvailableOffCall) {
|
||||
return false
|
||||
}
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= 31) {
|
||||
audioManager.availableCommunicationDevices.any { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
} else if (Build.VERSION.SDK_INT >= 23) {
|
||||
audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).any { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
} else {
|
||||
hasBluetoothMicConnectedLegacy()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun hasBluetoothMicConnectedLegacy(): Boolean {
|
||||
val mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
||||
return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled &&
|
||||
mBluetoothAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothHeadset.STATE_CONNECTED
|
||||
}
|
||||
|
||||
fun startBluetooth(callback: Recorder, fileDescriptor: ParcelFileDescriptor) {
|
||||
Log.d(TAG, "Starting Bluetooth SCO for voice memo.")
|
||||
this.callback = callback
|
||||
this.fileDescriptor = fileDescriptor
|
||||
register()
|
||||
audioManager.startBluetoothSco()
|
||||
}
|
||||
|
||||
fun stopBluetooth() {
|
||||
if (bluetoothSessionAlive) {
|
||||
Log.d(TAG, "Stopping Bluetooth SCO for voice memo.")
|
||||
bluetoothSessionAlive = false
|
||||
unregister()
|
||||
if (audioManager.isBluetoothScoOn) {
|
||||
audioManager.stopBluetoothSco()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
if (action == AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) {
|
||||
val state: Int = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1)
|
||||
when (state) {
|
||||
AudioManager.SCO_AUDIO_STATE_CONNECTED -> try {
|
||||
bluetoothSessionAlive = true
|
||||
callback?.start(fileDescriptor)
|
||||
Log.d(TAG, "Bluetooth SCO connected.")
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
AudioManager.SCO_AUDIO_STATE_CONNECTING -> Log.d(TAG, "Bluetooth SCO in connecting state.")
|
||||
AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> {
|
||||
Log.d(TAG, "Bluetooth SCO disconnected.")
|
||||
stopBluetooth()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "BluetoothMicManager"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue