VoIP: fix a bunch of issues

This commit is contained in:
ganfra 2021-01-26 18:50:21 +01:00
parent 05361c13f1
commit 88e18a8640
13 changed files with 151 additions and 102 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -79,7 +79,7 @@ class CallService : VectorService() {
callManager = vectorComponent().webRtcCallManager()
avatarRenderer = vectorComponent().avatarRenderer()
alertManager = vectorComponent().alertManager()
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext, notificationUtils)
callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
}
@ -98,21 +98,17 @@ class CallService : VectorService() {
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 -> {
@ -136,15 +132,12 @@ class CallService : VectorService() {
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
}
// ================================================================================
@ -158,10 +151,8 @@ class CallService : VectorService() {
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)
@ -202,13 +193,14 @@ class CallService : VectorService() {
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()
@ -225,11 +217,9 @@ class CallService : VectorService() {
}
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")
@ -251,10 +241,8 @@ class CallService : VectorService() {
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)
@ -262,8 +250,28 @@ class CallService : VectorService() {
mxCall = call.mxCall,
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
)
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) {
connections[callConnection.callId] = callConnection
@ -274,7 +282,7 @@ class CallService : VectorService() {
}
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"

View File

@ -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

View File

@ -21,11 +21,7 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import im.vector.app.core.services.BluetoothHeadsetReceiver
import im.vector.app.core.services.WiredHeadsetStateReceiver
@ -61,7 +57,6 @@ internal class API21AudioDeviceDetector(private val context: Context,
}
}
private fun isWiredHeadsetOn(): Boolean {
return audioManager.isWiredHeadsetOn
}
@ -82,7 +77,6 @@ internal class API21AudioDeviceDetector(private val context: Context,
}
}
/**
* Helper method to trigger an audio route update when devices change. It
* makes sure the operation is performed on the audio thread.
@ -119,7 +113,6 @@ internal class API21AudioDeviceDetector(private val context: Context,
onAudioDeviceChange()
}
override fun stop() {
Timber.i("Stop using $this as the audio device handler")
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(context, it) }

View File

@ -30,7 +30,7 @@ internal class API23AudioDeviceDetector(private val audioManager: AudioManager,
private val onAudioDeviceChangeRunner = Runnable {
val devices: MutableSet<CallAudioManager.Device> = HashSet()
val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_ALL)
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)

View File

@ -18,6 +18,8 @@ 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
@ -25,7 +27,7 @@ import java.util.concurrent.Executors
class CallAudioManager(private val context: Context, val configChange: (() -> Unit)?) {
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private val audioManager: AudioManager? = context.getSystemService()
private var audioDeviceDetector: AudioDeviceDetector? = null
private var audioDeviceRouter: AudioDeviceRouter? = null
@ -56,8 +58,11 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
}
private fun setup() {
if (audioManager == null) {
return
}
audioDeviceDetector?.stop()
audioDeviceDetector = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
audioDeviceDetector = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
API23AudioDeviceDetector(audioManager, this)
} else {
API21AudioDeviceDetector(context, audioManager, this)

View File

@ -16,7 +16,6 @@
package im.vector.app.features.call.webrtc
import im.vector.app.features.call.audio.CallAudioManager
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.webrtc.DataChannel

View File

@ -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.audio.CallAudioManager
import im.vector.app.features.call.CameraEventsHandlerAdapter
import im.vector.app.features.call.CameraProxy
import im.vector.app.features.call.CameraType
@ -92,7 +91,7 @@ class WebRtcCall(val mxCall: MxCall,
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() {}
@ -725,7 +724,7 @@ class WebRtcCall(val mxCall: MxCall,
GlobalScope.launch(dispatcher) {
release()
}
onCallEnded(this)
onCallEnded(callId)
if (originatedByMe) {
if (wasRinging) {
mxCall.reject()

View File

@ -21,9 +21,7 @@ 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.VectorCallActivity
import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.utils.EglUtils
@ -186,22 +184,34 @@ class WebRtcCallManager @Inject constructor(
this.currentCall.setAndNotify(call)
}
private fun onCallEnded(call: WebRtcCall) {
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}")
CallService.onCallTerminated(context, call.callId)
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")
}
@ -225,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,
@ -261,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
}
@ -268,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()) {
@ -294,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,
@ -367,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)
}
}

View File

@ -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.

View File

@ -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"