From 19a15bedfa02669f29392ac5c9a77fdf39927867 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 24 Oct 2023 13:50:24 +0200 Subject: [PATCH] Meetings can be scheduled --- .../fragment/ScheduleMeetingFragment.kt | 26 +- .../meetings/viewmodel/MeetingViewModel.kt | 6 + .../viewmodel/ScheduleMeetingViewModel.kt | 168 +++- .../main/res/layout-land/chat_fragment.xml | 2 +- .../res/layout-land/contacts_fragment.xml | 2 +- .../main/res/layout-land/history_fragment.xml | 2 +- .../res/layout-land/meetings_fragment.xml | 2 +- .../res/layout/meeting_schedule_fragment.xml | 921 +++++++++--------- ...meeting_schedule_participant_list_cell.xml | 58 ++ .../main/res/navigation/main_nav_graph.xml | 15 +- app/src/main/res/values-land/dimen.xml | 8 +- app/src/main/res/values/dimen.xml | 4 +- app/src/main/res/values/styles.xml | 3 + 13 files changed, 762 insertions(+), 455 deletions(-) create mode 100644 app/src/main/res/layout/meeting_schedule_participant_list_cell.xml diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/ScheduleMeetingFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/ScheduleMeetingFragment.kt index 1b8316f66..81dad2dbb 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/ScheduleMeetingFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/ScheduleMeetingFragment.kt @@ -33,6 +33,7 @@ import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import org.linphone.R +import org.linphone.core.tools.Log import org.linphone.databinding.MeetingScheduleFragmentBinding import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.meetings.viewmodel.ScheduleMeetingViewModel @@ -58,8 +59,6 @@ class ScheduleMeetingFragment : GenericFragment() { } override fun goBack(): Boolean { - sharedViewModel.closeSlidingPaneEvent.value = Event(true) - // If not done, when going back to MeetingsList this fragment will be created again return findNavController().popBackStack() } @@ -150,5 +149,28 @@ class ScheduleMeetingFragment : GenericFragment() { } picker.show(parentFragmentManager, "End time picker") } + + binding.setPickParticipantsClickListener { + Log.i("$TAG Going into participant picker fragment") + val action = ScheduleMeetingFragmentDirections.actionScheduleMeetingFragmentToAddParticipantsFragment() + findNavController().navigate(action) + } + + viewModel.conferenceCreatedEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Conference was scheduled, leaving fragment and ask list to refresh") + sharedViewModel.forceRefreshMeetingsListEvent.value = Event(true) + goBack() + } + } + + sharedViewModel.listOfSelectedSipUrisEvent.observe(viewLifecycleOwner) { + it.consume { list -> + Log.i( + "$TAG Found [${list.size}] new participants to add to the meeting, let's do it" + ) + viewModel.addParticipants(list) + } + } } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt index d3a7f84dc..0bc31adfa 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt @@ -194,6 +194,12 @@ class MeetingViewModel @UiThread constructor() : ViewModel() { speakersList.add(ParticipantModel(participant, isOrganizer)) } } + + if (allSpeaker) { + Log.i("$TAG All participants have Speaker role, considering it is a meeting") + participantsList.addAll(speakersList) + } + if (!organizerFound && organizer != null) { Log.i("$TAG Organizer not found in participants list, adding it to participants list") participantsList.add(ParticipantModel(organizer, true)) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt index 97430080f..c858f5e59 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt @@ -20,14 +20,25 @@ package org.linphone.ui.main.meetings.viewmodel import androidx.annotation.UiThread +import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import java.util.Calendar import java.util.Locale import java.util.TimeZone +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.core.Address +import org.linphone.core.ChatRoom +import org.linphone.core.ConferenceScheduler +import org.linphone.core.ConferenceSchedulerListenerStub +import org.linphone.core.Factory +import org.linphone.core.Participant +import org.linphone.core.ParticipantInfo import org.linphone.core.tools.Log +import org.linphone.ui.main.model.SelectedAddressModel import org.linphone.utils.AppUtils +import org.linphone.utils.Event import org.linphone.utils.TimestampUtils class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { @@ -57,6 +68,12 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { val sendInvitations = MutableLiveData() + val participants = MutableLiveData>() + + val operationInProgress = MutableLiveData() + + val conferenceCreatedEvent = MutableLiveData>() + private var startTimestamp = 0L private var endTimestamp = 0L @@ -66,10 +83,76 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { internal var endHour = 0 internal var endMinutes = 0 + private lateinit var conferenceScheduler: ConferenceScheduler + + private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() { + @WorkerThread + override fun onStateChanged( + conferenceScheduler: ConferenceScheduler, + state: ConferenceScheduler.State? + ) { + Log.i("$TAG Conference state changed [$state]") + when (state) { + ConferenceScheduler.State.Error -> { + operationInProgress.postValue(false) + // TODO: show error toast + } + ConferenceScheduler.State.Ready -> { + val conferenceAddress = conferenceScheduler.info?.uri + Log.i( + "$TAG Conference info created, address will be ${conferenceAddress?.asStringUriOnly()}" + ) + if (sendInvitations.value == true) { + Log.i("$TAG User asked for invitations to be sent, let's do it") + val chatRoomParams = coreContext.core.createDefaultChatRoomParams() + chatRoomParams.isGroupEnabled = false + chatRoomParams.backend = ChatRoom.Backend.FlexisipChat + chatRoomParams.isEncryptionEnabled = true + chatRoomParams.subject = "Meeting invitation" // Won't be used + conferenceScheduler.sendInvitations(chatRoomParams) + } else { + Log.i("$TAG User didn't asked for invitations to be sent") + operationInProgress.postValue(false) + conferenceCreatedEvent.postValue(Event(true)) + } + } + else -> { + } + } + } + + @WorkerThread + override fun onInvitationsSent( + conferenceScheduler: ConferenceScheduler, + failedInvitations: Array? + ) { + when (val failedCount = failedInvitations?.size) { + 0 -> { + Log.i("$TAG All invitations have been sent") + } + participants.value.orEmpty().size -> { + Log.e("$TAG No invitation sent!") + // TODO: show error toast + } + else -> { + Log.w("$TAG [$failedCount] invitations couldn't have been sent for:") + for (failed in failedInvitations.orEmpty()) { + Log.w(failed.asStringUriOnly()) + } + // TODO: show error toast + } + } + + operationInProgress.postValue(false) + conferenceCreatedEvent.postValue(Event(true)) + } + } + init { isBroadcastSelected.value = false showBroadcastHelp.value = false allDayMeeting.value = false + sendInvitations.value = true val now = System.currentTimeMillis() val cal = Calendar.getInstance() @@ -109,7 +192,16 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { } } ) - sendInvitations.value = true + } + + override fun onCleared() { + super.onCleared() + + coreContext.postOnCoreThread { + if (::conferenceScheduler.isInitialized) { + conferenceScheduler.removeListener(conferenceSchedulerListener) + } + } } @UiThread @@ -171,6 +263,80 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { showBroadcastHelp.value = false } + @UiThread + fun addParticipants(toAdd: ArrayList) { + coreContext.postOnCoreThread { + val list = arrayListOf() + for (participant in toAdd) { + val address = Factory.instance().createAddress(participant) + if (address == null) { + Log.e("$TAG Failed to parse [$participant] as address!") + } else { + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + address + ) + val model = SelectedAddressModel(address, avatarModel) { + // onRemoveFromSelection + } + list.add(model) + } + } + participants.postValue(list) + } + } + + @UiThread + fun schedule() { + coreContext.postOnCoreThread { core -> + Log.i( + "$TAG Scheduling ${if (isBroadcastSelected.value == true) "broadcast" else "meeting"}" + ) + operationInProgress.postValue(true) + + val localAccount = core.defaultAccount + val localAddress = localAccount?.params?.identityAddress + + val conferenceInfo = Factory.instance().createConferenceInfo() + conferenceInfo.organizer = localAddress + conferenceInfo.subject = subject.value + conferenceInfo.description = description.value + + val startTime = startTimestamp / 1000 // Linphone expects timestamp in seconds + conferenceInfo.dateTime = startTime + val duration = ((endTimestamp - startTimestamp) / 1000).toInt() // Linphone expects duration in seconds + conferenceInfo.duration = duration + + val participantsList = participants.value.orEmpty() + val participantsInfoList = arrayListOf() + for (participant in participantsList) { + val info = Factory.instance().createParticipantInfo(participant.address) + if (info == null) { + Log.e( + "$TAG Failed to create Participant Info from address [${participant.address.asStringUriOnly()}]" + ) + continue + } + + // For meetings, all participants must have Speaker role + info.role = Participant.Role.Speaker + participantsInfoList.add(info) + } + + val participantsInfoArray = arrayOfNulls(participantsInfoList.size) + participantsInfoList.toArray(participantsInfoArray) + conferenceInfo.setParticipantInfos(participantsInfoArray) + + if (!::conferenceScheduler.isInitialized) { + conferenceScheduler = core.createConferenceScheduler() + conferenceScheduler.addListener(conferenceSchedulerListener) + } + + conferenceScheduler.account = localAccount + // Will trigger the conference creation/update automatically + conferenceScheduler.info = conferenceInfo + } + } + @UiThread private fun computeDateLabels() { val start = TimestampUtils.toString( diff --git a/app/src/main/res/layout-land/chat_fragment.xml b/app/src/main/res/layout-land/chat_fragment.xml index 35d35ed0d..d7f70c2bb 100644 --- a/app/src/main/res/layout-land/chat_fragment.xml +++ b/app/src/main/res/layout-land/chat_fragment.xml @@ -14,7 +14,7 @@ diff --git a/app/src/main/res/layout-land/contacts_fragment.xml b/app/src/main/res/layout-land/contacts_fragment.xml index 82213983c..9dda56568 100644 --- a/app/src/main/res/layout-land/contacts_fragment.xml +++ b/app/src/main/res/layout-land/contacts_fragment.xml @@ -14,7 +14,7 @@ diff --git a/app/src/main/res/layout-land/history_fragment.xml b/app/src/main/res/layout-land/history_fragment.xml index 39f7e1818..1f429aee0 100644 --- a/app/src/main/res/layout-land/history_fragment.xml +++ b/app/src/main/res/layout-land/history_fragment.xml @@ -14,7 +14,7 @@ diff --git a/app/src/main/res/layout-land/meetings_fragment.xml b/app/src/main/res/layout-land/meetings_fragment.xml index cbcfda647..178545578 100644 --- a/app/src/main/res/layout-land/meetings_fragment.xml +++ b/app/src/main/res/layout-land/meetings_fragment.xml @@ -14,7 +14,7 @@ diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 1952ebb7a..dcdf27411 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -19,479 +19,522 @@ + - + android:layout_height="match_parent"> - + - + - + - + - + - - - - - - - - - + android:layout_height="wrap_content"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/meeting_schedule_participant_list_cell.xml b/app/src/main/res/layout/meeting_schedule_participant_list_cell.xml new file mode 100644 index 000000000..6264f5ce4 --- /dev/null +++ b/app/src/main/res/layout/meeting_schedule_participant_list_cell.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index c0870c5ee..6f7a53d3e 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -260,7 +260,15 @@ android:id="@+id/scheduleMeetingFragment" android:name="org.linphone.ui.main.meetings.fragment.ScheduleMeetingFragment" android:label="ScheduleMeetingFragment" - tools:layout="@layout/meeting_schedule_fragment"/> + tools:layout="@layout/meeting_schedule_fragment"> + + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimen.xml b/app/src/main/res/values-land/dimen.xml index 7ebbe3a9e..ec242d9ee 100644 --- a/app/src/main/res/values-land/dimen.xml +++ b/app/src/main/res/values-land/dimen.xml @@ -1,11 +1,9 @@ + 450dp + 450dp + 110dp 125dp 235dp - - 75dp - 425dp - - 500dp \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 6589b67c5..9dc1c5dc4 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -5,9 +5,7 @@ 32dp 75dp - 280dp - - 355dp + 300dp 300dp 14dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3f272ac1a..c40202942 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -145,6 +145,9 @@ @color/white @color/switch_track_color @color/transparent_color + @font/noto_sans + @color/gray_main2_600 + 14sp