diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index eafe99f86b..652534a7ff 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.riotx">
+
+
diff --git a/vector/src/main/java/im/vector/riotx/core/services/BluetoothHeadsetReceiver.kt b/vector/src/main/java/im/vector/riotx/core/services/BluetoothHeadsetReceiver.kt
new file mode 100644
index 0000000000..a56c4c73c6
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/services/BluetoothHeadsetReceiver.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.services
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothClass
+import android.bluetooth.BluetoothDevice
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import java.lang.ref.WeakReference
+
+class BluetoothHeadsetReceiver : BroadcastReceiver() {
+
+ interface EventListener {
+ fun onBTHeadsetEvent(event: BTHeadsetPlugEvent)
+ }
+
+ var delegate: WeakReference? = null
+
+ data class BTHeadsetPlugEvent(
+ val plugged: Boolean,
+ val headsetName: String?,
+ /**
+ * BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE
+ * BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO
+ * AUDIO_VIDEO_WEARABLE_HEADSET
+ */
+ val deviceClass: Int
+ )
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ // This intent will have 3 extras:
+ // EXTRA_CONNECTION_STATE - The current connection state
+ // EXTRA_PREVIOUS_CONNECTION_STATE}- The previous connection state.
+ // BluetoothDevice#EXTRA_DEVICE - The remote device.
+ // EXTRA_CONNECTION_STATE or EXTRA_PREVIOUS_CONNECTION_STATE can be any of
+ // STATE_DISCONNECTED}, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING
+
+ val headsetConnected = when (intent?.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)) {
+ BluetoothAdapter.STATE_CONNECTED -> true
+ BluetoothAdapter.STATE_DISCONNECTED -> false
+ else -> return // ignore intermediate states
+ }
+
+ val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ val deviceName = device?.name
+ when (device?.bluetoothClass?.deviceClass) {
+ BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE,
+ BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO,
+ BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET -> {
+ // filter only device that we care about for
+ delegate?.get()?.onBTHeadsetEvent(
+ BTHeadsetPlugEvent(
+ plugged = headsetConnected,
+ headsetName = deviceName,
+ deviceClass = device.bluetoothClass.deviceClass
+ )
+ )
+ }
+ else -> return
+ }
+ }
+
+ companion object {
+ fun createAndRegister(context: Context, listener: EventListener): BluetoothHeadsetReceiver {
+ val receiver = BluetoothHeadsetReceiver()
+ receiver.delegate = WeakReference(listener)
+ context.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED))
+ return receiver
+ }
+
+ fun unRegister(context: Context, receiver: BluetoothHeadsetReceiver) {
+ context.unregisterReceiver(receiver)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt
index 832f484542..723cfe3add 100644
--- a/vector/src/main/java/im/vector/riotx/core/services/CallService.kt
+++ b/vector/src/main/java/im/vector/riotx/core/services/CallService.kt
@@ -33,7 +33,7 @@ import timber.log.Timber
/**
* Foreground service to manage calls
*/
-class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener {
+class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
private val connections = mutableMapOf()
@@ -43,10 +43,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
private var callRingPlayer: CallRingPlayer? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
+ private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
// A media button receiver receives and helps translate hardware media playback buttons,
// such as those found on wired and wireless headsets, into the appropriate callbacks in your app
- private var mediaSession : MediaSessionCompat? = null
+ private var mediaSession: MediaSessionCompat? = null
private val mediaSessionButtonCallback = object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
val keyEvent = mediaButtonEvent?.getParcelableExtra(Intent.EXTRA_KEY_EVENT) ?: return false
@@ -64,6 +65,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
callRingPlayer = CallRingPlayer(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
+ bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
}
override fun onDestroy() {
@@ -71,6 +73,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
callRingPlayer?.stop()
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
wiredHeadsetStateReceiver = null
+ bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
+ bluetoothHeadsetStateReceiver = null
mediaSession?.release()
mediaSession = null
}
@@ -365,6 +369,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("## VOIP: onHeadsetEvent $event")
- webRtcPeerConnectionManager.onWireDeviceEvent(event)
+ webRtcPeerConnectionManager.onWiredDeviceEvent(event)
+ }
+
+ override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
+ Timber.v("## VOIP: onBTHeadsetEvent $event")
+ webRtcPeerConnectionManager.onWirelessDeviceEvent(event)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt b/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt
index ad2eae510f..9290d9a3e8 100644
--- a/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/call/CallAudioManager.kt
@@ -16,28 +16,59 @@
package im.vector.riotx.features.call
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioManager
import im.vector.matrix.android.api.session.call.MxCall
+import im.vector.riotx.core.services.WiredHeadsetStateReceiver
import timber.log.Timber
+import java.util.concurrent.Executors
class CallAudioManager(
- val applicationContext: Context
+ val applicationContext: Context,
+ val configChange: (() -> Unit)?
) {
enum class SoundDevice {
PHONE,
SPEAKER,
- HEADSET
+ HEADSET,
+ WIRELESS_HEADSET
}
- private val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ /*
+ * if all calls to audio manager not in the same thread it's not working well...
+ */
+ private val executor = Executors.newSingleThreadExecutor()
+
+ private var audioManager: AudioManager? = null
private var savedIsSpeakerPhoneOn = false
private var savedIsMicrophoneMute = false
private var savedAudioMode = AudioManager.MODE_INVALID
+ private var connectedBlueToothHeadset: BluetoothProfile? = null
+ private var wantsBluetoothConnection = false
+
+ init {
+ executor.execute {
+ audioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ }
+ val bm = applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+ bm?.adapter?.getProfileProxy(applicationContext, object : BluetoothProfile.ServiceListener {
+ override fun onServiceDisconnected(profile: Int) {
+ connectedBlueToothHeadset = null
+ }
+
+ override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
+ connectedBlueToothHeadset = proxy
+ configChange?.invoke()
+ }
+ }, BluetoothProfile.HEADSET)
+ }
+
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
// Called on the listener to notify if the audio focus for this listener has been changed.
@@ -49,6 +80,7 @@ class CallAudioManager(
fun startForCall(mxCall: MxCall) {
Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}")
+ val audioManager = audioManager ?: return
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
savedIsMicrophoneMute = audioManager.isMicrophoneMute
savedAudioMode = audioManager.mode
@@ -72,77 +104,150 @@ class CallAudioManager(
// Always disable microphone mute during a WebRTC call.
setMicrophoneMute(false)
- // If there are no headset, start video output in speaker
- // (you can't watch the video and have the phone close to your ear)
- if (mxCall.isVideoCall && !isHeadsetOn()) {
- setSpeakerphoneOn(true)
- } else {
- // if a headset is plugged, sound will be directed to it
- // (can't really force earpiece when headset is plugged)
- setSpeakerphoneOn(false)
+ executor.execute {
+ // If there are no headset, start video output in speaker
+ // (you can't watch the video and have the phone close to your ear)
+ if (mxCall.isVideoCall && !isHeadsetOn()) {
+ Timber.v("##VOIP: AudioManager default to speaker ")
+ setCurrentSoundDevice(SoundDevice.SPEAKER)
+ } else {
+ // if a wired headset is plugged, sound will be directed to it
+ // (can't really force earpiece when headset is plugged)
+ if (isBluetoothHeadsetOn()) {
+ Timber.v("##VOIP: AudioManager default to WIRELESS_HEADSET ")
+ setCurrentSoundDevice(SoundDevice.WIRELESS_HEADSET)
+ // try now in case already connected?
+ audioManager.isBluetoothScoOn = true
+ } else {
+ Timber.v("##VOIP: AudioManager default to PHONE/HEADSET ")
+ setCurrentSoundDevice(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE)
+ }
+ }
}
}
fun getAvailableSoundDevices(): List {
- return listOf(
- if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE,
- SoundDevice.SPEAKER
- )
+ return ArrayList().apply {
+ if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET)
+ add(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE)
+ add(SoundDevice.SPEAKER)
+ }
}
fun stop() {
Timber.v("## VOIP: AudioManager stopCall")
+ executor.execute {
+ // Restore previously stored audio states.
+ setSpeakerphoneOn(savedIsSpeakerPhoneOn)
+ setMicrophoneMute(savedIsMicrophoneMute)
+ audioManager?.mode = savedAudioMode
- // Restore previously stored audio states.
- setSpeakerphoneOn(savedIsSpeakerPhoneOn)
- setMicrophoneMute(savedIsMicrophoneMute)
- audioManager.mode = savedAudioMode
-
- @Suppress("DEPRECATION")
- audioManager.abandonAudioFocus(audioFocusChangeListener)
+ @Suppress("DEPRECATION")
+ audioManager?.abandonAudioFocus(audioFocusChangeListener)
+ }
}
fun getCurrentSoundDevice(): SoundDevice {
+ val audioManager = audioManager ?: return SoundDevice.PHONE
if (audioManager.isSpeakerphoneOn) {
return SoundDevice.SPEAKER
} else {
+ if (isBluetoothHeadsetOn() && (wantsBluetoothConnection || audioManager.isBluetoothScoOn)) return SoundDevice.WIRELESS_HEADSET
return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE
}
}
fun setCurrentSoundDevice(device: SoundDevice) {
- when (device) {
- SoundDevice.HEADSET,
- SoundDevice.PHONE -> setSpeakerphoneOn(false)
- SoundDevice.SPEAKER -> setSpeakerphoneOn(true)
+ executor.execute {
+ Timber.v("## VOIP setCurrentSoundDevice $device")
+ when (device) {
+ SoundDevice.HEADSET,
+ SoundDevice.PHONE -> {
+ wantsBluetoothConnection = false
+ if (isBluetoothHeadsetOn()) {
+ audioManager?.stopBluetoothSco()
+ audioManager?.isBluetoothScoOn = false
+ }
+ setSpeakerphoneOn(false)
+ }
+ SoundDevice.SPEAKER -> {
+ setSpeakerphoneOn(true)
+ wantsBluetoothConnection = false
+ audioManager?.stopBluetoothSco()
+ audioManager?.isBluetoothScoOn = false
+ }
+ SoundDevice.WIRELESS_HEADSET -> {
+ setSpeakerphoneOn(false)
+ // I cannot directly do it, i have to start then wait that it's connected
+ // to route to bt
+ audioManager?.startBluetoothSco()
+ wantsBluetoothConnection = true
+ }
+ }
+
+ configChange?.invoke()
+ }
+ }
+
+ fun bluetoothStateChange(plugged: Boolean) {
+ executor.execute {
+ if (plugged && wantsBluetoothConnection) {
+ audioManager?.isBluetoothScoOn = true
+ } else if (!plugged && !wantsBluetoothConnection) {
+ audioManager?.stopBluetoothSco()
+ }
+
+ configChange?.invoke()
+ }
+ }
+
+ fun wiredStateChange(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
+ executor.execute {
+ // if it's plugged and speaker is on we should route to headset
+ if (event.plugged && getCurrentSoundDevice() == SoundDevice.SPEAKER) {
+ setCurrentSoundDevice(CallAudioManager.SoundDevice.HEADSET)
+ } else if (!event.plugged) {
+ // if it's unplugged ? always route to speaker?
+ // this is questionable?
+ if (!wantsBluetoothConnection) {
+ setCurrentSoundDevice(SoundDevice.SPEAKER)
+ }
+ }
+ configChange?.invoke()
}
}
private fun isHeadsetOn(): Boolean {
+ return isWiredHeadsetOn() || isBluetoothHeadsetOn()
+ }
+
+ private fun isWiredHeadsetOn(): Boolean {
@Suppress("DEPRECATION")
- return audioManager.isWiredHeadsetOn || audioManager.isBluetoothScoOn
+ return audioManager?.isWiredHeadsetOn ?: false
+ }
+
+ private fun isBluetoothHeadsetOn(): Boolean {
+ return connectedBlueToothHeadset != null
}
/** Sets the speaker phone mode. */
private fun setSpeakerphoneOn(on: Boolean) {
Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on")
- val wasOn = audioManager.isSpeakerphoneOn
+ val wasOn = audioManager?.isSpeakerphoneOn ?: false
if (wasOn == on) {
return
}
- audioManager.isSpeakerphoneOn = on
+ audioManager?.isSpeakerphoneOn = on
}
/** Sets the microphone mute state. */
private fun setMicrophoneMute(on: Boolean) {
Timber.v("## VOIP: AudioManager setMicrophoneMute $on")
- val wasMuted = audioManager.isMicrophoneMute
+ val wasMuted = audioManager?.isMicrophoneMute ?: false
if (wasMuted == on) {
return
}
- audioManager.isMicrophoneMute = on
-
- audioManager.isMusicActive
+ audioManager?.isMicrophoneMute = on
}
/** true if the device has a telephony radio with data
diff --git a/vector/src/main/java/im/vector/riotx/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/call/CallControlsBottomSheet.kt
index 70c6113160..010a89cf2a 100644
--- a/vector/src/main/java/im/vector/riotx/features/call/CallControlsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/call/CallControlsBottomSheet.kt
@@ -55,6 +55,10 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private fun showSoundDeviceChooser(available: List, current: CallAudioManager.SoundDevice) {
val soundDevices = available.map {
when (it) {
+ CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
+ text = getString(R.string.sound_device_wireless_headset)
+ textStyle = if (current == it) "bold" else "normal"
+ }
CallAudioManager.SoundDevice.PHONE -> span {
text = getString(R.string.sound_device_phone)
textStyle = if (current == it) "bold" else "normal"
@@ -82,6 +86,9 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
getString(R.string.sound_device_headset) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
}
+ getString(R.string.sound_device_wireless_headset) -> {
+ callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.WIRELESS_HEADSET))
+ }
}
}
.setNegativeButton(R.string.cancel, null)
@@ -104,6 +111,7 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
+ CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
}
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
index 9fbc38d816..57c3f75f55 100644
--- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
@@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.call.CallCandidatesConten
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.core.services.BluetoothHeadsetReceiver
import im.vector.riotx.core.services.CallService
import im.vector.riotx.core.services.WiredHeadsetStateReceiver
import io.reactivex.disposables.Disposable
@@ -88,7 +89,11 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCallsListeners.remove(listener)
}
- val audioManager = CallAudioManager(context.applicationContext)
+ val audioManager = CallAudioManager(context.applicationContext) {
+ currentCallsListeners.forEach {
+ tryThis { it.onAudioDevicesChange(this) }
+ }
+ }
data class CallContext(
val mxCall: MxCall,
@@ -672,19 +677,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
close()
}
- fun onWireDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
+ fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
+ Timber.v("## VOIP onWiredDeviceEvent $event")
currentCall ?: return
- // if it's plugged and speaker is on we should route to headset
- if (event.plugged && audioManager.getCurrentSoundDevice() == CallAudioManager.SoundDevice.SPEAKER) {
- audioManager.setCurrentSoundDevice(CallAudioManager.SoundDevice.HEADSET)
- } else if (!event.plugged) {
- // if it's unplugged ? always route to speaker?
- // this is questionable?
- audioManager.setCurrentSoundDevice(CallAudioManager.SoundDevice.SPEAKER)
- }
- currentCallsListeners.forEach {
- it.onAudioDevicesChange(this)
- }
+ // sometimes we received un-wanted unplugged...
+ audioManager.wiredStateChange(event)
+ }
+
+ fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
+ Timber.v("## VOIP onWirelessDeviceEvent $event")
+ audioManager.bluetoothStateChange(event.plugged)
}
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 7d67e30a70..a94f6f80d3 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -218,6 +218,7 @@
Phone
Speaker
Headset
+ Wireless Headset
Send files