Merge remote-tracking branch 'origin/calls' into calls
# Conflicts: # app/build.gradle # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt # app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java # app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt # app/src/main/res/values/strings.xml # libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt # libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt # libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
This commit is contained in:
commit
e689ab9753
|
@ -302,8 +302,8 @@
|
|||
android:exported="true"
|
||||
android:theme="@style/Theme.Session.DayNight.NoActionBar" />
|
||||
<activity android:name="org.thoughtcrime.securesms.calls.WebRtcCallActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:showForAllUsers="true"
|
||||
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
|
||||
android:theme="@style/Theme.Session.CallActivity">
|
||||
|
|
|
@ -8,7 +8,9 @@ import android.content.IntentFilter
|
|||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.MenuItem
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -25,6 +27,7 @@ import network.loki.messenger.databinding.ActivityWebrtcBinding
|
|||
import org.apache.commons.lang3.time.DurationFormatUtils
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
|
@ -33,9 +36,15 @@ import org.thoughtcrime.securesms.service.WebRtcCallService
|
|||
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.AudioDevice.*
|
||||
import java.util.*
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_CONNECTED
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_INCOMING
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_OUTGOING
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_PRE_INIT
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
@ -62,6 +71,17 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
}
|
||||
private var hangupReceiver: BroadcastReceiver? = null
|
||||
|
||||
private val rotationListener by lazy {
|
||||
object : OrientationEventListener(this) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if ((orientation + 15) % 90 < 30) {
|
||||
viewModel.deviceRotation = orientation
|
||||
updateControlsRotation(orientation.quadrantRotation() * -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
|
@ -80,6 +100,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
rotationListener.enable()
|
||||
binding = ActivityWebrtcBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
|
@ -165,6 +186,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
hangupReceiver?.let { receiver ->
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
||||
}
|
||||
rotationListener.disable()
|
||||
}
|
||||
|
||||
private fun answerCall() {
|
||||
|
@ -172,8 +194,20 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
ContextCompat.startForegroundService(this, answerIntent)
|
||||
}
|
||||
|
||||
private fun updateControls(state: CallViewModel.State? = null) {
|
||||
private fun updateControlsRotation(newRotation: Int) {
|
||||
with (binding) {
|
||||
val rotation = newRotation.toFloat()
|
||||
remoteRecipient.rotation = rotation
|
||||
speakerPhoneButton.rotation = rotation
|
||||
microphoneButton.rotation = rotation
|
||||
enableCameraButton.rotation = rotation
|
||||
switchCameraButton.rotation = rotation
|
||||
endCallButton.rotation = rotation
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateControls(state: CallViewModel.State? = null) {
|
||||
with(binding) {
|
||||
if (state == null) {
|
||||
if (wantsToAnswer) {
|
||||
controlGroup.isVisible = true
|
||||
|
@ -181,7 +215,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
incomingControlGroup.isVisible = false
|
||||
}
|
||||
} else {
|
||||
|
||||
controlGroup.isVisible = state in listOf(
|
||||
CALL_CONNECTED,
|
||||
CALL_OUTGOING,
|
||||
|
@ -191,6 +224,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
state !in listOf(CALL_CONNECTED, CALL_RINGING, CALL_PRE_INIT) || wantsToAnswer
|
||||
incomingControlGroup.isVisible =
|
||||
state in listOf(CALL_RINGING, CALL_PRE_INIT) && !wantsToAnswer
|
||||
reconnectingText.isVisible = state == CALL_RECONNECTING
|
||||
endCallButton.isVisible = endCallButton.isVisible || state == CALL_RECONNECTING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,6 +245,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
|
||||
launch {
|
||||
viewModel.callState.collect { state ->
|
||||
Log.d("Loki", "Consuming view model state $state")
|
||||
when (state) {
|
||||
CALL_RINGING -> {
|
||||
if (wantsToAnswer) {
|
||||
|
|
|
@ -7,16 +7,17 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.IBinder
|
||||
import android.os.ResultReceiver
|
||||
import android.telephony.PhoneStateListener
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.FutureTaskListener
|
||||
import org.session.libsession.utilities.Util
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
|
||||
|
@ -26,18 +27,36 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN
|
|||
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.util.ContextProvider
|
||||
import org.thoughtcrime.securesms.webrtc.*
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager.CallState.*
|
||||
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel
|
||||
import org.thoughtcrime.securesms.webrtc.HangUpRtcOnPstnCallAnsweredListener
|
||||
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver
|
||||
import org.thoughtcrime.securesms.webrtc.NetworkChangeReceiver
|
||||
import org.thoughtcrime.securesms.webrtc.PeerConnectionException
|
||||
import org.thoughtcrime.securesms.webrtc.PowerButtonReceiver
|
||||
import org.thoughtcrime.securesms.webrtc.ProximityLockRelease
|
||||
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
|
||||
import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver
|
||||
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
|
||||
import org.thoughtcrime.securesms.webrtc.data.Event
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager
|
||||
import org.webrtc.*
|
||||
import org.webrtc.PeerConnection.IceConnectionState.*
|
||||
import java.util.*
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.PeerConnection.IceConnectionState.CONNECTED
|
||||
import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
|
||||
import org.webrtc.PeerConnection.IceConnectionState.FAILED
|
||||
import org.webrtc.RtpReceiver
|
||||
import org.webrtc.SessionDescription
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import org.thoughtcrime.securesms.webrtc.data.State as CallState
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
||||
|
@ -58,16 +77,15 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"
|
||||
const val ACTION_SCREEN_OFF = "SCREEN_OFF"
|
||||
const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT"
|
||||
const val ACTION_CHECK_RECONNECT = "CHECK_RECONNECT"
|
||||
const val ACTION_CHECK_RECONNECT_TIMEOUT = "CHECK_RECONNECT_TIMEOUT"
|
||||
const val ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL"
|
||||
const val ACTION_WANTS_TO_ANSWER = "WANTS_TO_ANSWER"
|
||||
|
||||
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"
|
||||
const val ACTION_REMOTE_HANGUP = "REMOTE_HANGUP"
|
||||
const val ACTION_REMOTE_BUSY = "REMOTE_BUSY"
|
||||
const val ACTION_REMOTE_VIDEO_MUTE = "REMOTE_VIDEO_MUTE"
|
||||
const val ACTION_ICE_CONNECTED = "ICE_CONNECTED"
|
||||
|
||||
const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID"
|
||||
|
@ -85,7 +103,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
const val EXTRA_WANTS_TO_ANSWER = "wants_to_answer"
|
||||
|
||||
const val INVALID_NOTIFICATION_ID = -1
|
||||
private const val TIMEOUT_SECONDS = 30L
|
||||
private const val TIMEOUT_SECONDS = 90L
|
||||
private const val RECONNECT_SECONDS = 5L
|
||||
private const val MAX_RECONNECTS = 5
|
||||
|
||||
fun cameraEnabled(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_SET_MUTE_VIDEO)
|
||||
|
@ -170,6 +190,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
@Inject lateinit var callManager: CallManager
|
||||
|
||||
private var wantsToAnswer = false
|
||||
private var currentTimeouts = 0
|
||||
private var isNetworkAvailable = true
|
||||
private var activeNetwork: Network? = null
|
||||
private var scheduledTimeout: ScheduledFuture<*>? = null
|
||||
private var scheduledReconnect: ScheduledFuture<*>? = null
|
||||
|
||||
private val lockManager by lazy { LockManager(this) }
|
||||
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
||||
|
@ -190,6 +215,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(WebRtcCallActivity.ACTION_END))
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.IDLE)
|
||||
callManager.stop()
|
||||
wantsToAnswer = false
|
||||
currentTimeouts = 0
|
||||
isNetworkAvailable = true
|
||||
activeNetwork = null
|
||||
scheduledTimeout?.cancel(false)
|
||||
scheduledReconnect?.cancel(false)
|
||||
scheduledTimeout = null
|
||||
scheduledReconnect = null
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
|
@ -211,10 +244,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
serviceExecutor.execute {
|
||||
callManager.handleRemoteHangup()
|
||||
|
||||
if (callManager.currentConnectionState in arrayOf(STATE_REMOTE_RINGING, STATE_ANSWERING, STATE_LOCAL_RINGING)) {
|
||||
if (callManager.currentConnectionState in CallState.CAN_DECLINE_STATES) {
|
||||
callManager.recipient?.let { recipient ->
|
||||
insertMissedCall(recipient, true)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
terminate()
|
||||
}
|
||||
|
@ -224,12 +258,11 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
if (intent == null || intent.action == null) return START_NOT_STICKY
|
||||
serviceExecutor.execute {
|
||||
val action = intent.action
|
||||
Log.d("Loki", "Handling ${intent.action}")
|
||||
Log.i("Loki", "Handling ${intent.action}")
|
||||
when {
|
||||
action == ACTION_INCOMING_RING && isSameCall(intent) && !isPreOffer() -> handleNewOffer(intent)
|
||||
action == ACTION_INCOMING_RING && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> 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 && isPreOffer() -> handleIncomingRing(intent)
|
||||
action == ACTION_OUTGOING_CALL && isIdle() -> handleOutgoingCall(intent)
|
||||
action == ACTION_ANSWER_CALL -> handleAnswerCall(intent)
|
||||
|
@ -241,10 +274,12 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent)
|
||||
action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent)
|
||||
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
|
||||
action == ACTION_RESPONSE_MESSAGE && isSameCall(intent) && callManager.currentConnectionState == CallState.Reconnecting -> handleResponseMessage(intent)
|
||||
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
|
||||
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
|
||||
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
|
||||
action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(intent)
|
||||
action == ACTION_CHECK_RECONNECT -> handleCheckReconnect(intent)
|
||||
action == ACTION_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent)
|
||||
action == ACTION_UPDATE_AUDIO -> handleUpdateAudio(intent)
|
||||
}
|
||||
|
@ -256,15 +291,14 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
super.onCreate()
|
||||
callManager.registerListener(this)
|
||||
wantsToAnswer = false
|
||||
isNetworkAvailable = true
|
||||
registerIncomingPstnCallReceiver()
|
||||
registerWiredHeadsetStateReceiver()
|
||||
registerWantsToAnswerReceiver()
|
||||
getSystemService(TelephonyManager::class.java)
|
||||
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
|
||||
registerUncaughtExceptionHandler()
|
||||
networkChangedReceiver = NetworkChangeReceiver { available ->
|
||||
networkChange(available)
|
||||
}
|
||||
networkChangedReceiver = NetworkChangeReceiver(::networkChange)
|
||||
networkChangedReceiver!!.register(this)
|
||||
}
|
||||
|
||||
|
@ -300,57 +334,38 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
|
||||
insertMissedCall(recipient, false)
|
||||
|
||||
if (callState == STATE_IDLE) {
|
||||
if (callState == CallState.Idle) {
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdateAudio(intent: Intent) {
|
||||
val audioCommand = intent.getParcelableExtra<AudioManagerCommand>(EXTRA_AUDIO_COMMAND)!!
|
||||
if (callManager.currentConnectionState !in arrayOf(STATE_DIALING, STATE_CONNECTED, STATE_LOCAL_RINGING)) {
|
||||
if (callManager.currentConnectionState !in arrayOf(CallState.Connected, *CallState.PENDING_CONNECTION_STATES)) {
|
||||
Log.w(TAG, "handling audio command not in call")
|
||||
return
|
||||
}
|
||||
callManager.handleAudioCommand(audioCommand)
|
||||
}
|
||||
|
||||
private fun handleBusyMessage(intent: Intent) {
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
val callId = getCallId(intent)
|
||||
if (callManager.currentConnectionState != STATE_DIALING || callManager.callId != callId || callManager.recipient != recipient) {
|
||||
Log.w(TAG,"Got busy message for inactive session...")
|
||||
return
|
||||
}
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_BUSY)
|
||||
callManager.startOutgoingRinger(OutgoingRinger.Type.BUSY)
|
||||
Util.runOnMainDelayed({
|
||||
startService(
|
||||
Intent(this, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_LOCAL_HANGUP)
|
||||
)
|
||||
}, WebRtcCallActivity.BUSY_SIGNAL_DELAY_FINISH)
|
||||
}
|
||||
|
||||
private fun handleNewOffer(intent: Intent) {
|
||||
if (callManager.currentConnectionState !in arrayOf(STATE_CONNECTED, STATE_DIALING, STATE_ANSWERING)) {
|
||||
Log.w(TAG,"trying to handle new offer from non-connecting state")
|
||||
return
|
||||
}
|
||||
|
||||
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return
|
||||
val callId = getCallId(intent)
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
callManager.onNewOffer(offer, callId, recipient)
|
||||
callManager.onNewOffer(offer, callId, recipient).fail {
|
||||
Log.e("Loki", "Error handling new offer", it)
|
||||
callManager.postConnectionError()
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePreOffer(intent: Intent) {
|
||||
if (!callManager.isIdle()) {
|
||||
Log.d(TAG, "Handling pre-offer from non-idle state")
|
||||
Log.w(TAG, "Handling pre-offer from non-idle state")
|
||||
return
|
||||
}
|
||||
val callId = getCallId(intent)
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
val sentTimestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
|
||||
|
||||
if (isIncomingMessageExpired(intent)) {
|
||||
insertMissedCall(recipient, true)
|
||||
|
@ -358,17 +373,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
return
|
||||
}
|
||||
|
||||
setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient)
|
||||
callManager.onPreOffer(callId, recipient, sentTimestamp)
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT)
|
||||
callManager.initializeAudioForCall()
|
||||
callManager.startIncomingRinger()
|
||||
callManager.setAudioEnabled(true)
|
||||
callManager.onPreOffer(callId, recipient) {
|
||||
setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient)
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT)
|
||||
callManager.initializeAudioForCall()
|
||||
callManager.startIncomingRinger()
|
||||
callManager.setAudioEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIncomingRing(intent: Intent) {
|
||||
if (!callManager.isPreOffer() && !callManager.isIdle()) throw IllegalStateException("Incoming ring on non-idle")
|
||||
|
||||
val callId = getCallId(intent)
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
val preOffer = callManager.preOfferCallData
|
||||
|
@ -380,120 +394,119 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
|
||||
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return
|
||||
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
|
||||
if (wantsToAnswer) {
|
||||
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
|
||||
} else {
|
||||
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient)
|
||||
|
||||
callManager.onIncomingRing(offer, callId, recipient, timestamp) {
|
||||
if (wantsToAnswer) {
|
||||
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
|
||||
} else {
|
||||
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient)
|
||||
}
|
||||
callManager.clearPendingIceUpdates()
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_RINGING)
|
||||
registerPowerButtonReceiver()
|
||||
}
|
||||
callManager.clearPendingIceUpdates()
|
||||
callManager.onIncomingRing(offer, callId, recipient, timestamp)
|
||||
callManager.postConnectionEvent(STATE_LOCAL_RINGING)
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_RINGING)
|
||||
registerPowerButtonReceiver()
|
||||
}
|
||||
|
||||
private fun handleOutgoingCall(intent: Intent) {
|
||||
if (callManager.currentConnectionState != STATE_IDLE) throw IllegalStateException("Dialing from non-idle")
|
||||
callManager.postConnectionEvent(Event.SendPreOffer) {
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
callManager.recipient = recipient
|
||||
val callId = UUID.randomUUID()
|
||||
callManager.callId = callId
|
||||
|
||||
callManager.postConnectionEvent(STATE_DIALING)
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
callManager.recipient = recipient
|
||||
val callId = UUID.randomUUID()
|
||||
callManager.callId = callId
|
||||
callManager.initializeVideo(this)
|
||||
|
||||
callManager.initializeVideo(this)
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_OUTGOING)
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
|
||||
callManager.initializeAudioForCall()
|
||||
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
|
||||
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
|
||||
callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING)
|
||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
callManager.setAudioEnabled(true)
|
||||
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_OUTGOING)
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
|
||||
callManager.initializeAudioForCall()
|
||||
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
|
||||
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
|
||||
callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING)
|
||||
timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
callManager.setAudioEnabled(true)
|
||||
val expectedState = callManager.currentConnectionState
|
||||
val expectedCallId = callManager.callId
|
||||
|
||||
val expectedState = callManager.currentConnectionState
|
||||
val expectedCallId = callManager.callId
|
||||
|
||||
try {
|
||||
val offerFuture = callManager.onOutgoingCall(this)
|
||||
offerFuture.fail { e ->
|
||||
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) {
|
||||
Log.e(TAG,e)
|
||||
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
|
||||
terminate()
|
||||
try {
|
||||
val offerFuture = callManager.onOutgoingCall(this)
|
||||
offerFuture.fail { e ->
|
||||
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) {
|
||||
Log.e(TAG,e)
|
||||
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
|
||||
callManager.postConnectionError()
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG,e)
|
||||
callManager.postConnectionError()
|
||||
terminate()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG,e)
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAnswerCall(intent: Intent) {
|
||||
val recipient = callManager.recipient ?: return
|
||||
|
||||
if (callManager.currentConnectionState != STATE_LOCAL_RINGING) {
|
||||
if (callManager.currentConnectionState == STATE_PRE_OFFER) {
|
||||
// show answer state from pre-offer
|
||||
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
|
||||
}
|
||||
Log.e(TAG, "Can only answer from ringing!")
|
||||
return
|
||||
}
|
||||
|
||||
val pending = callManager.pendingOffer ?: return
|
||||
val callId = callManager.callId ?: return
|
||||
val timestamp = callManager.pendingOfferTime
|
||||
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
|
||||
|
||||
if (callManager.currentConnectionState != CallState.RemoteRing) {
|
||||
Log.e(TAG, "Can only answer from ringing!")
|
||||
return
|
||||
}
|
||||
|
||||
intent.putExtra(EXTRA_CALL_ID, callId)
|
||||
intent.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address)
|
||||
intent.putExtra(EXTRA_REMOTE_DESCRIPTION, pending)
|
||||
intent.putExtra(EXTRA_TIMESTAMP, timestamp)
|
||||
|
||||
callManager.silenceIncomingRinger()
|
||||
callManager.postConnectionEvent(STATE_ANSWERING)
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
|
||||
|
||||
if (isIncomingMessageExpired(intent)) {
|
||||
insertMissedCall(recipient, true)
|
||||
terminate()
|
||||
return
|
||||
}
|
||||
|
||||
timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
||||
callManager.initializeAudioForCall()
|
||||
callManager.initializeVideo(this)
|
||||
|
||||
val expectedState = callManager.currentConnectionState
|
||||
val expectedCallId = callManager.callId
|
||||
|
||||
try {
|
||||
val answerFuture = callManager.onIncomingCall(this)
|
||||
answerFuture.fail { e ->
|
||||
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) {
|
||||
Log.e(TAG, e)
|
||||
insertMissedCall(recipient, true)
|
||||
terminate()
|
||||
}
|
||||
val didHangup = callManager.postConnectionEvent(Event.TimeOut) {
|
||||
insertMissedCall(recipient, true)
|
||||
terminate()
|
||||
}
|
||||
if (didHangup) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
callManager.postConnectionEvent(Event.SendAnswer) {
|
||||
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
|
||||
|
||||
callManager.silenceIncomingRinger()
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
|
||||
|
||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
||||
callManager.initializeAudioForCall()
|
||||
callManager.initializeVideo(this)
|
||||
|
||||
val expectedState = callManager.currentConnectionState
|
||||
val expectedCallId = callManager.callId
|
||||
|
||||
try {
|
||||
val answerFuture = callManager.onIncomingCall(this)
|
||||
answerFuture.fail { e ->
|
||||
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) {
|
||||
Log.e(TAG, e)
|
||||
insertMissedCall(recipient, true)
|
||||
callManager.postConnectionError()
|
||||
terminate()
|
||||
}
|
||||
}
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
|
||||
callManager.setAudioEnabled(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG,e)
|
||||
callManager.postConnectionError()
|
||||
terminate()
|
||||
}
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
|
||||
callManager.setAudioEnabled(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG,e)
|
||||
terminate()
|
||||
}
|
||||
// DatabaseComponent.get(this).smsDatabase().insertReceivedCall(recipient)
|
||||
}
|
||||
|
||||
private fun handleDenyCall(intent: Intent) {
|
||||
if (callManager.currentConnectionState != STATE_LOCAL_RINGING && !callManager.isPreOffer()) {
|
||||
Log.e(TAG,"Can only deny from ringing!")
|
||||
return
|
||||
}
|
||||
|
||||
callManager.handleDenyCall()
|
||||
terminate()
|
||||
}
|
||||
|
@ -538,7 +551,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
private fun handleResponseMessage(intent: Intent) {
|
||||
try {
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
if (callManager.isCurrentUser(recipient) && callManager.currentConnectionState == STATE_LOCAL_RINGING) {
|
||||
if (callManager.isCurrentUser(recipient) && callManager.currentConnectionState in CallState.OUTGOING_STATES) {
|
||||
handleLocalHangup(intent)
|
||||
return
|
||||
}
|
||||
|
@ -559,7 +572,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
Log.w(TAG,"sdp info not of equal length")
|
||||
return
|
||||
}
|
||||
val iceCandidates = (0 until sdpMids.size).map { index ->
|
||||
val iceCandidates = sdpMids.indices.map { index ->
|
||||
IceCandidate(
|
||||
sdpMids[index],
|
||||
sdpLineIndexes[index],
|
||||
|
@ -571,20 +584,23 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
|
||||
private fun handleIceConnected(intent: Intent) {
|
||||
val recipient = callManager.recipient ?: return
|
||||
if (callManager.currentConnectionState in arrayOf(STATE_ANSWERING, STATE_DIALING)) {
|
||||
callManager.postConnectionEvent(STATE_CONNECTED)
|
||||
val connected = callManager.postConnectionEvent(Event.Connect) {
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_CONNECTED)
|
||||
} else {
|
||||
Log.w(TAG, "Got ice connected out of state")
|
||||
setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
|
||||
callManager.startCommunication(lockManager)
|
||||
}
|
||||
if (!connected) {
|
||||
Log.e("Loki", "Error handling ice connected state transition")
|
||||
callManager.postConnectionError()
|
||||
terminate()
|
||||
}
|
||||
|
||||
setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
|
||||
|
||||
callManager.startCommunication(lockManager)
|
||||
}
|
||||
|
||||
private fun handleIsInCallQuery(intent: Intent) {
|
||||
|
||||
val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return
|
||||
val currentState = callManager.currentConnectionState
|
||||
val isInCall = if (currentState in arrayOf(*CallState.PENDING_CONNECTION_STATES, CallState.Connected)) 1 else 0
|
||||
listener.send(isInCall, bundleOf())
|
||||
}
|
||||
|
||||
private fun registerPowerButtonReceiver() {
|
||||
|
@ -595,11 +611,30 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCheckReconnect(intent: Intent) {
|
||||
val callId = callManager.callId ?: return
|
||||
val numTimeouts = ++currentTimeouts
|
||||
|
||||
if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= MAX_RECONNECTS) {
|
||||
Log.i("Loki", "Trying to re-connect")
|
||||
callManager.networkReestablished()
|
||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
} else if (numTimeouts < MAX_RECONNECTS) {
|
||||
Log.i("Loki", "Network isn't available, timeouts == $numTimeouts out of $MAX_RECONNECTS")
|
||||
scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS)
|
||||
} else {
|
||||
Log.i("Loki", "Network isn't available, timing out")
|
||||
handleLocalHangup(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun handleCheckTimeout(intent: Intent) {
|
||||
val callId = callManager.callId ?: return
|
||||
val callState = callManager.currentConnectionState
|
||||
|
||||
if (callId == getCallId(intent) && (callState !in arrayOf(STATE_CONNECTED) || callManager.iceState == CHECKING)) {
|
||||
if (callId == getCallId(intent) && (callState !in arrayOf(CallState.Connected, CallState.Connecting))) {
|
||||
Log.w(TAG, "Timing out call: $callId")
|
||||
handleLocalHangup(intent)
|
||||
}
|
||||
|
@ -648,9 +683,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
callReceiver?.let { receiver ->
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
networkChangedReceiver?.let { receiver ->
|
||||
receiver.unregister(this)
|
||||
}
|
||||
networkChangedReceiver?.unregister(this)
|
||||
wantsToAnswerReceiver?.let { receiver ->
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
|
||||
}
|
||||
|
@ -658,12 +691,35 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
callReceiver = null
|
||||
uncaughtExceptionHandlerManager?.unregister()
|
||||
wantsToAnswer = false
|
||||
currentTimeouts = 0
|
||||
isNetworkAvailable = false
|
||||
activeNetwork = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun networkChange(networkAvailable: Boolean) {
|
||||
if (networkAvailable && !callManager.isReestablishing && callManager.currentConnectionState in arrayOf(STATE_CONNECTED)) {
|
||||
callManager.networkReestablished()
|
||||
private fun networkChange(networkAvailable: Boolean) {
|
||||
Log.d("Loki", "flipping network available to $networkAvailable")
|
||||
isNetworkAvailable = networkAvailable
|
||||
if (networkAvailable && !callManager.isReestablishing && callManager.currentConnectionState == CallState.Connected) {
|
||||
Log.d("Loki", "Should reconnected")
|
||||
}
|
||||
}
|
||||
|
||||
private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context): Runnable {
|
||||
override fun run() {
|
||||
val intent = Intent(context, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_CHECK_RECONNECT)
|
||||
.putExtra(EXTRA_CALL_ID, callId)
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context): Runnable {
|
||||
override fun run() {
|
||||
val intent = Intent(context, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_CHECK_RECONNECT_TIMEOUT)
|
||||
.putExtra(EXTRA_CALL_ID, callId)
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -677,16 +733,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
}
|
||||
|
||||
private abstract class FailureListener<V>(
|
||||
expectedState: CallManager.CallState,
|
||||
expectedState: CallState,
|
||||
expectedCallId: UUID?,
|
||||
getState: () -> Pair<CallManager.CallState, UUID?>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
||||
getState: () -> Pair<CallState, UUID?>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
||||
override fun onSuccessContinue(result: V) {}
|
||||
}
|
||||
|
||||
private abstract class SuccessOnlyListener<V>(
|
||||
expectedState: CallManager.CallState,
|
||||
expectedState: CallState,
|
||||
expectedCallId: UUID?,
|
||||
getState: () -> Pair<CallManager.CallState, UUID>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
||||
getState: () -> Pair<CallState, UUID>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
|
||||
override fun onFailureContinue(throwable: Throwable?) {
|
||||
Log.e(TAG, throwable)
|
||||
throw AssertionError(throwable)
|
||||
|
@ -694,9 +750,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
}
|
||||
|
||||
private abstract class StateAwareListener<V>(
|
||||
private val expectedState: CallManager.CallState,
|
||||
private val expectedState: CallState,
|
||||
private val expectedCallId: UUID?,
|
||||
private val getState: ()->Pair<CallManager.CallState, UUID?>): FutureTaskListener<V> {
|
||||
private val getState: ()->Pair<CallState, UUID?>): FutureTaskListener<V> {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StateAwareListener::class.java)
|
||||
|
@ -732,9 +788,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
}
|
||||
|
||||
private fun isConsistentState(
|
||||
expectedState: CallManager.CallState,
|
||||
expectedState: CallState,
|
||||
expectedCallId: UUID?,
|
||||
currentState: CallManager.CallState,
|
||||
currentState: CallState,
|
||||
currentCallId: UUID?
|
||||
): Boolean {
|
||||
return expectedState == currentState && expectedCallId == currentCallId
|
||||
|
@ -742,19 +798,50 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
|
|||
|
||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {}
|
||||
|
||||
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
|
||||
if (newState in arrayOf(CONNECTED, COMPLETED)) {
|
||||
val intent = Intent(this, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_ICE_CONNECTED)
|
||||
startService(intent)
|
||||
} else if (newState == FAILED) {
|
||||
val intent = Intent(this, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_LOCAL_HANGUP)
|
||||
.putExtra(EXTRA_CALL_ID, callManager.callId)
|
||||
fun Context.getCurrentNetwork(): Network? {
|
||||
val cm = this.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
return cm.activeNetwork
|
||||
}
|
||||
|
||||
startService(intent)
|
||||
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
|
||||
newState?.let { state -> processIceConnectionChange(state) }
|
||||
}
|
||||
|
||||
private fun processIceConnectionChange(newState: PeerConnection.IceConnectionState) {
|
||||
serviceExecutor.execute {
|
||||
if (newState == CONNECTED) {
|
||||
scheduledTimeout?.cancel(false)
|
||||
scheduledReconnect?.cancel(false)
|
||||
scheduledTimeout = null
|
||||
scheduledReconnect = null
|
||||
activeNetwork = getCurrentNetwork()
|
||||
|
||||
val intent = Intent(this, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_ICE_CONNECTED)
|
||||
startService(intent)
|
||||
} else if (newState in arrayOf(FAILED, DISCONNECTED) && scheduledReconnect == null) {
|
||||
callManager.resetPeerConnection()
|
||||
callManager.callId?.let { callId ->
|
||||
callManager.postConnectionEvent(Event.IceDisconnect) {
|
||||
val currentNetwork = getCurrentNetwork()
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING)
|
||||
if (activeNetwork != currentNetwork || currentNetwork == null) {
|
||||
Log.i("Loki", "Starting reconnect timer")
|
||||
scheduledReconnect = timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), RECONNECT_SECONDS, TimeUnit.SECONDS)
|
||||
} else {
|
||||
Log.i("Loki", "Starting timeout, awaiting new reconnect")
|
||||
callManager.postConnectionEvent(Event.PrepareForNewOffer) {
|
||||
scheduledTimeout = timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
val intent = hangupIntent(this)
|
||||
startService(intent)
|
||||
}
|
||||
}
|
||||
Log.i("Loki", "onIceConnectionChange: $newState")
|
||||
}
|
||||
Log.d(TAG, "onIceConnectionChange: $newState")
|
||||
}
|
||||
|
||||
override fun onIceConnectionReceivingChange(p0: Boolean) {}
|
||||
|
|
|
@ -4,8 +4,12 @@ import android.content.Context
|
|||
import android.telephony.TelephonyManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
|
@ -18,27 +22,41 @@ import org.session.libsession.utilities.Util
|
|||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.*
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdate
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.RecipientUpdate
|
||||
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoEnabled
|
||||
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
|
||||
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice
|
||||
import org.thoughtcrime.securesms.webrtc.data.Event
|
||||
import org.thoughtcrime.securesms.webrtc.data.StateProcessor
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager
|
||||
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
|
||||
import org.thoughtcrime.securesms.webrtc.video.CameraState
|
||||
import org.webrtc.*
|
||||
import org.thoughtcrime.securesms.webrtc.video.RemoteRotationVideoProxySink
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.DefaultVideoDecoderFactory
|
||||
import org.webrtc.DefaultVideoEncoderFactory
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaConstraints
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.PeerConnection.IceConnectionState
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import org.webrtc.RtpReceiver
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.ArrayDeque
|
||||
import java.util.UUID
|
||||
import org.thoughtcrime.securesms.webrtc.data.State as CallState
|
||||
|
||||
class CallManager(context: Context, audioManager: AudioManagerCompat, private val storage: StorageProtocol): PeerConnection.Observer,
|
||||
SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer {
|
||||
|
||||
enum class CallState {
|
||||
STATE_IDLE, STATE_PRE_OFFER, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
|
||||
}
|
||||
|
||||
sealed class StateEvent {
|
||||
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
|
||||
data class VideoEnabled(val isEnabled: Boolean): StateEvent()
|
||||
|
@ -57,19 +75,6 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
val HANGUP_JSON by lazy { buildJsonObject { put("hangup", true) } }
|
||||
|
||||
private val TAG = Log.tag(CallManager::class.java)
|
||||
val CONNECTED_STATES = arrayOf(CallState.STATE_CONNECTED)
|
||||
val PENDING_CONNECTION_STATES = arrayOf(
|
||||
CallState.STATE_DIALING,
|
||||
CallState.STATE_ANSWERING,
|
||||
CallState.STATE_LOCAL_RINGING,
|
||||
CallState.STATE_REMOTE_RINGING,
|
||||
CallState.STATE_PRE_OFFER,
|
||||
)
|
||||
val OUTGOING_STATES = arrayOf(
|
||||
CallState.STATE_DIALING,
|
||||
CallState.STATE_REMOTE_RINGING,
|
||||
CallState.STATE_CONNECTED
|
||||
)
|
||||
private const val DATA_CHANNEL_NAME = "signaling"
|
||||
}
|
||||
|
||||
|
@ -91,8 +96,9 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
val videoEvents = _videoEvents.asSharedFlow()
|
||||
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false))
|
||||
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
|
||||
private val _connectionEvents = MutableStateFlow<StateEvent>(CallStateUpdate(CallState.STATE_IDLE))
|
||||
val connectionEvents = _connectionEvents.asSharedFlow()
|
||||
|
||||
private val stateProcessor = StateProcessor(CallState.Idle)
|
||||
|
||||
private val _callStateEvents = MutableStateFlow(CallViewModel.State.CALL_PENDING)
|
||||
val callStateEvents = _callStateEvents.asSharedFlow()
|
||||
private val _recipientEvents = MutableStateFlow(RecipientUpdate.UNKNOWN)
|
||||
|
@ -103,7 +109,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
val audioDeviceEvents = _audioDeviceEvents.asSharedFlow()
|
||||
|
||||
val currentConnectionState
|
||||
get() = (_connectionEvents.value as CallStateUpdate).state
|
||||
get() = stateProcessor.currentState
|
||||
|
||||
val currentCallState
|
||||
get() = _callStateEvents.value
|
||||
|
@ -133,6 +139,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
private val outgoingIceDebouncer = Debouncer(200L)
|
||||
|
||||
var localRenderer: SurfaceViewRenderer? = null
|
||||
var remoteRotationSink: RemoteRotationVideoProxySink? = null
|
||||
var remoteRenderer: SurfaceViewRenderer? = null
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
|
||||
|
@ -156,20 +163,25 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger)
|
||||
}
|
||||
|
||||
fun postConnectionEvent(newState: CallState) {
|
||||
_connectionEvents.value = CallStateUpdate(newState)
|
||||
fun postConnectionEvent(transition: Event, onSuccess: ()->Unit): Boolean {
|
||||
return stateProcessor.processEvent(transition, onSuccess)
|
||||
}
|
||||
|
||||
fun postConnectionError(): Boolean {
|
||||
return stateProcessor.processEvent(Event.Error)
|
||||
}
|
||||
|
||||
fun postViewModelState(newState: CallViewModel.State) {
|
||||
Log.d("Loki", "Posting view model state $newState")
|
||||
_callStateEvents.value = newState
|
||||
}
|
||||
|
||||
fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.STATE_IDLE
|
||||
fun isBusy(context: Context, callId: UUID) = callId != this.callId && (currentConnectionState != CallState.Idle
|
||||
|| context.getSystemService(TelephonyManager::class.java).callState != TelephonyManager.CALL_STATE_IDLE)
|
||||
|
||||
fun isPreOffer() = currentConnectionState == CallState.STATE_PRE_OFFER
|
||||
fun isPreOffer() = currentConnectionState == CallState.RemotePreOffer
|
||||
|
||||
fun isIdle() = currentConnectionState == CallState.STATE_IDLE
|
||||
fun isIdle() = currentConnectionState == CallState.Idle
|
||||
|
||||
fun isCurrentUser(recipient: Recipient) = recipient.address.serialize() == storage.getUserPublicKey()
|
||||
|
||||
|
@ -177,12 +189,20 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
Util.runOnMainSync {
|
||||
val base = EglBase.create()
|
||||
eglBase = base
|
||||
localRenderer = SurfaceViewRenderer(context)
|
||||
remoteRenderer = SurfaceViewRenderer(context)
|
||||
localRenderer = SurfaceViewRenderer(context).apply {
|
||||
// setScalingType(SCALE_ASPECT_FIT)
|
||||
}
|
||||
|
||||
remoteRenderer = SurfaceViewRenderer(context).apply {
|
||||
// setScalingType(SCALE_ASPECT_FIT)
|
||||
}
|
||||
remoteRotationSink = RemoteRotationVideoProxySink()
|
||||
|
||||
|
||||
localRenderer?.init(base.eglBaseContext, null)
|
||||
localRenderer?.setMirror(true)
|
||||
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
|
||||
remoteRenderer?.init(base.eglBaseContext, null)
|
||||
remoteRotationSink!!.setSink(remoteRenderer!!)
|
||||
|
||||
val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
|
||||
val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext)
|
||||
|
@ -205,7 +225,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
}
|
||||
|
||||
fun setAudioEnabled(isEnabled: Boolean) {
|
||||
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
|
||||
currentConnectionState.withState(*CallState.CAN_HANGUP_STATES) {
|
||||
peerConnection?.setAudioEnabled(isEnabled)
|
||||
_audioEvents.value = AudioEnabled(true)
|
||||
}
|
||||
|
@ -216,6 +236,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
}
|
||||
|
||||
override fun onIceConnectionChange(newState: IceConnectionState) {
|
||||
Log.d("Loki", "New ice connection state = $newState")
|
||||
iceState = newState
|
||||
peerConnectionObservers.forEach { listener -> listener.onIceConnectionChange(newState) }
|
||||
if (newState == IceConnectionState.CONNECTED) {
|
||||
|
@ -279,7 +300,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
if (stream.videoTracks != null && stream.videoTracks.size == 1) {
|
||||
val videoTrack = stream.videoTracks.first()
|
||||
videoTrack.setEnabled(true)
|
||||
videoTrack.addSink(remoteRenderer)
|
||||
videoTrack.addSink(remoteRotationSink)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,75 +349,89 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
_audioDeviceEvents.value = AudioDeviceUpdate(activeDevice, devices)
|
||||
}
|
||||
|
||||
private fun CallState.withState(vararg expected: CallState, transition: () -> Unit) {
|
||||
if (this in expected) transition()
|
||||
else Log.w(TAG,"Tried to transition state $this but expected $expected")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
signalAudioManager.handleCommand(AudioManagerCommand.Stop(currentConnectionState in OUTGOING_STATES))
|
||||
peerConnection?.dispose()
|
||||
peerConnection = null
|
||||
val isOutgoing = currentConnectionState in CallState.OUTGOING_STATES
|
||||
stateProcessor.processEvent(Event.Cleanup) {
|
||||
signalAudioManager.handleCommand(AudioManagerCommand.Stop(isOutgoing))
|
||||
peerConnection?.dispose()
|
||||
peerConnection = null
|
||||
|
||||
localRenderer?.release()
|
||||
remoteRenderer?.release()
|
||||
eglBase?.release()
|
||||
localRenderer?.release()
|
||||
remoteRotationSink?.release()
|
||||
remoteRenderer?.release()
|
||||
eglBase?.release()
|
||||
|
||||
localRenderer = null
|
||||
remoteRenderer = null
|
||||
eglBase = null
|
||||
localRenderer = null
|
||||
remoteRenderer = null
|
||||
eglBase = null
|
||||
|
||||
_connectionEvents.value = CallStateUpdate(CallState.STATE_IDLE)
|
||||
localCameraState = CameraState.UNKNOWN
|
||||
recipient = null
|
||||
callId = null
|
||||
pendingOfferTime = -1
|
||||
pendingOffer = null
|
||||
callStartTime = -1
|
||||
_audioEvents.value = AudioEnabled(false)
|
||||
_videoEvents.value = VideoEnabled(false)
|
||||
_remoteVideoEvents.value = VideoEnabled(false)
|
||||
pendingOutgoingIceUpdates.clear()
|
||||
pendingIncomingIceUpdates.clear()
|
||||
localCameraState = CameraState.UNKNOWN
|
||||
recipient = null
|
||||
callId = null
|
||||
pendingOfferTime = -1
|
||||
pendingOffer = null
|
||||
callStartTime = -1
|
||||
_audioEvents.value = AudioEnabled(false)
|
||||
_videoEvents.value = VideoEnabled(false)
|
||||
_remoteVideoEvents.value = VideoEnabled(false)
|
||||
pendingOutgoingIceUpdates.clear()
|
||||
pendingIncomingIceUpdates.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
||||
localCameraState = newCameraState
|
||||
}
|
||||
|
||||
fun onPreOffer(callId: UUID, recipient: Recipient, sentTimestamp: Long) {
|
||||
if (preOfferCallData != null) {
|
||||
Log.d(TAG, "Received new pre-offer when we are already expecting one")
|
||||
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
|
||||
stateProcessor.processEvent(Event.ReceivePreOffer) {
|
||||
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)
|
||||
onSuccess()
|
||||
}
|
||||
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"))
|
||||
|
||||
val connection = peerConnection ?: return Promise.ofFail(NullPointerException("No peer connection"))
|
||||
val connection = peerConnection ?: return Promise.ofFail(NullPointerException("No peer connection wrapper"))
|
||||
|
||||
connection.setNewOffer(SessionDescription(SessionDescription.Type.OFFER, offer))
|
||||
val answer = connection.createAnswer(MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||
})
|
||||
val answerMessage = CallMessage.answer(answer.description, callId)
|
||||
return MessageSender.sendNonDurably(answerMessage, recipient.address)
|
||||
val reconnected = stateProcessor.processEvent(Event.ReceiveOffer) && stateProcessor.processEvent(Event.SendAnswer)
|
||||
return if (reconnected) {
|
||||
Log.i("Loki", "Handling new offer, restarting ice session")
|
||||
connection.setNewRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, offer))
|
||||
// re-established an ice
|
||||
val answer = connection.createAnswer(MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||
})
|
||||
connection.setLocalDescription(answer)
|
||||
pendingIncomingIceUpdates.toList().forEach { update ->
|
||||
connection.addIceCandidate(update)
|
||||
}
|
||||
pendingIncomingIceUpdates.clear()
|
||||
val answerMessage = CallMessage.answer(answer.description, callId)
|
||||
Log.i("Loki", "Posting new answer")
|
||||
MessageSender.sendNonDurably(answerMessage, recipient.address)
|
||||
} else {
|
||||
Promise.ofFail(Exception("Couldn't reconnect from current state"))
|
||||
}
|
||||
}
|
||||
|
||||
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long) {
|
||||
if (currentConnectionState !in arrayOf(CallState.STATE_IDLE, CallState.STATE_PRE_OFFER)) return
|
||||
|
||||
this.callId = callId
|
||||
this.recipient = recipient
|
||||
this.pendingOffer = offer
|
||||
this.pendingOfferTime = callTime
|
||||
initializeAudioForCall()
|
||||
startIncomingRinger()
|
||||
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long, onSuccess: () -> Unit) {
|
||||
postConnectionEvent(Event.ReceiveOffer) {
|
||||
this.callId = callId
|
||||
this.recipient = recipient
|
||||
this.pendingOffer = offer
|
||||
this.pendingOfferTime = callTime
|
||||
initializeAudioForCall()
|
||||
startIncomingRinger()
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise<Unit, Exception> {
|
||||
|
@ -453,7 +488,12 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
?: return Promise.ofFail(NullPointerException("localRenderer is null"))
|
||||
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
|
||||
|
||||
val connection = PeerConnectionWrapper(
|
||||
val sentOffer = stateProcessor.processEvent(Event.SendOffer)
|
||||
|
||||
if (!sentOffer) {
|
||||
return Promise.ofFail(Exception("Couldn't transition to sent offer state"))
|
||||
} else {
|
||||
val connection = PeerConnectionWrapper(
|
||||
context,
|
||||
factory,
|
||||
this,
|
||||
|
@ -461,25 +501,24 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
this,
|
||||
base,
|
||||
isAlwaysTurn
|
||||
)
|
||||
)
|
||||
|
||||
peerConnection = connection
|
||||
localCameraState = connection.getCameraState()
|
||||
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
|
||||
dataChannel.registerObserver(this)
|
||||
this.dataChannel = dataChannel
|
||||
val offer = connection.createOffer(MediaConstraints())
|
||||
connection.setLocalDescription(offer)
|
||||
peerConnection = connection
|
||||
localCameraState = connection.getCameraState()
|
||||
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
|
||||
dataChannel.registerObserver(this)
|
||||
this.dataChannel = dataChannel
|
||||
val offer = connection.createOffer(MediaConstraints())
|
||||
connection.setLocalDescription(offer)
|
||||
|
||||
Log.i(TAG, "Sending offer: ${offer.description}")
|
||||
|
||||
return MessageSender.sendNonDurably(CallMessage.preOffer(
|
||||
return MessageSender.sendNonDurably(CallMessage.preOffer(
|
||||
callId
|
||||
), recipient.address).bind {
|
||||
MessageSender.sendNonDurably(CallMessage.offer(
|
||||
), recipient.address).bind {
|
||||
MessageSender.sendNonDurably(CallMessage.offer(
|
||||
offer.description,
|
||||
callId
|
||||
), recipient.address)
|
||||
), recipient.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,9 +529,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
val callId = callId ?: return
|
||||
val recipient = recipient ?: return
|
||||
val userAddress = storage.getUserPublicKey() ?: return
|
||||
MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress))
|
||||
MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
|
||||
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED)
|
||||
stateProcessor.processEvent(Event.DeclineCall) {
|
||||
MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress))
|
||||
MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
|
||||
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleLocalHangup(intentRecipient: Recipient?) {
|
||||
|
@ -503,6 +544,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
val sendHangup = intentRecipient == null || (intentRecipient == recipient && recipient.address.serialize() != currentUserPublicKey)
|
||||
|
||||
postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
|
||||
stateProcessor.processEvent(Event.Hangup)
|
||||
if (sendHangup) {
|
||||
dataChannel?.let { channel ->
|
||||
val buffer = DataChannel.Buffer(ByteBuffer.wrap(HANGUP_JSON.toString().encodeToByteArray()), false)
|
||||
|
@ -518,10 +560,14 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
|
||||
fun handleRemoteHangup() {
|
||||
when (currentConnectionState) {
|
||||
CallState.STATE_DIALING,
|
||||
CallState.STATE_REMOTE_RINGING -> postViewModelState(CallViewModel.State.RECIPIENT_UNAVAILABLE)
|
||||
CallState.LocalRing,
|
||||
CallState.RemoteRing -> postViewModelState(CallViewModel.State.RECIPIENT_UNAVAILABLE)
|
||||
else -> postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
|
||||
}
|
||||
if (!stateProcessor.processEvent(Event.Hangup)) {
|
||||
Log.e("Loki", "")
|
||||
stateProcessor.processEvent(Event.Error)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetMuteAudio(muted: Boolean) {
|
||||
|
@ -539,7 +585,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
channel.send(buffer)
|
||||
}
|
||||
|
||||
if (currentConnectionState == CallState.STATE_CONNECTED) {
|
||||
if (currentConnectionState == CallState.Connected) {
|
||||
if (connection.isVideoEnabled()) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO)
|
||||
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
|
||||
}
|
||||
|
@ -558,13 +604,19 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
peerConnection?.let { connection ->
|
||||
connection.flipCamera()
|
||||
localCameraState = connection.getCameraState()
|
||||
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDeviceRotation(newRotation: Int) {
|
||||
peerConnection?.setDeviceRotation(newRotation)
|
||||
remoteRotationSink?.rotation = newRotation
|
||||
}
|
||||
|
||||
fun handleWiredHeadsetChanged(present: Boolean) {
|
||||
if (currentConnectionState in arrayOf(CallState.STATE_CONNECTED,
|
||||
CallState.STATE_DIALING,
|
||||
CallState.STATE_REMOTE_RINGING)) {
|
||||
if (currentConnectionState in arrayOf(CallState.Connected,
|
||||
CallState.LocalRing,
|
||||
CallState.RemoteRing)) {
|
||||
if (present && signalAudioManager.isSpeakerphoneOn()) {
|
||||
signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.WIRED_HEADSET))
|
||||
} else if (!present && !signalAudioManager.isSpeakerphoneOn() && !signalAudioManager.isBluetoothScoOn() && localCameraState.enabled) {
|
||||
|
@ -574,24 +626,26 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
}
|
||||
|
||||
fun handleScreenOffChange() {
|
||||
if (currentConnectionState in arrayOf(CallState.STATE_ANSWERING, CallState.STATE_LOCAL_RINGING)) {
|
||||
if (currentConnectionState in arrayOf(CallState.Connecting, CallState.LocalRing)) {
|
||||
signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) {
|
||||
if (currentConnectionState !in arrayOf(CallState.STATE_DIALING, CallState.STATE_CONNECTED) || recipient != this.recipient || callId != this.callId) {
|
||||
if (recipient != this.recipient || callId != this.callId) {
|
||||
Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing")
|
||||
return
|
||||
}
|
||||
|
||||
val connection = peerConnection ?: throw AssertionError("assert")
|
||||
stateProcessor.processEvent(Event.ReceiveAnswer) {
|
||||
val connection = peerConnection ?: throw AssertionError("assert")
|
||||
|
||||
connection.setRemoteDescription(answer)
|
||||
while (pendingIncomingIceUpdates.isNotEmpty()) {
|
||||
connection.addIceCandidate(pendingIncomingIceUpdates.pop())
|
||||
connection.setRemoteDescription(answer)
|
||||
while (pendingIncomingIceUpdates.isNotEmpty()) {
|
||||
connection.addIceCandidate(pendingIncomingIceUpdates.pop())
|
||||
}
|
||||
queueOutgoingIce(callId, recipient)
|
||||
}
|
||||
queueOutgoingIce(callId, recipient)
|
||||
}
|
||||
|
||||
fun handleRemoteIceCandidate(iceCandidates: List<IceCandidate>, callId: UUID) {
|
||||
|
@ -601,11 +655,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
}
|
||||
|
||||
val connection = peerConnection
|
||||
if (connection != null && connection.readyForIce) {
|
||||
if (connection != null && connection.readyForIce && currentConnectionState != CallState.Reconnecting) {
|
||||
Log.i("Loki", "Handling connection ice candidate")
|
||||
iceCandidates.forEach { candidate ->
|
||||
connection.addIceCandidate(candidate)
|
||||
}
|
||||
} else {
|
||||
Log.i("Loki", "Handling add to pending ice candidate")
|
||||
pendingIncomingIceUpdates.addAll(iceCandidates)
|
||||
}
|
||||
}
|
||||
|
@ -637,20 +693,22 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
|
|||
val callId = callId ?: return
|
||||
val recipient = recipient ?: return
|
||||
|
||||
if (isReestablishing) return
|
||||
isReestablishing = true
|
||||
Log.d("Loki", "start re-establish")
|
||||
postConnectionEvent(Event.NetworkReconnect) {
|
||||
Log.d("Loki", "start re-establish")
|
||||
|
||||
val offer = connection.createOffer(MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||
})
|
||||
connection.setLocalDescription(offer)
|
||||
val offer = connection.createNewOffer(MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||
})
|
||||
connection.setLocalDescription(offer)
|
||||
|
||||
MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address).success {
|
||||
isReestablishing = false
|
||||
MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId), recipient.address)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetPeerConnection() {
|
||||
peerConnection?.resetPeerConnection()
|
||||
}
|
||||
|
||||
interface WebRtcListener: PeerConnection.Observer {
|
||||
fun onHangup()
|
||||
}
|
||||
|
|
|
@ -14,7 +14,13 @@ import org.session.libsession.messaging.messages.control.CallMessage
|
|||
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.*
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.END_CALL
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
|
||||
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||
import org.thoughtcrime.securesms.util.CallNotificationBuilder
|
||||
|
@ -29,7 +35,9 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
|||
val nextMessage = WebRtcUtils.SIGNAL_QUEUE.receive()
|
||||
Log.d("Loki", nextMessage.type?.name ?: "CALL MESSAGE RECEIVED")
|
||||
val sender = nextMessage.sender ?: continue
|
||||
if (!storage.conversationHasOutgoing(sender) && storage.getUserPublicKey() != sender) continue
|
||||
val approvedContact = Recipient.from(context, Address.fromSerialized(sender), false).isApproved
|
||||
Log.i("Loki", "Contact is approved?: $approvedContact")
|
||||
if (!approvedContact && storage.getUserPublicKey() != sender) continue
|
||||
|
||||
if (!textSecurePreferences.isCallNotificationsEnabled()) {
|
||||
Log.d("Loki","Dropping call message if call notifications disabled")
|
||||
|
@ -49,7 +57,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
|
|||
END_CALL -> incomingHangup(nextMessage)
|
||||
ICE_CANDIDATES -> handleIceCandidates(nextMessage)
|
||||
PRE_OFFER -> incomingPreOffer(nextMessage)
|
||||
PROVISIONAL_ANSWER -> {} // TODO: if necessary
|
||||
PROVISIONAL_ANSWER, null -> {} // TODO: if necessary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
|||
CALL_RINGING,
|
||||
CALL_BUSY,
|
||||
CALL_DISCONNECTED,
|
||||
CALL_RECONNECTING,
|
||||
|
||||
NETWORK_FAILURE,
|
||||
RECIPIENT_UNAVAILABLE,
|
||||
|
@ -66,6 +67,12 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
|||
val remoteVideoEnabledState
|
||||
get() = callManager.remoteVideoEvents.map { it.isEnabled }
|
||||
|
||||
var deviceRotation: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
callManager.setDeviceRotation(value)
|
||||
}
|
||||
|
||||
val currentCallState
|
||||
get() = callManager.currentCallState
|
||||
|
||||
|
|
|
@ -6,8 +6,6 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.util.SparseArray
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class NetworkChangeReceiver(private val onNetworkChangedCallback: (Boolean)->Unit) {
|
||||
|
@ -20,58 +18,58 @@ class NetworkChangeReceiver(private val onNetworkChangedCallback: (Boolean)->Uni
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkNetworks() {
|
||||
|
||||
}
|
||||
|
||||
val defaultObserver = object: ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.d("Loki", "onAvailable: $network")
|
||||
Log.i("Loki", "onAvailable: $network")
|
||||
networkList += network
|
||||
onNetworkChangedCallback(networkList.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun onLosing(network: Network, maxMsToLive: Int) {
|
||||
Log.d("Loki", "onLosing: $network, maxMsToLive: $maxMsToLive")
|
||||
Log.i("Loki", "onLosing: $network, maxMsToLive: $maxMsToLive")
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d("Loki", "onLost: $network")
|
||||
Log.i("Loki", "onLost: $network")
|
||||
networkList -= network
|
||||
onNetworkChangedCallback(networkList.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun onUnavailable() {
|
||||
Log.d("Loki", "onUnavailable")
|
||||
Log.i("Loki", "onUnavailable")
|
||||
}
|
||||
}
|
||||
|
||||
fun receiveBroadcast(context: Context, intent: Intent) {
|
||||
Log.d("Loki", intent.toString())
|
||||
onNetworkChangedCallback(context.isConnected())
|
||||
val connected = context.isConnected()
|
||||
Log.i("Loki", "received broadcast, network connected: $connected")
|
||||
onNetworkChangedCallback(connected)
|
||||
}
|
||||
|
||||
fun Context.isConnected() : Boolean {
|
||||
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
return cm.activeNetworkInfo?.isConnected ?: false
|
||||
return cm.activeNetwork != null
|
||||
}
|
||||
|
||||
fun register(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
cm.registerDefaultNetworkCallback(defaultObserver)
|
||||
} else {
|
||||
val intentFilter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")
|
||||
context.registerReceiver(broadcastDelegate, intentFilter)
|
||||
}
|
||||
val intentFilter = IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")
|
||||
context.registerReceiver(broadcastDelegate, intentFilter)
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
// cm.registerDefaultNetworkCallback(defaultObserver)
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
fun unregister(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
cm.unregisterNetworkCallback(defaultObserver)
|
||||
} else {
|
||||
context.unregisterReceiver(broadcastDelegate)
|
||||
}
|
||||
context.unregisterReceiver(broadcastDelegate)
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
// cm.unregisterNetworkCallback(defaultObserver)
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,42 +1,63 @@
|
|||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.SettableFuture
|
||||
import org.thoughtcrime.securesms.webrtc.video.Camera
|
||||
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
|
||||
import org.thoughtcrime.securesms.webrtc.video.CameraState
|
||||
import org.webrtc.*
|
||||
import org.thoughtcrime.securesms.webrtc.video.RotationVideoSink
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.DataChannel
|
||||
import org.webrtc.EglBase
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.MediaConstraints
|
||||
import org.webrtc.MediaStream
|
||||
import org.webrtc.PeerConnection
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import org.webrtc.SdpObserver
|
||||
import org.webrtc.SessionDescription
|
||||
import org.webrtc.SurfaceTextureHelper
|
||||
import org.webrtc.VideoSink
|
||||
import org.webrtc.VideoSource
|
||||
import org.webrtc.VideoTrack
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.random.asKotlinRandom
|
||||
|
||||
class PeerConnectionWrapper(context: Context,
|
||||
factory: PeerConnectionFactory,
|
||||
observer: PeerConnection.Observer,
|
||||
localRenderer: VideoSink,
|
||||
cameraEventListener: CameraEventListener,
|
||||
eglBase: EglBase,
|
||||
relay: Boolean = false) {
|
||||
class PeerConnectionWrapper(private val context: Context,
|
||||
private val factory: PeerConnectionFactory,
|
||||
private val observer: PeerConnection.Observer,
|
||||
private val localRenderer: VideoSink,
|
||||
private val cameraEventListener: CameraEventListener,
|
||||
private val eglBase: EglBase,
|
||||
private val relay: Boolean = false): CameraEventListener {
|
||||
|
||||
private val peerConnection: PeerConnection
|
||||
private var peerConnection: PeerConnection? = null
|
||||
private val audioTrack: AudioTrack
|
||||
private val audioSource: AudioSource
|
||||
private val camera: Camera
|
||||
private val mediaStream: MediaStream
|
||||
private val videoSource: VideoSource?
|
||||
private val videoTrack: VideoTrack?
|
||||
private val rotationVideoSink = RotationVideoSink()
|
||||
|
||||
val readyForIce
|
||||
get() = peerConnection.localDescription != null && peerConnection.remoteDescription != null
|
||||
get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null
|
||||
|
||||
init {
|
||||
val iceServers = listOf("freyr","fenrir","frigg","angus","hereford","holstein","brahman").map { sub ->
|
||||
PeerConnection.IceServer.builder("turn:$sub.getsession.org").setUsername("session202111").setPassword("053c268164bc7bd7").createIceServer()
|
||||
private fun initPeerConnection() {
|
||||
val random = SecureRandom().asKotlinRandom()
|
||||
val iceServers = listOf("freyr","fenrir","frigg","angus","hereford","holstein", "brahman").shuffled(random).take(2).map { sub ->
|
||||
PeerConnection.IceServer.builder("turn:$sub.getsession.org")
|
||||
.setUsername("session202111")
|
||||
.setPassword("053c268164bc7bd7")
|
||||
.createIceServer()
|
||||
}
|
||||
|
||||
val constraints = MediaConstraints().apply {
|
||||
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
|
||||
}
|
||||
val audioConstraints = MediaConstraints().apply {
|
||||
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
|
||||
}
|
||||
|
||||
val configuration = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
|
||||
|
@ -46,36 +67,49 @@ class PeerConnectionWrapper(context: Context,
|
|||
}
|
||||
}
|
||||
|
||||
peerConnection = factory.createPeerConnection(configuration, constraints, observer)!!
|
||||
peerConnection.setAudioPlayout(true)
|
||||
peerConnection.setAudioRecording(true)
|
||||
val newPeerConnection = factory.createPeerConnection(configuration, constraints, observer)!!
|
||||
peerConnection = newPeerConnection
|
||||
newPeerConnection.setAudioPlayout(true)
|
||||
newPeerConnection.setAudioRecording(true)
|
||||
|
||||
val mediaStream = factory.createLocalMediaStream("ARDAMS")
|
||||
newPeerConnection.addStream(mediaStream)
|
||||
}
|
||||
|
||||
init {
|
||||
val audioConstraints = MediaConstraints().apply {
|
||||
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
|
||||
}
|
||||
|
||||
mediaStream = factory.createLocalMediaStream("ARDAMS")
|
||||
audioSource = factory.createAudioSource(audioConstraints)
|
||||
audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource)
|
||||
audioTrack.setEnabled(true)
|
||||
mediaStream.addTrack(audioTrack)
|
||||
|
||||
camera = Camera(context, cameraEventListener)
|
||||
if (camera.capturer != null) {
|
||||
videoSource = factory.createVideoSource(false)
|
||||
videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource)
|
||||
val newCamera = Camera(context, this)
|
||||
camera = newCamera
|
||||
|
||||
camera.capturer.initialize(
|
||||
SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.eglBaseContext),
|
||||
context,
|
||||
videoSource.capturerObserver
|
||||
if (newCamera.capturer != null) {
|
||||
val newVideoSource = factory.createVideoSource(false)
|
||||
videoSource = newVideoSource
|
||||
val newVideoTrack = factory.createVideoTrack("ARDAMSv0", newVideoSource)
|
||||
videoTrack = newVideoTrack
|
||||
|
||||
rotationVideoSink.setObserver(newVideoSource.capturerObserver)
|
||||
newCamera.capturer.initialize(
|
||||
SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.eglBaseContext),
|
||||
context,
|
||||
rotationVideoSink
|
||||
)
|
||||
|
||||
videoTrack.addSink(localRenderer)
|
||||
videoTrack.setEnabled(false)
|
||||
mediaStream.addTrack(videoTrack)
|
||||
rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT
|
||||
rotationVideoSink.setSink(localRenderer)
|
||||
newVideoTrack.setEnabled(false)
|
||||
mediaStream.addTrack(newVideoTrack)
|
||||
} else {
|
||||
videoSource = null
|
||||
videoTrack = null
|
||||
}
|
||||
|
||||
peerConnection.addStream(mediaStream)
|
||||
initPeerConnection()
|
||||
}
|
||||
|
||||
fun getCameraState(): CameraState {
|
||||
|
@ -88,12 +122,12 @@ class PeerConnectionWrapper(context: Context,
|
|||
negotiated = true
|
||||
id = 548
|
||||
}
|
||||
return peerConnection.createDataChannel(channelName, dataChannelConfiguration)
|
||||
return peerConnection!!.createDataChannel(channelName, dataChannelConfiguration)
|
||||
}
|
||||
|
||||
fun addIceCandidate(candidate: IceCandidate) {
|
||||
// TODO: filter logic based on known servers
|
||||
peerConnection.addIceCandidate(candidate)
|
||||
peerConnection!!.addIceCandidate(candidate)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
|
@ -102,14 +136,14 @@ class PeerConnectionWrapper(context: Context,
|
|||
videoSource?.dispose()
|
||||
|
||||
audioSource.dispose()
|
||||
peerConnection.close()
|
||||
peerConnection.dispose()
|
||||
peerConnection?.close()
|
||||
peerConnection?.dispose()
|
||||
}
|
||||
|
||||
fun setNewOffer(description: SessionDescription) {
|
||||
fun setNewRemoteDescription(description: SessionDescription) {
|
||||
val future = SettableFuture<Boolean>()
|
||||
|
||||
peerConnection.setRemoteDescription(object: SdpObserver {
|
||||
peerConnection!!.setRemoteDescription(object: SdpObserver {
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||
throw AssertionError()
|
||||
}
|
||||
|
@ -139,7 +173,7 @@ class PeerConnectionWrapper(context: Context,
|
|||
fun setRemoteDescription(description: SessionDescription) {
|
||||
val future = SettableFuture<Boolean>()
|
||||
|
||||
peerConnection.setRemoteDescription(object: SdpObserver {
|
||||
peerConnection!!.setRemoteDescription(object: SdpObserver {
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||
throw AssertionError()
|
||||
}
|
||||
|
@ -169,7 +203,7 @@ class PeerConnectionWrapper(context: Context,
|
|||
fun createAnswer(mediaConstraints: MediaConstraints) : SessionDescription {
|
||||
val future = SettableFuture<SessionDescription>()
|
||||
|
||||
peerConnection.createAnswer(object:SdpObserver {
|
||||
peerConnection!!.createAnswer(object:SdpObserver {
|
||||
override fun onCreateSuccess(sdp: SessionDescription?) {
|
||||
future.set(sdp)
|
||||
}
|
||||
|
@ -203,10 +237,40 @@ class PeerConnectionWrapper(context: Context,
|
|||
return SessionDescription(sessionDescription.type, updatedSdp)
|
||||
}
|
||||
|
||||
fun createNewOffer(mediaConstraints: MediaConstraints): SessionDescription {
|
||||
val future = SettableFuture<SessionDescription>()
|
||||
|
||||
peerConnection!!.createOffer(object:SdpObserver {
|
||||
override fun onCreateSuccess(sdp: SessionDescription?) {
|
||||
future.set(sdp)
|
||||
}
|
||||
|
||||
override fun onSetSuccess() {
|
||||
throw AssertionError()
|
||||
}
|
||||
|
||||
override fun onCreateFailure(p0: String?) {
|
||||
future.setException(PeerConnectionException(p0))
|
||||
}
|
||||
|
||||
override fun onSetFailure(p0: String?) {
|
||||
throw AssertionError()
|
||||
}
|
||||
}, mediaConstraints)
|
||||
|
||||
try {
|
||||
return correctSessionDescription(future.get())
|
||||
} catch (e: InterruptedException) {
|
||||
throw AssertionError()
|
||||
} catch (e: ExecutionException) {
|
||||
throw PeerConnectionException(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun createOffer(mediaConstraints: MediaConstraints): SessionDescription {
|
||||
val future = SettableFuture<SessionDescription>()
|
||||
|
||||
peerConnection.createOffer(object:SdpObserver {
|
||||
peerConnection!!.createOffer(object:SdpObserver {
|
||||
override fun onCreateSuccess(sdp: SessionDescription?) {
|
||||
future.set(sdp)
|
||||
}
|
||||
|
@ -236,7 +300,7 @@ class PeerConnectionWrapper(context: Context,
|
|||
fun setLocalDescription(sdp: SessionDescription) {
|
||||
val future = SettableFuture<Boolean>()
|
||||
|
||||
peerConnection.setLocalDescription(object: SdpObserver {
|
||||
peerConnection!!.setLocalDescription(object: SdpObserver {
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||
|
||||
}
|
||||
|
@ -262,14 +326,19 @@ class PeerConnectionWrapper(context: Context,
|
|||
}
|
||||
|
||||
fun setCommunicationMode() {
|
||||
peerConnection.setAudioPlayout(true)
|
||||
peerConnection.setAudioRecording(true)
|
||||
peerConnection?.setAudioPlayout(true)
|
||||
peerConnection?.setAudioRecording(true)
|
||||
}
|
||||
|
||||
fun setAudioEnabled(isEnabled: Boolean) {
|
||||
audioTrack.setEnabled(isEnabled)
|
||||
}
|
||||
|
||||
fun setDeviceRotation(rotation: Int) {
|
||||
Log.d("Loki", "rotation: $rotation")
|
||||
rotationVideoSink.rotation = rotation
|
||||
}
|
||||
|
||||
fun setVideoEnabled(isEnabled: Boolean) {
|
||||
videoTrack?.let { track ->
|
||||
track.setEnabled(isEnabled)
|
||||
|
@ -283,4 +352,14 @@ class PeerConnectionWrapper(context: Context,
|
|||
camera.flip()
|
||||
}
|
||||
|
||||
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
|
||||
// mirror rotation offset
|
||||
rotationVideoSink.mirrored = newCameraState.activeDirection == CameraState.Direction.FRONT
|
||||
cameraEventListener.onCameraSwitchCompleted(newCameraState)
|
||||
}
|
||||
|
||||
fun resetPeerConnection() {
|
||||
peerConnection?.close()
|
||||
initPeerConnection()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.thoughtcrime.securesms.webrtc.data
|
||||
|
||||
// get the video rotation from a specific rotation, locked into 90 degree
|
||||
// chunks offset by 45 degrees
|
||||
fun Int.quadrantRotation() = when (this % 360) {
|
||||
in 315 .. 360,
|
||||
in 0 until 45 -> 0
|
||||
in 45 until 135 -> 90
|
||||
in 135 until 225 -> 180
|
||||
else -> 270
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package org.thoughtcrime.securesms.webrtc.data
|
||||
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.webrtc.data.State.Companion.CAN_DECLINE_STATES
|
||||
import org.thoughtcrime.securesms.webrtc.data.State.Companion.CAN_HANGUP_STATES
|
||||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
object RemotePreOffer : State()
|
||||
object RemoteRing : State()
|
||||
object LocalPreOffer : State()
|
||||
object LocalRing : State()
|
||||
object Connecting : State()
|
||||
object Connected : State()
|
||||
object Reconnecting : State()
|
||||
object PendingReconnect : State()
|
||||
object Disconnected : State()
|
||||
companion object {
|
||||
|
||||
val ALL_STATES = arrayOf(
|
||||
Idle, RemotePreOffer, RemoteRing, LocalPreOffer, LocalRing,
|
||||
Connecting, Connected, Reconnecting, Disconnected
|
||||
)
|
||||
|
||||
val CAN_DECLINE_STATES = arrayOf(RemotePreOffer, RemoteRing)
|
||||
val PENDING_CONNECTION_STATES = arrayOf(
|
||||
LocalPreOffer,
|
||||
LocalRing,
|
||||
RemotePreOffer,
|
||||
RemoteRing,
|
||||
Connecting,
|
||||
)
|
||||
val OUTGOING_STATES = arrayOf(
|
||||
LocalPreOffer,
|
||||
LocalRing,
|
||||
)
|
||||
val CAN_HANGUP_STATES =
|
||||
arrayOf(
|
||||
RemotePreOffer,
|
||||
RemoteRing,
|
||||
LocalPreOffer,
|
||||
LocalRing,
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting
|
||||
)
|
||||
val CAN_RECEIVE_ICE_STATES =
|
||||
arrayOf(RemoteRing, LocalRing, Connecting, Connected, Reconnecting)
|
||||
}
|
||||
|
||||
fun withState(vararg expectedState: State, body: () -> Unit) {
|
||||
if (this in expectedState) {
|
||||
body()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class Event(vararg val expectedStates: State, val outputState: State) {
|
||||
object ReceivePreOffer :
|
||||
Event(State.Idle, outputState = State.RemotePreOffer)
|
||||
|
||||
object ReceiveOffer :
|
||||
Event(State.RemotePreOffer, State.Reconnecting, outputState = State.RemoteRing)
|
||||
|
||||
object SendPreOffer : Event(State.Idle, outputState = State.LocalPreOffer)
|
||||
object SendOffer : Event(State.LocalPreOffer, outputState = State.LocalRing)
|
||||
object SendAnswer : Event(State.RemoteRing, outputState = State.Connecting)
|
||||
object ReceiveAnswer :
|
||||
Event(State.LocalRing, State.Reconnecting, outputState = State.Connecting)
|
||||
|
||||
object Connect : Event(State.Connecting, outputState = State.Connected)
|
||||
object IceFailed : Event(State.Connecting, outputState = State.Disconnected)
|
||||
object IceDisconnect : Event(State.Connected, outputState = State.PendingReconnect)
|
||||
object NetworkReconnect : Event(State.PendingReconnect, outputState = State.Reconnecting)
|
||||
object PrepareForNewOffer : Event(State.PendingReconnect, outputState = State.Reconnecting)
|
||||
object TimeOut :
|
||||
Event(
|
||||
State.Connecting,
|
||||
State.LocalRing,
|
||||
State.RemoteRing,
|
||||
State.Reconnecting,
|
||||
outputState = State.Disconnected
|
||||
)
|
||||
|
||||
object Error : Event(*State.ALL_STATES, outputState = State.Disconnected)
|
||||
object DeclineCall : Event(*CAN_DECLINE_STATES, outputState = State.Disconnected)
|
||||
object Hangup : Event(*CAN_HANGUP_STATES, outputState = State.Disconnected)
|
||||
object Cleanup : Event(State.Disconnected, outputState = State.Idle)
|
||||
}
|
||||
|
||||
open class StateProcessor(initialState: State) {
|
||||
private var _currentState: State = initialState
|
||||
val currentState get() = _currentState
|
||||
|
||||
open fun processEvent(event: Event, sideEffect: () -> Unit = {}): Boolean {
|
||||
if (currentState in event.expectedStates) {
|
||||
Log.i(
|
||||
"Loki-Call",
|
||||
"succeeded transitioning from ${currentState::class.simpleName} to ${event.outputState::class.simpleName} with ${event::class.simpleName}"
|
||||
)
|
||||
_currentState = event.outputState
|
||||
sideEffect()
|
||||
return true
|
||||
}
|
||||
Log.e(
|
||||
"Loki-Call",
|
||||
"error transitioning from ${currentState::class.simpleName} to ${event.outputState::class.simpleName} with ${event::class.simpleName}"
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package org.thoughtcrime.securesms.webrtc.video
|
||||
|
||||
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
|
||||
import org.webrtc.VideoFrame
|
||||
import org.webrtc.VideoSink
|
||||
|
||||
class RemoteRotationVideoProxySink: VideoSink {
|
||||
|
||||
private var targetSink: VideoSink? = null
|
||||
|
||||
var rotation: Int = 0
|
||||
|
||||
override fun onFrame(frame: VideoFrame?) {
|
||||
val thisSink = targetSink ?: return
|
||||
val thisFrame = frame ?: return
|
||||
|
||||
val quadrantRotation = rotation.quadrantRotation()
|
||||
val modifiedRotation = thisFrame.rotation - quadrantRotation
|
||||
|
||||
val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs)
|
||||
thisSink.onFrame(newFrame)
|
||||
}
|
||||
|
||||
fun setSink(videoSink: VideoSink) {
|
||||
targetSink = videoSink
|
||||
}
|
||||
|
||||
fun release() {
|
||||
targetSink = null
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.thoughtcrime.securesms.webrtc.video
|
||||
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
|
||||
import org.webrtc.CapturerObserver
|
||||
import org.webrtc.VideoFrame
|
||||
import org.webrtc.VideoProcessor
|
||||
import org.webrtc.VideoSink
|
||||
import java.lang.ref.SoftReference
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class RotationVideoSink: CapturerObserver, VideoProcessor {
|
||||
|
||||
var rotation: Int = 0
|
||||
var mirrored = false
|
||||
|
||||
private val capturing = AtomicBoolean(false)
|
||||
private var capturerObserver = SoftReference<CapturerObserver>(null)
|
||||
private var sink = SoftReference<VideoSink>(null)
|
||||
|
||||
override fun onCapturerStarted(ignored: Boolean) {
|
||||
capturing.set(true)
|
||||
}
|
||||
|
||||
override fun onCapturerStopped() {
|
||||
capturing.set(false)
|
||||
}
|
||||
|
||||
override fun onFrameCaptured(videoFrame: VideoFrame?) {
|
||||
// rotate if need
|
||||
val observer = capturerObserver.get()
|
||||
if (videoFrame == null || observer == null || !capturing.get()) return
|
||||
|
||||
val quadrantRotation = rotation.quadrantRotation()
|
||||
|
||||
val newFrame = VideoFrame(videoFrame.buffer, (videoFrame.rotation + quadrantRotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1) % 360, videoFrame.timestampNs)
|
||||
val localFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1, videoFrame.timestampNs)
|
||||
|
||||
observer.onFrameCaptured(newFrame)
|
||||
sink.get()?.onFrame(localFrame)
|
||||
}
|
||||
|
||||
override fun setSink(sink: VideoSink?) {
|
||||
this.sink = SoftReference(sink)
|
||||
}
|
||||
|
||||
fun setObserver(videoSink: CapturerObserver?) {
|
||||
capturerObserver = SoftReference(videoSink)
|
||||
}
|
||||
}
|
|
@ -10,14 +10,17 @@
|
|||
<FrameLayout
|
||||
android:id="@+id/remote_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/remote_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:layout_gravity="center"/>
|
||||
</FrameLayout>
|
||||
<ImageView
|
||||
android:id="@+id/remote_recipient"
|
||||
|
@ -43,6 +46,16 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/remote_recipient"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/remote_loading_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="@string/WebRtcCallActivity_Reconnecting"
|
||||
android:id="@+id/reconnecting_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionCallText"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -917,5 +917,6 @@
|
|||
<string name="CallNotificationBuilder_first_call_title">Call Missed</string>
|
||||
<string name="CallNotificationBuilder_first_call_message">You missed a call because you need to enable the \'Voice and video calls\' permission in the Privacy Settings.</string>
|
||||
<string name="WebRtcCallActivity_Session_Call">Session Call</string>
|
||||
<string name="WebRtcCallActivity_Reconnecting">Reconnecting…</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
package org.thoughtcrime.securesms.calls
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.MockedStatic
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.mockStatic
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.webrtc.data.Event
|
||||
import org.thoughtcrime.securesms.webrtc.data.State
|
||||
|
||||
class CallStateMachineTests {
|
||||
|
||||
private lateinit var stateProcessor: TestStateProcessor
|
||||
|
||||
lateinit var mock: MockedStatic<Log>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
stateProcessor = TestStateProcessor(State.Idle)
|
||||
mock = mockStatic(Log::class.java).apply {
|
||||
`when`<Unit> { Log.e(any(), any(), any()) }.then { invocation ->
|
||||
val msg = invocation.getArgument<Any>(1)
|
||||
println(msg)
|
||||
}
|
||||
`when`<Unit> { Log.i(any(), any(), any()) }.then { invocation ->
|
||||
val msg = invocation.getArgument<Any>(1)
|
||||
println(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun teardown() {
|
||||
mock.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should transition to full connection from remote offer`() {
|
||||
val executions = listOf(
|
||||
Event.ReceivePreOffer,
|
||||
Event.ReceiveOffer,
|
||||
Event.SendAnswer,
|
||||
Event.Connect
|
||||
)
|
||||
executions.forEach { event ->
|
||||
stateProcessor.processEvent(event)
|
||||
}
|
||||
|
||||
assertEquals(stateProcessor.transitions, executions.size)
|
||||
assertEquals(stateProcessor.currentState, State.Connected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should transition to full connection from local offer`() {
|
||||
val executions = listOf(
|
||||
Event.ReceivePreOffer,
|
||||
Event.ReceiveOffer,
|
||||
Event.SendAnswer,
|
||||
Event.Connect
|
||||
)
|
||||
executions.forEach { event ->
|
||||
stateProcessor.processEvent(event)
|
||||
}
|
||||
|
||||
assertEquals(stateProcessor.transitions, executions.size)
|
||||
assertEquals(stateProcessor.currentState, State.Connected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not transition to connected from idle`() {
|
||||
val executions = listOf(
|
||||
Event.Connect
|
||||
)
|
||||
executions.forEach { event ->
|
||||
stateProcessor.processEvent(event)
|
||||
}
|
||||
|
||||
assertEquals(stateProcessor.transitions, 0)
|
||||
assertEquals(stateProcessor.currentState, State.Idle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not transition to connecting from local and remote offers`() {
|
||||
val executions = listOf(
|
||||
Event.SendPreOffer,
|
||||
Event.SendOffer,
|
||||
Event.ReceivePreOffer,
|
||||
Event.ReceiveOffer
|
||||
)
|
||||
|
||||
val validTransitions = 2
|
||||
|
||||
executions.forEach { event ->
|
||||
stateProcessor.processEvent(event)
|
||||
}
|
||||
|
||||
assertEquals(stateProcessor.transitions, validTransitions)
|
||||
assertEquals(stateProcessor.currentState, State.LocalRing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot answer in local ring`() {
|
||||
val executions = listOf(
|
||||
Event.SendPreOffer,
|
||||
Event.SendOffer,
|
||||
Event.SendAnswer
|
||||
)
|
||||
|
||||
val validTransitions = 2
|
||||
|
||||
executions.forEach { event ->
|
||||
stateProcessor.processEvent(event)
|
||||
}
|
||||
|
||||
assertEquals(stateProcessor.transitions, validTransitions)
|
||||
assertEquals(stateProcessor.currentState, State.LocalRing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test full state cycles`() {
|
||||
val executions = listOf(
|
||||
Event.ReceivePreOffer,
|
||||
Event.ReceiveOffer,
|
||||
Event.SendAnswer,
|
||||
Event.Connect,
|
||||
Event.Hangup,
|
||||
Event.Cleanup,
|
||||
Event.SendPreOffer,
|
||||
Event.SendOffer,
|
||||
Event.ReceiveAnswer,
|
||||
Event.Connect,
|
||||
Event.IceDisconnect,
|
||||
Event.NetworkReconnect,
|
||||
Event.ReceiveAnswer,
|
||||
Event.Connect,
|
||||
Event.Hangup,
|
||||
Event.Cleanup,
|
||||
Event.ReceivePreOffer,
|
||||
Event.ReceiveOffer,
|
||||
Event.SendAnswer,
|
||||
Event.Connect,
|
||||
Event.IceDisconnect,
|
||||
Event.PrepareForNewOffer,
|
||||
Event.ReceiveOffer,
|
||||
Event.SendAnswer,
|
||||
Event.Connect,
|
||||
Event.Hangup,
|
||||
Event.Cleanup,
|
||||
Event.ReceivePreOffer,
|
||||
Event.ReceiveOffer,
|
||||
Event.SendAnswer,
|
||||
Event.IceFailed,
|
||||
Event.Cleanup,
|
||||
Event.ReceivePreOffer,
|
||||
Event.DeclineCall,
|
||||
Event.Cleanup
|
||||
)
|
||||
|
||||
executions.forEach { event -> stateProcessor.processEvent(event) }
|
||||
|
||||
assertEquals(State.Idle, stateProcessor.currentState)
|
||||
assertEquals(executions.size, stateProcessor.transitions)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.thoughtcrime.securesms.calls
|
||||
|
||||
import org.thoughtcrime.securesms.webrtc.data.Event
|
||||
import org.thoughtcrime.securesms.webrtc.data.State
|
||||
import org.thoughtcrime.securesms.webrtc.data.StateProcessor
|
||||
|
||||
class TestStateProcessor(initial: State): StateProcessor(initial) {
|
||||
|
||||
private var _transitions = 0
|
||||
val transitions get() = _transitions
|
||||
|
||||
override fun processEvent(event: Event, sideEffect: () -> Unit): Boolean {
|
||||
val didExecute = super.processEvent(event, sideEffect)
|
||||
if (didExecute) _transitions++
|
||||
|
||||
return didExecute
|
||||
}
|
||||
}
|
|
@ -1,20 +1,22 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class CursorRecyclerViewAdapterTest {
|
||||
private CursorRecyclerViewAdapter adapter;
|
||||
private Context context;
|
||||
|
|
Loading…
Reference in New Issue