Specific layout for when conference layout is active speaker & alone or 2

This commit is contained in:
Sylvain Berfini 2022-08-23 17:08:11 +02:00
parent 740a935525
commit 1761843c1b
10 changed files with 548 additions and 43 deletions

View file

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

View file

@ -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<VoipConferenceCallFragmentBinding
if (displayMode == ConferenceDisplayMode.ACTIVE_SPEAKER) {
if (conferenceViewModel.conferenceExists.value == true) {
Log.i("[Conference Call] Local participant is in conference and current layout is active speaker, updating Core's native window id")
val window =
binding.root.findViewById<RoundCornersTextureView>(R.id.conference_active_speaker_remote_video)
val window = binding.root.findViewById<RoundCornersTextureView>(R.id.conference_active_speaker_remote_video)
coreContext.core.nativeVideoWindowId = window
val preview = binding.root.findViewById<RoundCornersTextureView>(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<VoipConferenceCallFragmentBinding
}
}
conferenceViewModel.secondParticipantJoinedEvent.observe(
viewLifecycleOwner
) {
it.consume {
switchToActiveSpeakerLayoutForTwoParticipants()
}
}
conferenceViewModel.moreThanTwoParticipantsJoinedEvent.observe(
viewLifecycleOwner
) {
it.consume {
switchToActiveSpeakerLayoutForMoreThanTwoParticipants()
}
}
conferenceViewModel.conference.observe(
viewLifecycleOwner
) { conference ->
@ -163,6 +182,8 @@ class ConferenceCallFragment : GenericFragment<VoipConferenceCallFragmentBinding
.make(binding.coordinator, R.string.conference_last_user, Snackbar.LENGTH_LONG)
.setAnchorView(binding.primaryButtons.hangup)
.show()
switchToActiveSpeakerLayoutWhenAlone()
}
}
@ -278,9 +299,21 @@ class ConferenceCallFragment : GenericFragment<VoipConferenceCallFragmentBinding
controlsViewModel.hideExtraButtons(true)
}
override fun onResume() {
super.onResume()
if (conferenceViewModel.conferenceCreationPending.value == false) {
when (conferenceViewModel.conferenceParticipantDevices.value.orEmpty().size) {
1 -> 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<VoipConferenceCallFragmentBinding
startActivity(intent)
}
private fun showSnackBar(resourceId: Int) {
Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show()
}
private fun startTimer(timerId: Int) {
val timer: Chronometer? = binding.root.findViewById(timerId)
if (timer == null) {
@ -349,4 +378,190 @@ class ConferenceCallFragment : GenericFragment<VoipConferenceCallFragmentBinding
set.applyTo(constraintLayout)
}
private fun animateConstraintLayout(
constraintLayout: ConstraintLayout,
set: ConstraintSet
) {
val trans = AutoTransition()
trans.duration = 500
trans.interpolator = AccelerateDecelerateInterpolator()
TransitionManager.beginDelayedTransition(constraintLayout, trans)
set.applyTo(constraintLayout)
}
private fun switchToActiveSpeakerLayoutForMoreThanTwoParticipants() {
if (conferenceViewModel.conferenceDisplayMode.value != ConferenceDisplayMode.ACTIVE_SPEAKER) return
val constraintLayout =
binding.root.findViewById<ConstraintLayout>(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<ConstraintLayout>(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<ConstraintLayout>(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)
}
}
}

View file

@ -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<List<ConferenceParticipantData>>()
val conferenceParticipantDevices = MutableLiveData<List<ConferenceParticipantDeviceData>>()
val conferenceDisplayMode = MutableLiveData<ConferenceDisplayMode>()
val activeSpeakerConferenceParticipantDevices = MediatorLiveData<List<ConferenceParticipantDeviceData>>()
val isRecording = MutableLiveData<Boolean>()
val isRemotelyRecorded = MutableLiveData<Boolean>()
val maxParticipantsForMosaicLayout = corePreferences.maxConferenceParticipantsForMosaicLayout
val moreThanTwoParticipants = MutableLiveData<Boolean>()
val speakingParticipant = MutableLiveData<ConferenceParticipantDeviceData>()
val meParticipant = MutableLiveData<ConferenceParticipantDeviceData>()
val participantAdminStatusChangedEvent: MutableLiveData<Event<ConferenceParticipantData>> by lazy {
MutableLiveData<Event<ConferenceParticipantData>>()
@ -65,6 +70,14 @@ class ConferenceViewModel : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
val secondParticipantJoinedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val moreThanTwoParticipantsJoinedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
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<ConferenceParticipantDeviceData>()
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<ConferenceParticipantDeviceData>): ArrayList<ConferenceParticipantDeviceData> {

View file

@ -75,7 +75,7 @@ class ControlsViewModel : ViewModel() {
val proximitySensorEnabled = MediatorLiveData<Boolean>()
val showTakeSnaptshotButton = MutableLiveData<Boolean>()
val showTakeSnapshotButton = MutableLiveData<Boolean>()
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
@ -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 {

View file

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

View file

@ -50,13 +50,6 @@
android:orientation="horizontal"
app:layout_constraintGuide_percent="1" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{controlsViewModel.fullScreenMode || controlsViewModel.pipMode ? View.GONE : View.VISIBLE}"
app:constraint_referenced_ids="remote_name,active_conference_timer,toggle_conference_recording,toggle_pause_conference" />
<TextView
android:id="@+id/remote_name"
style="@style/call_header_title"
@ -117,11 +110,18 @@
app:layout_constraintEnd_toEndOf="parent"
android:visibility="@{conferenceViewModel.isRemotelyRecorded ? View.VISIBLE : View.GONE, default=gone}" />
<androidx.constraintlayout.widget.Group
android:id="@+id/active_speaker_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{conferenceViewModel.conferenceParticipantDevices.size() == 1 ? View.GONE : View.VISIBLE, default=gone}"
app:constraint_referenced_ids="active_speaker_background,speaking_participant_avatar,conference_active_speaker_remote_video,speaking_participant_name" />
<org.linphone.activities.voip.views.ScrollDotsView
android:id="@+id/scroll_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{controlsViewModel.pipMode ? View.GONE : View.VISIBLE}"
android:visibility="@{controlsViewModel.pipMode || !conferenceViewModel.moreThanTwoParticipants ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintBottom_toBottomOf="@id/hinge_bottom"
@ -137,9 +137,11 @@
android:id="@+id/miniatures"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
android:layout_marginTop="5dp"
app:layout_constraintTop_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toStartOf="@id/scroll_indicator"
app:layout_constraintBottom_toBottomOf="@id/hinge_bottom"
android:visibility="@{conferenceViewModel.moreThanTwoParticipants ? View.VISIBLE : View.GONE, default=gone}"
android:scrollbars="none">
<com.google.android.flexbox.FlexboxLayout
@ -147,7 +149,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:alignItems="stretch"
app:entries="@{conferenceViewModel.conferenceParticipantDevices}"
app:entries="@{conferenceViewModel.activeSpeakerConferenceParticipantDevices}"
app:flexDirection="column"
app:flexWrap="nowrap"
app:justifyContent="flex_start"
@ -173,6 +175,7 @@
android:layout_height="0dp"
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_toBottomOf="@id/active_speaker_background"
@ -182,6 +185,16 @@
app:layout_constraintTop_toTopOf="@id/active_speaker_background"
app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" />
<ProgressBar
android:id="@+id/speaking_participant_joining"
android:layout_width="@dimen/voip_conference_participant_joining_icon_size_active_speaker"
android:layout_height="@dimen/voip_conference_participant_joining_icon_size_active_speaker"
android:indeterminate="true"
android:indeterminateDrawable="@drawable/icon_spinner_rotating"
android:visibility="@{conferenceViewModel.speakingParticipant.isJoining ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintLeft_toLeftOf="@id/active_speaker_background"
app:layout_constraintTop_toTopOf="@id/active_speaker_background" />
<org.linphone.activities.voip.views.RoundCornersTextureView
android:id="@+id/conference_active_speaker_remote_video"
android:layout_width="0dp"
@ -203,6 +216,109 @@
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
app:layout_constraintStart_toStartOf="@id/active_speaker_background" />
<View
android:id="@+id/local_participant_background"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="@{controlsViewModel.fullScreenMode ? @dimen/margin_0dp : @dimen/voip_active_speaker_top_margin, default=@dimen/voip_active_speaker_top_margin}"
android:background="@drawable/shape_remote_background"
android:onClick="@{() -> controlsViewModel.toggleFullScreen()}"/>
<ImageView
android:id="@+id/local_participant_avatar"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
android:contentDescription="@null"
coilSelfAvatar="@{conferenceViewModel.meParticipant}"
android:background="@drawable/generated_avatar_bg"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background"
app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" />
<org.linphone.activities.voip.views.RoundCornersTextureView
android:id="@+id/local_preview_video_surface"
android:layout_width="0dp"
android:layout_height="0dp"
app:alignTopRight="false"
app:displayMode="occupy_all_space"
android:visibility="@{conferenceViewModel.meParticipant.isInConference &amp;&amp; controlsViewModel.isSendingVideo ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<TextView
android:id="@+id/local_participant_name"
style="@style/call_remote_name_font"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:text="@{conferenceViewModel.meParticipant.contact.name ?? conferenceViewModel.meParticipant.displayName}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"/>
<ImageView
android:id="@+id/switch_camera"
android:layout_width="@dimen/conference_miniature_switch_camera_icon_size"
android:layout_height="@dimen/conference_miniature_switch_camera_icon_size"
android:layout_margin="5dp"
android:contentDescription="@string/content_description_switch_camera"
android:onClick="@{() -> controlsViewModel.switchCamera()}"
android:src="@drawable/icon_call_camera_switch"
android:visibility="@{controlsViewModel.isSwitchCameraAvailable &amp;&amp; !controlsViewModel.pipMode &amp;&amp; !controlsViewModel.fullScreenMode ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<ImageView
android:id="@+id/local_participant_speaking_border"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:src="@drawable/shape_conference_active_speaker_border"
android:visibility="@{conferenceViewModel.meParticipant.isSpeaking ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintLeft_toLeftOf="@id/local_participant_background"
app:layout_constraintRight_toRightOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<ImageView
android:id="@+id/local_participant_muted"
android:layout_width="@dimen/voip_conference_participant_mic_muted_icon_size_active_speaker"
android:layout_height="@dimen/voip_conference_participant_mic_muted_icon_size_active_speaker"
android:layout_margin="5dp"
android:background="@drawable/shape_toggle_pressed_background"
android:contentDescription="@string/content_description_conference_participant_mic_muted"
android:padding="2dp"
android:src="@drawable/icon_mic_muted"
android:visibility="@{conferenceViewModel.meParticipant.isMuted ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintLeft_toLeftOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<ImageView
android:id="@+id/local_participant_paused"
android:layout_width="@dimen/voip_conference_active_speaker_miniature_avatar_size"
android:layout_height="@dimen/voip_conference_active_speaker_miniature_avatar_size"
android:background="@drawable/shape_button_background"
android:contentDescription="@string/content_description_participant_is_paused"
android:src="@drawable/icon_pause"
android:visibility="@{conferenceViewModel.conferenceCreationPending || conferenceViewModel.meParticipant.isInConference || conferenceViewModel.meParticipant.isJoining ? View.GONE : View.VISIBLE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -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" />
<ProgressBar
android:id="@+id/speaking_participant_joining"
android:layout_width="@dimen/voip_conference_participant_joining_icon_size_active_speaker"
android:layout_height="@dimen/voip_conference_participant_joining_icon_size_active_speaker"
android:indeterminate="true"
android:indeterminateDrawable="@drawable/icon_spinner_rotating"
android:visibility="@{conferenceViewModel.speakingParticipant.isJoining ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintLeft_toLeftOf="@id/active_speaker_background"
app:layout_constraintTop_toTopOf="@id/active_speaker_background" />
<org.linphone.activities.voip.views.RoundCornersTextureView
android:id="@+id/conference_active_speaker_remote_video"
android:layout_width="0dp"
@ -170,9 +181,11 @@
android:id="@+id/miniatures"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="5dp"
app:layout_constraintStart_toEndOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/scroll_indicator"
android:visibility="@{conferenceViewModel.moreThanTwoParticipants ? View.VISIBLE : View.GONE, default=gone}"
android:scrollbars="none">
<com.google.android.flexbox.FlexboxLayout
@ -180,7 +193,7 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:alignItems="stretch"
app:entries="@{conferenceViewModel.conferenceParticipantDevices}"
app:entries="@{conferenceViewModel.activeSpeakerConferenceParticipantDevices}"
app:flexDirection="row"
app:flexWrap="nowrap"
app:justifyContent="flex_start"
@ -192,7 +205,7 @@
android:id="@+id/scroll_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{controlsViewModel.pipMode ? View.GONE : View.VISIBLE}"
android:visibility="@{controlsViewModel.pipMode || !conferenceViewModel.moreThanTwoParticipants ? View.GONE : View.VISIBLE, default=gone}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/hinge_bottom"
@ -204,6 +217,109 @@
app:selectedDot="@{0}"
app:selectedDotColor="@color/voip_dark_gray" />
<View
android:id="@+id/local_participant_background"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/top_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="@{controlsViewModel.fullScreenMode ? @dimen/margin_0dp : @dimen/voip_active_speaker_top_margin, default=@dimen/voip_active_speaker_top_margin}"
android:background="@drawable/shape_remote_background"
android:onClick="@{() -> controlsViewModel.toggleFullScreen()}"/>
<ImageView
android:id="@+id/local_participant_avatar"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
android:contentDescription="@null"
coilSelfAvatar="@{conferenceViewModel.meParticipant}"
android:background="@drawable/generated_avatar_bg"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background"
app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" />
<org.linphone.activities.voip.views.RoundCornersTextureView
android:id="@+id/local_preview_video_surface"
android:layout_width="0dp"
android:layout_height="0dp"
app:alignTopRight="false"
app:displayMode="occupy_all_space"
android:visibility="@{conferenceViewModel.meParticipant.isInConference &amp;&amp; controlsViewModel.isSendingVideo ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<TextView
android:id="@+id/local_participant_name"
style="@style/call_remote_name_font"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:text="@{conferenceViewModel.meParticipant.contact.name ?? conferenceViewModel.meParticipant.displayName}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"/>
<ImageView
android:id="@+id/switch_camera"
android:layout_width="@dimen/conference_miniature_switch_camera_icon_size"
android:layout_height="@dimen/conference_miniature_switch_camera_icon_size"
android:layout_margin="5dp"
android:contentDescription="@string/content_description_switch_camera"
android:onClick="@{() -> controlsViewModel.switchCamera()}"
android:src="@drawable/icon_call_camera_switch"
android:visibility="@{controlsViewModel.isSwitchCameraAvailable &amp;&amp; !controlsViewModel.pipMode &amp;&amp; !controlsViewModel.fullScreenMode ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<ImageView
android:id="@+id/local_participant_speaking_border"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@null"
android:src="@drawable/shape_conference_active_speaker_border"
android:visibility="@{conferenceViewModel.meParticipant.isSpeaking ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintLeft_toLeftOf="@id/local_participant_background"
app:layout_constraintRight_toRightOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<ImageView
android:id="@+id/local_participant_muted"
android:layout_width="@dimen/voip_conference_participant_mic_muted_icon_size_active_speaker"
android:layout_height="@dimen/voip_conference_participant_mic_muted_icon_size_active_speaker"
android:layout_margin="5dp"
android:background="@drawable/shape_toggle_pressed_background"
android:contentDescription="@string/content_description_conference_participant_mic_muted"
android:padding="2dp"
android:src="@drawable/icon_mic_muted"
android:visibility="@{conferenceViewModel.meParticipant.isMuted ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintLeft_toLeftOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
<ImageView
android:id="@+id/local_participant_paused"
android:layout_width="@dimen/voip_conference_active_speaker_miniature_avatar_size"
android:layout_height="@dimen/voip_conference_active_speaker_miniature_avatar_size"
android:background="@drawable/shape_button_background"
android:contentDescription="@string/content_description_participant_is_paused"
android:src="@drawable/icon_pause"
android:visibility="@{conferenceViewModel.conferenceCreationPending || conferenceViewModel.meParticipant.isInConference || conferenceViewModel.meParticipant.isJoining ? View.GONE : View.VISIBLE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/local_participant_background"
app:layout_constraintEnd_toEndOf="@id/local_participant_background"
app:layout_constraintStart_toStartOf="@id/local_participant_background"
app:layout_constraintTop_toTopOf="@id/local_participant_background" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -58,8 +58,8 @@
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_width="@dimen/conference_miniature_switch_camera_icon_size"
android:layout_height="@dimen/conference_miniature_switch_camera_icon_size"
android:layout_margin="5dp"
android:contentDescription="@string/content_description_switch_camera"
android:onClick="@{() -> data.switchCamera()}"

View file

@ -198,7 +198,7 @@
android:onClick="@{() -> callsViewModel.takeSnapshot()}"
android:enabled="@{!callsViewModel.currentCallData.isPaused &amp;&amp; !callsViewModel.currentCallData.isRemotelyPaused}"
android:src="@drawable/icon_call_screenshot"
android:visibility="@{!controlsViewModel.showTakeSnaptshotButton || controlsViewModel.fullScreenMode || controlsViewModel.pipMode ? View.GONE : View.VISIBLE}"
android:visibility="@{!controlsViewModel.showTakeSnapshotButton || controlsViewModel.fullScreenMode || controlsViewModel.pipMode ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toStartOf="@id/background"
app:layout_constraintTop_toBottomOf="@id/record_call" />

View file

@ -78,4 +78,5 @@
<dimen name="chat_message_content_preview_max_width">120dp</dimen>
<dimen name="voip_conference_active_speaker_dots_margin">5dp</dimen>
<dimen name="conference_waiting_room_buttons_max_width">250dp</dimen>
<dimen name="conference_miniature_switch_camera_icon_size">25dp</dimen>
</resources>