From b510b064cfdb161b223af9984f63777454278efa Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 11 Jul 2024 09:44:17 +1000 Subject: [PATCH] [SES-1652] Swap video views in calls (#1533) * WIP: swap video views * feat: swap video views * minor fixes * minor fix * minor fix * update libsession-util * Revert "update libsession-util" This reverts commit 0d386e706e78d86147728cccb80636d920006d98. * reverse updating libsession-util * update libsession-util * Tweaking colors to match designs * More theme fixes * WebRTC rework Only using two sinks and swapping between them Reworked the device rotation logic as it didn't work well with pitch ( you could tip the device front to back and the rotation went out of whack, so had to resort to more robust calculation for the device orientation. Had to use a deprecated sensor setting but it's the only one I could use that works. * 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 * Showing floating video inset only when there is at least one video stream active, hiding it when both are inactive * Rotating controls on rotation * Clean up * Review feedback * OrientationManager The new OrientationManager encapsulate the orientation logic and sends out a mutable state flow * PR feedback --------- Co-authored-by: Ryan Zhao Co-authored-by: Ryan ZHAO <> --- .../securesms/calls/OrientationManager.kt | 87 +++++++++ .../securesms/calls/WebRtcCallActivity.kt | 175 ++++++++++++------ .../securesms/service/WebRtcCallService.kt | 1 + .../org/thoughtcrime/securesms/ui/Themes.kt | 2 +- .../securesms/webrtc/CallManager.kt | 129 +++++++++---- .../securesms/webrtc/CallViewModel.kt | 31 ++-- .../securesms/webrtc/Orientation.kt | 8 + .../securesms/webrtc/PeerConnectionWrapper.kt | 4 +- .../securesms/webrtc/VideoState.kt | 17 ++ .../securesms/webrtc/data/CallUtils.kt | 11 -- .../video/RemoteRotationVideoProxySink.kt | 5 +- .../webrtc/video/RotationVideoSink.kt | 15 +- .../drawable/conversation_view_background.xml | 2 +- .../default_bottom_sheet_background.xml | 2 +- .../drawable/default_dialog_background.xml | 2 +- .../main/res/drawable/dialog_background.xml | 2 +- .../ic_baseline_screen_rotation_alt_24.xml | 10 + app/src/main/res/drawable/ic_clear_data.xml | 2 +- .../main/res/drawable/preference_bottom.xml | 2 +- .../main/res/drawable/preference_middle.xml | 2 +- .../main/res/drawable/preference_single.xml | 2 +- .../drawable/preference_single_no_padding.xml | 2 +- app/src/main/res/drawable/preference_top.xml | 2 +- .../layout/activity_appearance_settings.xml | 4 +- .../res/layout/activity_blocked_contacts.xml | 2 +- app/src/main/res/layout/activity_webrtc.xml | 42 ++++- .../main/res/layout/default_group_chip.xml | 2 +- app/src/main/res/layout/view_conversation.xml | 6 +- .../res/layout/view_search_bottom_bar.xml | 2 +- app/src/main/res/values/attrs.xml | 5 +- app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/styles.xml | 5 +- app/src/main/res/values/themes.xml | 45 ++--- libsession/src/main/res/values/attrs.xml | 3 - libsession/src/main/res/values/colors.xml | 1 - 36 files changed, 436 insertions(+), 201 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt create mode 100644 app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt new file mode 100644 index 0000000000..baae40bcb2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.calls + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.provider.Settings +import androidx.core.content.ContextCompat.getSystemService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity.SENSOR_SERVICE +import org.thoughtcrime.securesms.webrtc.Orientation +import kotlin.math.asin + +class OrientationManager(private val context: Context): SensorEventListener { + private var sensorManager: SensorManager? = null + private var rotationVectorSensor: Sensor? = null + + private val _orientation = MutableStateFlow(Orientation.UNKNOWN) + val orientation: StateFlow = _orientation + + fun startOrientationListener(){ + // create the sensor manager if it's still null + if(sensorManager == null) { + sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager + } + + if(rotationVectorSensor == null) { + rotationVectorSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + } + + sensorManager?.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_UI) + } + + fun stopOrientationListener(){ + sensorManager?.unregisterListener(this) + } + + fun destroy(){ + stopOrientationListener() + sensorManager = null + rotationVectorSensor = null + _orientation.value = Orientation.UNKNOWN + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) { + // if auto-rotate is off, bail and send UNKNOWN + if (!isAutoRotateOn()) { + _orientation.value = Orientation.UNKNOWN + return + } + + // Get the quaternion from the rotation vector sensor + val quaternion = FloatArray(4) + SensorManager.getQuaternionFromVector(quaternion, event.values) + + // Calculate Euler angles from the quaternion + val pitch = asin(2.0 * (quaternion[0] * quaternion[2] - quaternion[3] * quaternion[1])) + + // Convert radians to degrees + val pitchDegrees = Math.toDegrees(pitch).toFloat() + + // Determine the device's orientation based on the pitch and roll values + val currentOrientation = when { + pitchDegrees > 45 -> Orientation.LANDSCAPE + pitchDegrees < -45 -> Orientation.REVERSED_LANDSCAPE + else -> Orientation.PORTRAIT + } + + if (currentOrientation != _orientation.value) { + _orientation.value = currentOrientation + } + } + } + + //Function to check if Android System Auto-rotate is on or off + private fun isAutoRotateOn(): Boolean { + return Settings.System.getInt( + context.contentResolver, + Settings.System.ACCELEROMETER_ROTATION, 0 + ) == 1 + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt index d46321863b..acc92c3c94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -5,11 +5,17 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.graphics.Outline +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorManager import android.media.AudioManager import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.MenuItem -import android.view.OrientationEventListener +import android.view.View +import android.view.ViewOutlineProvider import android.view.WindowManager import androidx.activity.viewModels import androidx.core.content.ContextCompat @@ -21,7 +27,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -import android.provider.Settings import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityWebrtcBinding @@ -43,8 +48,10 @@ import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_OUTGOING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_PRE_INIT import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING +import org.thoughtcrime.securesms.webrtc.Orientation import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE +import kotlin.math.asin @AndroidEntryPoint class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { @@ -71,16 +78,13 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { } private var hangupReceiver: BroadcastReceiver? = null - private val rotationListener by lazy { - object : OrientationEventListener(this) { - override fun onOrientationChanged(orientation: Int) { - if ((orientation + 15) % 90 < 30) { - viewModel.deviceRotation = orientation -// updateControlsRotation(orientation.quadrantRotation() * -1) - } - } - } - } + /** + * We need to track the device's orientation so we can calculate whether or not to rotate the video streams + * This works a lot better than using `OrientationEventListener > onOrientationChanged' + * which gives us a rotation angle that doesn't take into account pitch vs roll, so tipping the device from front to back would + * trigger the video rotation logic, while we really only want it when the device is in portrait or landscape. + */ + private var orientationManager = OrientationManager(this) override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { @@ -102,13 +106,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - // Only enable auto-rotate if system auto-rotate is enabled - if (isAutoRotateOn()) { - rotationListener.enable() - } else { - rotationListener.disable() - } - binding = ActivityWebrtcBinding.inflate(layoutInflater) setContentView(binding.root) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { @@ -136,6 +133,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(false) } + binding.floatingRendererContainer.setOnClickListener { + viewModel.swapVideos() + } + binding.microphoneButton.setOnClickListener { val audioEnabledIntent = WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled) @@ -174,7 +175,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { Permissions.with(this) .request(Manifest.permission.CAMERA) .onAllGranted { - val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled) + val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoState.value.userVideoEnabled) startService(intent) } .execute() @@ -191,14 +192,44 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { onBackPressed() } + lifecycleScope.launch { + orientationManager.orientation.collect { orientation -> + viewModel.deviceOrientation = orientation + updateControlsRotation() + } + } + + clipFloatingInsets() } - //Function to check if Android System Auto-rotate is on or off - private fun isAutoRotateOn(): Boolean { - return Settings.System.getInt( - contentResolver, - Settings.System.ACCELEROMETER_ROTATION, 0 - ) == 1 + /** + * 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 + } + + override fun onResume() { + super.onResume() + orientationManager.startOrientationListener() + + } + + override fun onPause() { + super.onPause() + orientationManager.stopOrientationListener() } override fun onDestroy() { @@ -206,7 +237,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { hangupReceiver?.let { receiver -> LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } - rotationListener.disable() + + orientationManager.destroy() } private fun answerCall() { @@ -214,15 +246,31 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { ContextCompat.startForegroundService(this, answerIntent) } - private fun updateControlsRotation(newRotation: Int) { + private fun updateControlsRotation() { with (binding) { - val rotation = newRotation.toFloat() - remoteRecipient.rotation = rotation - speakerPhoneButton.rotation = rotation - microphoneButton.rotation = rotation - enableCameraButton.rotation = rotation - switchCameraButton.rotation = rotation - endCallButton.rotation = rotation + val rotation = when(viewModel.deviceOrientation){ + Orientation.LANDSCAPE -> -90f + Orientation.REVERSED_LANDSCAPE -> 90f + else -> 0f + } + + remoteRecipient.animate().cancel() + remoteRecipient.animate().rotation(rotation).start() + + speakerPhoneButton.animate().cancel() + speakerPhoneButton.animate().rotation(rotation).start() + + microphoneButton.animate().cancel() + microphoneButton.animate().rotation(rotation).start() + + enableCameraButton.animate().cancel() + enableCameraButton.animate().rotation(rotation).start() + + switchCameraButton.animate().cancel() + switchCameraButton.animate().rotation(rotation).start() + + endCallButton.animate().cancel() + endCallButton.animate().rotation(rotation).start() } } @@ -346,34 +394,43 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { } } + // handle video state launch { - viewModel.localVideoEnabledState.collect { isEnabled -> - binding.localRenderer.removeAllViews() - if (isEnabled) { - viewModel.localRenderer?.let { surfaceView -> - surfaceView.setZOrderOnTop(true) - - // Mirror the video preview of the person making the call to prevent disorienting them - surfaceView.setMirror(true) - - binding.localRenderer.addView(surfaceView) + viewModel.videoState.collect { state -> + binding.floatingRenderer.removeAllViews() + binding.fullscreenRenderer.removeAllViews() + + // the floating video inset (empty or not) should be shown + // the moment we have either of the video streams + val showFloatingContainer = state.userVideoEnabled || state.remoteVideoEnabled + binding.floatingRendererContainer.isVisible = showFloatingContainer + binding.swapViewIcon.isVisible = showFloatingContainer + + // handle fullscreen video window + if(state.showFullscreenVideo()){ + viewModel.fullscreenRenderer?.let { surfaceView -> + binding.fullscreenRenderer.addView(surfaceView) + binding.fullscreenRenderer.isVisible = true + binding.remoteRecipient.isVisible = false } + } else { + binding.fullscreenRenderer.isVisible = false + binding.remoteRecipient.isVisible = true } - binding.localRenderer.isVisible = isEnabled - binding.enableCameraButton.isSelected = isEnabled - } - } - launch { - viewModel.remoteVideoEnabledState.collect { isEnabled -> - binding.remoteRenderer.removeAllViews() - if (isEnabled) { - viewModel.remoteRenderer?.let { surfaceView -> - binding.remoteRenderer.addView(surfaceView) + // handle floating video window + if(state.showFloatingVideo()){ + viewModel.floatingRenderer?.let { surfaceView -> + binding.floatingRenderer.addView(surfaceView) + binding.floatingRenderer.isVisible = true + binding.swapViewIcon.bringToFront() } + } else { + binding.floatingRenderer.isVisible = false } - binding.remoteRenderer.isVisible = isEnabled - binding.remoteRecipient.isVisible = !isEnabled + + // handle buttons + binding.enableCameraButton.isSelected = state.userVideoEnabled } } } @@ -388,7 +445,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { override fun onStop() { super.onStop() uiJob?.cancel() - binding.remoteRenderer.removeAllViews() - binding.localRenderer.removeAllViews() + binding.fullscreenRenderer.removeAllViews() + binding.floatingRenderer.removeAllViews() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index cfe1f38f58..36106123a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -81,6 +81,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener { const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID" const val EXTRA_ENABLED = "ENABLED" const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND" + const val EXTRA_SWAPPED = "is_video_swapped" const val EXTRA_MUTE = "mute_value" const val EXTRA_AVAILABLE = "enabled_value" const val EXTRA_REMOTE_DESCRIPTION = "remote_description" diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 3fa861fb71..c1ac53baa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -34,7 +34,7 @@ fun AppTheme( ) { val extraColors = LocalContext.current.run { ExtraColors( - settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground), + settingsBackground = getColorFromTheme(R.attr.backgroundSecondary), prominentButtonColor = getColorFromTheme(R.attr.prominentButtonColor), ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index ff5e481895..52cc0ad322 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -6,11 +6,14 @@ import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind @@ -51,6 +54,7 @@ import org.webrtc.MediaStream import org.webrtc.PeerConnection import org.webrtc.PeerConnection.IceConnectionState import org.webrtc.PeerConnectionFactory +import org.webrtc.RendererCommon import org.webrtc.RtpReceiver import org.webrtc.SessionDescription import org.webrtc.SurfaceViewRenderer @@ -105,10 +109,15 @@ class CallManager( private val _audioEvents = MutableStateFlow(AudioEnabled(false)) val audioEvents = _audioEvents.asSharedFlow() - private val _videoEvents = MutableStateFlow(VideoEnabled(false)) - val videoEvents = _videoEvents.asSharedFlow() - private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false)) - val remoteVideoEvents = _remoteVideoEvents.asSharedFlow() + + private val _videoState: MutableStateFlow = MutableStateFlow( + VideoState( + swapped = false, + userVideoEnabled = false, + remoteVideoEnabled = false + ) + ) + val videoState = _videoState private val stateProcessor = StateProcessor(CallState.Idle) @@ -151,9 +160,9 @@ class CallManager( private val outgoingIceDebouncer = Debouncer(200L) - var localRenderer: SurfaceViewRenderer? = null + var floatingRenderer: SurfaceViewRenderer? = null var remoteRotationSink: RemoteRotationVideoProxySink? = null - var remoteRenderer: SurfaceViewRenderer? = null + var fullscreenRenderer: SurfaceViewRenderer? = null private var peerConnectionFactory: PeerConnectionFactory? = null fun clearPendingIceUpdates() { @@ -216,20 +225,18 @@ class CallManager( Util.runOnMainSync { val base = EglBase.create() eglBase = base - localRenderer = SurfaceViewRenderer(context).apply { -// setScalingType(SCALE_ASPECT_FIT) - } + floatingRenderer = SurfaceViewRenderer(context) + floatingRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + + fullscreenRenderer = SurfaceViewRenderer(context) + fullscreenRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) - remoteRenderer = SurfaceViewRenderer(context).apply { -// setScalingType(SCALE_ASPECT_FIT) - } remoteRotationSink = RemoteRotationVideoProxySink() - localRenderer?.init(base.eglBaseContext, null) - localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) - remoteRenderer?.init(base.eglBaseContext, null) - remoteRotationSink!!.setSink(remoteRenderer!!) + floatingRenderer?.init(base.eglBaseContext, null) + fullscreenRenderer?.init(base.eglBaseContext, null) + remoteRotationSink!!.setSink(fullscreenRenderer!!) val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true) val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext) @@ -363,7 +370,8 @@ class CallManager( val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] } val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject if (json.containsKey("video")) { - _remoteVideoEvents.value = VideoEnabled((json["video"] as JsonPrimitive).boolean) + _videoState.update { it.copy(remoteVideoEnabled = json["video"]?.jsonPrimitive?.boolean ?: false) } + handleMirroring() } else if (json.containsKey("hangup")) { peerConnectionObservers.forEach(WebRtcListener::onHangup) } @@ -383,13 +391,13 @@ class CallManager( peerConnection?.dispose() peerConnection = null - localRenderer?.release() + floatingRenderer?.release() remoteRotationSink?.release() - remoteRenderer?.release() + fullscreenRenderer?.release() eglBase?.release() - localRenderer = null - remoteRenderer = null + floatingRenderer = null + fullscreenRenderer = null eglBase = null localCameraState = CameraState.UNKNOWN @@ -399,8 +407,11 @@ class CallManager( pendingOffer = null callStartTime = -1 _audioEvents.value = AudioEnabled(false) - _videoEvents.value = VideoEnabled(false) - _remoteVideoEvents.value = VideoEnabled(false) + _videoState.value = VideoState( + swapped = false, + userVideoEnabled = false, + remoteVideoEnabled = false + ) pendingOutgoingIceUpdates.clear() pendingIncomingIceUpdates.clear() } @@ -411,7 +422,7 @@ class CallManager( // 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. - localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) + handleMirroring() } fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) { @@ -469,7 +480,7 @@ class CallManager( val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null")) val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null")) val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) - val local = localRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) + val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) val connection = PeerConnectionWrapper( context, @@ -515,7 +526,7 @@ class CallManager( ?: return Promise.ofFail(NullPointerException("recipient is null")) val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) - val local = localRenderer + val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) @@ -609,13 +620,58 @@ class CallManager( } } + fun swapVideos() { + // update the state + _videoState.update { it.copy(swapped = !it.swapped) } + handleMirroring() + + if (_videoState.value.swapped) { + peerConnection?.rotationVideoSink?.setSink(fullscreenRenderer) + floatingRenderer?.let{remoteRotationSink?.setSink(it) } + } else { + peerConnection?.rotationVideoSink?.apply { + setSink(floatingRenderer) + } + fullscreenRenderer?.let{ remoteRotationSink?.setSink(it) } + } + } + fun handleSetMuteAudio(muted: Boolean) { _audioEvents.value = AudioEnabled(!muted) peerConnection?.setAudioEnabled(!muted) } + /** + * Returns the renderer currently showing the user's video, not the contact's + */ + private fun getUserRenderer() = if(_videoState.value.swapped) fullscreenRenderer else floatingRenderer + + /** + * Returns the renderer currently showing the contact's video, not the user's + */ + private fun getRemoteRenderer() = if(_videoState.value.swapped) floatingRenderer else fullscreenRenderer + + /** + * Makes sure the user's renderer applies mirroring if necessary + */ + 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) { - _videoEvents.value = VideoEnabled(!muted) + _videoState.update { it.copy(userVideoEnabled = !muted) } + handleMirroring() + val connection = peerConnection ?: return connection.setVideoEnabled(!muted) dataChannel?.let { channel -> @@ -651,9 +707,18 @@ class CallManager( } } - fun setDeviceRotation(newRotation: Int) { - peerConnection?.setDeviceRotation(newRotation) - remoteRotationSink?.rotation = newRotation + fun setDeviceOrientation(orientation: Orientation) { + // set rotation to the video based on the device's orientation and the camera facing direction + val rotation = when (orientation) { + Orientation.PORTRAIT -> 0 + Orientation.LANDSCAPE -> if (isCameraFrontFacing()) 90 else -90 + Orientation.REVERSED_LANDSCAPE -> 270 + else -> 0 + } + + // apply the rotation to the streams + peerConnection?.setDeviceRotation(rotation) + remoteRotationSink?.rotation = rotation } fun handleWiredHeadsetChanged(present: Boolean) { @@ -721,7 +786,7 @@ class CallManager( connection.setCommunicationMode() setAudioEnabled(true) dataChannel?.let { channel -> - val toSend = if (!_videoEvents.value.isEnabled) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON + val toSend = if (_videoState.value.userVideoEnabled) VIDEO_ENABLED_JSON else VIDEO_DISABLED_JSON val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false) channel.send(buffer) } @@ -750,6 +815,8 @@ class CallManager( fun isInitiator(): Boolean = peerConnection?.isInitiator() == true + fun isCameraFrontFacing() = localCameraState.activeDirection != CameraState.Direction.BACK + interface WebRtcListener: PeerConnection.Observer { fun onHangup() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt index 4f27e5d1ad..f49e2d3333 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.webrtc import androidx.lifecycle.ViewModel 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.onEach import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager @@ -29,16 +31,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V UNTRUSTED_IDENTITY, } - val localRenderer: SurfaceViewRenderer? - get() = callManager.localRenderer + val floatingRenderer: SurfaceViewRenderer? + get() = callManager.floatingRenderer - val remoteRenderer: SurfaceViewRenderer? - get() = callManager.remoteRenderer - - private var _videoEnabled: Boolean = false - - val videoEnabled: Boolean - get() = _videoEnabled + val fullscreenRenderer: SurfaceViewRenderer? + get() = callManager.fullscreenRenderer private var _microphoneEnabled: Boolean = true @@ -59,18 +56,13 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V get() = callManager.audioEvents.map { it.isEnabled } .onEach { _microphoneEnabled = it } - val localVideoEnabledState - get() = callManager.videoEvents - .map { it.isEnabled } - .onEach { _videoEnabled = it } + val videoState: StateFlow + get() = callManager.videoState - val remoteVideoEnabledState - get() = callManager.remoteVideoEvents.map { it.isEnabled } - - var deviceRotation: Int = 0 + var deviceOrientation: Orientation = Orientation.UNKNOWN set(value) { field = value - callManager.setDeviceRotation(value) + callManager.setDeviceOrientation(value) } val currentCallState @@ -85,4 +77,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V val callStartTime: Long get() = callManager.callStartTime + fun swapVideos() { + callManager.swapVideos() + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt new file mode 100644 index 0000000000..05370fda4a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt @@ -0,0 +1,8 @@ +package org.thoughtcrime.securesms.webrtc + +enum class Orientation { + PORTRAIT, + LANDSCAPE, + REVERSED_LANDSCAPE, + UNKNOWN +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index f78b93d6b9..b61edbb6d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -41,7 +41,7 @@ class PeerConnectionWrapper(private val context: Context, private val mediaStream: MediaStream private val videoSource: VideoSource? private val videoTrack: VideoTrack? - private val rotationVideoSink = RotationVideoSink() + public val rotationVideoSink = RotationVideoSink() val readyForIce get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null @@ -103,7 +103,7 @@ class PeerConnectionWrapper(private val context: Context, context, rotationVideoSink ) - rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT + rotationVideoSink.setSink(localRenderer) newVideoTrack.setEnabled(false) mediaStream.addTrack(newVideoTrack) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt new file mode 100644 index 0000000000..55bb04038a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt deleted file mode 100644 index dc9f07d051..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.thoughtcrime.securesms.webrtc.data - -// get the video rotation from a specific rotation, locked into 90 degree -// chunks offset by 45 degrees -fun Int.quadrantRotation() = when (this % 360) { - in 315 .. 360, - in 0 until 45 -> 0 - in 45 until 135 -> 90 - in 135 until 225 -> 180 - else -> 270 -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt index 2b0caef89c..62b78d6ec8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.webrtc.video -import org.thoughtcrime.securesms.webrtc.data.quadrantRotation + import org.webrtc.VideoFrame import org.webrtc.VideoSink @@ -14,8 +14,7 @@ class RemoteRotationVideoProxySink: VideoSink { val thisSink = targetSink ?: return val thisFrame = frame ?: return - val quadrantRotation = rotation.quadrantRotation() - val modifiedRotation = thisFrame.rotation - quadrantRotation + val modifiedRotation = thisFrame.rotation - rotation val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs) thisSink.onFrame(newFrame) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt index ec43daf2ef..3522f06f9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.webrtc.video -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.webrtc.data.quadrantRotation + import org.webrtc.CapturerObserver import org.webrtc.VideoFrame import org.webrtc.VideoProcessor @@ -12,7 +11,6 @@ import java.util.concurrent.atomic.AtomicBoolean class RotationVideoSink: CapturerObserver, VideoProcessor { var rotation: Int = 0 - var mirrored = false private val capturing = AtomicBoolean(false) private var capturerObserver = SoftReference(null) @@ -31,13 +29,14 @@ class RotationVideoSink: CapturerObserver, VideoProcessor { val observer = capturerObserver.get() if (videoFrame == null || observer == null || !capturing.get()) return - val quadrantRotation = rotation.quadrantRotation() - - val newFrame = VideoFrame(videoFrame.buffer, (videoFrame.rotation + quadrantRotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1) % 360, videoFrame.timestampNs) - val localFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1, videoFrame.timestampNs) + // cater for frame rotation so that the video is always facing up as we rotate pas a certain point + val newFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation - rotation, videoFrame.timestampNs) + // the frame we are sending to our contact needs to cater for rotation observer.onFrameCaptured(newFrame) - sink.get()?.onFrame(localFrame) + + // the frame we see on the user's phone doesn't require changes + sink.get()?.onFrame(videoFrame) } override fun setSink(sink: VideoSink?) { diff --git a/app/src/main/res/drawable/conversation_view_background.xml b/app/src/main/res/drawable/conversation_view_background.xml index 2f177318e0..50e38698b4 100644 --- a/app/src/main/res/drawable/conversation_view_background.xml +++ b/app/src/main/res/drawable/conversation_view_background.xml @@ -4,6 +4,6 @@ android:color="?android:colorControlHighlight"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_bottom_sheet_background.xml b/app/src/main/res/drawable/default_bottom_sheet_background.xml index 63532b0d05..19300aae39 100644 --- a/app/src/main/res/drawable/default_bottom_sheet_background.xml +++ b/app/src/main/res/drawable/default_bottom_sheet_background.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - + - + diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml index d607bfc022..e546e1f84c 100644 --- a/app/src/main/res/drawable/dialog_background.xml +++ b/app/src/main/res/drawable/dialog_background.xml @@ -6,6 +6,6 @@ android:insetBottom="16dp"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml b/app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml new file mode 100644 index 0000000000..553db9c082 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_data.xml b/app/src/main/res/drawable/ic_clear_data.xml index 320015bb23..84465dd4cb 100644 --- a/app/src/main/res/drawable/ic_clear_data.xml +++ b/app/src/main/res/drawable/ic_clear_data.xml @@ -7,5 +7,5 @@ android:pathData="M19.907,7.674H19.907H4.54H4.54C4.317,7.674 4.095,7.719 3.888,7.806L3.888,7.806C3.681,7.893 3.491,8.023 3.334,8.189C3.176,8.355 3.054,8.554 2.978,8.775L3.922,9.097L2.978,8.775C2.903,8.996 2.877,9.231 2.904,9.465L2.904,9.465L2.904,9.469L4.555,23.412C4.555,23.413 4.555,23.413 4.555,23.414C4.603,23.823 4.807,24.189 5.111,24.447C5.415,24.705 5.798,24.84 6.187,24.84H6.188H18.26H18.26C18.649,24.84 19.032,24.705 19.336,24.447C19.64,24.189 19.844,23.823 19.892,23.414C19.892,23.413 19.892,23.413 19.892,23.412L21.543,9.469L21.544,9.465C21.57,9.231 21.544,8.996 21.469,8.775L21.469,8.775C21.393,8.554 21.271,8.355 21.113,8.189C20.956,8.023 20.766,7.893 20.559,7.806L20.17,8.728L20.559,7.806C20.352,7.719 20.13,7.674 19.907,7.674ZM21.412,1.84H3.031C2.045,1.84 1.149,2.609 1.149,3.674V5.828C1.149,6.893 2.045,7.662 3.031,7.662H21.412C22.398,7.662 23.294,6.893 23.294,5.828V3.674C23.294,2.609 22.398,1.84 21.412,1.84Z" android:strokeWidth="2" android:fillColor="#FF3A3A" - android:strokeColor="?colorPrimaryDark"/> + android:strokeColor="?backgroundSecondary"/> diff --git a/app/src/main/res/drawable/preference_bottom.xml b/app/src/main/res/drawable/preference_bottom.xml index b6c5f506fd..888778b1cb 100644 --- a/app/src/main/res/drawable/preference_bottom.xml +++ b/app/src/main/res/drawable/preference_bottom.xml @@ -6,7 +6,7 @@ android:bottom="@dimen/small_spacing" > - + diff --git a/app/src/main/res/drawable/preference_middle.xml b/app/src/main/res/drawable/preference_middle.xml index bf27aacc72..287645ab83 100644 --- a/app/src/main/res/drawable/preference_middle.xml +++ b/app/src/main/res/drawable/preference_middle.xml @@ -4,7 +4,7 @@ - + - + diff --git a/app/src/main/res/drawable/preference_single_no_padding.xml b/app/src/main/res/drawable/preference_single_no_padding.xml index 252ab0aea3..483894fcc2 100644 --- a/app/src/main/res/drawable/preference_single_no_padding.xml +++ b/app/src/main/res/drawable/preference_single_no_padding.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/drawable/preference_top.xml b/app/src/main/res/drawable/preference_top.xml index 8f56ddc870..180aa9f73f 100644 --- a/app/src/main/res/drawable/preference_top.xml +++ b/app/src/main/res/drawable/preference_top.xml @@ -6,7 +6,7 @@ android:top="@dimen/small_spacing" > - + diff --git a/app/src/main/res/layout/activity_appearance_settings.xml b/app/src/main/res/layout/activity_appearance_settings.xml index ce33af82f1..c4886708a3 100644 --- a/app/src/main/res/layout/activity_appearance_settings.xml +++ b/app/src/main/res/layout/activity_appearance_settings.xml @@ -25,7 +25,7 @@ app:cardElevation="0dp" app:cardCornerRadius="@dimen/dialog_corner_radius" android:layout_marginHorizontal="@dimen/medium_spacing" - app:cardBackgroundColor="?colorSettingsBackground" + app:cardBackgroundColor="?backgroundSecondary" android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -111,6 +111,7 @@ android:layout_height="wrap_content"/> + android:layout_width="0dp" + android:background="?backgroundSecondary"> + + android:layout_height="wrap_content" + android:layout_gravity="center"/> + + + + tools:text="8"/> @@ -115,8 +114,7 @@ android:textColor="?unreadIndicatorTextColor" android:textSize="@dimen/very_small_font_size" android:textStyle="bold" - android:text="@" - tools:textColor="?android:textColorPrimary" /> + android:text="@" /> diff --git a/app/src/main/res/layout/view_search_bottom_bar.xml b/app/src/main/res/layout/view_search_bottom_bar.xml index 7c08377a70..51bdd881ff 100644 --- a/app/src/main/res/layout/view_search_bottom_bar.xml +++ b/app/src/main/res/layout/view_search_bottom_bar.xml @@ -61,7 +61,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:padding="8dp" - android:background="?colorPrimaryDark" + android:background="?backgroundSecondary" app:SpinKit_Color="?android:textColorPrimary" android:visibility="gone"/> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ae38d0c057..aa356619fe 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -29,6 +29,7 @@ + @@ -56,8 +57,6 @@ - - @@ -97,7 +96,6 @@ - @@ -152,7 +150,6 @@ - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index eae5a2b167..a427629180 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -43,8 +43,6 @@ #ffbbbbbb #ff808080 #ff595959 - #ff4d4d4d - #ff383838 #0f000000 #26000000 @@ -126,7 +124,7 @@ #31F196 - #111111 + #000000 #1B1B1B #2D2D2D #414141 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 6471f73e52..f43d643b9e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -32,6 +32,7 @@ 250dp 64dp 8dp + 11dp 4dp 8dp 8dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 412eec96db..77140251ec 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -24,8 +24,7 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 428d37c5ef..eba219b924 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,7 @@ @@ -316,6 +314,7 @@ @color/classic_dark_6 @color/classic_dark_0 @color/classic_dark_0 + @color/classic_dark_1 ?android:textColorPrimary ?colorAccent @color/classic_dark_6 @@ -325,12 +324,10 @@ @color/gray27 ?colorPrimary @color/navigation_bar - @color/classic_dark_1 @style/Classic.Dark.BottomSheet ?android:textColorPrimary ?actionBarPopupTheme @color/classic_dark_1 - @color/classic_dark_1 @color/classic_dark_3 @color/classic_dark_3 @color/classic_dark_3 @@ -353,10 +350,10 @@ #00000000 @color/classic_dark_1 ?colorCellBackground - @color/classic_dark_2 + @color/classic_dark_1 ?android:textColorSecondary - @color/classic_dark_3 - @color/classic_dark_6 + ?colorAccent + @color/classic_dark_0 @color/classic_dark_2 @@ -368,7 +365,7 @@ @color/classic_dark_0 - @color/classic_dark_3 + @color/classic_dark_2 @color/classic_dark_6 ?colorAccent @color/classic_dark_0 @@ -394,7 +391,7 @@ @color/classic_light_0 @color/classic_light_6 - @color/classic_light_5 + @color/classic_light_5 @color/classic_light_6 ?android:textColorPrimary ?colorAccent @@ -406,7 +403,6 @@ ?colorPrimary @color/classic_light_navigation_bar @color/classic_light_6 - @color/classic_light_5 @color/classic_light_3 @color/classic_light_3 @color/classic_light_3 @@ -441,7 +437,7 @@ ?colorCellBackground @color/classic_light_6 ?android:textColorSecondary - @color/classic_light_3 + ?colorAccent @color/classic_light_0 @color/classic_light_4 @@ -454,7 +450,7 @@ @color/classic_light_6 - @color/classic_light_3 + @color/classic_light_4 @color/classic_light_0 ?colorAccent @color/classic_light_0 @@ -482,6 +478,7 @@ @color/ocean_dark_7 @color/ocean_dark_2 @color/ocean_dark_2 + @color/ocean_dark_1 @color/ocean_dark_7 ?colorAccent @color/ocean_dark_7 @@ -494,7 +491,6 @@ ?colorPrimary ?colorPrimaryDark @color/ocean_dark_3 - @color/ocean_dark_1 @color/ocean_dark_4 @color/ocean_dark_4 @color/ocean_dark_4 @@ -520,7 +516,7 @@ #00000000 @color/ocean_dark_3 ?colorCellBackground - @color/ocean_dark_4 + @color/ocean_dark_3 ?android:textColorSecondary ?colorAccent @color/ocean_dark_0 @@ -565,6 +561,7 @@ @color/ocean_light_1 @color/ocean_light_7 @color/ocean_light_6 + @color/ocean_light_6 @color/ocean_light_1 ?colorAccent @color/ocean_light_1 @@ -577,7 +574,6 @@ @color/ocean_light_7 @color/ocean_light_6 @color/ocean_light_5 - @color/ocean_light_6 @color/ocean_light_3 @color/ocean_light_4 @color/ocean_light_4 @@ -641,7 +637,7 @@ ?input_bar_button_background_opaque_border ?colorAccent ?colorCellBackground - @color/ocean_light_6 + @color/ocean_light_5 ?android:textColorSecondary @color/ocean_light_5 @@ -655,6 +651,7 @@ @color/classic_dark_6 @color/classic_dark_0 @color/classic_dark_0 + @color/classic_dark_1 ?android:textColorPrimary ?colorAccent ?colorAccent @@ -665,12 +662,10 @@ @color/gray27 ?colorPrimary @color/compose_view_background - @color/classic_dark_1 @style/Classic.Dark.BottomSheet ?android:textColorPrimary ?actionBarPopupTheme @color/classic_dark_1 - @color/classic_dark_1 @color/classic_dark_3 @style/Dark.Popup @null @@ -688,10 +683,10 @@ #00000000 @color/classic_dark_1 ?colorCellBackground - @color/classic_dark_2 + @color/classic_dark_1 ?android:textColorSecondary - @color/classic_dark_3 - @color/classic_dark_6 + ?colorAccent + @color/classic_dark_0 @color/classic_dark_2 @@ -703,7 +698,7 @@ @color/classic_dark_0 - @color/classic_dark_3 + @color/classic_dark_2 @color/classic_dark_6 ?colorAccent @color/classic_dark_0 diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index 474fe565e0..a412c13e76 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -53,8 +53,6 @@ - - @@ -93,7 +91,6 @@ - diff --git a/libsession/src/main/res/values/colors.xml b/libsession/src/main/res/values/colors.xml index a15aa4163a..9f94120b59 100644 --- a/libsession/src/main/res/values/colors.xml +++ b/libsession/src/main/res/values/colors.xml @@ -40,7 +40,6 @@ #ffbbbbbb #ff808080 #ff595959 - #ff4d4d4d #ff383838 #30000000