feat: ringers and better state handling
This commit is contained in:
parent
3684457280
commit
3d0e5541d0
|
@ -31,7 +31,6 @@
|
|||
android:required="false" />
|
||||
|
||||
<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.USE_FINGERPRINT" />
|
||||
<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.RAISED_THREAD_PRIORITY" />
|
||||
<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.READ_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
|
|
|
@ -10,21 +10,26 @@ import android.os.Bundle
|
|||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
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.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.avatars.ProfileContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel
|
||||
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.*
|
||||
import org.webrtc.IceCandidate
|
||||
import java.util.*
|
||||
|
||||
|
@ -45,6 +50,7 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
|
|||
private val viewModel by viewModels<CallViewModel>()
|
||||
|
||||
private val candidates: MutableList<IceCandidate> = mutableListOf()
|
||||
private val glide by lazy { GlideApp.with(this) }
|
||||
|
||||
private lateinit var callAddress: Address
|
||||
private lateinit var callId: UUID
|
||||
|
@ -63,7 +69,7 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
|
|||
super.onCreate(savedInstanceState, ready)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setContentView(R.layout.activity_webrtc_tests)
|
||||
setContentView(R.layout.activity_webrtc)
|
||||
volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
||||
|
||||
initializeResources()
|
||||
|
@ -88,7 +94,13 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
|
|||
},IntentFilter(ACTION_END))
|
||||
|
||||
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 {
|
||||
|
@ -99,7 +111,6 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
|
|||
startService(WebRtcCallService.hangupIntent(this))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun initializeResources() {
|
||||
|
@ -115,22 +126,69 @@ class WebRtcCallActivity: PassphraseRequiredActionBarActivity() {
|
|||
|
||||
uiJob = lifecycleScope.launch {
|
||||
|
||||
viewModel.callState.collect { state ->
|
||||
if (state == CallViewModel.State.CALL_CONNECTED) {
|
||||
// call connected, render the surfaces
|
||||
remote_renderer.removeAllViews()
|
||||
local_renderer.removeAllViews()
|
||||
viewModel.remoteRenderer?.let { remote_renderer.addView(it) }
|
||||
viewModel.localRenderer?.let { local_renderer.addView(it) }
|
||||
launch {
|
||||
viewModel.callState.collect { state ->
|
||||
remote_loading_view.isVisible = state != CALL_CONNECTED
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
super.onStop()
|
||||
uiJob?.cancel()
|
||||
|
|
|
@ -194,11 +194,9 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
|
|||
action == ACTION_BLUETOOTH_CHANGE -> handleBluetoothChange(intent)
|
||||
action == ACTION_WIRED_HEADSET_CHANGE -> handleWiredHeadsetChanged(intent)
|
||||
action == ACTION_SCREEN_OFF -> handleScreenOffChange(intent)
|
||||
action == ACTION_REMOTE_VIDEO_MUTE -> handleRemoteVideoMute(intent)
|
||||
action == ACTION_RESPONSE_MESSAGE -> handleResponseMessage(intent)
|
||||
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
|
||||
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
|
||||
action == ACTION_CALL_CONNECTED -> handleCallConnected(intent)
|
||||
action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(intent)
|
||||
action == ACTION_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent)
|
||||
}
|
||||
|
@ -285,6 +283,7 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
|
|||
callManager.clearPendingIceUpdates()
|
||||
callManager.onIncomingRing(offer, callId, recipient, timestamp)
|
||||
callManager.postConnectionEvent(STATE_LOCAL_RINGING)
|
||||
callManager.postViewModelState(CallViewModel.State.CALL_RINGING)
|
||||
if (TextSecurePreferences.isCallNotificationsEnabled(this)) {
|
||||
callManager.startIncomingRinger()
|
||||
}
|
||||
|
@ -440,14 +439,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
|
|||
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) {
|
||||
try {
|
||||
val recipient = getRemoteRecipient(intent)
|
||||
|
@ -492,10 +483,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
|
|||
callManager.startCommunication(lockManager)
|
||||
}
|
||||
|
||||
private fun handleCallConnected(intent: Intent) {
|
||||
|
||||
}
|
||||
|
||||
private fun handleIsInCallQuery(intent: Intent) {
|
||||
|
||||
}
|
||||
|
@ -508,9 +495,6 @@ class WebRtcCallService: Service(), PeerConnection.Observer {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private fun handleCheckTimeout(intent: Intent) {
|
||||
val callId = callManager.callId ?: return
|
||||
val callState = callManager.currentConnectionState
|
||||
|
|
|
@ -2,11 +2,18 @@ package org.thoughtcrime.securesms.webrtc
|
|||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
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.sending_receiving.MessageSender
|
||||
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.CallMessage.Type.ICE_CANDIDATES
|
||||
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.OutgoingRinger
|
||||
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.CameraState
|
||||
import org.webrtc.*
|
||||
import java.lang.NullPointerException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
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 VideoEnabled(val isEnabled: Boolean): StateEvent()
|
||||
data class CallStateUpdate(val state: CallState): StateEvent()
|
||||
data class RecipientUpdate(val recipient: Recipient?): StateEvent() {
|
||||
companion object {
|
||||
val UNKNOWN = RecipientUpdate(recipient = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -75,21 +87,23 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
peerConnectionObservers.remove(listener)
|
||||
}
|
||||
|
||||
private val _audioEvents = MutableStateFlow(StateEvent.AudioEnabled(false))
|
||||
private val _audioEvents = MutableStateFlow(AudioEnabled(false))
|
||||
val audioEvents = _audioEvents.asSharedFlow()
|
||||
private val _videoEvents = MutableStateFlow(StateEvent.VideoEnabled(false))
|
||||
private val _videoEvents = MutableStateFlow(VideoEnabled(false))
|
||||
val videoEvents = _videoEvents.asSharedFlow()
|
||||
private val _remoteVideoEvents = MutableStateFlow(StateEvent.VideoEnabled(false))
|
||||
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false))
|
||||
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()
|
||||
private val _callStateEvents = MutableStateFlow(CallViewModel.State.CALL_PENDING)
|
||||
val callStateEvents = _callStateEvents.asSharedFlow()
|
||||
private val _recipientEvents = MutableStateFlow(RecipientUpdate.UNKNOWN)
|
||||
val recipientEvents = _recipientEvents.asSharedFlow()
|
||||
private var localCameraState: CameraState = CameraState.UNKNOWN
|
||||
private var bluetoothAvailable = false
|
||||
|
||||
val currentConnectionState
|
||||
get() = (_connectionEvents.value as StateEvent.CallStateUpdate).state
|
||||
get() = (_connectionEvents.value as CallStateUpdate).state
|
||||
|
||||
private val networkExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
|
@ -99,6 +113,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
var pendingOfferTime: Long = -1
|
||||
var callId: UUID? = null
|
||||
var recipient: Recipient? = null
|
||||
set(value) {
|
||||
field = value
|
||||
_recipientEvents.value = StateEvent.RecipientUpdate(value)
|
||||
}
|
||||
|
||||
fun getCurrentCallState(): Pair<CallState, UUID?> = currentConnectionState to callId
|
||||
|
||||
|
@ -131,7 +149,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
}
|
||||
|
||||
fun postConnectionEvent(newState: CallState) {
|
||||
_connectionEvents.value = StateEvent.CallStateUpdate(newState)
|
||||
_connectionEvents.value = CallStateUpdate(newState)
|
||||
}
|
||||
|
||||
fun postViewModelState(newState: CallViewModel.State) {
|
||||
|
@ -192,14 +210,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
fun setAudioEnabled(isEnabled: Boolean) {
|
||||
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
|
||||
peerConnection?.setAudioEnabled(isEnabled)
|
||||
_audioEvents.value = StateEvent.AudioEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun setVideoEnabled(isEnabled: Boolean) {
|
||||
currentConnectionState.withState(*(CONNECTED_STATES + PENDING_CONNECTION_STATES)) {
|
||||
peerConnection?.setVideoEnabled(isEnabled)
|
||||
_videoEvents.value = StateEvent.VideoEnabled(true)
|
||||
_audioEvents.value = AudioEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,7 +310,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
Log.i(TAG,"onMessage...")
|
||||
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>) {
|
||||
|
@ -324,12 +341,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
remoteRenderer = null
|
||||
eglBase = null
|
||||
|
||||
_connectionEvents.value = StateEvent.CallStateUpdate(CallState.STATE_IDLE)
|
||||
_connectionEvents.value = CallStateUpdate(CallState.STATE_IDLE)
|
||||
localCameraState = CameraState.UNKNOWN
|
||||
recipient = null
|
||||
callId = null
|
||||
_audioEvents.value = StateEvent.AudioEnabled(false)
|
||||
_videoEvents.value = StateEvent.VideoEnabled(false)
|
||||
_audioEvents.value = AudioEnabled(false)
|
||||
_videoEvents.value = VideoEnabled(false)
|
||||
_remoteVideoEvents.value = VideoEnabled(false)
|
||||
pendingOutgoingIceUpdates.clear()
|
||||
pendingIncomingIceUpdates.clear()
|
||||
}
|
||||
|
@ -348,6 +366,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
startIncomingRinger()
|
||||
}
|
||||
|
||||
fun onReconnect(newOffer: String): Promise<Unit, Exception> {
|
||||
return task {}
|
||||
}
|
||||
|
||||
fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise<Unit, Exception> {
|
||||
val callId = callId ?: return Promise.ofFail(NullPointerException("callId 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}")
|
||||
|
||||
return MessageSender.sendNonDurably(CallMessage.offer(
|
||||
offer.description,
|
||||
return MessageSender.sendNonDurably(CallMessage.preOffer(
|
||||
callId
|
||||
), recipient.address)
|
||||
), recipient.address).bind {
|
||||
MessageSender.sendNonDurably(CallMessage.offer(
|
||||
offer.description,
|
||||
callId
|
||||
), recipient.address)
|
||||
}
|
||||
}
|
||||
|
||||
fun callNotSetup(): Boolean =
|
||||
|
@ -450,13 +476,13 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
}
|
||||
|
||||
fun handleSetMuteAudio(muted: Boolean) {
|
||||
_audioEvents.value = StateEvent.AudioEnabled(!muted)
|
||||
peerConnection?.setAudioEnabled(_audioEvents.value.isEnabled)
|
||||
_audioEvents.value = AudioEnabled(!muted)
|
||||
peerConnection?.setAudioEnabled(!muted)
|
||||
}
|
||||
|
||||
fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) {
|
||||
_videoEvents.value = StateEvent.VideoEnabled(!muted)
|
||||
peerConnection?.setVideoEnabled(_videoEvents.value.isEnabled)
|
||||
_videoEvents.value = VideoEnabled(!muted)
|
||||
peerConnection?.setVideoEnabled(!muted)
|
||||
dataChannel?.let { channel ->
|
||||
val toSend = if (muted) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON
|
||||
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) {
|
||||
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")
|
||||
|
@ -563,8 +578,15 @@ class CallManager(context: Context, audioManager: AudioManagerCompat): PeerConne
|
|||
if (localCameraState.enabled) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO)
|
||||
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
|
||||
connection.setCommunicationMode()
|
||||
connection.setAudioEnabled(_audioEvents.value.isEnabled)
|
||||
connection.setVideoEnabled(localCameraState.enabled)
|
||||
setAudioEnabled(true)
|
||||
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 {
|
||||
while (isActive) {
|
||||
val nextMessage = WebRtcUtils.SIGNAL_QUEUE.receive()
|
||||
Log.d("Loki", nextMessage.toString())
|
||||
Log.d("Loki", nextMessage.type?.name ?: "CALL MESSAGE RECEIVED")
|
||||
when (nextMessage.type) {
|
||||
OFFER -> incomingCall(nextMessage)
|
||||
ANSWER -> incomingAnswer(nextMessage)
|
||||
END_CALL -> incomingHangup(nextMessage)
|
||||
ICE_CANDIDATES -> handleIceCandidates(nextMessage)
|
||||
PRE_OFFER -> incomingCall(nextMessage)
|
||||
PRE_OFFER -> incomingPreOffer(nextMessage)
|
||||
PROVISIONAL_ANSWER -> {} // TODO: if necessary
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,10 @@ class CallMessageProcessor(private val context: Context, lifecycle: Lifecycle) {
|
|||
context.startService(iceIntent)
|
||||
}
|
||||
|
||||
private fun incomingPreOffer(callMessage: CallMessage) {
|
||||
// handle notification state
|
||||
}
|
||||
|
||||
private fun incomingCall(callMessage: CallMessage) {
|
||||
val recipientAddress = callMessage.sender ?: return
|
||||
val callId = callMessage.callId ?: return
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package org.thoughtcrime.securesms.webrtc
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -15,6 +19,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
|||
val remoteRenderer: SurfaceViewRenderer?
|
||||
get() = callManager.remoteRenderer
|
||||
|
||||
private var _videoEnabled: Boolean = false
|
||||
|
||||
val videoEnabled: Boolean
|
||||
get() = _videoEnabled
|
||||
|
||||
enum class State {
|
||||
CALL_PENDING,
|
||||
|
||||
|
@ -31,14 +40,17 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
|||
UNTRUSTED_IDENTITY,
|
||||
}
|
||||
|
||||
val localAudioEnabledState = callManager.audioEvents.map { it.isEnabled }
|
||||
val localVideoEnabledState = callManager.videoEvents.map { it.isEnabled }
|
||||
val remoteVideoEnabledState = callManager.remoteVideoEvents.map { it.isEnabled }
|
||||
val callState = callManager.callStateEvents
|
||||
|
||||
// set up listeners for establishing connection toggling video / audio
|
||||
init {
|
||||
|
||||
}
|
||||
val localAudioEnabledState
|
||||
get() = callManager.audioEvents.map { it.isEnabled }
|
||||
val localVideoEnabledState
|
||||
get() = callManager.videoEvents
|
||||
.map { it.isEnabled }
|
||||
.onEach { _videoEnabled = it }
|
||||
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
|
||||
|
||||
init {
|
||||
val stun = PeerConnection.IceServer.builder("stun:freyr.getsession.org:5349").createIceServer()
|
||||
val turn = PeerConnection.IceServer.builder("turn:freyr.getsession.org:5349").setUsername("webrtc").setPassword("webrtc").createIceServer()
|
||||
val iceServers = listOf(stun,turn)
|
||||
val turn = PeerConnection.IceServer.builder("turn:freyr.getsession.org").setUsername("session").setPassword("session").createIceServer()
|
||||
val iceServers = listOf(turn)
|
||||
|
||||
val constraints = MediaConstraints().apply {
|
||||
optional.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
|
||||
}
|
||||
val audioConstraints = MediaConstraints().apply {
|
||||
|
|
|
@ -66,9 +66,12 @@ class SignalAudioManager(private val context: Context,
|
|||
private var wiredHeadsetReceiver: WiredHeadsetReceiver? = null
|
||||
|
||||
fun handleCommand(command: AudioManagerCommand) {
|
||||
if (command == AudioManagerCommand.Initialize) {
|
||||
initialize()
|
||||
return
|
||||
}
|
||||
handler?.post {
|
||||
when (command) {
|
||||
is AudioManagerCommand.Initialize -> initialize()
|
||||
is AudioManagerCommand.Shutdown -> shutdown()
|
||||
is AudioManagerCommand.UpdateAudioDeviceState -> updateAudioDeviceState()
|
||||
is AudioManagerCommand.Start -> start()
|
||||
|
|
|
@ -32,6 +32,8 @@ class SignalBluetoothManager(
|
|||
}
|
||||
private set
|
||||
|
||||
private fun hasPermission() = false
|
||||
|
||||
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||
private var bluetoothDevice: BluetoothDevice? = null
|
||||
private var bluetoothHeadset: BluetoothHeadset? = null
|
||||
|
@ -90,7 +92,7 @@ class SignalBluetoothManager(
|
|||
|
||||
Log.d(TAG, "stop(): state: $state")
|
||||
|
||||
if (bluetoothAdapter == null) {
|
||||
if (bluetoothAdapter == null || !hasPermission()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -123,6 +125,7 @@ class SignalBluetoothManager(
|
|||
|
||||
fun startScoAudio(): Boolean {
|
||||
handler.assertHandlerThread()
|
||||
if (!hasPermission()) return false
|
||||
|
||||
Log.i(TAG, "startScoAudio(): $state attempts: $scoConnectionAttempts")
|
||||
|
||||
|
@ -147,6 +150,7 @@ class SignalBluetoothManager(
|
|||
|
||||
fun stopScoAudio() {
|
||||
handler.assertHandlerThread()
|
||||
if (!hasPermission()) return
|
||||
|
||||
Log.i(TAG, "stopScoAudio(): $state")
|
||||
|
||||
|
@ -162,6 +166,7 @@ class SignalBluetoothManager(
|
|||
|
||||
fun updateDevice() {
|
||||
handler.assertHandlerThread()
|
||||
if (!hasPermission()) return
|
||||
|
||||
Log.d(TAG, "updateDevice(): state: $state")
|
||||
|
||||
|
@ -195,6 +200,7 @@ class SignalBluetoothManager(
|
|||
|
||||
private fun onBluetoothTimeout() {
|
||||
Log.i(TAG, "onBluetoothTimeout: state: $state bluetoothHeadset: $bluetoothHeadset")
|
||||
if (!hasPermission()) return
|
||||
|
||||
if (state == State.UNINITIALIZED || bluetoothHeadset == null || state != State.CONNECTING) {
|
||||
return
|
||||
|
|
|
@ -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>
|
|
@ -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_constraintTop_toTopOf="parent"/>
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
android:id="@+id/remove_loading_view"
|
||||
android:id="@+id/remote_loading_view"
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -24,12 +24,16 @@
|
|||
android:layout_gravity="center"
|
||||
tools:visibility="visible"
|
||||
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
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:elevation="8dp"
|
||||
app:layout_constraintDimensionRatio="h,9:16"
|
||||
android:layout_margin="@dimen/large_spacing"
|
||||
app:layout_constraintWidth_percent="0.2"
|
||||
|
@ -79,25 +83,37 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="@dimen/large_spacing"
|
||||
app:layout_constraintHorizontal_bias="0.2"
|
||||
app:layout_constraintHorizontal_bias="0.1"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/enableCameraButton"
|
||||
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"
|
||||
app:tint="@color/unimportant"
|
||||
android:backgroundTint="@color/unimportant_button_background"
|
||||
android:layout_width="@dimen/large_button_height"
|
||||
android:layout_height="@dimen/large_button_height"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/switchCameraButton"
|
||||
app:layout_constraintEnd_toStartOf="@id/endCallButton"
|
||||
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
|
||||
android:id="@+id/speakerPhoneButton"
|
||||
android:background="@drawable/circle_tintable"
|
||||
|
@ -111,7 +127,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="@dimen/large_spacing"
|
||||
app:layout_constraintHorizontal_bias="0.8"
|
||||
app:layout_constraintHorizontal_bias="0.9"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -15,8 +15,8 @@ class CallMessage(): ControlMessage() {
|
|||
|
||||
override val ttl: Long = 300000L // 30s
|
||||
|
||||
override fun isValid(): Boolean = super.isValid() && type != null
|
||||
&& (!sdps.isNullOrEmpty() || type == SignalServiceProtos.CallMessage.Type.END_CALL)
|
||||
override fun isValid(): Boolean = super.isValid() && type != null && callId != null
|
||||
&& (!sdps.isNullOrEmpty() || type in listOf(SignalServiceProtos.CallMessage.Type.END_CALL,SignalServiceProtos.CallMessage.Type.PRE_OFFER))
|
||||
|
||||
constructor(type: SignalServiceProtos.CallMessage.Type,
|
||||
sdps: List<String>,
|
||||
|
@ -40,6 +40,13 @@ class CallMessage(): ControlMessage() {
|
|||
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,
|
||||
listOf(sdp),
|
||||
listOf(),
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<dimen name="small_profile_picture_size">36dp</dimen>
|
||||
<dimen name="medium_profile_picture_size">46dp</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="border_thickness">1dp</dimen>
|
||||
<dimen name="new_conversation_button_collapsed_size">60dp</dimen>
|
||||
|
|
Loading…
Reference in New Issue