diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt index 40713e730..d8c592b73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -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() 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 -> { } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index e62611d8c..ad0c6a70e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -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, 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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt index e5d20c41a..4f9b5034f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CallNotificationBuilder.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 2bbf98563..0780c1257 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -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 { 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt index 86ff154bf..0b393651b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallMessageProcessor.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt index 1fd3584c7..a9fd8ec6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt new file mode 100644 index 000000000..dfc2dc6fb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PreOffer.kt @@ -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) \ No newline at end of file