Fix/video call rotation and avatars (#1548)

* Simplifying profile picture view

We don't need the isLarge option as the component's size is always set.
Using profilePictureView in the call screen.

* Swapping avatars between user and contact's

* Adding the user's avatar for when it  needs to be displayed

* Making sure we never invert the contact's landscape rotation

* Update app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt

Co-authored-by: Andrew <andrewgallasch@gmail.com>

* Update app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt

Co-authored-by: Andrew <andrewgallasch@gmail.com>

* Update app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt

Co-authored-by: Andrew <andrewgallasch@gmail.com>

---------

Co-authored-by: Andrew <andrewgallasch@gmail.com>
This commit is contained in:
ThomasSession 2024-07-17 15:16:12 +10:00 committed by GitHub
parent 3bac04c863
commit 01cd449794
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 89 additions and 92 deletions

View File

@ -33,6 +33,8 @@ import network.loki.messenger.databinding.ActivityWebrtcBinding
import org.apache.commons.lang3.time.DurationFormatUtils import org.apache.commons.lang3.time.DurationFormatUtils
import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -200,6 +202,16 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
clipFloatingInsets() clipFloatingInsets()
// set up the user avatar
TextSecurePreferences.getLocalNumber(this)?.let{
val username = TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(it)
binding.userAvatar.apply {
publicKey = it
displayName = username
update()
}
}
} }
/** /**
@ -254,8 +266,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
else -> 0f else -> 0f
} }
remoteRecipient.animate().cancel() userAvatar.animate().cancel()
remoteRecipient.animate().rotation(rotation).start() userAvatar.animate().rotation(rotation).start()
contactAvatar.animate().cancel()
contactAvatar.animate().rotation(rotation).start()
speakerPhoneButton.animate().cancel() speakerPhoneButton.animate().cancel()
speakerPhoneButton.animate().rotation(rotation).start() speakerPhoneButton.animate().rotation(rotation).start()
@ -328,44 +342,20 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
launch { launch {
viewModel.recipient.collect { latestRecipient -> viewModel.recipient.collect { latestRecipient ->
binding.contactAvatar.recycle()
if (latestRecipient.recipient != null) { if (latestRecipient.recipient != null) {
val publicKey = latestRecipient.recipient.address.serialize() val contactPublicKey = latestRecipient.recipient.address.serialize()
val displayName = getUserDisplayName(publicKey) val contactDisplayName = getUserDisplayName(contactPublicKey)
supportActionBar?.title = displayName supportActionBar?.title = contactDisplayName
val signalProfilePicture = latestRecipient.recipient.contactPhoto binding.remoteRecipientName.text = contactDisplayName
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
val sizeInPX = // sort out the contact's avatar
resources.getDimensionPixelSize(R.dimen.extra_large_profile_picture_size) binding.contactAvatar.apply {
binding.remoteRecipientName.text = displayName publicKey = contactPublicKey
if (signalProfilePicture != null && avatar != "0" && avatar != "") { displayName = contactDisplayName
glide.clear(binding.remoteRecipient) update()
glide.load(signalProfilePicture)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.circleCrop()
.error(
AvatarPlaceholderGenerator.generate(
this@WebRtcCallActivity,
sizeInPX,
publicKey,
displayName
)
)
.into(binding.remoteRecipient)
} else {
glide.clear(binding.remoteRecipient)
glide.load(
AvatarPlaceholderGenerator.generate(
this@WebRtcCallActivity,
sizeInPX,
publicKey,
displayName
)
)
.diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop()
.into(binding.remoteRecipient)
} }
} else {
glide.clear(binding.remoteRecipient)
} }
} }
} }
@ -400,22 +390,16 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
binding.floatingRenderer.removeAllViews() binding.floatingRenderer.removeAllViews()
binding.fullscreenRenderer.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 // handle fullscreen video window
if(state.showFullscreenVideo()){ if(state.showFullscreenVideo()){
viewModel.fullscreenRenderer?.let { surfaceView -> viewModel.fullscreenRenderer?.let { surfaceView ->
binding.fullscreenRenderer.addView(surfaceView) binding.fullscreenRenderer.addView(surfaceView)
binding.fullscreenRenderer.isVisible = true binding.fullscreenRenderer.isVisible = true
binding.remoteRecipient.isVisible = false hideAvatar()
} }
} else { } else {
binding.fullscreenRenderer.isVisible = false binding.fullscreenRenderer.isVisible = false
binding.remoteRecipient.isVisible = true showAvatar(state.swapped)
} }
// handle floating video window // handle floating video window
@ -429,6 +413,15 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
binding.floatingRenderer.isVisible = false binding.floatingRenderer.isVisible = false
} }
// 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
// make sure to default to the contact's avatar if the floating container is not visible
if (!showFloatingContainer) showAvatar(false)
// handle buttons // handle buttons
binding.enableCameraButton.isSelected = state.userVideoEnabled binding.enableCameraButton.isSelected = state.userVideoEnabled
} }
@ -436,6 +429,20 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
} }
/**
* Shows the avatar image.
* If @showUserAvatar is true, the user's avatar is shown, otherwise the contact's avatar is shown.
*/
private fun showAvatar(showUserAvatar: Boolean) {
binding.userAvatar.isVisible = showUserAvatar
binding.contactAvatar.isVisible = !showUserAvatar
}
private fun hideAvatar() {
binding.userAvatar.isVisible = false
binding.contactAvatar.isVisible = false
}
private fun getUserDisplayName(publicKey: String): String { private fun getUserDisplayName(publicKey: String): String {
val contact = val contact =
DatabaseComponent.get(this).sessionContactDatabase().getContactWithSessionID(publicKey) DatabaseComponent.get(this).sessionContactDatabase().getContactWithSessionID(publicKey)

View File

@ -33,7 +33,6 @@ class ProfilePictureView @JvmOverloads constructor(
var displayName: String? = null var displayName: String? = null
var additionalPublicKey: String? = null var additionalPublicKey: String? = null
var additionalDisplayName: String? = null var additionalDisplayName: String? = null
var isLarge = false
private val profilePicturesCache = mutableMapOf<View, Recipient>() private val profilePicturesCache = mutableMapOf<View, Recipient>()
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default) private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
@ -90,29 +89,25 @@ class ProfilePictureView @JvmOverloads constructor(
fun update() { fun update() {
val publicKey = publicKey ?: return Log.w(TAG, "Could not find public key to update profile picture") val publicKey = publicKey ?: return Log.w(TAG, "Could not find public key to update profile picture")
val additionalPublicKey = additionalPublicKey val additionalPublicKey = additionalPublicKey
// if we have a multi avatar setup
if (additionalPublicKey != null) { if (additionalPublicKey != null) {
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName) setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName) setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
binding.doubleModeImageViewContainer.visibility = View.VISIBLE binding.doubleModeImageViewContainer.visibility = View.VISIBLE
} else {
// clear single image
glide.clear(binding.singleModeImageView)
binding.singleModeImageView.visibility = View.INVISIBLE
} else { // single image mode
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
binding.singleModeImageView.visibility = View.VISIBLE
// clear multi image
glide.clear(binding.doubleModeImageView1) glide.clear(binding.doubleModeImageView1)
glide.clear(binding.doubleModeImageView2) glide.clear(binding.doubleModeImageView2)
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
} }
if (additionalPublicKey == null && !isLarge) {
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
binding.singleModeImageView.visibility = View.VISIBLE
} else {
glide.clear(binding.singleModeImageView)
binding.singleModeImageView.visibility = View.INVISIBLE
}
if (additionalPublicKey == null && isLarge) {
setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName)
binding.largeSingleModeImageView.visibility = View.VISIBLE
} else {
glide.clear(binding.largeSingleModeImageView)
binding.largeSingleModeImageView.visibility = View.INVISIBLE
}
} }
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {

View File

@ -56,7 +56,6 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
with(binding) { with(binding) {
profilePictureView.publicKey = publicKey profilePictureView.publicKey = publicKey
profilePictureView.isLarge = true
profilePictureView.update(recipient) profilePictureView.update(recipient)
nameTextViewContainer.visibility = View.VISIBLE nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener { nameTextViewContainer.setOnClickListener {

View File

@ -123,7 +123,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
view.apply { view.apply {
publicKey = hexEncodedPublicKey publicKey = hexEncodedPublicKey
displayName = getDisplayName() displayName = getDisplayName()
isLarge = true
update() update()
} }
} }

View File

@ -61,6 +61,7 @@ import org.webrtc.SurfaceViewRenderer
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.UUID import java.util.UUID
import kotlin.math.abs
import org.thoughtcrime.securesms.webrtc.data.State as CallState import org.thoughtcrime.securesms.webrtc.data.State as CallState
class CallManager( class CallManager(
@ -718,7 +719,7 @@ class CallManager(
// apply the rotation to the streams // apply the rotation to the streams
peerConnection?.setDeviceRotation(rotation) peerConnection?.setDeviceRotation(rotation)
remoteRotationSink?.rotation = rotation remoteRotationSink?.rotation = abs(rotation) // abs as we never need the remote video to be inverted
} }
fun handleWiredHeadsetChanged(present: Boolean) { fun handleWiredHeadsetChanged(present: Boolean) {

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape <shape
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="oval">
<solid android:color="@color/profile_picture_background" /> <solid android:color="@color/profile_picture_background" />
<corners android:radius="40dp" />
</shape> </shape>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/profile_picture_background" />
<corners android:radius="23dp" />
</shape>

View File

@ -23,8 +23,20 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"/> android:layout_gravity="center"/>
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/remote_recipient" <org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/userAvatar"
app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
app:layout_constraintBottom_toBottomOf="@id/fullscreen_renderer_container"
app:layout_constraintVertical_bias="0.4"
android:layout_width="@dimen/extra_large_profile_picture_size"
android:layout_height="@dimen/extra_large_profile_picture_size"
android:visibility="gone"/>
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/contactAvatar"
app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container" app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container" app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container" app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
@ -71,9 +83,9 @@
android:foregroundGravity="center" android:foregroundGravity="center"
android:visibility="gone" android:visibility="gone"
app:SpinKit_Color="@color/core_white" app:SpinKit_Color="@color/core_white"
app:layout_constraintEnd_toEndOf="@+id/remote_recipient" app:layout_constraintEnd_toEndOf="@+id/contactAvatar"
app:layout_constraintStart_toStartOf="@+id/remote_recipient" app:layout_constraintStart_toStartOf="@+id/contactAvatar"
app:layout_constraintTop_toBottomOf="@id/remote_recipient" app:layout_constraintTop_toBottomOf="@id/contactAvatar"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView

View File

@ -27,17 +27,11 @@
</RelativeLayout> </RelativeLayout>
<ImageView <ImageView
android:scaleType="centerCrop"
android:id="@+id/singleModeImageView" android:id="@+id/singleModeImageView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"
android:background="@drawable/profile_picture_view_medium_background" />
<ImageView
android:id="@+id/largeSingleModeImageView"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:layout_width="@dimen/large_profile_picture_size" android:layout_width="match_parent"
android:layout_height="@dimen/large_profile_picture_size" android:layout_height="match_parent"
android:background="@drawable/profile_picture_view_large_background" /> android:adjustViewBounds="true"
android:background="@drawable/profile_picture_view_background" />
</merge> </merge>