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:
Harris 2022-03-17 17:32:32 +11:00
commit e689ab9753
17 changed files with 1076 additions and 396 deletions

View File

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

View File

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

View File

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

View File

@ -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()
}

View File

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

View File

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

View File

@ -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 {
//
// }
}
}

View File

@ -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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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