feat: add pre-offer information and action handling in web rtc call service

This commit is contained in:
Harris 2021-11-19 16:04:28 +11:00
parent 276f808ca3
commit 8e56f76fc1
7 changed files with 99 additions and 28 deletions

View File

@ -5,13 +5,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.BlendMode
import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable
import android.media.AudioManager
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
@ -26,7 +22,6 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
@ -36,15 +31,14 @@ import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.CallViewModel
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.*
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.*
import org.webrtc.IceCandidate
import java.util.*
@AndroidEntryPoint
class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
companion object {
const val ACTION_PRE_OFFER = "pre-offer"
const val ACTION_ANSWER = "answer"
const val ACTION_END = "end-call"
@ -54,6 +48,7 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
private val viewModel by viewModels<CallViewModel>()
private val glide by lazy { GlideApp.with(this) }
private var uiJob: Job? = null
private var wantsToAnswer = false
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
@ -88,8 +83,11 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
.execute()
if (intent.action == ACTION_ANSWER) {
val answerIntent = WebRtcCallService.acceptCallIntent(this)
ContextCompat.startForegroundService(this,answerIntent)
answerCall()
}
if (intent.action == ACTION_PRE_OFFER) {
wantsToAnswer = true
}
speakerPhoneButton.setOnClickListener {
@ -141,6 +139,11 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
}
private fun answerCall() {
val answerIntent = WebRtcCallService.acceptCallIntent(this)
ContextCompat.startForegroundService(this,answerIntent)
}
override fun onStart() {
super.onStart()
@ -160,6 +163,9 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
viewModel.callState.collect { state ->
when (state) {
CALL_RINGING -> {
if (wantsToAnswer) {
answerCall()
}
}
CALL_OUTGOING -> {
}

View File

@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
import android.os.ResultReceiver
import android.telephony.PhoneStateListener
@ -22,6 +21,7 @@ import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.util.CallNotificationBuilder
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_ESTABLISHED
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_CONNECTING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.webrtc.*
@ -58,6 +58,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT"
const val ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL"
const val ACTION_PRE_OFFER = "PRE_OFFER"
const val ACTION_RESPONSE_MESSAGE = "RESPONSE_MESSAGE"
const val ACTION_ICE_MESSAGE = "ICE_MESSAGE"
const val ACTION_CALL_CONNECTED = "CALL_CONNECTED"
@ -113,6 +114,12 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
.putExtra(EXTRA_CALL_ID, callId)
.putExtra(EXTRA_REMOTE_DESCRIPTION, sdp)
fun preOffer(context: Context, address: Address, callId: UUID) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_PRE_OFFER)
.putExtra(EXTRA_RECIPIENT_ADDRESS, address)
.putExtra(EXTRA_CALL_ID, callId)
fun iceCandidates(context: Context, address: Address, iceCandidates: List<IceCandidate>, callId: UUID) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_ICE_MESSAGE)
@ -176,7 +183,10 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
return callManager.callId == expectedCallId
}
private fun isBusy() = callManager.isBusy(this)
private fun isPreOffer() = callManager.isPreOffer()
private fun isBusy(intent: Intent) = callManager.isBusy(this, getCallId(intent))
private fun isIdle() = callManager.isIdle()
@ -188,10 +198,11 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
val action = intent.action
Log.d("Loki", "Handling ${intent.action}")
when {
action == ACTION_INCOMING_RING && isSameCall(intent) -> handleNewOffer(intent)
action == ACTION_INCOMING_RING && isBusy() -> handleBusyCall(intent)
action == ACTION_INCOMING_RING && isSameCall(intent) && !isPreOffer() -> handleNewOffer(intent)
action == ACTION_PRE_OFFER && isIdle() -> handlePreOffer(intent)
action == ACTION_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent)
action == ACTION_REMOTE_BUSY -> handleBusyMessage(intent)
action == ACTION_INCOMING_RING && isIdle() -> handleIncomingRing(intent)
action == ACTION_INCOMING_RING && isPreOffer() -> handleIncomingRing(intent)
action == ACTION_OUTGOING_CALL && isIdle() -> handleOutgoingCall(intent)
action == ACTION_ANSWER_CALL -> handleAnswerCall(intent)
action == ACTION_DENY_CALL -> handleDenyCall(intent)
@ -295,16 +306,28 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
callManager.onNewOffer(offer, callId, recipient)
}
private fun handlePreOffer(intent: Intent) {
if (!callManager.isIdle()) {
Log.d(TAG, "Handling pre-offer from non-idle state")
return
}
val callId = getCallId(intent)
val recipient = getRemoteRecipient(intent)
setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient)
callManager.onPreOffer(callId, recipient)
callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT)
callManager.initializeAudioForCall()
callManager.startIncomingRinger()
}
private fun handleIncomingRing(intent: Intent) {
if (callManager.currentConnectionState != STATE_IDLE) throw IllegalStateException("Incoming ring on non-idle")
if (!callManager.isPreOffer() && !callManager.isIdle()) throw IllegalStateException("Incoming ring on non-idle")
val callId = getCallId(intent)
val recipient = getRemoteRecipient(intent)
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient)
}
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient)
callManager.clearPendingIceUpdates()
callManager.onIncomingRing(offer, callId, recipient, timestamp)
callManager.postConnectionEvent(STATE_LOCAL_RINGING)
@ -404,7 +427,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
}
private fun handleDenyCall(intent: Intent) {
if (callManager.currentConnectionState != STATE_LOCAL_RINGING) {
if (callManager.currentConnectionState != STATE_LOCAL_RINGING && !callManager.isPreOffer()) {
Log.e(TAG,"Can only deny from ringing!")
return
}
@ -523,7 +546,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState
if (callId == getCallId(intent) && callState != STATE_CONNECTED) {
if (callId == getCallId(intent) && callState !in arrayOf(STATE_CONNECTED)) {
Log.w(TAG, "Timing out call: $callId")
handleLocalHangup(intent)
}

View File

@ -22,6 +22,7 @@ class CallNotificationBuilder {
const val TYPE_OUTGOING_RINGING = 2
const val TYPE_ESTABLISHED = 3
const val TYPE_INCOMING_CONNECTING = 4
const val TYPE_INCOMING_PRE_OFFER = 5
@JvmStatic
fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification {
@ -46,6 +47,23 @@ class CallNotificationBuilder {
builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting))
builder.priority = NotificationCompat.PRIORITY_LOW
}
TYPE_INCOMING_PRE_OFFER -> {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call))
.setCategory(NotificationCompat.CATEGORY_CALL)
builder.addAction(getServiceNotificationAction(
context,
WebRtcCallService.ACTION_DENY_CALL,
R.drawable.ic_close_grey600_32dp,
R.string.NotificationBarManager__deny_call
))
builder.addAction(getActivityNotificationAction(
context,
WebRtcCallActivity.ACTION_PRE_OFFER,
R.drawable.ic_phone_grey600_32dp,
R.string.NotificationBarManager__answer_call
))
builder.priority = NotificationCompat.PRIORITY_HIGH
}
TYPE_INCOMING_RINGING -> {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call))
.setCategory(NotificationCompat.CATEGORY_CALL)

