feat: ringers and better state handling

This commit is contained in:
jubb
2021-11-12 12:21:05 +11:00
parent 3684457280
commit 3d0e5541d0
14 changed files with 229 additions and 98 deletions

View File

@@ -31,7 +31,6 @@
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" /> <uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
@@ -53,7 +52,6 @@
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" /> <uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH" tools:node="remove"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />

View File

@@ -10,21 +10,26 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.jakewharton.rxbinding3.view.clicks import com.bumptech.glide.load.engine.DiskCacheStrategy
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_webrtc_tests.* import kotlinx.android.synthetic.main.activity_webrtc.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.webrtc.CallViewModel import org.thoughtcrime.securesms.webrtc.CallViewModel
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.*
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import java.util.* import java.util.*
@@ -45,6 +50,7 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
private val viewModel by viewModels<CallViewModel>() private val viewModel by viewModels<CallViewModel>()
private val candidates: MutableList<IceCandidate> = mutableListOf() private val candidates: MutableList<IceCandidate> = mutableListOf()
private val glide by lazy { GlideApp.with(this) }
private lateinit var callAddress: Address private lateinit var callAddress: Address
private lateinit var callId: UUID private lateinit var callId: UUID
@@ -63,7 +69,7 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
super.onCreate(savedInstanceState, ready) super.onCreate(savedInstanceState, ready)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContentView(R.layout.activity_webrtc_tests) setContentView(R.layout.activity_webrtc)
volumeControlStream = AudioManager.STREAM_VOICE_CALL volumeControlStream = AudioManager.STREAM_VOICE_CALL
initializeResources() initializeResources()
@@ -88,7 +94,13 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
},IntentFilter(ACTION_END)) },IntentFilter(ACTION_END))
enableCameraButton.setOnClickListener { enableCameraButton.setOnClickListener {
startService(WebRtcCallService.cameraEnabled(this, true)) Permissions.with(this)
.request(Manifest.permission.CAMERA)
.onAllGranted {
val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled)
startService(intent)
}
.execute()
} }
switchCameraButton.setOnClickListener { switchCameraButton.setOnClickListener {
@@ -99,7 +111,6 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
startService(WebRtcCallService.hangupIntent(this)) startService(WebRtcCallService.hangupIntent(this))
} }
} }
private fun initializeResources() { private fun initializeResources() {
@@ -115,22 +126,69 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
uiJob = lifecycleScope.launch { uiJob = lifecycleScope.launch {
viewModel.callState.collect { state -> launch {
if (state == CallViewModel.State.CALL_CONNECTED) { viewModel.callState.collect { state ->
// call connected, render the surfaces remote_loading_view.isVisible = state != CALL_CONNECTED
remote_renderer.removeAllViews()
local_renderer.removeAllViews()
viewModel.remoteRenderer?.let { remote_renderer.addView(it) }
viewModel.localRenderer?.let { local_renderer.addView(it) }
} }
} }
viewModel.remoteVideoEnabledState.collect { launch {
viewModel.recipient.collect { latestRecipient ->
if (latestRecipient.recipient != null) {
val signalProfilePicture = latestRecipient.recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.clear(remote_recipient)
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(remote_recipient)
} else {
val publicKey = latestRecipient.recipient.address.serialize()
val displayName = getUserDisplayName(publicKey)
val sizeInPX = resources.getDimensionPixelSize(R.dimen.extra_large_profile_picture_size)
glide.clear(remote_recipient)
glide.load(AvatarPlaceholderGenerator.generate(this@WebRtcCallActivity, sizeInPX, publicKey, displayName))
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(remote_recipient)
}
} else {
glide.clear(remote_recipient)
}
}
}
launch {
viewModel.localVideoEnabledState.collect { isEnabled ->
local_renderer.removeAllViews()
if (isEnabled) {
viewModel.localRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
local_renderer.addView(surfaceView)
}
}
local_renderer.isVisible = isEnabled
enableCameraButton.setImageResource(
if (isEnabled) R.drawable.ic_baseline_videocam_off_24
else R.drawable.ic_baseline_videocam_24
)
}
}
launch {
viewModel.remoteVideoEnabledState.collect { isEnabled ->
remote_renderer.removeAllViews()
if (isEnabled) {
viewModel.remoteRenderer?.let { remote_renderer.addView(it) }
}
remote_renderer.isVisible = isEnabled
remote_recipient.isVisible = !isEnabled
}
} }
} }
} }
fun getUserDisplayName(publicKey: String): String {
val contact = DatabaseComponent.get(this).sessionContactDatabase().getContactWithSessionID(publicKey)
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
uiJob?.cancel() uiJob?.cancel()

View File

@@ -194,11 +194,9 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
action == ACTION_BLUETOOTH_CHANGE -> handleBluetoothChange(intent) action == ACTION_BLUETOOTH_CHANGE -> handleBluetoothChange(intent)
action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent) action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent)
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent) action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
action == ACTION_REMOTE_VIDEO_MUTE -> handleRemoteVideoMute(intent)
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent) action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent) action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent) action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
action == ACTION_CALL_CONNECTED -> handleCallConnected(intent)
action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(intent) action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(intent)
action == ACTION_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent) action == ACTION_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent)
} }
@@ -285,6 +283,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
callManager.clearPendingIceUpdates() callManager.clearPendingIceUpdates()
callManager.onIncomingRing(offer, callId, recipient, timestamp) callManager.onIncomingRing(offer, callId, recipient, timestamp)
callManager.postConnectionEvent(STATE_LOCAL_RINGING) callManager.postConnectionEvent(STATE_LOCAL_RINGING)
callManager.postViewModelState(CallViewModel.State.CALL_RINGING)
if (TextSecurePreferences.isCallNotificationsEnabled(this)) { if (TextSecurePreferences.isCallNotificationsEnabled(this)) {
callManager.startIncomingRinger() callManager.startIncomingRinger()
} }
@@ -440,14 +439,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
callManager.handleScreenOffChange() callManager.handleScreenOffChange()
} }
private fun handleRemoteVideoMute(intent: Intent) {
val muted = intent.getBooleanExtra(EXTRA_MUTE, false)
val callId = intent.getSerializableExtra(EXTRA_CALL_ID) as UUID
callManager.handleRemoteVideoMute(muted, callId)
}
private fun handleResponseMessage(intent: Intent) { private fun handleResponseMessage(intent: Intent) {
try { try {
val recipient = getRemoteRecipient(intent) val recipient = getRemoteRecipient(intent)
@@ -492,10 +483,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
callManager.startCommunication(lockManager) callManager.startCommunication(lockManager)
} }
private fun handleCallConnected(intent: Intent) {
}
private fun handleIsInCallQuery(intent: Intent) { private fun handleIsInCallQuery(intent: Intent) {
} }
@@ -508,9 +495,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
} }
} }
private fun handleCheckTimeout(intent: Intent) { private fun handleCheckTimeout(intent: Intent) {
val callId = callManager.callId ?: return val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState val callState = callManager.currentConnectionState

View File

@@ -2,11 +2,18 @@ package org.thoughtcrime.securesms.webrtc
import android.content.Context import android.content.Context
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task
import nl.komponents.kovenant.then
import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Debouncer
@@ -15,6 +22,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.*
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
@@ -23,7 +31,6 @@ import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
import org.thoughtcrime.securesms.webrtc.video.CameraState import org.thoughtcrime.securesms.webrtc.video.CameraState
import org.webrtc.* import org.webrtc.*
import java.lang.NullPointerException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -40,6 +47,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
data class AudioEnabled(val isEnabled: Boolean): StateEvent() data class AudioEnabled(val isEnabled: Boolean): StateEvent()
data class VideoEnabled(val isEnabled: Boolean): StateEvent() data class VideoEnabled(val isEnabled: Boolean): StateEvent()
data class CallStateUpdate(val state: CallState): StateEvent() data class CallStateUpdate(val state: CallState): StateEvent()
data class RecipientUpdate(val recipient: Recipient?): StateEvent() {
companion object {
val UNKNOWN = RecipientUpdate(recipient = null)
}
}
} }
companion object { companion object {
@@ -75,21 +87,23 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
peerConnectionObservers.remove(listener) peerConnectionObservers.remove(listener)
} }
private val _audioEvents = MutableStateFlow(StateEvent.AudioEnabled(false)) private val _audioEvents = MutableStateFlow(AudioEnabled(false))
val audioEvents = _audioEvents.asSharedFlow() val audioEvents = _audioEvents.asSharedFlow()
private val _videoEvents = MutableStateFlow(StateEvent.VideoEnabled(false)) private val _videoEvents = MutableStateFlow(VideoEnabled(false))
val videoEvents = _videoEvents.asSharedFlow() val videoEvents = _videoEvents.asSharedFlow()
private val _remoteVideoEvents = MutableStateFlow(StateEvent.VideoEnabled(false)) private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false))
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow() val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
private val _connectionEvents = MutableStateFlow<StateEvent>(StateEvent.CallStateUpdate(CallState.STATE_IDLE)) private val _connectionEvents = MutableStateFlow<StateEvent>(CallStateUpdate(CallState.STATE_IDLE))
val connectionEvents = _connectionEvents.asSharedFlow() val connectionEvents = _connectionEvents.asSharedFlow()
private val _callStateEvents = MutableStateFlow(CallViewModel.State.CALL_PENDING) private val _callStateEvents = MutableStateFlow(CallViewModel.State.CALL_PENDING)
val callStateEvents = _callStateEvents.asSharedFlow() val callStateEvents = _callStateEvents.asSharedFlow()
private val _recipientEvents = MutableStateFlow(RecipientUpdate.UNKNOWN)
val recipientEvents = _recipientEvents.asSharedFlow()
private var localCameraState: CameraState = CameraState.UNKNOWN private var localCameraState: CameraState = CameraState.UNKNOWN
private var bluetoothAvailable = false private var bluetoothAvailable = false
val currentConnectionState val currentConnectionState
get() = (_connectionEvents.value as StateEvent.CallStateUpdate).state get() = (_connectionEvents.value as CallStateUpdate).state
private val networkExecutor = Executors.newSingleThreadExecutor() private val networkExecutor = Executors.newSingleThreadExecutor()
@@ -99,6 +113,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
var pendingOfferTime: Long = -1 var pendingOfferTime: Long = -1
var callId: UUID? = null var callId: UUID? = null
var recipient: Recipient? = null var recipient: Recipient? = null
set(value) {
field = value
_recipientEvents.value = StateEvent.RecipientUpdate(value)
}
fun getCurrentCallState(): Pair<CallState, UUID?> = currentConnectionState to callId fun getCurrentCallState(): Pair<CallState, UUID?> = currentConnectionState to callId
@@ -131,7 +149,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
} }
fun postConnectionEvent(newState: CallState) { fun postConnectionEvent(newState: CallState) {
_connectionEvents.value = StateEvent.CallStateUpdate(newState) _connectionEvents.value = CallStateUpdate(newState)
} }
fun postViewModelState(newState: CallViewModel.State) { fun postViewModelState(newState: CallViewModel.State) {
@@ -192,14 +210,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
fun setAudioEnabled(isEnabled: Boolean) { fun setAudioEnabled(isEnabled: Boolean) {
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) { currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
peerConnection?.setAudioEnabled(isEnabled) peerConnection?.setAudioEnabled(isEnabled)
_audioEvents.value = StateEvent.AudioEnabled(true) _audioEvents.value = AudioEnabled(true)
}
}
fun setVideoEnabled(isEnabled: Boolean) {
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
peerConnection?.setVideoEnabled(isEnabled)
_videoEvents.value = StateEvent.VideoEnabled(true)
} }
} }
@@ -299,7 +310,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
Log.i(TAG,"onMessage...") Log.i(TAG,"onMessage...")
buffer ?: return buffer ?: return
Log.i(TAG,"received: ${buffer.data}") try {
val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] }
val videoEnabled = Json.decodeFromString(VideoEnabledMessage.serializer(), byteArray.decodeToString())
_remoteVideoEvents.value = VideoEnabled(videoEnabled.video)
} catch (e: Exception) {
Log.e(TAG, "Failed to deserialize data channel message", e)
}
} }
override fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>) { override fun onAudioDeviceChanged(activeDevice: AudioDevice, devices: Set<AudioDevice>) {
@@ -324,12 +341,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
remoteRenderer = null remoteRenderer = null
eglBase = null eglBase = null
_connectionEvents.value = StateEvent.CallStateUpdate(CallState.STATE_IDLE) _connectionEvents.value = CallStateUpdate(CallState.STATE_IDLE)
localCameraState = CameraState.UNKNOWN localCameraState = CameraState.UNKNOWN
recipient = null recipient = null
callId = null callId = null
_audioEvents.value = StateEvent.AudioEnabled(false) _audioEvents.value = AudioEnabled(false)
_videoEvents.value = StateEvent.VideoEnabled(false) _videoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false)
pendingOutgoingIceUpdates.clear() pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear() pendingIncomingIceUpdates.clear()
} }
@@ -348,6 +366,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
startIncomingRinger() startIncomingRinger()
} }
fun onReconnect(newOffer: String): Promise<Unit, Exception> {
return task {}
}
fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise<Unit, Exception> { fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise<Unit, Exception> {
val callId = callId ?: return Promise.ofFail(NullPointerException("callId is null")) val callId = callId ?: return Promise.ofFail(NullPointerException("callId is null"))
val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null")) val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null"))
@@ -418,10 +440,14 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
Log.i(TAG, "Sending offer: ${offer.description}") Log.i(TAG, "Sending offer: ${offer.description}")
return MessageSender.sendNonDurably(CallMessage.offer( return MessageSender.sendNonDurably(CallMessage.preOffer(
offer.description,
callId callId
), recipient.address) ), recipient.address).bind {
MessageSender.sendNonDurably(CallMessage.offer(
offer.description,
callId
), recipient.address)
}
} }
fun callNotSetup(): Boolean = fun callNotSetup(): Boolean =
@@ -450,13 +476,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
} }
fun handleSetMuteAudio(muted: Boolean) { fun handleSetMuteAudio(muted: Boolean) {
_audioEvents.value = StateEvent.AudioEnabled(!muted) _audioEvents.value = AudioEnabled(!muted)
peerConnection?.setAudioEnabled(_audioEvents.value.isEnabled) peerConnection?.setAudioEnabled(!muted)
} }
fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) { fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) {
_videoEvents.value = StateEvent.VideoEnabled(!muted) _videoEvents.value = VideoEnabled(!muted)
peerConnection?.setVideoEnabled(_videoEvents.value.isEnabled) peerConnection?.setVideoEnabled(!muted)
dataChannel?.let { channel -> dataChannel?.let { channel ->
val toSend = if (muted) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON val toSend = if (muted) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON
val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false) val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false)
@@ -507,17 +533,6 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
} }
} }
fun handleRemoteVideoMute(muted: Boolean, intentCallId: UUID) {
val recipient = recipient ?: return
val callId = callId ?: return
if (currentConnectionState != CallState.STATE_CONNECTED || callId != intentCallId) {
Log.w(TAG,"Got video toggle for inactive call, ignoring..")
return
}
_remoteVideoEvents.value = StateEvent.VideoEnabled(!muted)
}
fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) { fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) {
if (currentConnectionState != CallState.STATE_DIALING || recipient != this.recipient || callId != this.callId) { if (currentConnectionState != CallState.STATE_DIALING || recipient != this.recipient || callId != this.callId) {
Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing") Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing")
@@ -563,8 +578,15 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
if (localCameraState.enabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO) if (localCameraState.enabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO)
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL) else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
connection.setCommunicationMode() connection.setCommunicationMode()
connection.setAudioEnabled(_audioEvents.value.isEnabled) setAudioEnabled(true)
connection.setVideoEnabled(localCameraState.enabled) dataChannel?.let { channel ->
val toSend = if (!_videoEvents.value.isEnabled) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON
val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false)
channel.send(buffer)
}
} }
@Serializable
data class VideoEnabledMessage(val video: Boolean)
} }

