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

View File

@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ResultReceiver import android.os.ResultReceiver
import android.telephony.PhoneStateListener 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
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_ESTABLISHED 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_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_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.webrtc.* import org.thoughtcrime.securesms.webrtc.*
@@ -58,6 +58,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT" const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT"
const val ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL" 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_RESPONSE_MESSAGE = "RESPONSE_MESSAGE"
const val ACTION_ICE_MESSAGE = "ICE_MESSAGE" const val ACTION_ICE_MESSAGE = "ICE_MESSAGE"
const val ACTION_CALL_CONNECTED = "CALL_CONNECTED" const val ACTION_CALL_CONNECTED = "CALL_CONNECTED"
@@ -113,6 +114,12 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
.putExtra(EXTRA_CALL_ID, callId) .putExtra(EXTRA_CALL_ID, callId)
.putExtra(EXTRA_REMOTE_DESCRIPTION, sdp) .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) = fun iceCandidates(context: Context, address: Address, iceCandidates: List<IceCandidate>, callId: UUID) =
Intent(context, WebRtcCallService::class.java) Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_ICE_MESSAGE) .setAction(ACTION_ICE_MESSAGE)
@@ -176,7 +183,10 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
return callManager.callId == expectedCallId 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() private fun isIdle() = callManager.isIdle()
@@ -188,10 +198,11 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
val action = intent.action val action = intent.action
Log.d("Loki", "Handling ${intent.action}") Log.d("Loki", "Handling ${intent.action}")
when { when {
action == ACTION_INCOMING_RING && isSameCall(intent) -> handleNewOffer(intent) action == ACTION_INCOMING_RING && isSameCall(intent) && !isPreOffer() -> handleNewOffer(intent)
action == ACTION_INCOMING_RING && isBusy() -> handleBusyCall(intent) action == ACTION_PRE_OFFER && isIdle() -> handlePreOffer(intent)
action == ACTION_INCOMING_RING && isBusy(intent) -> handleBusyCall(intent)
action == ACTION_REMOTE_BUSY -> handleBusyMessage(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_OUTGOING_CALL && isIdle() -> handleOutgoingCall(intent)
action == ACTION_ANSWER_CALL -> handleAnswerCall(intent) action == ACTION_ANSWER_CALL -> handleAnswerCall(intent)
action == ACTION_DENY_CALL -> handleDenyCall(intent) action == ACTION_DENY_CALL -> handleDenyCall(intent)
@@ -295,16 +306,28 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
callManager.onNewOffer(offer, callId, recipient) 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) { 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 callId = getCallId(intent)
val recipient = getRemoteRecipient(intent) val recipient = getRemoteRecipient(intent)
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1) 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.clearPendingIceUpdates()
callManager.onIncomingRing(offer, callId, recipient, timestamp) callManager.onIncomingRing(offer, callId, recipient, timestamp)
callManager.postConnectionEvent(STATE_LOCAL_RINGING) callManager.postConnectionEvent(STATE_LOCAL_RINGING)
@@ -404,7 +427,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
} }
private fun handleDenyCall(intent: Intent) { 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!") Log.e(TAG,"Can only deny from ringing!")
return return
} }
@@ -523,7 +546,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
val callId = callManager.callId ?: return val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState 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") Log.w(TAG, "Timing out call: $callId")
handleLocalHangup(intent) handleLocalHangup(intent)
} }

View File

@@ -22,6 +22,7 @@ class CallNotificationBuilder {
const val TYPE_OUTGOING_RINGING = 2 const val TYPE_OUTGOING_RINGING = 2
const val TYPE_ESTABLISHED = 3 const val TYPE_ESTABLISHED = 3
const val TYPE_INCOMING_CONNECTING = 4 const val TYPE_INCOMING_CONNECTING = 4
const val TYPE_INCOMING_PRE_OFFER = 5
@JvmStatic @JvmStatic
fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification { fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification {
@@ -46,6 +47,23 @@ class CallNotificationBuilder {
builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting)) builder.setContentText(context.getString(R.string.CallNotificationBuilder_connecting))
builder.priority = NotificationCompat.PRIORITY_LOW 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 -> { TYPE_INCOMING_RINGING -> {
builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call)) builder.setContentText(context.getString(R.string.NotificationBarManager__incoming_signal_call))
.setCategory(NotificationCompat.CATEGORY_CALL) .setCategory(NotificationCompat.CATEGORY_CALL)

View File

@@ -36,7 +36,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
CallDataListener, CameraEventListener, DataChannel.Observer { CallDataListener, CameraEventListener, DataChannel.Observer {
enum class CallState { 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 { sealed class StateEvent {
@@ -101,7 +101,6 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
private val _audioDeviceEvents = MutableStateFlow(AudioDeviceUpdate(AudioDevice.NONE, setOf())) private val _audioDeviceEvents = MutableStateFlow(AudioDeviceUpdate(AudioDevice.NONE, setOf()))
val audioDeviceEvents = _audioDeviceEvents.asSharedFlow() val audioDeviceEvents = _audioDeviceEvents.asSharedFlow()
val currentConnectionState val currentConnectionState
get() = (_connectionEvents.value as CallStateUpdate).state get() = (_connectionEvents.value as CallStateUpdate).state
@@ -111,6 +110,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
var pendingOffer: String? = null var pendingOffer: String? = null
var pendingOfferTime: Long = -1 var pendingOfferTime: Long = -1
var preOfferCallData: PreOffer? = null
var callId: UUID? = null var callId: UUID? = null
var recipient: Recipient? = null var recipient: Recipient? = null
set(value) { set(value) {
@@ -163,8 +163,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
} }
fun isBusy(context: Context) = currentConnectionState != CallState.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 || context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE)
fun isPreOffer() = currentConnectionState == CallState.STATE_PRE_OFFER
fun isIdle() = currentConnectionState == CallState.STATE_IDLE fun isIdle() = currentConnectionState == CallState.STATE_IDLE
@@ -347,6 +349,16 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
localCameraState = newCameraState 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> { fun onNewOffer(offer: String, callId: UUID, recipient: Recipient): Promise<Unit, Exception> {
if (callId != this.callId) return Promise.ofFail(NullPointerException("No callId")) if (callId != this.callId) return Promise.ofFail(NullPointerException("No callId"))
if (recipient != this.recipient) return Promise.ofFail(NullPointerException("No recipient")) 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) { 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.callId = callId
this.recipient = recipient this.recipient = recipient

View File

@@ -77,6 +77,14 @@ class CallMessageProcessor(private val context: Context, lifecycle: Lifecycle) {
private fun incomingPreOffer(callMessage: CallMessage) { private fun incomingPreOffer(callMessage: CallMessage) {
// handle notification state // 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) { private fun incomingCall(callMessage: CallMessage) {

View File

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