mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 10:27:49 +00:00
feat: ringers and better state handling
This commit is contained in:
@@ -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" />
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
}
|
}
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
}
|
}
|
@@ -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 {
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
10
app/src/main/res/drawable/ic_baseline_videocam_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_videocam_24.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_baseline_videocam_off_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_videocam_off_24.xml
Normal 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>
|
@@ -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>
|
@@ -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(),
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user