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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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