Headset support
+ detect plug/unplugg
This commit is contained in:
parent
30d47b4fa6
commit
3e2d892fb5
@ -30,7 +30,7 @@ import timber.log.Timber
|
||||
/**
|
||||
* Foreground service to manage calls
|
||||
*/
|
||||
class CallService : VectorService() {
|
||||
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener {
|
||||
|
||||
private val connections = mutableMapOf<String, CallConnection>()
|
||||
|
||||
@ -49,11 +49,21 @@ class CallService : VectorService() {
|
||||
|
||||
private var callRingPlayer: CallRingPlayer? = null
|
||||
|
||||
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationUtils = vectorComponent().notificationUtils()
|
||||
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
|
||||
callRingPlayer = CallRingPlayer(applicationContext)
|
||||
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
callRingPlayer?.stop()
|
||||
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
|
||||
wiredHeadsetStateReceiver = null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@ -217,11 +227,6 @@ class CallService : VectorService() {
|
||||
myStopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
callRingPlayer?.stop()
|
||||
}
|
||||
|
||||
fun addConnection(callConnection: CallConnection) {
|
||||
connections[callConnection.callId] = callConnection
|
||||
}
|
||||
@ -335,4 +340,9 @@ class CallService : VectorService() {
|
||||
return this@CallService
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
Timber.v("## VOIP: onHeadsetEvent $event")
|
||||
webRtcPeerConnectionManager.onWireDeviceEvent(event)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Dynamic broadcast receiver to detect headset plug/unplug
|
||||
*/
|
||||
class WiredHeadsetStateReceiver : BroadcastReceiver() {
|
||||
|
||||
interface HeadsetEventListener {
|
||||
fun onHeadsetEvent(event: HeadsetPlugEvent)
|
||||
}
|
||||
|
||||
var delegate: WeakReference<HeadsetEventListener>? = null
|
||||
|
||||
data class HeadsetPlugEvent(
|
||||
val plugged: Boolean,
|
||||
val headsetName: String?,
|
||||
val hasMicrophone: Boolean
|
||||
)
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
// The intent will have the following extra values:
|
||||
// state 0 for unplugged, 1 for plugged
|
||||
// name Headset type, human readable string
|
||||
// microphone 1 if headset has a microphone, 0 otherwise
|
||||
|
||||
val isPlugged = when (intent?.getIntExtra("state", -1)) {
|
||||
0 -> false
|
||||
1 -> true
|
||||
else -> return Unit.also {
|
||||
Timber.v("## VOIP WiredHeadsetStateReceiver invalid state")
|
||||
}
|
||||
}
|
||||
val hasMicrophone = when (intent.getIntExtra("microphone", -1)) {
|
||||
1 -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
delegate?.get()?.onHeadsetEvent(
|
||||
HeadsetPlugEvent(plugged = isPlugged, headsetName = intent.getStringExtra("name"), hasMicrophone = hasMicrophone)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createAndRegister(context: Context, listener: HeadsetEventListener): WiredHeadsetStateReceiver {
|
||||
val receiver = WiredHeadsetStateReceiver()
|
||||
receiver.delegate = WeakReference(listener)
|
||||
val action = if (Build.VERSION.SDK_INT >= 21) {
|
||||
AudioManager.ACTION_HEADSET_PLUG
|
||||
} else {
|
||||
Intent.ACTION_HEADSET_PLUG
|
||||
}
|
||||
context.registerReceiver(receiver, IntentFilter(action))
|
||||
return receiver
|
||||
}
|
||||
|
||||
fun unRegister(context: Context, receiver: WiredHeadsetStateReceiver) {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,8 @@ class CallAudioManager(
|
||||
|
||||
enum class SoundDevice {
|
||||
PHONE,
|
||||
SPEAKER
|
||||
SPEAKER,
|
||||
HEADSET
|
||||
}
|
||||
|
||||
private val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
@ -71,14 +72,24 @@ class CallAudioManager(
|
||||
// Always disable microphone mute during a WebRTC call.
|
||||
setMicrophoneMute(false)
|
||||
|
||||
// TODO check if there are headsets?
|
||||
if (mxCall.isVideoCall) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAvailableSoundDevices(): List<SoundDevice> {
|
||||
return listOf(
|
||||
if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE,
|
||||
SoundDevice.SPEAKER
|
||||
)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Timber.v("## VOIP: AudioManager stopCall")
|
||||
|
||||
@ -91,21 +102,27 @@ class CallAudioManager(
|
||||
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
||||
}
|
||||
|
||||
fun getCurrentSoundDevice() : SoundDevice {
|
||||
fun getCurrentSoundDevice(): SoundDevice {
|
||||
if (audioManager.isSpeakerphoneOn) {
|
||||
return SoundDevice.SPEAKER
|
||||
} else {
|
||||
return SoundDevice.PHONE
|
||||
return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentSoundDevice(device: SoundDevice) {
|
||||
fun setCurrentSoundDevice(device: SoundDevice) {
|
||||
when (device) {
|
||||
SoundDevice.HEADSET,
|
||||
SoundDevice.PHONE -> setSpeakerphoneOn(false)
|
||||
SoundDevice.SPEAKER -> setSpeakerphoneOn(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isHeadsetOn(): Boolean {
|
||||
@Suppress("DEPRECATION")
|
||||
return audioManager.isWiredHeadsetOn || audioManager.isBluetoothScoOn
|
||||
}
|
||||
|
||||
/** Sets the speaker phone mode. */
|
||||
private fun setSpeakerphoneOn(on: Boolean) {
|
||||
Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on")
|
||||
|
@ -23,6 +23,7 @@ import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_call_controls.*
|
||||
import me.gujun.android.span.span
|
||||
|
||||
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
override fun getLayoutResId() = R.layout.bottom_sheet_call_controls
|
||||
@ -37,25 +38,54 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
callControlsSoundDevice.clickableView.debouncedClicks {
|
||||
val soundDevices = listOf(
|
||||
getString(R.string.sound_device_phone),
|
||||
getString(R.string.sound_device_speaker)
|
||||
)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setItems(soundDevices.toTypedArray()) { d, n ->
|
||||
d.cancel()
|
||||
when (soundDevices[n]) {
|
||||
getString(R.string.sound_device_phone) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
|
||||
}
|
||||
getString(R.string.sound_device_speaker) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
|
||||
}
|
||||
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
||||
}
|
||||
|
||||
callViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||
showSoundDeviceChooser(it.available, it.current)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) {
|
||||
val soundDevices = available.map {
|
||||
when (it) {
|
||||
CallAudioManager.SoundDevice.PHONE -> span {
|
||||
text = getString(R.string.sound_device_phone)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.SPEAKER -> span {
|
||||
text = getString(R.string.sound_device_speaker)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.HEADSET -> span {
|
||||
text = getString(R.string.sound_device_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setItems(soundDevices.toTypedArray()) { d, n ->
|
||||
d.cancel()
|
||||
when (soundDevices[n].toString()) {
|
||||
getString(R.string.sound_device_phone) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
|
||||
}
|
||||
getString(R.string.sound_device_speaker) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
|
||||
}
|
||||
getString(R.string.sound_device_headset) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
// override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@ -73,6 +103,7 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
callControlsSoundDevice.subTitle = when (state.soundDevice) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -340,14 +340,14 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
|
||||
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
||||
Timber.v("## VOIP handleViewEvents $event")
|
||||
when (event) {
|
||||
VectorCallViewEvents.DismissNoCall -> {
|
||||
VectorCallViewEvents.DismissNoCall -> {
|
||||
CallService.onNoActiveCall(this)
|
||||
finish()
|
||||
}
|
||||
is VectorCallViewEvents.ConnectionTimout -> {
|
||||
is VectorCallViewEvents.ConnectionTimeout -> {
|
||||
onErrorTimoutConnect(event.turn)
|
||||
}
|
||||
null -> {
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ data class VectorCallViewState(
|
||||
val isVideoEnabled: Boolean = true,
|
||||
val isVideoCaptureInError: Boolean = false,
|
||||
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
||||
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
||||
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
||||
val callState: Async<CallState> = Uninitialized
|
||||
) : MvRxState
|
||||
@ -60,12 +61,17 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
||||
object ToggleMute : VectorCallViewActions()
|
||||
object ToggleVideo : VectorCallViewActions()
|
||||
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
|
||||
object SwitchSoundDevice : VectorCallViewActions()
|
||||
}
|
||||
|
||||
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||
|
||||
object DismissNoCall : VectorCallViewEvents()
|
||||
data class ConnectionTimout(val turn: TurnServerResponse?) : VectorCallViewEvents()
|
||||
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
|
||||
data class ShowSoundDeviceChooser(
|
||||
val available: List<CallAudioManager.SoundDevice>,
|
||||
val current: CallAudioManager.SoundDevice
|
||||
) : VectorCallViewEvents()
|
||||
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||
// object CallAccepted : VectorCallViewEvents()
|
||||
@ -96,11 +102,11 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
override fun run() {
|
||||
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimout(null))
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
|
||||
}
|
||||
|
||||
override fun onSuccess(data: TurnServerResponse) {
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimout(data))
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -124,6 +130,15 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
copy(isVideoCaptureInError = captureInError)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {
|
||||
setState {
|
||||
copy(
|
||||
availableSoundDevices = mgr.audioManager.getAvailableSoundDevices(),
|
||||
soundDevice = mgr.audioManager.getCurrentSoundDevice()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@ -143,7 +158,8 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
isVideoCall = mxCall.isVideoCall,
|
||||
callState = Success(mxCall.state),
|
||||
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
|
||||
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice()
|
||||
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice(),
|
||||
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices()
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
@ -163,7 +179,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun handle(action: VectorCallViewActions) = withState {
|
||||
override fun handle(action: VectorCallViewActions) = withState { state ->
|
||||
when (action) {
|
||||
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall()
|
||||
VectorCallViewActions.AcceptCall -> {
|
||||
@ -179,24 +195,21 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
webRtcPeerConnectionManager.endCall()
|
||||
}
|
||||
VectorCallViewActions.ToggleMute -> {
|
||||
withState {
|
||||
val muted = it.isAudioMuted
|
||||
webRtcPeerConnectionManager.muteCall(!muted)
|
||||
setState {
|
||||
copy(isAudioMuted = !muted)
|
||||
}
|
||||
val muted = state.isAudioMuted
|
||||
webRtcPeerConnectionManager.muteCall(!muted)
|
||||
setState {
|
||||
copy(isAudioMuted = !muted)
|
||||
}
|
||||
}
|
||||
VectorCallViewActions.ToggleVideo -> {
|
||||
withState {
|
||||
if (it.isVideoCall) {
|
||||
val videoEnabled = it.isVideoEnabled
|
||||
webRtcPeerConnectionManager.enableVideo(!videoEnabled)
|
||||
setState {
|
||||
copy(isVideoEnabled = !videoEnabled)
|
||||
}
|
||||
if (state.isVideoCall) {
|
||||
val videoEnabled = state.isVideoEnabled
|
||||
webRtcPeerConnectionManager.enableVideo(!videoEnabled)
|
||||
setState {
|
||||
copy(isVideoEnabled = !videoEnabled)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
is VectorCallViewActions.ChangeAudioDevice -> {
|
||||
webRtcPeerConnectionManager.audioManager.setCurrentSoundDevice(action.device)
|
||||
@ -206,6 +219,11 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
VectorCallViewActions.SwitchSoundDevice -> {
|
||||
_viewEvents.post(
|
||||
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice)
|
||||
)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ 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.CallService
|
||||
import im.vector.riotx.core.services.WiredHeadsetStateReceiver
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.reactivex.subjects.ReplaySubject
|
||||
@ -75,6 +76,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
interface CurrentCallListener {
|
||||
fun onCurrentCallChange(call: MxCall?)
|
||||
fun onCaptureStateChanged(captureInError: Boolean)
|
||||
fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {}
|
||||
}
|
||||
|
||||
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
|
||||
@ -657,6 +659,21 @@ class WebRtcPeerConnectionManager @Inject constructor(
|
||||
close()
|
||||
}
|
||||
|
||||
fun onWireDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||
val call = currentCall ?: return
|
||||
if (call.mxCall.callId != callAnswerContent.callId) return Unit.also {
|
||||
|
@ -217,6 +217,7 @@
|
||||
<string name="call_select_sound_device">Select Sound Device</string>
|
||||
<string name="sound_device_phone">Phone</string>
|
||||
<string name="sound_device_speaker">Speaker</string>
|
||||
<string name="sound_device_headset">Headset</string>
|
||||
|
||||
|
||||
<string name="option_send_files">Send files</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user