diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt index f79851720..5455d5d48 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt @@ -63,6 +63,8 @@ class ChatMessageContentData( val isConferenceSchedule = MutableLiveData() val isConferenceUpdated = MutableLiveData() val isConferenceCancelled = MutableLiveData() + val isBroadcast = MutableLiveData() + val isSpeaker = MutableLiveData() val fileName = MutableLiveData() val filePath = MutableLiveData() @@ -378,14 +380,26 @@ class ChatMessageContentData( var participantsCount = conferenceInfo.participants.size val organizer = conferenceInfo.organizer var organizerFound = false - if (organizer != null) { - for (participant in conferenceInfo.participants) { + var allSpeaker = true + isSpeaker.value = true + for (info in conferenceInfo.participantInfos) { + val participant = info.address + if (participant.weakEqual(chatMessage.chatRoom.localAddress)) { + isSpeaker.value = info.role == Participant.Role.Speaker + } + + if (info.role == Participant.Role.Listener) { + allSpeaker = false + } + + if (organizer != null) { if (participant.weakEqual(organizer)) { organizerFound = true - break } } } + isBroadcast.value = allSpeaker == false + if (!organizerFound) participantsCount += 1 // +1 for organizer conferenceParticipantCount.value = String.format( AppUtils.getString(R.string.conference_invite_participants_count), diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt index c49947b2d..dd8828aab 100644 --- a/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt @@ -19,15 +19,35 @@ */ package org.linphone.activities.main.conference.data +import androidx.lifecycle.MutableLiveData import org.linphone.contact.GenericContactData import org.linphone.core.Address import org.linphone.utils.LinphoneUtils class ConferenceSchedulingParticipantData( - private val sipAddress: Address, + val sipAddress: Address, val showLimeBadge: Boolean = false, - val showDivider: Boolean = true + val showDivider: Boolean = true, + val showBroadcastControls: Boolean = false, + val speaker: Boolean = false, + private val onAddedToSpeakers: ((data: ConferenceSchedulingParticipantData) -> Unit)? = null, + private val onRemovedFromSpeakers: ((data: ConferenceSchedulingParticipantData) -> Unit)? = null ) : GenericContactData(sipAddress) { + val isSpeaker = MutableLiveData() + val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress) + + init { + isSpeaker.value = speaker + } + + fun changeIsSpeaker() { + isSpeaker.value = isSpeaker.value == false + if (isSpeaker.value == true) { + onAddedToSpeakers?.invoke(this) + } else { + onRemovedFromSpeakers?.invoke(this) + } + } } diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt index 04b3d1151..3c9717287 100644 --- a/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt @@ -25,6 +25,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.ConferenceInfo import org.linphone.core.ConferenceInfo.State +import org.linphone.core.Participant import org.linphone.core.tools.Log import org.linphone.utils.LinphoneUtils import org.linphone.utils.TimestampUtils @@ -45,9 +46,12 @@ class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val is val participantsExpanded = MutableLiveData() val showDuration = MutableLiveData() val isConferenceCancelled = MutableLiveData() + val isBroadcast = MutableLiveData() + val speakersExpanded = MutableLiveData() init { expanded.value = false + isBroadcast.value = false address.value = conferenceInfo.uri?.asStringUriOnly() subject.value = conferenceInfo.subject @@ -141,24 +145,48 @@ class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val is private fun computeParticipantsLists() { var participantsListShort = "" var participantsListExpanded = "" + var speakersListExpanded = "" + + var allSpeaker = true + for (info in conferenceInfo.participantInfos) { + val participant = info.address + Log.i( + "[Scheduled Conference] Conference [${subject.value}] participant [${participant.asStringUriOnly()}] is a [${info.role}]" + ) - for (participant in conferenceInfo.participants) { val contact = coreContext.contactsManager.findContactByAddress(participant) val name = if (contact != null) { contact.name } else { - LinphoneUtils.getDisplayName( - participant - ) + LinphoneUtils.getDisplayName(participant) } val address = participant.asStringUriOnly() participantsListShort += "$name, " - participantsListExpanded += "$name ($address)\n" + when (info.role) { + Participant.Role.Speaker -> { + speakersListExpanded += "$name ($address)\n" + } + Participant.Role.Listener -> { + participantsListExpanded += "$name ($address)\n" + allSpeaker = false + } + else -> { // For meetings created before 5.3 SDK + participantsListExpanded += "$name ($address)\n" + } + } } participantsListShort = participantsListShort.dropLast(2) participantsListExpanded = participantsListExpanded.dropLast(1) + speakersListExpanded = speakersListExpanded.dropLast(1) participantsShort.value = participantsListShort participantsExpanded.value = participantsListExpanded + speakersExpanded.value = speakersListExpanded + + // If all participants have Speaker role then it is a meeting, else it is a broadcast + isBroadcast.value = allSpeaker == false + Log.i( + "[Scheduled Conference] Conference [${subject.value}] is a ${if (allSpeaker) "meeting" else "broadcast"}" + ) } } diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt index c856aad5a..f282c96a7 100644 --- a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt @@ -53,6 +53,9 @@ class ConferenceWaitingRoomFragment : GenericFragment() val isUpdate = MutableLiveData() + val isBroadcastAllowed = MutableLiveData() + val mode = MutableLiveData() + val modesList: List + val formattedDate = MutableLiveData() val formattedTime = MutableLiveData() @@ -50,6 +56,7 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { val sendInviteViaEmail = MutableLiveData() val participantsData = MutableLiveData>() + val speakersData = MutableLiveData>() val address = MutableLiveData
() @@ -74,6 +81,8 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { private var confInfo: ConferenceInfo? = null private val conferenceScheduler = coreContext.core.createConferenceScheduler() + private val selectedSpeakersAddresses = MutableLiveData>() + private val listener = object : ConferenceSchedulerListenerStub() { override fun onStateChanged( conferenceScheduler: ConferenceScheduler, @@ -154,6 +163,13 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { scheduleForLater.value = false isUpdate.value = false + isBroadcastAllowed.value = !corePreferences.disableBroadcastConference + modesList = arrayListOf( + AppUtils.getString(R.string.conference_schedule_mode_meeting), + AppUtils.getString(R.string.conference_schedule_mode_broadcast) + ) + mode.value = modesList.first() // Meeting by default + isEncrypted.value = false sendInviteViaChat.value = true sendInviteViaEmail.value = false @@ -185,6 +201,7 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { override fun onCleared() { conferenceScheduler.removeListener(listener) participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) + speakersData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) super.onCleared() } @@ -195,6 +212,7 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { } fun populateFromConferenceInfo(conferenceInfo: ConferenceInfo) { + // Pre-set data from existing conference info, used when editing an already scheduled broadcast or meeting confInfo = conferenceInfo address.value = conferenceInfo.uri @@ -213,10 +231,26 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { scheduleForLater.value = conferenceDuration > 0 val participantsList = arrayListOf
() - for (participant in conferenceInfo.participants) { + val speakersList = arrayListOf
() + for (info in conferenceInfo.participantInfos) { + val participant = info.address participantsList.add(participant) + if (info.role == Participant.Role.Speaker) { + speakersList.add(participant) + } + } + if (participantsList.count() == speakersList.count()) { + // All participants are speaker, this is a meeting, clear speakers + Log.i("[Conference Creation] Conference info is a meeting") + speakersList.clear() + mode.value = modesList.first() + } else { + Log.i("[Conference Creation] Conference info is a broadcast") + mode.value = modesList.last() } selectedAddresses.value = participantsList + selectedSpeakersAddresses.value = speakersList + computeParticipantsData() } @@ -241,14 +275,57 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { fun computeParticipantsData() { participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) - val list = arrayListOf() + speakersData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) + + val participantsList = arrayListOf() + val speakersList = arrayListOf() for (address in selectedAddresses.value.orEmpty()) { - val data = ConferenceSchedulingParticipantData(address, isEncrypted.value == true) - list.add(data) + val isSpeaker = address in selectedSpeakersAddresses.value.orEmpty() + val data = ConferenceSchedulingParticipantData( + address, + showLimeBadge = isEncrypted.value == true, + showBroadcastControls = isModeBroadcastCurrentlySelected(), + speaker = isSpeaker, + onAddedToSpeakers = { data -> + Log.i( + "[Conference Creation] Participant [${address.asStringUriOnly()}] added to speakers" + ) + val participants = arrayListOf() + participants.addAll(participantsData.value.orEmpty()) + participants.remove(data) + participantsData.value = participants + + val speakers = arrayListOf() + speakers.addAll(speakersData.value.orEmpty()) + speakers.add(data) + speakersData.value = speakers + }, + onRemovedFromSpeakers = { data -> + Log.i( + "[Conference Creation] Participant [${address.asStringUriOnly()}] removed from speakers" + ) + val speakers = arrayListOf() + speakers.addAll(speakersData.value.orEmpty()) + speakers.remove(data) + speakersData.value = speakers + + val participants = arrayListOf() + participants.addAll(participantsData.value.orEmpty()) + participants.add(data) + participantsData.value = participants + } + ) + + if (isSpeaker) { + speakersList.add(data) + } else { + participantsList.add(data) + } } - participantsData.value = list + participantsData.value = participantsList + speakersData.value = speakersList } fun createConference() { @@ -260,8 +337,6 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { conferenceCreationInProgress.value = true val core = coreContext.core - val participants = arrayOfNulls
(selectedAddresses.value.orEmpty().size) - selectedAddresses.value?.toArray(participants) val localAccount = core.defaultAccount val localAddress = localAccount?.params?.identityAddress @@ -273,7 +348,25 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { conferenceInfo.organizer = localAddress conferenceInfo.subject = subject.value conferenceInfo.description = description.value - conferenceInfo.setParticipants(participants) + + val participants = arrayOfNulls(selectedAddresses.value.orEmpty().size) + var index = 0 + val isBroadcast = isModeBroadcastCurrentlySelected() + for (participant in participantsData.value.orEmpty()) { + val info = Factory.instance().createParticipantInfo(participant.sipAddress) + // For meetings, all participants must have Speaker role + info?.role = if (isBroadcast) Participant.Role.Listener else Participant.Role.Speaker + participants[index] = info + index += 1 + } + for (speaker in speakersData.value.orEmpty()) { + val info = Factory.instance().createParticipantInfo(speaker.sipAddress) + info?.role = Participant.Role.Speaker + participants[index] = info + index += 1 + } + conferenceInfo.setParticipantInfos(participants) + if (scheduleForLater.value == true) { val startTime = getConferenceStartTimestamp() conferenceInfo.dateTime = startTime @@ -287,6 +380,10 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { conferenceScheduler.info = conferenceInfo } + fun isModeBroadcastCurrentlySelected(): Boolean { + return mode.value == AppUtils.getString(R.string.conference_schedule_mode_broadcast) + } + private fun computeTimeZonesList(): List { return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted() } diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt index 9238283ec..c3bc4fd8a 100644 --- a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt @@ -61,6 +61,8 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() { val networkReachable = MutableLiveData() + val isConferenceBroadcastWithListenerRole = MutableLiveData() + val askPermissionEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -198,7 +200,10 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() { } layoutMenuSelected.value = false - updateLayout() + when (core.defaultConferenceLayout) { + Conference.Layout.Grid -> setMosaicLayout() + else -> setActiveSpeakerLayout() + } if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) { setBluetoothAudioRoute() @@ -216,6 +221,44 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() { super.onCleared() } + fun findConferenceInfoByAddress(stringAddress: String?) { + if (stringAddress != null) { + val address = Factory.instance().createAddress(stringAddress) + if (address != null) { + val conferenceInfo = coreContext.core.findConferenceInformationFromUri(address) + if (conferenceInfo != null) { + val myself = conferenceInfo.participantInfos.find { + it.address.asStringUriOnly() == coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly() + } + if (myself != null) { + Log.i( + "[Conference Waiting Room] Found our participant, it's role is [${myself.role}]" + ) + val areWeListener = myself.role == Participant.Role.Listener + isConferenceBroadcastWithListenerRole.value = areWeListener + + if (areWeListener) { + callParams.isVideoEnabled = false + callParams.videoDirection = MediaDirection.Inactive + updateVideoState() + updateLayout() + } + } else { + Log.e( + "[Conference Waiting Room] Failed to find ourselves in participants info" + ) + } + } else { + Log.e( + "[Conference Waiting Room] Failed to find conference info using address [$stringAddress]" + ) + } + } + } else { + Log.e("[Conference Waiting Room] Can't find conference info using null address!") + } + } + fun cancel() { cancelConferenceJoiningEvent.value = Event(true) } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt index 51db98b06..0fadbee7f 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt @@ -35,8 +35,16 @@ class ConferencesSettingsViewModel : GenericSettingsViewModel() { val layoutLabels = MutableLiveData>() private val layoutValues = arrayListOf() + val enableBroadcastListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.disableBroadcastConference = !newValue + } + } + val enableBroadcast = MutableLiveData() + init { initLayoutsList() + enableBroadcast.value = !prefs.disableBroadcastConference } private fun initLayoutsList() { diff --git a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt index 43d54d4a5..d0e679273 100644 --- a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt +++ b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt @@ -34,10 +34,13 @@ class ConferenceParticipantData( val isAdmin = MutableLiveData() val isMeAdmin = MutableLiveData() + val isSpeaker = MutableLiveData() init { isAdmin.value = participant.isAdmin isMeAdmin.value = conference.me.isAdmin + isSpeaker.value = participant.role == Participant.Role.Speaker + Log.i( "[Conference Participant] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}" ) 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 087632476..c1a9bfda5 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 @@ -56,9 +56,13 @@ class ConferenceViewModel : ViewModel() { val twoOrMoreParticipants = MutableLiveData() val moreThanTwoParticipants = MutableLiveData() + val speakingParticipantFound = MutableLiveData() val speakingParticipant = MutableLiveData() val meParticipant = MutableLiveData() + val isBroadcast = MutableLiveData() + val isMeListenerOnly = MutableLiveData() + val participantAdminStatusChangedEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -207,6 +211,7 @@ class ConferenceViewModel : ViewModel() { speakingParticipant.value?.isActiveSpeaker?.value = false device.isActiveSpeaker.value = true speakingParticipant.value = device!! + speakingParticipantFound.value = true } else if (device == null) { Log.w( "[Conference] Participant device [${participantDevice.address.asStringUriOnly()}] is the active speaker but couldn't find it in devices list" @@ -528,6 +533,23 @@ class ConferenceViewModel : ViewModel() { val activelySpeakingParticipantDevice = conference.activeSpeakerParticipantDevice var foundActivelySpeakingParticipantDevice = false + speakingParticipantFound.value = false + + val conferenceInfo = conference.core.findConferenceInformationFromUri( + conference.conferenceAddress + ) + var allSpeaker = true + for (info in conferenceInfo?.participantInfos.orEmpty()) { + if (info.role == Participant.Role.Listener) { + allSpeaker = false + } + } + isBroadcast.value = !allSpeaker + if (!allSpeaker) { + Log.i( + "[Conference] Not all participants are speaker, considering it is a broadcast" + ) + } for (participant in participantsList) { val participantDevices = participant.devices @@ -539,6 +561,18 @@ class ConferenceViewModel : ViewModel() { Log.i( "[Conference] Participant device found: ${device.name} (${device.address.asStringUriOnly()})" ) + + val info = conferenceInfo?.participantInfos?.find { + it.address.weakEqual(participant.address) + } + if (info != null) { + Log.i("[Conference] Participant role is [${info.role.name}]") + val listener = info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown + if (listener) { + continue + } + } + val deviceData = ConferenceParticipantDeviceData(device, false) devices.add(deviceData) @@ -549,6 +583,7 @@ class ConferenceViewModel : ViewModel() { speakingParticipant.value = deviceData deviceData.isActiveSpeaker.value = true foundActivelySpeakingParticipantDevice = true + speakingParticipantFound.value = true } } } @@ -560,12 +595,26 @@ class ConferenceViewModel : ViewModel() { val deviceData = devices.first() speakingParticipant.value = deviceData deviceData.isActiveSpeaker.value = true + speakingParticipantFound.value = false } for (device in conference.me.devices) { Log.i( "[Conference] Participant device for myself found: ${device.name} (${device.address.asStringUriOnly()})" ) + + val info = conferenceInfo?.participantInfos?.find { + it.address.weakEqual(device.address) + } + if (info != null) { + Log.i("[Conference] Me role is [${info.role.name}]") + val listener = info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown + isMeListenerOnly.value = listener + if (listener) { + continue + } + } + val deviceData = ConferenceParticipantDeviceData(device, true) devices.add(deviceData) meParticipant.value = deviceData @@ -601,6 +650,7 @@ class ConferenceViewModel : ViewModel() { if (speakingParticipant.value == null) { speakingParticipant.value = deviceData deviceData.isActiveSpeaker.value = true + speakingParticipantFound.value = false } conferenceParticipantDevices.value = sortedDevices @@ -641,6 +691,7 @@ class ConferenceViewModel : ViewModel() { val deviceData = devices[1] speakingParticipant.value = deviceData deviceData.isActiveSpeaker.value = true + speakingParticipantFound.value = false } conferenceParticipantDevices.value = devices diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index baf376e80..e65c90932 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -377,6 +377,12 @@ class CorePreferences constructor(private val context: Context) { config.setBool("app", "enter_video_conference_enable_full_screen_mode", value) } + var disableBroadcastConference: Boolean + get() = config.getBool("app", "disable_broadcast_conference_feature", true) + set(value) { + config.setBool("app", "disable_broadcast_conference_feature", value) + } + /* Assistant */ var firstStart: Boolean diff --git a/app/src/main/res/drawable/rect_orange_button.xml b/app/src/main/res/drawable/rect_orange_button.xml new file mode 100644 index 000000000..683e0fb36 --- /dev/null +++ b/app/src/main/res/drawable/rect_orange_button.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_orange_disabled_button.xml b/app/src/main/res/drawable/shape_rect_orange_disabled_button.xml new file mode 100644 index 000000000..7928e4f48 --- /dev/null +++ b/app/src/main/res/drawable/shape_rect_orange_disabled_button.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file 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 8106f0a2b..8a5301c07 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 @@ -166,6 +166,7 @@ android:id="@+id/active_speaker_background" android:layout_width="0dp" android:layout_height="0dp" + android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toBottomOf="@id/top_barrier" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/miniatures" @@ -179,6 +180,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:contentDescription="@null" + android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}" coilVoipContact="@{conferenceViewModel.speakingParticipant}" android:background="@drawable/generated_avatar_bg" app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" @@ -196,7 +198,7 @@ android:background="@drawable/shape_button_background" android:contentDescription="@string/content_description_participant_is_paused" android:src="@drawable/icon_pause" - android:visibility="@{conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}" + android:visibility="@{conferenceViewModel.speakingParticipantFound && conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}" app:layout_constraintDimensionRatio="1:1" app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" app:layout_constraintEnd_toEndOf="@id/active_speaker_background" @@ -253,6 +255,7 @@ android:id="@+id/local_participant_background" android:layout_width="0dp" android:layout_height="0dp" + android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/top_barrier" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -267,6 +270,7 @@ android:layout_height="0dp" android:layout_margin="10dp" android:contentDescription="@null" + android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}" coilSelfAvatar="@{conferenceViewModel.meParticipant}" android:background="@drawable/generated_avatar_bg" app:layout_constraintDimensionRatio="1:1" @@ -297,6 +301,7 @@ android:layout_marginStart="5dp" android:layout_marginEnd="5dp" android:layout_marginBottom="5dp" + android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}" 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" diff --git a/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml b/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml index 5ea3657d5..4ca0c747c 100644 --- a/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml +++ b/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml @@ -30,7 +30,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" style="@style/conference_invite_title_font" - android:text="@string/conference_invite_title" + android:text="@{data.isBroadcast ? @string/conference_broadcast_invite_title : @string/conference_invite_title, default=@string/conference_invite_title}" android:visibility="@{data.isConferenceUpdated || data.isConferenceCancelled ? View.GONE : View.VISIBLE}"/> + + - + android:orientation="horizontal"> + + + + + + - + + + + + android:text="@{data.isBroadcast ? @string/conference_schedule_broadcast_address_title : @string/conference_schedule_address_title, default=@string/conference_schedule_address_title}"/> + + + + + + + + + android:gravity="center_vertical" + android:background="@color/white_color"> + + + android:text="@{viewModel.isModeBroadcastCurrentlySelected() ? @string/conference_schedule_broadcast_summary : viewModel.scheduleForLater ? @string/conference_schedule_summary : @string/conference_group_call_summary, default=@string/conference_group_call_summary}"/> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/voip_buttons.xml b/app/src/main/res/layout/voip_buttons.xml index 3d488b41e..900732e93 100644 --- a/app/src/main/res/layout/voip_buttons.xml +++ b/app/src/main/res/layout/voip_buttons.xml @@ -46,10 +46,10 @@ android:layout_marginEnd="5dp" android:background="@drawable/button_background_reverse" android:contentDescription="@{callsViewModel.isMicrophoneMuted ? @string/content_description_disable_mic_mute : @string/content_description_enable_mic_mute}" - android:enabled="@{callsViewModel.isMuteMicrophoneEnabled}" + android:enabled="@{callsViewModel.isMuteMicrophoneEnabled && !conferenceViewModel.isMeListenerOnly}" android:onClick="@{() -> callsViewModel.toggleMuteMicrophone()}" android:padding="5dp" - android:selected="@{callsViewModel.isMicrophoneMuted}" + android:selected="@{callsViewModel.isMicrophoneMuted || conferenceViewModel.isMeListenerOnly}" android:src="@drawable/icon_toggle_mic" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="W,1:1" @@ -96,10 +96,10 @@ android:layout_marginStart="5dp" android:background="@drawable/button_background_reverse" android:contentDescription="@{controlsViewModel.isVideoEnabled && controlsViewModel.isSendingVideo ? @string/content_description_disable_video : @string/content_description_enable_video}" - android:enabled="@{controlsViewModel.isVideoAvailable && !controlsViewModel.isVideoUpdateInProgress}" + android:enabled="@{controlsViewModel.isVideoAvailable && !controlsViewModel.isVideoUpdateInProgress && !conferenceViewModel.isMeListenerOnly}" android:onClick="@{() -> (!conferenceViewModel.conferenceExists || conferenceViewModel.conferenceDisplayMode != ConferenceDisplayMode.AUDIO_ONLY) ? controlsViewModel.toggleVideo() : conferenceViewModel.switchLayoutFromAudioOnlyToActiveSpeaker()}" android:padding="5dp" - android:selected="@{controlsViewModel.isVideoEnabled && controlsViewModel.isSendingVideo}" + android:selected="@{controlsViewModel.isVideoEnabled && controlsViewModel.isSendingVideo && !conferenceViewModel.isMeListenerOnly}" android:visibility="@{controlsViewModel.hideVideo ? View.GONE : View.VISIBLE}" android:src="@drawable/icon_toggle_camera" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/voip_buttons_extra.xml b/app/src/main/res/layout/voip_buttons_extra.xml index 36a7cf554..f29954e58 100644 --- a/app/src/main/res/layout/voip_buttons_extra.xml +++ b/app/src/main/res/layout/voip_buttons_extra.xml @@ -39,6 +39,7 @@ android:gravity="center" android:onClick="@{() -> controlsViewModel.showNumpad()}" android:text="@string/call_action_numpad" + android:enabled="@{!conferenceViewModel.isBroadcast}" app:drawableTopCompat="@drawable/icon_call_numpad" app:layout_constraintBottom_toBottomOf="@id/chat" app:layout_constraintEnd_toStartOf="@id/call_stats" @@ -175,6 +176,7 @@ android:gravity="center" android:onClick="@{() -> controlsViewModel.goToCallsList()}" android:text="@string/call_action_calls_list" + android:enabled="@{!conferenceViewModel.isBroadcast}" app:drawableTopCompat="@drawable/icon_calls_list" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" 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 7289ea422..d8fc916f0 100644 --- a/app/src/main/res/layout/voip_conference_active_speaker.xml +++ b/app/src/main/res/layout/voip_conference_active_speaker.xml @@ -125,6 +125,7 @@ android:id="@+id/active_speaker_background" android:layout_width="0dp" android:layout_height="0dp" + android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toBottomOf="@id/top_barrier" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -139,6 +140,7 @@ android:layout_height="0dp" android:layout_margin="10dp" android:contentDescription="@null" + android:visibility="@{conferenceViewModel.speakingParticipantFound ? View.VISIBLE : View.GONE}" coilVoipContact="@{conferenceViewModel.speakingParticipant}" android:background="@drawable/generated_avatar_bg" app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" @@ -157,7 +159,7 @@ android:background="@drawable/shape_button_background" android:contentDescription="@string/content_description_participant_is_paused" android:src="@drawable/icon_pause" - android:visibility="@{conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}" + android:visibility="@{conferenceViewModel.speakingParticipantFound && conferenceViewModel.speakingParticipant.isInConference ? View.GONE : View.VISIBLE, default=gone}" app:layout_constraintDimensionRatio="1:1" app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" app:layout_constraintEnd_toEndOf="@id/active_speaker_background" @@ -254,6 +256,7 @@ android:id="@+id/local_participant_background" android:layout_width="0dp" android:layout_height="0dp" + android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/top_barrier" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -268,6 +271,7 @@ android:layout_height="0dp" android:layout_margin="10dp" android:contentDescription="@null" + android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}" coilSelfAvatar="@{conferenceViewModel.meParticipant}" android:background="@drawable/generated_avatar_bg" app:layout_constraintDimensionRatio="1:1" @@ -298,6 +302,7 @@ android:layout_marginStart="5dp" android:layout_marginEnd="5dp" android:layout_marginBottom="5dp" + android:visibility="@{conferenceViewModel.isMeListenerOnly ? View.GONE : View.VISIBLE}" 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" diff --git a/app/src/main/res/layout/voip_conference_participant_broadcast_cell.xml b/app/src/main/res/layout/voip_conference_participant_broadcast_cell.xml new file mode 100644 index 000000000..1641977cb --- /dev/null +++ b/app/src/main/res/layout/voip_conference_participant_broadcast_cell.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participants_fragment.xml b/app/src/main/res/layout/voip_conference_participants_fragment.xml index 71f7d12e6..d51cfe6ec 100644 --- a/app/src/main/res/layout/voip_conference_participants_fragment.xml +++ b/app/src/main/res/layout/voip_conference_participants_fragment.xml @@ -76,7 +76,7 @@ android:layout_height="wrap_content" android:orientation="vertical" app:entries="@{conferenceViewModel.conferenceParticipants}" - app:layout="@{@layout/voip_conference_participant_cell}" /> + app:layout="@{conferenceViewModel.isBroadcast ? @layout/voip_conference_participant_broadcast_cell : @layout/voip_conference_participant_cell}" /> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ea9be675e..538d3c8ec 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -799,4 +799,25 @@ %s réactions invitation à une conférence message vocal + Mode + Réunion + Webinar + Informations du webinar + Liste des intervenants + Choisissez au moins un intervenant + Choisissez au moins un participant + Planifier un webinar + Modifier le webinar + Réunion : + Webinar : + Intervenants + Participants + Invitation au webinar : + (vous êtes un intervenant) + Adresse du webinar + Vous n\'êtes pas un intervenant de ce webinar + Autoriser les webinar + Fonctionalité encore en béta ! + Intervenant + Contact is a speaker \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index eb3caf6a8..679b72f0f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,6 +6,7 @@ #e65000 #ffab4d + #4DFE5E00 #ff8000 #000000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a020efb6..c21b13a67 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -274,11 +274,15 @@ Meeting subject Group call subject Meeting address + Broadcast address Add a description Description Date Time Duration + Mode + Meeting + Broadcast Timezone Send invite via &appName; Send update via &appName; @@ -287,11 +291,17 @@ Would you like to encrypt the meeting? Invite will be sent out from my &appName; account Participants list + Speakers list + Select at least one speaker + Select at least one participant Organizer Meeting info Group call info + Broadcast info Schedule meeting Edit meeting + Schedule broadcast + Edit broadcast Start group call Meeting address copied into clipboard Failed to send meeting info to a participant @@ -300,6 +310,8 @@ Remote group call Local group call Meeting invite: + Broadcast invite: + (you are a speaker) Meeting has been updated: Meeting has been cancelled: Description @@ -311,6 +323,7 @@ Start Cancel Video is currently disabled + You aren\'t a speaker in this broadcast Meetings You can\'t change group call layout as there is too many participants There is too many participants for mosaic layout, switching to active speaker @@ -346,6 +359,11 @@ Conference has been cancelled by organizer You have cancelled the conference + Meeting: + Broadcast: + Speakers + Participants + Speaker Incoming Call @@ -746,6 +764,8 @@ Default layout + Allow broadcasts + Feature currently in beta! linphone_notification_service_id @@ -851,6 +871,7 @@ Remove contact from conversation Contact is an admin in this conversation Contact isn\'t an admin in this conversation + Contact is a speaker Messages are ephemeral in this conversation Notifications are disabled for this conversation Contact can be invited in encrypted conversations