mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-15 02:38:59 +03:00
Merge pull request #2727 from vector-im/feature/fga/voip_fix_audio
Feature/fga/voip fix audio
This commit is contained in:
commit
73f9ef4232
23 changed files with 806 additions and 473 deletions
|
@ -126,7 +126,16 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
|||
private fun handleCallRejectEvent(event: Event) {
|
||||
val content = event.getClearContent().toModel<CallRejectContent>() ?: return
|
||||
val call = content.getCall() ?: return
|
||||
if (call.ourPartyId == content.partyId) {
|
||||
// Ignore remote echo
|
||||
return
|
||||
}
|
||||
activeCallHandler.removeCall(content.callId)
|
||||
if (event.senderId == userId) {
|
||||
// discard current call, it's rejected by another of my session
|
||||
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
|
||||
return
|
||||
}
|
||||
// No need to check party_id for reject because if we'd received either
|
||||
// an answer or reject, we wouldn't be in state InviteSent
|
||||
if (call.state != CallState.Dialing) {
|
||||
|
@ -177,6 +186,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
|||
}
|
||||
if (event.senderId == userId) {
|
||||
// discard current call, it's answered by another of my session
|
||||
activeCallHandler.removeCall(call.callId)
|
||||
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
|
||||
} else {
|
||||
if (call.opponentPartyId != null) {
|
||||
|
|
|
@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
|
|||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===87
|
||||
enum class===88
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
|
|
@ -322,6 +322,7 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation "androidx.sharetarget:sharetarget:1.0.0"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation "androidx.media:media:1.2.1"
|
||||
|
||||
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
|
||||
|
|
|
@ -16,33 +16,76 @@
|
|||
|
||||
package im.vector.app.core.services
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.content.Context
|
||||
import android.media.Ringtone
|
||||
import android.media.RingtoneManager
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.media.Ringtone
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
|
||||
class CallRingPlayerIncoming(
|
||||
context: Context
|
||||
context: Context,
|
||||
private val notificationUtils: NotificationUtils
|
||||
) {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private var r: Ringtone? = null
|
||||
private var ringtone: Ringtone? = null
|
||||
private var vibrator: Vibrator? = null
|
||||
|
||||
fun start() {
|
||||
val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
||||
r = RingtoneManager.getRingtone(applicationContext, notification)
|
||||
Timber.v("## VOIP Starting ringing incomming")
|
||||
r?.play()
|
||||
private val VIBRATE_PATTERN = longArrayOf(0, 400, 600)
|
||||
|
||||
fun start(fromBg: Boolean) {
|
||||
val audioManager = applicationContext.getSystemService<AudioManager>()
|
||||
val incomingCallChannel = notificationUtils.getChannelForIncomingCall(fromBg)
|
||||
val ringerMode = audioManager?.ringerMode
|
||||
if (ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
||||
playRingtoneIfNeeded(incomingCallChannel)
|
||||
} else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
|
||||
vibrateIfNeeded(incomingCallChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playRingtoneIfNeeded(incomingCallChannel: NotificationChannel?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && incomingCallChannel?.sound != null) {
|
||||
Timber.v("Ringtone already configured by notification channel")
|
||||
return
|
||||
}
|
||||
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
||||
ringtone = RingtoneManager.getRingtone(applicationContext, ringtoneUri)
|
||||
Timber.v("Play ringtone for incoming call")
|
||||
ringtone?.play()
|
||||
}
|
||||
|
||||
private fun vibrateIfNeeded(incomingCallChannel: NotificationChannel?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && incomingCallChannel?.shouldVibrate().orFalse()) {
|
||||
Timber.v("## Vibration already configured by notification channel")
|
||||
return
|
||||
}
|
||||
vibrator = applicationContext.getSystemService()
|
||||
Timber.v("Vibrate for incoming call")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val vibrationEffect = VibrationEffect.createWaveform(VIBRATE_PATTERN, 0)
|
||||
vibrator?.vibrate(vibrationEffect)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator?.vibrate(VIBRATE_PATTERN, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
r?.stop()
|
||||
ringtone?.stop()
|
||||
ringtone = null
|
||||
vibrator?.cancel()
|
||||
vibrator = null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,12 +98,12 @@ class CallRingPlayerOutgoing(
|
|||
private var player: MediaPlayer? = null
|
||||
|
||||
fun start() {
|
||||
val audioManager = applicationContext.getSystemService<AudioManager>()!!
|
||||
val audioManager: AudioManager? = applicationContext.getSystemService()
|
||||
player?.release()
|
||||
player = createPlayer()
|
||||
|
||||
// Check if sound is enabled
|
||||
val ringerMode = audioManager.ringerMode
|
||||
val ringerMode = audioManager?.ringerMode
|
||||
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
||||
try {
|
||||
if (player?.isPlaying == false) {
|
||||
|
@ -89,14 +132,14 @@ class CallRingPlayerOutgoing(
|
|||
|
||||
mediaPlayer.setOnErrorListener(MediaPlayerErrorListener())
|
||||
mediaPlayer.isLooping = true
|
||||
if (Build.VERSION.SDK_INT <= 21) {
|
||||
@Suppress("DEPRECATION")
|
||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING)
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
|
||||
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.build())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING)
|
||||
}
|
||||
return mediaPlayer
|
||||
} catch (failure: Throwable) {
|
||||
|
|
|
@ -44,7 +44,7 @@ import timber.log.Timber
|
|||
/**
|
||||
* Foreground service to manage calls
|
||||
*/
|
||||
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
||||
class CallService : VectorService() {
|
||||
|
||||
private val connections = mutableMapOf<String, CallConnection>()
|
||||
private val knownCalls = mutableSetOf<String>()
|
||||
|
@ -58,9 +58,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
|
||||
private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = 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
|
||||
|
@ -82,20 +79,14 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
callManager = vectorComponent().webRtcCallManager()
|
||||
avatarRenderer = vectorComponent().avatarRenderer()
|
||||
alertManager = vectorComponent().alertManager()
|
||||
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
|
||||
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext, notificationUtils)
|
||||
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
|
||||
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
|
||||
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
callRingPlayerIncoming?.stop()
|
||||
callRingPlayerOutgoing?.stop()
|
||||
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
|
||||
wiredHeadsetStateReceiver = null
|
||||
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
|
||||
bluetoothHeadsetStateReceiver = null
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
}
|
||||
|
@ -107,21 +98,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
setCallback(mediaSessionButtonCallback)
|
||||
}
|
||||
}
|
||||
if (intent == null) {
|
||||
// Service started again by the system.
|
||||
// TODO What do we do here?
|
||||
return START_STICKY
|
||||
}
|
||||
mediaSession?.let {
|
||||
// This ensures that the correct callbacks to MediaSessionCompat.Callback
|
||||
// will be triggered based on the incoming KeyEvent.
|
||||
MediaButtonReceiver.handleIntent(it, intent)
|
||||
}
|
||||
|
||||
when (intent.action) {
|
||||
when (intent?.action) {
|
||||
ACTION_INCOMING_RINGING_CALL -> {
|
||||
mediaSession?.isActive = true
|
||||
callRingPlayerIncoming?.start()
|
||||
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||
callRingPlayerIncoming?.start(fromBg)
|
||||
displayIncomingCallNotification(intent)
|
||||
}
|
||||
ACTION_OUTGOING_RINGING_CALL -> {
|
||||
|
@ -145,15 +132,12 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
handleCallTerminated(intent)
|
||||
}
|
||||
else -> {
|
||||
// Should not happen
|
||||
callRingPlayerIncoming?.stop()
|
||||
callRingPlayerOutgoing?.stop()
|
||||
myStopSelf()
|
||||
handleUnexpectedState(null)
|
||||
}
|
||||
}
|
||||
|
||||
// We want the system to restore the service if killed
|
||||
return START_STICKY
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
|
@ -167,10 +151,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
private fun displayIncomingCallNotification(intent: Intent) {
|
||||
Timber.v("## VOIP displayIncomingCallNotification $intent")
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
if (knownCalls.contains(callId)) {
|
||||
Timber.v("Call already notified $callId$")
|
||||
return
|
||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||
handleUnexpectedState(callId)
|
||||
}
|
||||
val isVideoCall = call.mxCall.isVideoCall
|
||||
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||
|
@ -211,13 +193,14 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
|
||||
private fun handleCallTerminated(intent: Intent) {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
alertManager.cancelAlert(callId)
|
||||
if (!knownCalls.remove(callId)) {
|
||||
Timber.v("Call terminated for unknown call $callId$")
|
||||
handleUnexpectedState(callId)
|
||||
return
|
||||
}
|
||||
val notification = notificationUtils.buildCallEndedNotification()
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
alertManager.cancelAlert(callId)
|
||||
if (knownCalls.isEmpty()) {
|
||||
mediaSession?.isActive = false
|
||||
myStopSelf()
|
||||
|
@ -234,11 +217,9 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
}
|
||||
|
||||
private fun displayOutgoingRingingCallNotification(intent: Intent) {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
if (knownCalls.contains(callId)) {
|
||||
Timber.v("Call already notified $callId$")
|
||||
return
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||
handleUnexpectedState(callId)
|
||||
}
|
||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
|
||||
|
@ -260,10 +241,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
private fun displayCallInProgressNotification(intent: Intent) {
|
||||
Timber.v("## VOIP displayCallInProgressNotification")
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
if (!knownCalls.contains(callId)) {
|
||||
Timber.v("Call in progress for unknown call $callId$")
|
||||
return
|
||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||
handleUnexpectedState(callId)
|
||||
}
|
||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||
alertManager.cancelAlert(callId)
|
||||
|
@ -271,7 +250,27 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
mxCall = call.mxCall,
|
||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
||||
)
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
if (knownCalls.isEmpty()) {
|
||||
startForeground(callId.hashCode(), notification)
|
||||
} else {
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
}
|
||||
knownCalls.add(callId)
|
||||
}
|
||||
|
||||
private fun handleUnexpectedState(callId: String?) {
|
||||
Timber.v("Fallback to clear everything")
|
||||
callRingPlayerIncoming?.stop()
|
||||
callRingPlayerOutgoing?.stop()
|
||||
if (callId != null) {
|
||||
notificationManager.cancel(callId.hashCode())
|
||||
}
|
||||
val notification = notificationUtils.buildCallEndedNotification()
|
||||
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
||||
if (knownCalls.isEmpty()) {
|
||||
mediaSession?.isActive = false
|
||||
myStopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
fun addConnection(callConnection: CallConnection) {
|
||||
|
@ -283,7 +282,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 6480
|
||||
private const val DEFAULT_NOTIFICATION_ID = 6480
|
||||
|
||||
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
||||
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
||||
|
@ -346,14 +345,4 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
return this@CallService
|
||||
}
|
||||
}
|
||||
|
||||
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
Timber.v("## VOIP: onHeadsetEvent $event")
|
||||
callManager.onWiredDeviceEvent(event)
|
||||
}
|
||||
|
||||
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
||||
Timber.v("## VOIP: onBTHeadsetEvent $event")
|
||||
callManager.onWirelessDeviceEvent(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.content.Context
|
|||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
|
|
|
@ -1,318 +0,0 @@
|
|||
/*
|
||||
* 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.app.features.call
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.AudioManager
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class CallAudioManager(
|
||||
val applicationContext: Context,
|
||||
val configChange: (() -> Unit)?
|
||||
) {
|
||||
|
||||
enum class SoundDevice {
|
||||
PHONE,
|
||||
SPEAKER,
|
||||
HEADSET,
|
||||
WIRELESS_HEADSET
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||
|
||||
init {
|
||||
executor.execute {
|
||||
audioManager = applicationContext.getSystemService()
|
||||
}
|
||||
val bm = applicationContext.getSystemService<BluetoothManager>()
|
||||
val adapter = bm?.adapter
|
||||
Timber.d("## VOIP Bluetooth adapter $adapter")
|
||||
bluetoothAdapter = adapter
|
||||
adapter?.getProfileProxy(applicationContext, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
Timber.d("## VOIP onServiceDisconnected $profile")
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
connectedBlueToothHeadset = null
|
||||
configChange?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
|
||||
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
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.
|
||||
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
|
||||
// and whether that loss is transient, or whether the new focus holder will hold it for an
|
||||
// unknown amount of time.
|
||||
Timber.v("## VOIP: Audio focus change $focusChange")
|
||||
}
|
||||
|
||||
fun startForCall(mxCall: MxCall) {
|
||||
Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}")
|
||||
}
|
||||
|
||||
private fun setupAudioManager(mxCall: MxCall) {
|
||||
Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}")
|
||||
val audioManager = audioManager ?: return
|
||||
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
|
||||
savedIsMicrophoneMute = audioManager.isMicrophoneMute
|
||||
savedAudioMode = audioManager.mode
|
||||
|
||||
// Request audio playout focus (without ducking) and install listener for changes in focus.
|
||||
|
||||
// Remove the deprecation forces us to use 2 different method depending on API level
|
||||
@Suppress("DEPRECATION") val result = audioManager.requestAudioFocus(audioFocusChangeListener,
|
||||
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Timber.d("## VOIP Audio focus request granted for VOICE_CALL streams")
|
||||
} else {
|
||||
Timber.d("## VOIP Audio focus request failed")
|
||||
}
|
||||
|
||||
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
|
||||
// required to be in this mode when playout and/or recording starts for
|
||||
// best possible VoIP performance.
|
||||
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
|
||||
// Always disable microphone mute during a WebRTC call.
|
||||
setMicrophoneMute(false)
|
||||
|
||||
adjustCurrentSoundDevice(mxCall)
|
||||
}
|
||||
|
||||
private fun adjustCurrentSoundDevice(mxCall: MxCall) {
|
||||
val audioManager = audioManager ?: return
|
||||
executor.execute {
|
||||
if (mxCall.state == CallState.LocalRinging && !isHeadsetOn()) {
|
||||
// Always use speaker if incoming call is in ringing state and a headset is not connected
|
||||
Timber.v("##VOIP: AudioManager default to SPEAKER (it is ringing)")
|
||||
setCurrentSoundDevice(SoundDevice.SPEAKER)
|
||||
} else if (mxCall.isVideoCall && !isHeadsetOn()) {
|
||||
// If there are no headset, start video output in speaker
|
||||
// (you can't watch the video and have the phone close to your ear)
|
||||
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 (isBluetoothHeadsetConnected(audioManager)) {
|
||||
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 onCallConnected(mxCall: MxCall) {
|
||||
Timber.v("##VOIP: AudioManager call answered, adjusting current sound device")
|
||||
setupAudioManager(mxCall)
|
||||
}
|
||||
|
||||
fun getAvailableSoundDevices(): List<SoundDevice> {
|
||||
return ArrayList<SoundDevice>().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
|
||||
|
||||
connectedBlueToothHeadset?.let {
|
||||
if (audioManager != null && isBluetoothHeadsetConnected(audioManager!!)) {
|
||||
audioManager?.stopBluetoothSco()
|
||||
audioManager?.isBluetoothScoOn = false
|
||||
audioManager?.isSpeakerphoneOn = false
|
||||
}
|
||||
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, it)
|
||||
}
|
||||
|
||||
audioManager?.mode = AudioManager.MODE_NORMAL
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
audioManager?.abandonAudioFocus(audioFocusChangeListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentSoundDevice(): SoundDevice {
|
||||
val audioManager = audioManager ?: return SoundDevice.PHONE
|
||||
if (audioManager.isSpeakerphoneOn) {
|
||||
return SoundDevice.SPEAKER
|
||||
} else {
|
||||
if (isBluetoothHeadsetConnected(audioManager)) return SoundDevice.WIRELESS_HEADSET
|
||||
return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBluetoothHeadsetConnected(audioManager: AudioManager) =
|
||||
isBluetoothHeadsetOn()
|
||||
&& !connectedBlueToothHeadset?.connectedDevices.isNullOrEmpty()
|
||||
&& (wantsBluetoothConnection || audioManager.isBluetoothScoOn)
|
||||
|
||||
fun setCurrentSoundDevice(device: SoundDevice) {
|
||||
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() || (audioManager?.let { isBluetoothHeadsetConnected(it) } ?: false)
|
||||
}
|
||||
|
||||
private fun isWiredHeadsetOn(): Boolean {
|
||||
@Suppress("DEPRECATION")
|
||||
return audioManager?.isWiredHeadsetOn ?: false
|
||||
}
|
||||
|
||||
private fun isBluetoothHeadsetOn(): Boolean {
|
||||
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
|
||||
try {
|
||||
if (connectedBlueToothHeadset == null) return false.also {
|
||||
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
|
||||
}
|
||||
if (audioManager?.isBluetoothScoAvailableOffCall == false) return false.also {
|
||||
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
|
||||
}
|
||||
return true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the speaker phone mode. */
|
||||
private fun setSpeakerphoneOn(on: Boolean) {
|
||||
Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on")
|
||||
val wasOn = audioManager?.isSpeakerphoneOn ?: false
|
||||
if (wasOn == on) {
|
||||
return
|
||||
}
|
||||
audioManager?.isSpeakerphoneOn = on
|
||||
}
|
||||
|
||||
/** Sets the microphone mute state. */
|
||||
private fun setMicrophoneMute(on: Boolean) {
|
||||
Timber.v("## VOIP: AudioManager setMicrophoneMute $on")
|
||||
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
||||
if (wasMuted == on) {
|
||||
return
|
||||
}
|
||||
audioManager?.isMicrophoneMute = on
|
||||
}
|
||||
|
||||
/** true if the device has a telephony radio with data
|
||||
* communication support. */
|
||||
private fun isThisPhone(): Boolean {
|
||||
return applicationContext.packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_TELEPHONY)
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ import com.airbnb.mvrx.activityViewModel
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetCallControlsBinding
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
|
||||
import me.gujun.android.span.span
|
||||
|
||||
|
@ -79,22 +80,22 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
}
|
||||
}
|
||||
|
||||
private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) {
|
||||
private fun showSoundDeviceChooser(available: Set<CallAudioManager.Device>, current: CallAudioManager.Device) {
|
||||
val soundDevices = available.map {
|
||||
when (it) {
|
||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
|
||||
CallAudioManager.Device.WIRELESS_HEADSET -> span {
|
||||
text = getString(R.string.sound_device_wireless_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.PHONE -> span {
|
||||
CallAudioManager.Device.PHONE -> span {
|
||||
text = getString(R.string.sound_device_phone)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.SPEAKER -> span {
|
||||
CallAudioManager.Device.SPEAKER -> span {
|
||||
text = getString(R.string.sound_device_speaker)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.SoundDevice.HEADSET -> span {
|
||||
CallAudioManager.Device.HEADSET -> span {
|
||||
text = getString(R.string.sound_device_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
|
@ -106,16 +107,16 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
when (soundDevices[n].toString()) {
|
||||
// TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations.
|
||||
getString(R.string.sound_device_phone) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE))
|
||||
}
|
||||
getString(R.string.sound_device_speaker) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER))
|
||||
}
|
||||
getString(R.string.sound_device_headset) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET))
|
||||
}
|
||||
getString(R.string.sound_device_wireless_headset) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.WIRELESS_HEADSET))
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,11 +126,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
|
||||
private fun renderState(state: VectorCallViewState) {
|
||||
views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
|
||||
views.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)
|
||||
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
||||
views.callControlsSoundDevice.subTitle = when (state.device) {
|
||||
CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone)
|
||||
CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker)
|
||||
CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset)
|
||||
CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
||||
}
|
||||
|
||||
views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.call
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
|
||||
sealed class VectorCallViewActions : VectorViewModelAction {
|
||||
object EndCall : VectorCallViewActions()
|
||||
|
@ -25,7 +26,7 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
|||
object ToggleMute : VectorCallViewActions()
|
||||
object ToggleVideo : VectorCallViewActions()
|
||||
object ToggleHoldResume: VectorCallViewActions()
|
||||
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
|
||||
data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions()
|
||||
object SwitchSoundDevice : VectorCallViewActions()
|
||||
object HeadSetButtonPressed : VectorCallViewActions()
|
||||
object ToggleCamera : VectorCallViewActions()
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.call
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
|
||||
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||
|
@ -24,8 +25,8 @@ sealed class VectorCallViewEvents : VectorViewEvents {
|
|||
object DismissNoCall : VectorCallViewEvents()
|
||||
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
|
||||
data class ShowSoundDeviceChooser(
|
||||
val available: List<CallAudioManager.SoundDevice>,
|
||||
val current: CallAudioManager.SoundDevice
|
||||
val available: Set<CallAudioManager.Device>,
|
||||
val current: CallAudioManager.Device
|
||||
) : VectorCallViewEvents()
|
||||
object ShowCallTransferScreen: VectorCallViewEvents()
|
||||
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.squareup.inject.assisted.Assisted
|
|||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
|
@ -133,17 +134,16 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onAudioDevicesChange() {
|
||||
val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice()
|
||||
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
|
||||
val currentSoundDevice = callManager.audioManager.selectedDevice ?: return
|
||||
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
|
||||
proximityManager.start()
|
||||
} else {
|
||||
proximityManager.stop()
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
|
||||
soundDevice = currentSoundDevice
|
||||
availableDevices = callManager.audioManager.availableDevices,
|
||||
device = currentSoundDevice
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -174,8 +174,8 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
callManager.addCurrentCallListener(currentCallListener)
|
||||
val item: MatrixItem? = session.getUser(webRtcCall.mxCall.opponentUserId)?.toMatrixItem()
|
||||
webRtcCall.addListener(callListener)
|
||||
val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice()
|
||||
if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) {
|
||||
val currentSoundDevice = callManager.audioManager.selectedDevice
|
||||
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
|
||||
proximityManager.start()
|
||||
}
|
||||
setState {
|
||||
|
@ -183,10 +183,10 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
isVideoCall = webRtcCall.mxCall.isVideoCall,
|
||||
callState = Success(webRtcCall.mxCall.state),
|
||||
callInfo = VectorCallViewState.CallInfo(callId, item),
|
||||
soundDevice = currentSoundDevice,
|
||||
device = currentSoundDevice ?: CallAudioManager.Device.PHONE,
|
||||
isLocalOnHold = webRtcCall.isLocalOnHold,
|
||||
isRemoteOnHold = webRtcCall.remoteOnHold,
|
||||
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
|
||||
availableDevices = callManager.audioManager.availableDevices,
|
||||
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
|
||||
canSwitchCamera = webRtcCall.canSwitchCamera(),
|
||||
formattedDuration = webRtcCall.formattedDuration(),
|
||||
|
@ -242,16 +242,11 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
call?.updateRemoteOnHold(!isRemoteOnHold)
|
||||
}
|
||||
is VectorCallViewActions.ChangeAudioDevice -> {
|
||||
callManager.callAudioManager.setCurrentSoundDevice(action.device)
|
||||
setState {
|
||||
copy(
|
||||
soundDevice = callManager.callAudioManager.getCurrentSoundDevice()
|
||||
)
|
||||
}
|
||||
callManager.audioManager.setAudioDevice(action.device)
|
||||
}
|
||||
VectorCallViewActions.SwitchSoundDevice -> {
|
||||
_viewEvents.post(
|
||||
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice)
|
||||
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device)
|
||||
)
|
||||
}
|
||||
VectorCallViewActions.HeadSetButtonPressed -> {
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.call
|
|||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
|
@ -34,8 +35,8 @@ data class VectorCallViewState(
|
|||
val isHD: Boolean = false,
|
||||
val isFrontCamera: Boolean = true,
|
||||
val canSwitchCamera: Boolean = true,
|
||||
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
||||
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
||||
val device: CallAudioManager.Device = CallAudioManager.Device.PHONE,
|
||||
val availableDevices: Set<CallAudioManager.Device> = emptySet(),
|
||||
val callState: Async<CallState> = Uninitialized,
|
||||
val otherKnownCallInfo: CallInfo? = null,
|
||||
val callInfo: CallInfo = CallInfo(callId),
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.
|
||||
*/
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package im.vector.app.features.call.audio
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.core.services.BluetoothHeadsetReceiver
|
||||
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
||||
import timber.log.Timber
|
||||
import java.util.HashSet
|
||||
|
||||
internal class API21AudioDeviceDetector(private val context: Context,
|
||||
private val audioManager: AudioManager,
|
||||
private val callAudioManager: CallAudioManager
|
||||
) : CallAudioManager.AudioDeviceDetector, WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
||||
|
||||
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||
private var connectedBlueToothHeadset: BluetoothProfile? = null
|
||||
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
|
||||
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
|
||||
|
||||
private val onAudioDeviceChangeRunner = Runnable {
|
||||
val devices = getAvailableSoundDevices()
|
||||
callAudioManager.replaceDevices(devices)
|
||||
Timber.i(" Available audio devices: $devices")
|
||||
callAudioManager.updateAudioRoute()
|
||||
}
|
||||
|
||||
private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> {
|
||||
return HashSet<CallAudioManager.Device>().apply {
|
||||
if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET)
|
||||
if (isWiredHeadsetOn()) {
|
||||
add(CallAudioManager.Device.HEADSET)
|
||||
} else {
|
||||
add(CallAudioManager.Device.PHONE)
|
||||
}
|
||||
add(CallAudioManager.Device.SPEAKER)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isWiredHeadsetOn(): Boolean {
|
||||
return audioManager.isWiredHeadsetOn
|
||||
}
|
||||
|
||||
private fun isBluetoothHeadsetOn(): Boolean {
|
||||
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
|
||||
try {
|
||||
if (connectedBlueToothHeadset == null) return false.also {
|
||||
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
|
||||
}
|
||||
if (!audioManager.isBluetoothScoAvailableOffCall) return false.also {
|
||||
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
|
||||
}
|
||||
return true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when devices change. It
|
||||
* makes sure the operation is performed on the audio thread.
|
||||
*/
|
||||
private fun onAudioDeviceChange() {
|
||||
callAudioManager.runInAudioThread(onAudioDeviceChangeRunner)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
Timber.i("Start using $this as the audio device handler")
|
||||
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(context, this)
|
||||
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this)
|
||||
val bm: BluetoothManager? = context.getSystemService()
|
||||
val adapter = bm?.adapter
|
||||
Timber.d("## VOIP Bluetooth adapter $adapter")
|
||||
bluetoothAdapter = adapter
|
||||
adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceDisconnected(profile: Int) {
|
||||
Timber.d("## VOIP onServiceDisconnected $profile")
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
connectedBlueToothHeadset = null
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
|
||||
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
|
||||
if (profile == BluetoothProfile.HEADSET) {
|
||||
connectedBlueToothHeadset = proxy
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
}
|
||||
}, BluetoothProfile.HEADSET)
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
Timber.i("Stop using $this as the audio device handler")
|
||||
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(context, it) }
|
||||
wiredHeadsetStateReceiver = null
|
||||
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(context, it) }
|
||||
bluetoothHeadsetStateReceiver = null
|
||||
}
|
||||
|
||||
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
Timber.v("onHeadsetEvent $event")
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
|
||||
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
||||
Timber.v("onBTHeadsetEvent $event")
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.app.features.call.audio
|
||||
|
||||
import android.media.AudioDeviceCallback
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import timber.log.Timber
|
||||
import java.util.HashSet
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
internal class API23AudioDeviceDetector(private val audioManager: AudioManager,
|
||||
private val callAudioManager: CallAudioManager
|
||||
) : CallAudioManager.AudioDeviceDetector {
|
||||
|
||||
private val onAudioDeviceChangeRunner = Runnable {
|
||||
val devices: MutableSet<CallAudioManager.Device> = HashSet()
|
||||
val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||
for (info in deviceInfos) {
|
||||
when (info.type) {
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET)
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE)
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER)
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET)
|
||||
}
|
||||
}
|
||||
callAudioManager.replaceDevices(devices)
|
||||
Timber.i(" Available audio devices: $devices")
|
||||
callAudioManager.updateAudioRoute()
|
||||
}
|
||||
private val audioDeviceCallback: AudioDeviceCallback = object : AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(
|
||||
addedDevices: Array<AudioDeviceInfo>) {
|
||||
Timber.d(" Audio devices added")
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(
|
||||
removedDevices: Array<AudioDeviceInfo>) {
|
||||
Timber.d(" Audio devices removed")
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to trigger an audio route update when devices change. It
|
||||
* makes sure the operation is performed on the audio thread.
|
||||
*/
|
||||
private fun onAudioDeviceChange() {
|
||||
callAudioManager.runInAudioThread(onAudioDeviceChangeRunner)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
Timber.i("Using $this as the audio device handler")
|
||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
|
||||
onAudioDeviceChange()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Constant defining a USB headset. Only available on API level >= 26.
|
||||
* The value of: AudioDeviceInfo.TYPE_USB_HEADSET
|
||||
*/
|
||||
private const val TYPE_USB_HEADSET = 22
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.app.features.call.audio
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
import java.util.HashSet
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class CallAudioManager(private val context: Context, val configChange: (() -> Unit)?) {
|
||||
|
||||
private val audioManager: AudioManager? = context.getSystemService()
|
||||
private var audioDeviceDetector: AudioDeviceDetector? = null
|
||||
private var audioDeviceRouter: AudioDeviceRouter? = null
|
||||
|
||||
enum class Device {
|
||||
PHONE,
|
||||
SPEAKER,
|
||||
HEADSET,
|
||||
WIRELESS_HEADSET
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
DEFAULT,
|
||||
AUDIO_CALL,
|
||||
VIDEO_CALL
|
||||
}
|
||||
|
||||
private var mode = Mode.DEFAULT
|
||||
private var _availableDevices: MutableSet<Device> = HashSet()
|
||||
val availableDevices: Set<Device>
|
||||
get() = _availableDevices
|
||||
|
||||
var selectedDevice: Device? = null
|
||||
private set
|
||||
private var userSelectedDevice: Device? = null
|
||||
|
||||
init {
|
||||
runInAudioThread { setup() }
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
if (audioManager == null) {
|
||||
return
|
||||
}
|
||||
audioDeviceDetector?.stop()
|
||||
audioDeviceDetector = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
API23AudioDeviceDetector(audioManager, this)
|
||||
} else {
|
||||
API21AudioDeviceDetector(context, audioManager, this)
|
||||
}
|
||||
audioDeviceDetector?.start()
|
||||
audioDeviceRouter = DefaultAudioDeviceRouter(audioManager, this)
|
||||
}
|
||||
|
||||
fun runInAudioThread(runnable: Runnable) {
|
||||
executor.execute(runnable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user selected audio device as the active audio device.
|
||||
*
|
||||
* @param device the desired device which will become active.
|
||||
*/
|
||||
fun setAudioDevice(device: Device) {
|
||||
runInAudioThread(Runnable {
|
||||
if (!_availableDevices.contains(device)) {
|
||||
Timber.w(" Audio device not available: $device")
|
||||
userSelectedDevice = null
|
||||
return@Runnable
|
||||
}
|
||||
if (mode != Mode.DEFAULT) {
|
||||
Timber.i(" User selected device set to: $device")
|
||||
userSelectedDevice = device
|
||||
updateAudioRoute(mode, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to set the current audio mode.
|
||||
*
|
||||
* @param mode the desired audio mode.
|
||||
* could be updated successfully, and it will be rejected otherwise.
|
||||
*/
|
||||
fun setMode(mode: Mode) {
|
||||
runInAudioThread {
|
||||
var success: Boolean
|
||||
try {
|
||||
success = updateAudioRoute(mode, false)
|
||||
} catch (e: Throwable) {
|
||||
success = false
|
||||
Timber.e(e, " Failed to update audio route for mode: " + mode)
|
||||
}
|
||||
if (success) {
|
||||
this@CallAudioManager.mode = mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the audio route for the given mode.
|
||||
*
|
||||
* @param mode the audio mode to be used when computing the audio route.
|
||||
* @return `true` if the audio route was updated successfully;
|
||||
* `false`, otherwise.
|
||||
*/
|
||||
private fun updateAudioRoute(mode: Mode, force: Boolean): Boolean {
|
||||
Timber.i(" Update audio route for mode: " + mode)
|
||||
if (!audioDeviceRouter?.setMode(mode).orFalse()) {
|
||||
return false
|
||||
}
|
||||
if (mode == Mode.DEFAULT) {
|
||||
selectedDevice = null
|
||||
userSelectedDevice = null
|
||||
return true
|
||||
}
|
||||
val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET)
|
||||
val headsetAvailable = _availableDevices.contains(Device.HEADSET)
|
||||
|
||||
// Pick the desired device based on what's available and the mode.
|
||||
var audioDevice: Device
|
||||
audioDevice = if (bluetoothAvailable) {
|
||||
Device.WIRELESS_HEADSET
|
||||
} else if (headsetAvailable) {
|
||||
Device.HEADSET
|
||||
} else if (mode == Mode.VIDEO_CALL) {
|
||||
Device.SPEAKER
|
||||
} else {
|
||||
Device.PHONE
|
||||
}
|
||||
// Consider the user's selection
|
||||
if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) {
|
||||
audioDevice = userSelectedDevice!!
|
||||
}
|
||||
|
||||
// If the previously selected device and the current default one
|
||||
// match, do nothing.
|
||||
if (!force && selectedDevice != null && selectedDevice == audioDevice) {
|
||||
return true
|
||||
}
|
||||
selectedDevice = audioDevice
|
||||
Timber.i(" Selected audio device: " + audioDevice)
|
||||
audioDeviceRouter?.setAudioRoute(audioDevice)
|
||||
configChange?.invoke()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the current device selection.
|
||||
*/
|
||||
fun resetSelectedDevice() {
|
||||
selectedDevice = null
|
||||
userSelectedDevice = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new device to the list of available devices.
|
||||
*
|
||||
* @param device The new device.
|
||||
*/
|
||||
fun addDevice(device: Device) {
|
||||
_availableDevices.add(device)
|
||||
resetSelectedDevice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a device from the list of available devices.
|
||||
*
|
||||
* @param device The old device to the removed.
|
||||
*/
|
||||
fun removeDevice(device: Device) {
|
||||
_availableDevices.remove(device)
|
||||
resetSelectedDevice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current list of available devices with a new one.
|
||||
*
|
||||
* @param devices The new devices list.
|
||||
*/
|
||||
fun replaceDevices(devices: Set<Device>) {
|
||||
_availableDevices.clear()
|
||||
_availableDevices.addAll(devices)
|
||||
resetSelectedDevice()
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sets the current audio route. Needed when devices changes have happened.
|
||||
*/
|
||||
fun updateAudioRoute() {
|
||||
if (mode != Mode.DEFAULT) {
|
||||
updateAudioRoute(mode, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sets the current audio route. Needed when focus is lost and regained.
|
||||
*/
|
||||
fun resetAudioRoute() {
|
||||
if (mode != Mode.DEFAULT) {
|
||||
updateAudioRoute(mode, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the modules implementing the actual audio device management.
|
||||
*/
|
||||
interface AudioDeviceDetector {
|
||||
/**
|
||||
* Start detecting audio device changes.
|
||||
*/
|
||||
fun start()
|
||||
|
||||
/**
|
||||
* Stop audio device detection.
|
||||
*/
|
||||
fun stop()
|
||||
}
|
||||
|
||||
interface AudioDeviceRouter {
|
||||
/**
|
||||
* Set the appropriate route for the given audio device.
|
||||
*
|
||||
* @param device Audio device for which the route must be set.
|
||||
*/
|
||||
fun setAudioRoute(device: Device)
|
||||
|
||||
/**
|
||||
* Set the given audio mode.
|
||||
*
|
||||
* @param mode The new audio mode to be used.
|
||||
* @return Whether the operation was successful or not.
|
||||
*/
|
||||
fun setMode(mode: Mode): Boolean
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Every audio operations should be launched on single thread
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.app.features.call.audio
|
||||
|
||||
import android.media.AudioManager
|
||||
import androidx.media.AudioAttributesCompat
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import androidx.media.AudioManagerCompat
|
||||
import timber.log.Timber
|
||||
|
||||
class DefaultAudioDeviceRouter(private val audioManager: AudioManager,
|
||||
private val callAudioManager: CallAudioManager
|
||||
) : CallAudioManager.AudioDeviceRouter, AudioManager.OnAudioFocusChangeListener {
|
||||
|
||||
private var audioFocusLost = false
|
||||
|
||||
private var focusRequestCompat: AudioFocusRequestCompat? = null
|
||||
|
||||
override fun setAudioRoute(device: CallAudioManager.Device) {
|
||||
audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER
|
||||
setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET)
|
||||
}
|
||||
|
||||
override fun setMode(mode: CallAudioManager.Mode): Boolean {
|
||||
if (mode === CallAudioManager.Mode.DEFAULT) {
|
||||
audioFocusLost = false
|
||||
audioManager.mode = AudioManager.MODE_NORMAL
|
||||
focusRequestCompat?.also {
|
||||
AudioManagerCompat.abandonAudioFocusRequest(audioManager, it)
|
||||
}
|
||||
focusRequestCompat = null
|
||||
audioManager.isSpeakerphoneOn = false
|
||||
setBluetoothAudioRoute(false)
|
||||
return true
|
||||
}
|
||||
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
audioManager.isMicrophoneMute = false
|
||||
|
||||
val audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(
|
||||
AudioAttributesCompat.Builder()
|
||||
.setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
)
|
||||
.setOnAudioFocusChangeListener(this)
|
||||
.build()
|
||||
.also {
|
||||
focusRequestCompat = it
|
||||
}
|
||||
|
||||
val gotFocus = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
|
||||
if (gotFocus == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
Timber.w(" Audio focus request failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set the output route to a Bluetooth device.
|
||||
*
|
||||
* @param enabled true if Bluetooth should use used, false otherwise.
|
||||
*/
|
||||
private fun setBluetoothAudioRoute(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
audioManager.startBluetoothSco()
|
||||
audioManager.isBluetoothScoOn = true
|
||||
} else {
|
||||
audioManager.isBluetoothScoOn = false
|
||||
audioManager.stopBluetoothSco()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [AudioManager.OnAudioFocusChangeListener] interface method. Called
|
||||
* when the audio focus of the system is updated.
|
||||
*
|
||||
* @param focusChange - The type of focus change.
|
||||
*/
|
||||
override fun onAudioFocusChange(focusChange: Int) {
|
||||
callAudioManager.runInAudioThread {
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
Timber.d(" Audio focus gained")
|
||||
if (audioFocusLost) {
|
||||
callAudioManager.resetAudioRoute()
|
||||
}
|
||||
audioFocusLost = false
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
Timber.d(" Audio focus lost")
|
||||
audioFocusLost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.app.features.call.telecom;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.jitsi.meet.sdk.ConnectionService;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public class CallConnectionService extends ConnectionService {
|
||||
}
|
|
@ -71,7 +71,7 @@ import im.vector.app.core.services.CallService
|
|||
|
||||
bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0)
|
||||
connection.setInitializing()
|
||||
return CallConnection(applicationContext, roomId, callId)
|
||||
return connection
|
||||
}
|
||||
|
||||
inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection {
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import org.webrtc.DataChannel
|
||||
|
@ -26,8 +25,7 @@ import org.webrtc.PeerConnection
|
|||
import org.webrtc.RtpReceiver
|
||||
import timber.log.Timber
|
||||
|
||||
class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
|
||||
private val callAudioManager: CallAudioManager) : PeerConnection.Observer {
|
||||
class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnection.Observer {
|
||||
|
||||
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
|
||||
Timber.v("## VOIP StreamObserver onConnectionChange: $newState")
|
||||
|
@ -38,7 +36,6 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall,
|
|||
*/
|
||||
PeerConnection.PeerConnectionState.CONNECTED -> {
|
||||
webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTED)
|
||||
callAudioManager.onCallConnected(webRtcCall.mxCall)
|
||||
}
|
||||
/**
|
||||
* One or more of the ICE transports on the connection is in the "failed" state.
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.hardware.camera2.CameraManager
|
|||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.core.services.CallService
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import im.vector.app.features.call.CameraEventsHandlerAdapter
|
||||
import im.vector.app.features.call.CameraProxy
|
||||
import im.vector.app.features.call.CameraType
|
||||
|
@ -86,14 +85,13 @@ private const val VIDEO_TRACK_ID = "ARDAMSv0"
|
|||
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
|
||||
|
||||
class WebRtcCall(val mxCall: MxCall,
|
||||
private val callAudioManager: CallAudioManager,
|
||||
private val rootEglBase: EglBase?,
|
||||
private val context: Context,
|
||||
private val dispatcher: CoroutineContext,
|
||||
private val sessionProvider: Provider<Session?>,
|
||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
||||
private val onCallEnded: (WebRtcCall) -> Unit) : MxCall.StateListener {
|
||||
private val onCallEnded: (String) -> Unit) : MxCall.StateListener {
|
||||
|
||||
interface Listener : MxCall.StateListener {
|
||||
fun onCaptureStateChanged() {}
|
||||
|
@ -256,7 +254,7 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
|
||||
}
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this, callAudioManager))
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this))
|
||||
}
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||
|
@ -317,6 +315,9 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
|
||||
private suspend fun setupOutgoingCall() = withContext(dispatcher) {
|
||||
tryOrNull {
|
||||
onCallBecomeActive(this@WebRtcCall)
|
||||
}
|
||||
val turnServer = getTurnServer()
|
||||
mxCall.state = CallState.CreateOffer
|
||||
// 1. Create RTCPeerConnection
|
||||
|
@ -723,7 +724,7 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
GlobalScope.launch(dispatcher) {
|
||||
release()
|
||||
}
|
||||
onCallEnded(this)
|
||||
onCallEnded(callId)
|
||||
if (originatedByMe) {
|
||||
if (wasRinging) {
|
||||
mxCall.reject()
|
||||
|
|
|
@ -21,11 +21,9 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.core.services.BluetoothHeadsetReceiver
|
||||
import im.vector.app.core.services.CallService
|
||||
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import im.vector.app.push.fcm.FcmHelper
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
|
@ -79,10 +77,12 @@ class WebRtcCallManager @Inject constructor(
|
|||
currentCallsListeners.remove(listener)
|
||||
}
|
||||
|
||||
val callAudioManager = CallAudioManager(context) {
|
||||
val audioManager = CallAudioManager(context) {
|
||||
currentCallsListeners.forEach {
|
||||
tryOrNull { it.onAudioDevicesChange() }
|
||||
}
|
||||
}.apply {
|
||||
setMode(CallAudioManager.Mode.DEFAULT)
|
||||
}
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
|
@ -180,25 +180,38 @@ class WebRtcCallManager @Inject constructor(
|
|||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}")
|
||||
val currentCall = getCurrentCall().takeIf { it != call }
|
||||
currentCall?.updateRemoteOnHold(onHold = true)
|
||||
audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL)
|
||||
this.currentCall.setAndNotify(call)
|
||||
}
|
||||
|
||||
private fun onCallEnded(call: WebRtcCall) {
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}")
|
||||
CallService.onCallTerminated(context, call.callId)
|
||||
callAudioManager.stop()
|
||||
callsByCallId.remove(call.mxCall.callId)
|
||||
callsByRoomId[call.mxCall.roomId]?.remove(call)
|
||||
if (getCurrentCall() == call) {
|
||||
private fun onCallEnded(callId: String) {
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: $callId")
|
||||
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
|
||||
Timber.v("On call ended for unknown call $callId")
|
||||
}
|
||||
CallService.onCallTerminated(context, callId)
|
||||
callsByRoomId[webRtcCall.roomId]?.remove(webRtcCall)
|
||||
if (getCurrentCall()?.callId == callId) {
|
||||
val otherCall = getCalls().lastOrNull()
|
||||
currentCall.setAndNotify(otherCall)
|
||||
}
|
||||
// This must be done in this thread
|
||||
executor.execute {
|
||||
// There is no active calls
|
||||
if (getCurrentCall() == null) {
|
||||
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
|
||||
peerConnectionFactory?.dispose()
|
||||
peerConnectionFactory = null
|
||||
audioManager.setMode(CallAudioManager.Mode.DEFAULT)
|
||||
// did we start background sync? so we should stop it
|
||||
if (isInBackground) {
|
||||
if (FcmHelper.isPushSupported()) {
|
||||
currentSession?.stopAnyBackgroundSync()
|
||||
} else {
|
||||
// for fdroid we should not stop, it should continue syncing
|
||||
// maybe we should restore default timeout/delay though?
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done")
|
||||
}
|
||||
|
@ -222,7 +235,6 @@ class WebRtcCallManager @Inject constructor(
|
|||
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||
val webRtcCall = createWebRtcCall(mxCall)
|
||||
currentCall.setAndNotify(webRtcCall)
|
||||
callAudioManager.startForCall(mxCall)
|
||||
|
||||
CallService.onOutgoingCallRinging(
|
||||
context = context.applicationContext,
|
||||
|
@ -244,7 +256,6 @@ class WebRtcCallManager @Inject constructor(
|
|||
private fun createWebRtcCall(mxCall: MxCall): WebRtcCall {
|
||||
val webRtcCall = WebRtcCall(
|
||||
mxCall = mxCall,
|
||||
callAudioManager = callAudioManager,
|
||||
rootEglBase = rootEglBase,
|
||||
context = context,
|
||||
dispatcher = dispatcher,
|
||||
|
@ -259,6 +270,9 @@ class WebRtcCallManager @Inject constructor(
|
|||
callsByCallId[mxCall.callId] = webRtcCall
|
||||
callsByRoomId.getOrPut(mxCall.roomId) { ArrayList(1) }
|
||||
.add(webRtcCall)
|
||||
if (getCurrentCall() == null) {
|
||||
currentCall.setAndNotify(webRtcCall)
|
||||
}
|
||||
return webRtcCall
|
||||
}
|
||||
|
||||
|
@ -266,18 +280,6 @@ class WebRtcCallManager @Inject constructor(
|
|||
callsByRoomId[roomId]?.forEach { it.endCall(originatedByMe) }
|
||||
}
|
||||
|
||||
fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
Timber.v("## VOIP onWiredDeviceEvent $event")
|
||||
getCurrentCall() ?: return
|
||||
// sometimes we received un-wanted unplugged...
|
||||
callAudioManager.wiredStateChange(event)
|
||||
}
|
||||
|
||||
fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
||||
Timber.v("## VOIP onWirelessDeviceEvent $event")
|
||||
callAudioManager.bluetoothStateChange(event.plugged)
|
||||
}
|
||||
|
||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
|
||||
if (getCallsByRoomId(mxCall.roomId).isNotEmpty()) {
|
||||
|
@ -292,7 +294,6 @@ class WebRtcCallManager @Inject constructor(
|
|||
createWebRtcCall(mxCall).apply {
|
||||
offerSdp = callInviteContent.offer
|
||||
}
|
||||
callAudioManager.startForCall(mxCall)
|
||||
// Start background service with notification
|
||||
CallService.onIncomingCallRinging(
|
||||
context = context,
|
||||
|
@ -365,21 +366,6 @@ class WebRtcCallManager @Inject constructor(
|
|||
|
||||
override fun onCallManagedByOtherSession(callId: String) {
|
||||
Timber.v("## VOIP onCallManagedByOtherSession: $callId")
|
||||
val webRtcCall = callsByCallId.remove(callId)
|
||||
if (webRtcCall != null) {
|
||||
callsByRoomId[webRtcCall.mxCall.roomId]?.remove(webRtcCall)
|
||||
}
|
||||
// TODO: handle this properly
|
||||
CallService.onCallTerminated(context, callId)
|
||||
|
||||
// did we start background sync? so we should stop it
|
||||
if (isInBackground) {
|
||||
if (FcmHelper.isPushSupported()) {
|
||||
currentSession?.stopAnyBackgroundSync()
|
||||
} else {
|
||||
// for fdroid we should not stop, it should continue syncing
|
||||
// maybe we should restore default timeout/delay though?
|
||||
}
|
||||
}
|
||||
onCallEnded(callId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,6 +208,10 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
})
|
||||
}
|
||||
|
||||
fun getChannel(channelId: String): NotificationChannel? {
|
||||
return notificationManager.getNotificationChannel(channelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a polling thread listener notification
|
||||
*
|
||||
|
@ -266,6 +270,11 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
return notification
|
||||
}
|
||||
|
||||
fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? {
|
||||
val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||
return getChannel(notificationChannel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an incoming call notification.
|
||||
* This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow.
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
android:textSize="16sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottomSheetActionSubTitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionIcon"
|
||||
app:layout_constraintStart_toEndOf="@+id/bottomSheetActionLeftIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
|
|
Loading…
Add table
Reference in a new issue