Video management logic update

Rounded corners for floating inset
Proper handling of video scaling based on video proportions
Proper handling of mirroring logic for floating/fullscreen videos depending on whether they are the user or the remote video and whether the camera is front facing or not
This commit is contained in:
ThomasSession 2024-07-06 11:36:21 +10:00
parent 95dc1d9f54
commit 1bc35723fa
7 changed files with 151 additions and 95 deletions

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Outline
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
@ -14,7 +15,11 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.WindowManager import android.view.WindowManager
import android.widget.FrameLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -49,6 +54,7 @@ import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING
import org.thoughtcrime.securesms.webrtc.Orientation import org.thoughtcrime.securesms.webrtc.Orientation
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE
import org.webrtc.RendererCommon
import kotlin.math.asin import kotlin.math.asin
@AndroidEntryPoint @AndroidEntryPoint
@ -137,9 +143,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventLis
} }
binding.floatingRendererContainer.setOnClickListener { binding.floatingRendererContainer.setOnClickListener {
val swapVideoViewIntent = viewModel.swapVideos()
WebRtcCallService.swapVideoViews(this, viewModel.toggleVideoSwap())
startService(swapVideoViewIntent)
} }
binding.microphoneButton.setOnClickListener { binding.microphoneButton.setOnClickListener {
@ -180,7 +184,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventLis
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.onAllGranted { .onAllGranted {
val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled) val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoState.value.userVideoEnabled)
startService(intent) startService(intent)
} }
.execute() .execute()
@ -197,6 +201,26 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventLis
onBackPressed() onBackPressed()
} }
clipFloatingInsets()
}
/**
* Makes sure the floating video inset has clipped rounded corners, included with the video stream itself
*/
private fun clipFloatingInsets() {
// clip the video inset with rounded corners
val videoInsetProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// all corners
outline.setRoundRect(
0, 0, view.width, view.height,
resources.getDimensionPixelSize(R.dimen.video_inset_radius).toFloat()
)
}
}
binding.floatingRendererContainer.outlineProvider = videoInsetProvider
binding.floatingRendererContainer.clipToOutline = true
} }
//Function to check if Android System Auto-rotate is on or off //Function to check if Android System Auto-rotate is on or off
@ -216,7 +240,11 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventLis
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
sensorManager.unregisterListener(this) try {
sensorManager.unregisterListener(this)
} catch (e: Exception) {
// the unregister can throw if the activity dies too quickly and the sensorManager is not initialised yet
}
} }
override fun onSensorChanged(event: SensorEvent) { override fun onSensorChanged(event: SensorEvent) {
@ -394,33 +422,37 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventLis
} }
} }
// handle video windows
launch { launch {
viewModel.localVideoEnabledState.collect { isEnabled -> viewModel.videoState.collect { state ->
binding.floatingRenderer.removeAllViews() binding.floatingRenderer.removeAllViews()
if (isEnabled) {
viewModel.floatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.floatingRenderer.addView(surfaceView)
}
}
binding.floatingRenderer.isVisible = isEnabled
binding.enableCameraButton.isSelected = isEnabled
//binding.swapViewIcon.bringToFront()
}
}
launch {
viewModel.remoteVideoEnabledState.collect { isEnabled ->
binding.fullscreenRenderer.removeAllViews() binding.fullscreenRenderer.removeAllViews()
if (isEnabled) {
// handle fullscreen video window
if(state.showFullscreenVideo()){
viewModel.fullscreenRenderer?.let { surfaceView -> viewModel.fullscreenRenderer?.let { surfaceView ->
binding.fullscreenRenderer.addView(surfaceView) binding.fullscreenRenderer.addView(surfaceView)
binding.fullscreenRenderer.isVisible = true
binding.remoteRecipient.isVisible = false
} }
} else {
binding.fullscreenRenderer.isVisible = false
binding.remoteRecipient.isVisible = true
} }
binding.fullscreenRenderer.isVisible = isEnabled
binding.remoteRecipient.isVisible = !isEnabled // handle floating video window
//binding.swapViewIcon.bringToFront() if(state.showFloatingVideo()){
viewModel.floatingRenderer?.let { surfaceView ->
binding.floatingRenderer.addView(surfaceView)
binding.floatingRenderer.isVisible = true
binding.swapViewIcon.bringToFront()
}
} else {
binding.floatingRenderer.isVisible = false
}
// handle buttons
binding.enableCameraButton.isSelected = state.userVideoEnabled
} }
} }
} }

View File