View File

@ -36,7 +36,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
CallDataListener, CameraEventListener, DataChannel.Observer {
enum class CallState {
STATE_IDLE, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
STATE_IDLE, STATE_PRE_OFFER, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
}
sealed class StateEvent {
@ -101,7 +101,6 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
private val _audioDeviceEvents = MutableStateFlow(AudioDeviceUpdate(AudioDevice.NONE, setOf()))
val audioDeviceEvents = _audioDeviceEvents.asSharedFlow()
val currentConnectionState
get() = (_connectionEvents.value as CallStateUpdate).state
@ -111,6 +110,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
var pendingOffer: String? = null
var pendingOfferTime: Long = -1
var preOfferCallData: PreOffer? = null
var callId: UUID? = null
var recipient: Recipient? = null
set(value) {
@ -163,8 +163,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
}
fun isBusy(context: Context) = currentConnectionState != CallState.STATE_IDLE
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE
fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.STATE_IDLE
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE)
fun isPreOffer() = currentConnectionState == CallState.STATE_PRE_OFFER
fun isIdle() = currentConnectionState == CallState.STATE_IDLE
@ -347,6 +349,16 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
localCameraState = newCameraState
}
fun onPreOffer(callId: UUID, recipient: Recipient) {
if (preOfferCallData != null) {
Log.d(TAG, "Received new pre-offer when we are already expecting one")
}
this.recipient = recipient
this.callId = callId
preOfferCallData = PreOffer(callId, recipient)
postConnectionEvent(CallState.STATE_PRE_OFFER)
}
fun onNewOffer(offer: String, callId: UUID, recipient: Recipient): Promise<Unit, Exception> {
if (callId != this.callId) return Promise.ofFail(NullPointerException("No callId"))
if (recipient != this.recipient) return Promise.ofFail(NullPointerException("No recipient"))
@ -361,7 +373,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
}
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long) {
if (currentConnectionState != CallState.STATE_IDLE) return
if (currentConnectionState !in arrayOf(CallState.STATE_IDLE, CallState.STATE_PRE_OFFER)) return
this.callId = callId
this.recipient = recipient

View File

@ -77,6 +77,14 @@ class CallMessageProcessor(private val context: Context, lifecycle: Lifecycle) {
private fun incomingPreOffer(callMessage: CallMessage) {
// handle notification state
val recipientAddress = callMessage.sender ?: return
val callId = callMessage.callId ?: return
val incomingIntent = WebRtcCallService.preOffer(
context = context,
address = Address.fromSerialized(recipientAddress),
callId = callId,
)
ContextCompat.startForegroundService(context, incomingIntent)
}
private fun incomingCall(callMessage: CallMessage) {

View File

@ -1,12 +1,9 @@
package org.thoughtcrime.securesms.webrtc
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.webrtc.SurfaceViewRenderer
import javax.inject.Inject
@ -33,6 +30,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
enum class State {
CALL_PENDING,
CALL_PRE_INIT,
CALL_INCOMING,
CALL_OUTGOING,
CALL_CONNECTED,

View File

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.webrtc
import org.session.libsession.utilities.recipients.Recipient
import java.util.*
data class PreOffer(val callId: UUID, val recipient: Recipient)