diff --git a/CHANGELOG.md b/CHANGELOG.md index 05aab4280..0154c87d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Group changes to describe their impact on the project, as follows: - Android 13 support, using new post notifications & media permissions - Call recordings can be exported - Setting to prevent international prefix from account to be applied to call & chat +- Themed app icon is now supported for Android 13+ ### Changed - In-call views have been re-designed diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt index e07438b1a..2e64ac931 100644 --- a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt +++ b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt @@ -21,9 +21,11 @@ package org.linphone.activities.voip.fragments import android.annotation.SuppressLint import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import android.os.SystemClock import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator import android.widget.Chronometer import android.widget.Toast import androidx.constraintlayout.widget.ConstraintLayout @@ -31,6 +33,8 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.navigation.navGraphViewModels +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager import androidx.window.layout.FoldingFeature import com.google.android.material.snackbar.Snackbar import org.linphone.LinphoneApplication.Companion.coreContext @@ -38,9 +42,6 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.activities.* import org.linphone.activities.main.MainActivity -import org.linphone.activities.navigateToCallsList -import org.linphone.activities.navigateToConferenceLayout -import org.linphone.activities.navigateToConferenceParticipants import org.linphone.activities.voip.ConferenceDisplayMode import org.linphone.activities.voip.viewmodels.CallsViewModel import org.linphone.activities.voip.viewmodels.ConferenceViewModel @@ -90,9 +91,11 @@ class ConferenceCallFragment : GenericFragment(R.id.conference_active_speaker_remote_video) + val window = binding.root.findViewById(R.id.conference_active_speaker_remote_video) coreContext.core.nativeVideoWindowId = window + + val preview = binding.root.findViewById(R.id.local_preview_video_surface) + conferenceViewModel.meParticipant.value?.setTextureView(preview) } else { Log.i("[Conference Call] Either not in conference or current layout isn't active speaker, updating Core's native window id") coreContext.core.nativeVideoWindowId = null @@ -115,6 +118,22 @@ class ConferenceCallFragment : GenericFragment @@ -163,6 +182,8 @@ class ConferenceCallFragment : GenericFragment switchToActiveSpeakerLayoutWhenAlone() + 2 -> switchToActiveSpeakerLayoutForTwoParticipants() + else -> switchToActiveSpeakerLayoutForMoreThanTwoParticipants() + } + } + } + private fun switchToFullScreenIfPossible(conference: Conference) { if (corePreferences.enableFullScreenWhenJoiningVideoConference) { - if (conference.currentParams.isVideoEnabled) { + if (conference.currentParams.isVideoEnabled && conferenceViewModel.conferenceCreationPending.value == false) { when { conference.me.devices.isEmpty() -> { Log.w("[Conference Call] Conference has video enabled but either our device hasn't joined yet") @@ -305,10 +338,6 @@ class ConferenceCallFragment : GenericFragment(R.id.conference_constraint_layout) + ?: return + val set = ConstraintSet() + set.clone(constraintLayout) + + set.clear(R.id.local_participant_background, ConstraintSet.TOP) + set.clear(R.id.local_participant_background, ConstraintSet.START) + set.clear(R.id.local_participant_background, ConstraintSet.BOTTOM) + set.clear(R.id.local_participant_background, ConstraintSet.END) + + val margin = resources.getDimension(R.dimen.voip_active_speaker_miniature_margin).toInt() + val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE + if (portraitOrientation) { + set.connect( + R.id.local_participant_background, + ConstraintSet.START, + R.id.conference_constraint_layout, + ConstraintSet.START, + margin + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.BOTTOM, + R.id.miniatures, + ConstraintSet.BOTTOM, + 0 + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.TOP, + R.id.miniatures, + ConstraintSet.TOP, + 0 + ) + } else { + set.connect( + R.id.local_participant_background, + ConstraintSet.TOP, + R.id.top_barrier, + ConstraintSet.BOTTOM, + 0 + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.START, + R.id.active_speaker_background, + ConstraintSet.END, + 0 + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.END, + R.id.scroll_indicator, + ConstraintSet.START, + 0 + ) + } + + val size = resources.getDimension(R.dimen.voip_active_speaker_miniature_size).toInt() + set.constrainWidth( + R.id.local_participant_background, + size + ) + set.constrainHeight( + R.id.local_participant_background, + size + ) + + if (corePreferences.enableAnimations) { + animateConstraintLayout(constraintLayout, set) + } else { + set.applyTo(constraintLayout) + } + } + + private fun switchToActiveSpeakerLayoutForTwoParticipants() { + if (conferenceViewModel.conferenceDisplayMode.value != ConferenceDisplayMode.ACTIVE_SPEAKER) return + + val constraintLayout = + binding.root.findViewById(R.id.conference_constraint_layout) + ?: return + val set = ConstraintSet() + set.clone(constraintLayout) + + set.clear(R.id.local_participant_background, ConstraintSet.TOP) + set.clear(R.id.local_participant_background, ConstraintSet.START) + set.clear(R.id.local_participant_background, ConstraintSet.BOTTOM) + set.clear(R.id.local_participant_background, ConstraintSet.END) + + val margin = resources.getDimension(R.dimen.voip_active_speaker_miniature_margin).toInt() + set.connect( + R.id.local_participant_background, + ConstraintSet.BOTTOM, + R.id.conference_constraint_layout, + ConstraintSet.BOTTOM, + margin + ) + // Don't know why but if we use END instead of RIGHT, margin isn't applied... + set.connect( + R.id.local_participant_background, + ConstraintSet.RIGHT, + R.id.conference_constraint_layout, + ConstraintSet.RIGHT, + margin + ) + + val size = resources.getDimension(R.dimen.voip_active_speaker_miniature_size).toInt() + set.constrainWidth( + R.id.local_participant_background, + size + ) + set.constrainHeight( + R.id.local_participant_background, + size + ) + + if (corePreferences.enableAnimations) { + animateConstraintLayout(constraintLayout, set) + } else { + set.applyTo(constraintLayout) + } + } + + private fun switchToActiveSpeakerLayoutWhenAlone() { + if (conferenceViewModel.conferenceDisplayMode.value != ConferenceDisplayMode.ACTIVE_SPEAKER) return + + val constraintLayout = + binding.root.findViewById(R.id.conference_constraint_layout) + ?: return + val set = ConstraintSet() + set.clone(constraintLayout) + + set.connect( + R.id.local_participant_background, + ConstraintSet.BOTTOM, + R.id.conference_constraint_layout, + ConstraintSet.BOTTOM, + 0 + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.END, + R.id.conference_constraint_layout, + ConstraintSet.END, + 0 + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.TOP, + R.id.top_barrier, + ConstraintSet.BOTTOM, + 0 + ) + set.connect( + R.id.local_participant_background, + ConstraintSet.START, + R.id.conference_constraint_layout, + ConstraintSet.START, + 0 + ) + + set.constrainWidth(R.id.local_participant_background, 0) + set.constrainHeight(R.id.local_participant_background, 0) + + if (corePreferences.enableAnimations) { + animateConstraintLayout(constraintLayout, set) + } else { + set.applyTo(constraintLayout) + } + } } diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt index 55b8f11cd..fef77b923 100644 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt @@ -19,6 +19,7 @@ */ package org.linphone.activities.voip.viewmodels +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext @@ -45,13 +46,17 @@ class ConferenceViewModel : ViewModel() { val conferenceParticipants = MutableLiveData>() val conferenceParticipantDevices = MutableLiveData>() val conferenceDisplayMode = MutableLiveData() + val activeSpeakerConferenceParticipantDevices = MediatorLiveData>() val isRecording = MutableLiveData() val isRemotelyRecorded = MutableLiveData() val maxParticipantsForMosaicLayout = corePreferences.maxConferenceParticipantsForMosaicLayout + val moreThanTwoParticipants = MutableLiveData() + val speakingParticipant = MutableLiveData() + val meParticipant = MutableLiveData() val participantAdminStatusChangedEvent: MutableLiveData> by lazy { MutableLiveData>() @@ -65,6 +70,14 @@ class ConferenceViewModel : ViewModel() { MutableLiveData>() } + val secondParticipantJoinedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val moreThanTwoParticipantsJoinedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + private val conferenceListener = object : ConferenceListenerStub() { override fun onParticipantAdded(conference: Conference, participant: Participant) { Log.i("[Conference] Participant added: ${participant.address.asStringUriOnly()}") @@ -77,11 +90,6 @@ class ConferenceViewModel : ViewModel() { if (conferenceParticipants.value.orEmpty().isEmpty()) { allParticipantsLeftEvent.value = Event(true) - // TODO: FIXME: Temporary workaround when alone in a conference in active speaker layout - val meDeviceData = conferenceParticipantDevices.value.orEmpty().firstOrNull() - if (meDeviceData != null) { - speakingParticipant.value = meDeviceData!! - } } } @@ -91,6 +99,12 @@ class ConferenceViewModel : ViewModel() { ) { Log.i("[Conference] Participant device added: ${participantDevice.address.asStringUriOnly()}") addParticipantDevice(participantDevice) + + if (conferenceParticipantDevices.value.orEmpty().size == 2) { + secondParticipantJoinedEvent.value = Event(true) + } else if (conferenceParticipantDevices.value.orEmpty().size == 3) { + moreThanTwoParticipantsJoinedEvent.value = Event(true) + } } override fun onParticipantDeviceRemoved( @@ -99,6 +113,10 @@ class ConferenceViewModel : ViewModel() { ) { Log.i("[Conference] Participant device removed: ${participantDevice.address.asStringUriOnly()}") removeParticipantDevice(participantDevice) + + if (conferenceParticipantDevices.value.orEmpty().size == 2) { + secondParticipantJoinedEvent.value = Event(true) + } } override fun onParticipantAdminStatusChanged( @@ -208,6 +226,9 @@ class ConferenceViewModel : ViewModel() { conferenceParticipants.value = arrayListOf() conferenceParticipantDevices.value = arrayListOf() + activeSpeakerConferenceParticipantDevices.addSource(conferenceParticipantDevices) { + activeSpeakerConferenceParticipantDevices.value = conferenceParticipantDevices.value.orEmpty().drop(1) + } subject.value = AppUtils.getString(R.string.conference_default_title) @@ -240,6 +261,7 @@ class ConferenceViewModel : ViewModel() { conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) conferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + activeSpeakerConferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) super.onCleared() } @@ -283,7 +305,13 @@ class ConferenceViewModel : ViewModel() { if (conferenceParticipants.value.orEmpty().isEmpty()) { firstToJoinEvent.value = Event(true) } + updateParticipantsDevicesList(conference) + if (conferenceParticipantDevices.value.orEmpty().size == 2) { + secondParticipantJoinedEvent.value = Event(true) + } else if (conferenceParticipantDevices.value.orEmpty().size > 2) { + moreThanTwoParticipantsJoinedEvent.value = Event(true) + } isConferenceLocallyPaused.value = !conference.isIn isMeAdmin.value = conference.me.isAdmin @@ -371,6 +399,8 @@ class ConferenceViewModel : ViewModel() { conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) conferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + activeSpeakerConferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + conferenceParticipants.value = arrayListOf() conferenceParticipantDevices.value = arrayListOf() } @@ -394,6 +424,7 @@ class ConferenceViewModel : ViewModel() { private fun updateParticipantsDevicesList(conference: Conference) { conferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + activeSpeakerConferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) val devices = arrayListOf() val participantsList = conference.participantList @@ -415,14 +446,12 @@ class ConferenceViewModel : ViewModel() { for (device in conference.me.devices) { Log.i("[Conference] Participant device for myself found: ${device.name} (${device.address.asStringUriOnly()})") val deviceData = ConferenceParticipantDeviceData(device, true) - if (devices.isEmpty()) { - // TODO: FIXME: Temporary workaround when alone in a conference in active speaker layout - speakingParticipant.value = deviceData - } devices.add(deviceData) + meParticipant.value = deviceData } conferenceParticipantDevices.value = devices + moreThanTwoParticipants.value = devices.size > 2 } private fun addParticipantDevice(device: ParticipantDevice) { @@ -448,6 +477,7 @@ class ConferenceViewModel : ViewModel() { } conferenceParticipantDevices.value = sortedDevices + moreThanTwoParticipants.value = sortedDevices.size > 2 } private fun removeParticipantDevice(device: ParticipantDevice) { @@ -456,6 +486,8 @@ class ConferenceViewModel : ViewModel() { for (participantDevice in conferenceParticipantDevices.value.orEmpty()) { if (participantDevice.participantDevice.address.asStringUriOnly() != device.address.asStringUriOnly()) { devices.add(participantDevice) + } else { + participantDevice.destroy() } } if (devices.size == conferenceParticipantDevices.value.orEmpty().size) { @@ -465,6 +497,7 @@ class ConferenceViewModel : ViewModel() { } conferenceParticipantDevices.value = devices + moreThanTwoParticipants.value = devices.size > 2 } private fun sortDevicesDataList(devices: List): ArrayList { diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt index 21752bcdb..61a6448be 100644 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt @@ -75,7 +75,7 @@ class ControlsViewModel : ViewModel() { val proximitySensorEnabled = MediatorLiveData() - val showTakeSnaptshotButton = MutableLiveData() + val showTakeSnapshotButton = MutableLiveData() val goToConferenceParticipantsListEvent: MutableLiveData> by lazy { MutableLiveData>() @@ -436,14 +436,15 @@ class ControlsViewModel : ViewModel() { } isVideoEnabled.value = enabled - showTakeSnaptshotButton.value = enabled && corePreferences.showScreenshotButton - isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton() - isSendingVideo.value = if (coreContext.core.currentCall?.conference != null) { + showTakeSnapshotButton.value = enabled && corePreferences.showScreenshotButton + var isVideoBeingSent = if (coreContext.core.currentCall?.conference != null) { val videoDirection = coreContext.core.currentCall?.currentParams?.videoDirection videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly } else { true } + isSendingVideo.value = isVideoBeingSent + isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton() && isVideoBeingSent } private fun shouldProximitySensorBeEnabled(): Boolean { diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 4d85806c6..031b2031e 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -337,11 +337,18 @@ private suspend fun loadContactPictureWithCoil( size: Int = 0, textSize: Int = 0, color: Int = 0, - textColor: Int = 0 + textColor: Int = 0, + defaultAvatar: String? = null ) { val context = imageView.context if (contact == null) { - imageView.load(R.drawable.icon_single_contact_avatar) + if (defaultAvatar != null) { + imageView.load(defaultAvatar) { + transformations(CircleCropTransformation()) + } + } else { + imageView.load(R.drawable.icon_single_contact_avatar) + } } else if (contact.showGroupChatAvatar) { imageView.load(AppCompatResources.getDrawable(context, R.drawable.icon_multiple_contacts_avatar)) } else { @@ -430,6 +437,21 @@ fun loadVoipContactPictureWithCoil(imageView: ImageView, contact: ContactDataInt } } +@BindingAdapter("coilSelfAvatar") +fun loadSelfAvatarWithCoil(imageView: ImageView, contact: ContactDataInterface?) { + val coroutineScope = contact?.coroutineScope ?: coreContext.coroutineScope + coroutineScope.launch { + withContext(Dispatchers.Main) { + loadContactPictureWithCoil( + imageView, contact, false, + R.dimen.voip_contact_avatar_max_size, R.dimen.voip_contact_avatar_text_size, + R.attr.voipBackgroundColor, R.color.white_color, + corePreferences.defaultAccountAvatarPath + ) + } + } +} + @BindingAdapter("coilGoneIfError") fun loadAvatarWithCoil(imageView: ImageView, path: String?) { if (path != null) { diff --git a/app/src/main/res/layout-land/voip_conference_active_speaker.xml b/app/src/main/res/layout-land/voip_conference_active_speaker.xml index 439b76420..8247bcc0a 100644 --- a/app/src/main/res/layout-land/voip_conference_active_speaker.xml +++ b/app/src/main/res/layout-land/voip_conference_active_speaker.xml @@ -50,13 +50,6 @@ android:orientation="horizontal" app:layout_constraintGuide_percent="1" /> - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_active_speaker.xml b/app/src/main/res/layout/voip_conference_active_speaker.xml index 61776a944..6ea3d9d2e 100644 --- a/app/src/main/res/layout/voip_conference_active_speaker.xml +++ b/app/src/main/res/layout/voip_conference_active_speaker.xml @@ -136,15 +136,26 @@ android:layout_margin="10dp" android:contentDescription="@null" coilVoipContact="@{conferenceViewModel.speakingParticipant}" + android:visibility="@{conferenceViewModel.speakingParticipant.isJoining ? View.GONE : View.VISIBLE}" android:background="@drawable/generated_avatar_bg" app:layout_constraintDimensionRatio="1:1" - app:layout_constraintBottom_toTopOf="@id/miniatures" + app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" app:layout_constraintEnd_toEndOf="@id/active_speaker_background" app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size" app:layout_constraintStart_toStartOf="@id/active_speaker_background" - app:layout_constraintTop_toBottomOf="@id/top_barrier" + app:layout_constraintTop_toTopOf="@id/active_speaker_background" app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" /> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml b/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml index c64221b77..283134df8 100644 --- a/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml +++ b/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml @@ -58,8 +58,8 @@ app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index b36fef9ab..edd606712 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -78,4 +78,5 @@ 120dp 5dp 250dp + 25dp \ No newline at end of file