View File

@@ -21,13 +21,13 @@ class CallMessageProcessor(private val context: Context, lifecycle: Lifecycle) {
lifecycle.coroutineScope.launch { lifecycle.coroutineScope.launch {
while (isActive) { while (isActive) {
val nextMessage = WebRtcUtils.SIGNAL_QUEUE.receive() val nextMessage = WebRtcUtils.SIGNAL_QUEUE.receive()
Log.d("Loki", nextMessage.toString()) Log.d("Loki", nextMessage.type?.name ?: "CALL MESSAGE RECEIVED")
when (nextMessage.type) { when (nextMessage.type) {
OFFER -> incomingCall(nextMessage) OFFER -> incomingCall(nextMessage)
ANSWER -> incomingAnswer(nextMessage) ANSWER -> incomingAnswer(nextMessage)
END_CALL -> incomingHangup(nextMessage) END_CALL -> incomingHangup(nextMessage)
ICE_CANDIDATES -> handleIceCandidates(nextMessage) ICE_CANDIDATES -> handleIceCandidates(nextMessage)
PRE_OFFER -> incomingCall(nextMessage) PRE_OFFER -> incomingPreOffer(nextMessage)
PROVISIONAL_ANSWER -> {} // TODO: if necessary PROVISIONAL_ANSWER -> {} // TODO: if necessary
} }
} }
@@ -69,6 +69,10 @@ class CallMessageProcessor(private val context: Context, lifecycle: Lifecycle) {
context.startService(iceIntent) context.startService(iceIntent)
} }
private fun incomingPreOffer(callMessage: CallMessage) {
// handle notification state
}
private fun incomingCall(callMessage: CallMessage) { private fun incomingCall(callMessage: CallMessage) {
val recipientAddress = callMessage.sender ?: return val recipientAddress = callMessage.sender ?: return
val callId = callMessage.callId ?: return val callId = callMessage.callId ?: return

View File

@@ -1,8 +1,12 @@
package org.thoughtcrime.securesms.webrtc package org.thoughtcrime.securesms.webrtc
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
import javax.inject.Inject import javax.inject.Inject
@@ -15,6 +19,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val remoteRenderer: SurfaceViewRenderer? val remoteRenderer: SurfaceViewRenderer?
get() = callManager.remoteRenderer get() = callManager.remoteRenderer
private var _videoEnabled: Boolean = false
val videoEnabled: Boolean
get() = _videoEnabled
enum class State { enum class State {
CALL_PENDING, CALL_PENDING,
@@ -31,14 +40,17 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
UNTRUSTED_IDENTITY, UNTRUSTED_IDENTITY,
} }
val localAudioEnabledState = callManager.audioEvents.map { it.isEnabled } val localAudioEnabledState
val localVideoEnabledState = callManager.videoEvents.map { it.isEnabled } get() = callManager.audioEvents.map { it.isEnabled }
val remoteVideoEnabledState = callManager.remoteVideoEvents.map { it.isEnabled } val localVideoEnabledState
val callState = callManager.callStateEvents get() = callManager.videoEvents
.map { it.isEnabled }
// set up listeners for establishing connection toggling video / audio .onEach { _videoEnabled = it }
init { val remoteVideoEnabledState
get() = callManager.remoteVideoEvents.map { it.isEnabled }
} val callState
get() = callManager.callStateEvents
val recipient
get() = callManager.recipientEvents
} }

View File

@@ -28,11 +28,11 @@ class PeerConnectionWrapper(context: Context,
get() = peerConnection.localDescription != null && peerConnection.remoteDescription != null get() = peerConnection.localDescription != null && peerConnection.remoteDescription != null
init { init {
val stun = PeerConnection.IceServer.builder("stun:freyr.getsession.org:5349").createIceServer() val turn = PeerConnection.IceServer.builder("turn:freyr.getsession.org").setUsername("session").setPassword("session").createIceServer()
val turn = PeerConnection.IceServer.builder("turn:freyr.getsession.org:5349").setUsername("webrtc").setPassword("webrtc").createIceServer() val iceServers = listOf(turn)
val iceServers = listOf(stun,turn)
val constraints = MediaConstraints().apply { val constraints = MediaConstraints().apply {
optional.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
} }
val audioConstraints = MediaConstraints().apply { val audioConstraints = MediaConstraints().apply {

View File

@@ -66,9 +66,12 @@ class SignalAudioManager(private val context: Context,
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
fun handleCommand(command: AudioManagerCommand) { fun handleCommand(command: AudioManagerCommand) {
if (command == AudioManagerCommand.Initialize) {
initialize()
return
}
handler?.post { handler?.post {
when (command) { when (command) {
is AudioManagerCommand.Initialize -> initialize()
is AudioManagerCommand.Shutdown -> shutdown() is AudioManagerCommand.Shutdown -> shutdown()
is AudioManagerCommand.UpdateAudioDeviceState -> updateAudioDeviceState() is AudioManagerCommand.UpdateAudioDeviceState -> updateAudioDeviceState()
is AudioManagerCommand.Start -> start() is AudioManagerCommand.Start -> start()

View File

@@ -32,6 +32,8 @@ class SignalBluetoothManager(
} }
private set private set
private fun hasPermission() = false
private var bluetoothAdapter: BluetoothAdapter? = null private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothDevice: BluetoothDevice? = null private var bluetoothDevice: BluetoothDevice? = null
private var bluetoothHeadset: BluetoothHeadset? = null private var bluetoothHeadset: BluetoothHeadset? = null
@@ -90,7 +92,7 @@ class SignalBluetoothManager(
Log.d(TAG, "stop(): state: $state") Log.d(TAG, "stop(): state: $state")
if (bluetoothAdapter == null) { if (bluetoothAdapter == null || !hasPermission()) {
return return
} }
@@ -123,6 +125,7 @@ class SignalBluetoothManager(
fun startScoAudio(): Boolean { fun startScoAudio(): Boolean {
handler.assertHandlerThread() handler.assertHandlerThread()
if (!hasPermission()) return false
Log.i(TAG, "startScoAudio(): $state attempts: $scoConnectionAttempts") Log.i(TAG, "startScoAudio(): $state attempts: $scoConnectionAttempts")
@@ -147,6 +150,7 @@ class SignalBluetoothManager(
fun stopScoAudio() { fun stopScoAudio() {
handler.assertHandlerThread() handler.assertHandlerThread()
if (!hasPermission()) return
Log.i(TAG, "stopScoAudio(): $state") Log.i(TAG, "stopScoAudio(): $state")
@@ -162,6 +166,7 @@ class SignalBluetoothManager(
fun updateDevice() { fun updateDevice() {
handler.assertHandlerThread() handler.assertHandlerThread()
if (!hasPermission()) return
Log.d(TAG, "updateDevice(): state: $state") Log.d(TAG, "updateDevice(): state: $state")
@@ -195,6 +200,7 @@ class SignalBluetoothManager(
private fun onBluetoothTimeout() { private fun onBluetoothTimeout() {
Log.i(TAG, "onBluetoothTimeout: state: $state bluetoothHeadset: $bluetoothHeadset") Log.i(TAG, "onBluetoothTimeout: state: $state bluetoothHeadset: $bluetoothHeadset")
if (!hasPermission()) return
if (state == State.UNINITIALIZED || bluetoothHeadset == null || state != State.CONNECTING) { if (state == State.UNINITIALIZED || bluetoothHeadset == null || state != State.CONNECTING) {
return return

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,6.5l-4,4V7c0,-0.55 -0.45,-1 -1,-1H9.82L21,17.18V6.5zM3.27,2L2,3.27 4.73,6H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.21,0 0.39,-0.08 0.54,-0.18L19.73,21 21,19.73 3.27,2z"/>
</vector>

View File

@@ -16,7 +16,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<com.github.ybq.android.spinkit.SpinKitView <com.github.ybq.android.spinkit.SpinKitView
android:id="@+id/remove_loading_view" android:id="@+id/remote_loading_view"
style="@style/SpinKitView.Large.ThreeBounce" style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -24,12 +24,16 @@
android:layout_gravity="center" android:layout_gravity="center"
tools:visibility="visible" tools:visibility="visible"
android:visibility="gone" /> android:visibility="gone" />
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/remote_recipient"
android:layout_gravity="center"
android:layout_width="@dimen/extra_large_profile_picture_size"
android:layout_height="@dimen/extra_large_profile_picture_size"/>
</FrameLayout> </FrameLayout>
<FrameLayout <FrameLayout
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:elevation="8dp"
app:layout_constraintDimensionRatio="h,9:16" app:layout_constraintDimensionRatio="h,9:16"
android:layout_margin="@dimen/large_spacing" android:layout_margin="@dimen/large_spacing"
app:layout_constraintWidth_percent="0.2" app:layout_constraintWidth_percent="0.2"
@@ -79,25 +83,37 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="@dimen/large_spacing" android:layout_marginBottom="@dimen/large_spacing"
app:layout_constraintHorizontal_bias="0.2" app:layout_constraintHorizontal_bias="0.1"
/> />
<ImageView <ImageView
android:id="@+id/enableCameraButton" android:id="@+id/enableCameraButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"
android:src="@drawable/ic_baseline_photo_camera_48" android:src="@drawable/ic_baseline_videocam_24"
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing"
app:tint="@color/unimportant" app:tint="@color/unimportant"
android:backgroundTint="@color/unimportant_button_background" android:backgroundTint="@color/unimportant_button_background"
android:layout_width="@dimen/large_button_height" android:layout_width="@dimen/large_button_height"
android:layout_height="@dimen/large_button_height" android:layout_height="@dimen/large_button_height"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@id/switchCameraButton"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@id/endCallButton"
android:layout_marginBottom="@dimen/large_spacing" android:layout_marginBottom="@dimen/large_spacing"
app:layout_constraintHorizontal_bias="0.2"
/> />
<ImageView
android:layout_width="@dimen/large_button_height"
android:layout_height="@dimen/large_button_height"
android:padding="@dimen/small_spacing"
android:src="@drawable/ic_microphone"
app:tint="@color/unimportant"
android:layout_marginBottom="@dimen/large_spacing"
app:layout_constraintBottom_toBottomOf="parent"
android:backgroundTint="@color/unimportant_button_background"
android:background="@drawable/circle_tintable"
app:layout_constraintEnd_toStartOf="@id/speakerPhoneButton"
app:layout_constraintStart_toEndOf="@id/endCallButton"/>
<ImageView <ImageView
android:id="@+id/speakerPhoneButton" android:id="@+id/speakerPhoneButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"
@@ -111,7 +127,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="@dimen/large_spacing" android:layout_marginBottom="@dimen/large_spacing"
app:layout_constraintHorizontal_bias="0.8" app:layout_constraintHorizontal_bias="0.9"
/> />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -15,8 +15,8 @@ class CallMessage(): ControlMessage() {
override val ttl: Long = 300000L // 30s override val ttl: Long = 300000L // 30s
override fun isValid(): Boolean = super.isValid() && type != null override fun isValid(): Boolean = super.isValid() && type != null && callId != null
&& (!sdps.isNullOrEmpty() || type == SignalServiceProtos.CallMessage.Type.END_CALL) && (!sdps.isNullOrEmpty() || type in listOf(SignalServiceProtos.CallMessage.Type.END_CALL,SignalServiceProtos.CallMessage.Type.PRE_OFFER))
constructor(type: SignalServiceProtos.CallMessage.Type, constructor(type: SignalServiceProtos.CallMessage.Type,
sdps: List<String>, sdps: List<String>,
@@ -40,6 +40,13 @@ class CallMessage(): ControlMessage() {
callId callId
) )
fun preOffer(callId: UUID) = CallMessage(SignalServiceProtos.CallMessage.Type.PRE_OFFER,
listOf(),
listOf(),
listOf(),
callId
)
fun offer(sdp: String, callId: UUID) = CallMessage(SignalServiceProtos.CallMessage.Type.OFFER, fun offer(sdp: String, callId: UUID) = CallMessage(SignalServiceProtos.CallMessage.Type.OFFER,
listOf(sdp), listOf(sdp),
listOf(), listOf(),

View File

@@ -20,6 +20,7 @@
<dimen name="small_profile_picture_size">36dp</dimen> <dimen name="small_profile_picture_size">36dp</dimen>
<dimen name="medium_profile_picture_size">46dp</dimen> <dimen name="medium_profile_picture_size">46dp</dimen>
<dimen name="large_profile_picture_size">76dp</dimen> <dimen name="large_profile_picture_size">76dp</dimen>
<dimen name="extra_large_profile_picture_size">128dp</dimen>
<dimen name="conversation_view_status_indicator_size">14dp</dimen> <dimen name="conversation_view_status_indicator_size">14dp</dimen>
<dimen name="border_thickness">1dp</dimen> <dimen name="border_thickness">1dp</dimen>
<dimen name="new_conversation_button_collapsed_size">60dp</dimen> <dimen name="new_conversation_button_collapsed_size">60dp</dimen>