@ -62,7 +62,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP" const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"
const val ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO" const val ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO"
const val ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO" const val ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO"
const val ACTION_SWAP_VIDEO_VIEW = "SWAP_VIDEO_VIEW"
const val ACTION_FLIP_CAMERA = "FLIP_CAMERA" const val ACTION_FLIP_CAMERA = "FLIP_CAMERA"
const val ACTION_UPDATE_AUDIO = "UPDATE_AUDIO" const val ACTION_UPDATE_AUDIO = "UPDATE_AUDIO"
const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE" const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"
@ -110,11 +109,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java) fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_ANSWER_CALL) .setAction(ACTION_ANSWER_CALL)
fun swapVideoViews(context: Context, swapped: Boolean) =
Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_SWAP_VIDEO_VIEW)
.putExtra(EXTRA_SWAPPED, swapped)
fun microphoneIntent(context: Context, enabled: Boolean) = fun microphoneIntent(context: Context, enabled: Boolean) =
Intent(context, WebRtcCallService::class.java) Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_SET_MUTE_AUDIO) .setAction(ACTION_SET_MUTE_AUDIO)
@ -299,7 +293,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
action == ACTION_DENY_CALL -> handleDenyCall(intent) action == ACTION_DENY_CALL -> handleDenyCall(intent)
action == ACTION_LOCAL_HANGUP -> handleLocalHangup(intent) action == ACTION_LOCAL_HANGUP -> handleLocalHangup(intent)
action == ACTION_REMOTE_HANGUP -> handleRemoteHangup(intent) action == ACTION_REMOTE_HANGUP -> handleRemoteHangup(intent)
action == ACTION_SWAP_VIDEO_VIEW ->handleSwapVideoView(intent)
action == ACTION_SET_MUTE_AUDIO -> handleSetMuteAudio(intent) action == ACTION_SET_MUTE_AUDIO -> handleSetMuteAudio(intent)
action == ACTION_SET_MUTE_VIDEO -> handleSetMuteVideo(intent) action == ACTION_SET_MUTE_VIDEO -> handleSetMuteVideo(intent)
action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent) action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent)
@ -602,11 +595,6 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
onHangup() onHangup()
} }
private fun handleSwapVideoView(intent: Intent) {
val swapped = intent.getBooleanExtra(EXTRA_SWAPPED, false)
callManager.handleSwapVideoView(swapped)
}
private fun handleSetMuteAudio(intent: Intent) { private fun handleSetMuteAudio(intent: Intent) {
val muted = intent.getBooleanExtra(EXTRA_MUTE, false) val muted = intent.getBooleanExtra(EXTRA_MUTE, false)
callManager.handleSetMuteAudio(muted) callManager.handleSetMuteAudio(muted)

View File

@ -6,6 +6,7 @@ import android.telephony.TelephonyManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
@ -51,6 +52,7 @@ import org.webrtc.MediaStream
import org.webrtc.PeerConnection import org.webrtc.PeerConnection
import org.webrtc.PeerConnection.IceConnectionState import org.webrtc.PeerConnection.IceConnectionState
import org.webrtc.PeerConnectionFactory import org.webrtc.PeerConnectionFactory
import org.webrtc.RendererCommon
import org.webrtc.RtpReceiver import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription import org.webrtc.SessionDescription
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
@ -67,7 +69,6 @@ class CallManager(
SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer { SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer {
sealed class StateEvent { sealed class StateEvent {
data class VideoSwapped(val isSwapped: Boolean): StateEvent()
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()
@ -106,10 +107,15 @@ class CallManager(
private val _audioEvents = MutableStateFlow(AudioEnabled(false)) private val _audioEvents = MutableStateFlow(AudioEnabled(false))
val audioEvents = _audioEvents.asSharedFlow() val audioEvents = _audioEvents.asSharedFlow()
private val _videoEvents = MutableStateFlow(VideoEnabled(false))
val videoEvents = _videoEvents.asSharedFlow() private val _videoState: MutableStateFlow<VideoState> = MutableStateFlow(
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false)) VideoState(
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow() swapped = false,
userVideoEnabled = false,
remoteVideoEnabled = false
)
)
val videoState = _videoState
private val stateProcessor = StateProcessor(CallState.Idle) private val stateProcessor = StateProcessor(CallState.Idle)
@ -221,8 +227,10 @@ class CallManager(
val base = EglBase.create() val base = EglBase.create()
eglBase = base eglBase = base
floatingRenderer = SurfaceViewRenderer(context) floatingRenderer = SurfaceViewRenderer(context)
floatingRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
fullscreenRenderer = SurfaceViewRenderer(context) fullscreenRenderer = SurfaceViewRenderer(context)
fullscreenRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
remoteRotationSink = RemoteRotationVideoProxySink() remoteRotationSink = RemoteRotationVideoProxySink()
@ -363,7 +371,8 @@ class CallManager(
val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] } val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] }
val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject
if (json.containsKey("video")) { if (json.containsKey("video")) {
_remoteVideoEvents.value = VideoEnabled((json["video"] as JsonPrimitive).boolean) _videoState.value = _videoState.value.copy(remoteVideoEnabled = (json["video"] as JsonPrimitive).boolean)
handleMirroring()
} else if (json.containsKey("hangup")) { } else if (json.containsKey("hangup")) {
peerConnectionObservers.forEach(WebRtcListener::onHangup) peerConnectionObservers.forEach(WebRtcListener::onHangup)
} }
@ -399,8 +408,11 @@ class CallManager(
pendingOffer = null pendingOffer = null
callStartTime = -1 callStartTime = -1
_audioEvents.value = AudioEnabled(false) _audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false) _videoState.value = VideoState(
_remoteVideoEvents.value = VideoEnabled(false) swapped = false,
userVideoEnabled = false,
remoteVideoEnabled = false
)
pendingOutgoingIceUpdates.clear() pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear() pendingIncomingIceUpdates.clear()
} }
@ -411,7 +423,7 @@ class CallManager(
// If the camera we've switched to is the front one then mirror it to match what someone // If the camera we've switched to is the front one then mirror it to match what someone
// would see when looking in the mirror rather than the left<-->right flipped version. // would see when looking in the mirror rather than the left<-->right flipped version.
handleUserMirroring() handleMirroring()
} }
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) { fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
@ -609,10 +621,19 @@ class CallManager(
} }
} }
fun handleSwapVideoView(swapped: Boolean) { fun swapVideos() {
videoSwapped = swapped videoSwapped = !videoSwapped
if (!swapped) { // update the state
_videoState.value = _videoState.value.copy(swapped = videoSwapped)
handleMirroring()
//todo TOM received rotated video shouldn't be full scale
//todo TOM make sure the swap icon is visible
//todo TOM Should we show the 'no video' inset straight away?
//todo TOM ios rotates the controls in landscape ( just the buttons though, not the whole ui??)
if (!videoSwapped) {
peerConnection?.rotationVideoSink?.apply { peerConnection?.rotationVideoSink?.apply {
setSink(floatingRenderer) setSink(floatingRenderer)
} }
@ -633,18 +654,31 @@ class CallManager(
*/ */
private fun getUserRenderer() = if(videoSwapped) fullscreenRenderer else floatingRenderer private fun getUserRenderer() = if(videoSwapped) fullscreenRenderer else floatingRenderer
/**
* Returns the renderer currently showing the contact's video, not the user's
*/
private fun getRemoteRenderer() = if(videoSwapped) floatingRenderer else fullscreenRenderer
/** /**
* Makes sure the user's renderer applies mirroring if necessary * Makes sure the user's renderer applies mirroring if necessary
*/ */
private fun handleUserMirroring() = getUserRenderer()?.setMirror(isCameraFrontFacing()) private fun handleMirroring() {
val videoState = _videoState.value
// if we have user video and the camera is front facing, make sure to mirror stream
if(videoState.userVideoEnabled) {
getUserRenderer()?.setMirror(isCameraFrontFacing())
}
// the remote video is never mirrored
if(videoState.remoteVideoEnabled){
getRemoteRenderer()?.setMirror(false)
}
}
fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) { fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) {
_videoEvents.value = VideoEnabled(!muted) _videoState.value = _videoState.value.copy(userVideoEnabled = !muted)
handleMirroring()
// if we have video and the camera is not front facing, make sure to mirror stream
if(!muted){
handleUserMirroring()
}
val connection = peerConnection ?: return val connection = peerConnection ?: return
connection.setVideoEnabled(!muted) connection.setVideoEnabled(!muted)
@ -761,7 +795,7 @@ class CallManager(
connection.setCommunicationMode() connection.setCommunicationMode()
setAudioEnabled(true) setAudioEnabled(true)
dataChannel?.let { channel -> dataChannel?.let { channel ->
val toSend = if (!_videoEvents.value.isEnabled) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON val toSend = if (!_videoState.value.userVideoEnabled) 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)
channel.send(buffer) channel.send(buffer)
} }
@ -790,7 +824,7 @@ class CallManager(
fun isInitiator(): Boolean = peerConnection?.isInitiator() == true fun isInitiator(): Boolean = peerConnection?.isInitiator() == true
fun isCameraFrontFacing() = localCameraState.activeDirection == CameraState.Direction.FRONT fun isCameraFrontFacing() = localCameraState.activeDirection != CameraState.Direction.BACK
interface WebRtcListener: PeerConnection.Observer { interface WebRtcListener: PeerConnection.Observer {
fun onHangup() fun onHangup()

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.webrtc
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
@ -35,15 +37,6 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val fullscreenRenderer: SurfaceViewRenderer? val fullscreenRenderer: SurfaceViewRenderer?
get() = callManager.fullscreenRenderer get() = callManager.fullscreenRenderer
private var _videoEnabled: Boolean = false
val videoEnabled: Boolean
get() = _videoEnabled
private var _remoteVideoEnabled: Boolean = false
private var _videoViewSwapped: Boolean = false
private var _microphoneEnabled: Boolean = true private var _microphoneEnabled: Boolean = true
val microphoneEnabled: Boolean val microphoneEnabled: Boolean
@ -63,15 +56,8 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
get() = callManager.audioEvents.map { it.isEnabled } get() = callManager.audioEvents.map { it.isEnabled }
.onEach { _microphoneEnabled = it } .onEach { _microphoneEnabled = it }
val localVideoEnabledState val videoState: StateFlow<VideoState>
get() = callManager.videoEvents get() = callManager.videoState
.map { it.isEnabled }
.onEach { _videoEnabled = it }
val remoteVideoEnabledState
get() = callManager.remoteVideoEvents
.map { it.isEnabled }
.onEach { _remoteVideoEnabled = it }
var deviceOrientation: Orientation = Orientation.UNKNOWN var deviceOrientation: Orientation = Orientation.UNKNOWN
set(value) { set(value) {
@ -91,11 +77,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val callStartTime: Long val callStartTime: Long
get() = callManager.callStartTime get() = callManager.callStartTime
/** fun swapVideos() {
* Toggles the video swapped state, and return the value post toggle callManager.swapVideos()
*/
fun toggleVideoSwap(): Boolean {
_videoViewSwapped = !_videoViewSwapped
return _videoViewSwapped
} }
} }

View File

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.webrtc
data class VideoState (
val swapped: Boolean,
val userVideoEnabled: Boolean,
val remoteVideoEnabled: Boolean
){
fun showFloatingVideo(): Boolean {
return userVideoEnabled && !swapped ||
remoteVideoEnabled && swapped
}
fun showFullscreenVideo(): Boolean {
return userVideoEnabled && swapped ||
remoteVideoEnabled && !swapped
}
}

View File

@ -20,7 +20,7 @@
<FrameLayout <FrameLayout
android:id="@+id/fullscreen_renderer" android:id="@+id/fullscreen_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_gravity="center"/> android:layout_gravity="center"/>
</FrameLayout> </FrameLayout>
<ImageView <ImageView
@ -132,7 +132,8 @@
android:elevation="8dp" android:elevation="8dp"
android:id="@+id/floating_renderer" android:id="@+id/floating_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="wrap_content"
android:layout_gravity="center"/>
<com.github.ybq.android.spinkit.SpinKitView <com.github.ybq.android.spinkit.SpinKitView
android:id="@+id/local_loading_view" android:id="@+id/local_loading_view"
style="@style/SpinKitView.Large.ThreeBounce" style="@style/SpinKitView.Large.ThreeBounce"
@ -142,19 +143,20 @@
android:layout_gravity="center" android:layout_gravity="center"
tools:visibility="visible" tools:visibility="visible"
android:visibility="gone" /> android:visibility="gone" />
<ImageView
android:id="@+id/swap_view_icon"
android:src="@drawable/ic_baseline_screen_rotation_alt_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:textColorPrimary"
android:layout_marginTop="@dimen/very_small_spacing"
android:layout_marginEnd="@dimen/very_small_spacing"
android:layout_gravity="end"
android:layout_width="14dp"
android:layout_height="14dp"/>
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/swap_view_icon"
android:src="@drawable/ic_baseline_screen_rotation_alt_24"
app:layout_constraintTop_toTopOf="@id/floating_renderer_container"
app:layout_constraintEnd_toEndOf="@id/floating_renderer_container"
app:tint="?android:textColorPrimary"
android:layout_marginTop="@dimen/very_small_spacing"
android:layout_marginEnd="@dimen/very_small_spacing"
android:layout_width="14dp"
android:layout_height="14dp"/>
<ImageView <ImageView
android:id="@+id/endCallButton" android:id="@+id/endCallButton"
android:background="@drawable/circle_tintable" android:background="@drawable/circle_tintable"

View File

@ -32,6 +32,7 @@
<dimen name="fake_chat_view_height">250dp</dimen> <dimen name="fake_chat_view_height">250dp</dimen>
<dimen name="setting_button_height">64dp</dimen> <dimen name="setting_button_height">64dp</dimen>
<dimen name="dialog_corner_radius">8dp</dimen> <dimen name="dialog_corner_radius">8dp</dimen>
<dimen name="video_inset_radius">11dp</dimen>
<dimen name="dialog_button_corner_radius">4dp</dimen> <dimen name="dialog_button_corner_radius">4dp</dimen>
<dimen name="pn_option_corner_radius">8dp</dimen> <dimen name="pn_option_corner_radius">8dp</dimen>
<dimen name="path_status_view_size">8dp</dimen> <dimen name="path_status_view_size">8dp</dimen>