From 3ae9740336436c59af0a9c40d1b8533e7e5c9b64 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 10 Oct 2023 18:11:52 +0200 Subject: [PATCH] Started meeting detail view --- .../meetings/adapter/MeetingsListAdapter.kt | 2 +- .../main/meetings/fragment/MeetingFragment.kt | 73 ++ .../meetings/fragment/MeetingsFragment.kt | 8 + .../meetings/fragment/MeetingsListFragment.kt | 14 +- .../fragment/ScheduleMeetingFragment.kt | 4 - .../ui/main/meetings/model/MeetingModel.kt | 2 +- .../main/meetings/model/ParticipantModel.kt | 47 + .../meetings/viewmodel/MeetingViewModel.kt | 191 ++++ .../viewmodel/ScheduleMeetingViewModel.kt | 2 - .../ui/main/viewmodel/SharedMainViewModel.kt | 4 + .../res/color/list_cell_background_color.xml | 9 + app/src/main/res/layout/meeting_fragment.xml | 324 ++++++- app/src/main/res/layout/meeting_list_cell.xml | 6 +- .../layout/meeting_participant_list_cell.xml | 84 ++ .../res/layout/meeting_schedule_fragment.xml | 871 +++++++++--------- .../res/navigation/meetings_nav_graph.xml | 9 +- app/src/main/res/values/strings.xml | 2 + 17 files changed, 1200 insertions(+), 452 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/meetings/model/ParticipantModel.kt create mode 100644 app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt create mode 100644 app/src/main/res/color/list_cell_background_color.xml create mode 100644 app/src/main/res/layout/meeting_participant_list_cell.xml diff --git a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt index e6391d8be..794b67381 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt @@ -83,7 +83,7 @@ class MeetingsListAdapter( lifecycleOwner = viewLifecycleOwner - binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition + binding.cardview.isSelected = bindingAdapterPosition == selectedAdapterPosition binding.setOnClickListener { meetingClickedEvent.value = Event(meetingModel) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingFragment.kt index d08be72e8..d550be022 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingFragment.kt @@ -19,14 +19,22 @@ */ package org.linphone.ui.main.meetings.fragment +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Bundle +import android.provider.CalendarContract import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.core.view.doOnPreDraw +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.linphone.core.tools.Log import org.linphone.databinding.MeetingFragmentBinding import org.linphone.ui.main.fragment.GenericFragment +import org.linphone.ui.main.meetings.viewmodel.MeetingViewModel import org.linphone.utils.Event @UiThread @@ -37,6 +45,10 @@ class MeetingFragment : GenericFragment() { private lateinit var binding: MeetingFragmentBinding + private lateinit var viewModel: MeetingViewModel + + private val args: MeetingFragmentArgs by navArgs() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -60,5 +72,66 @@ class MeetingFragment : GenericFragment() { postponeEnterTransition() binding.lifecycleOwner = viewLifecycleOwner + + viewModel = requireActivity().run { + ViewModelProvider(this)[MeetingViewModel::class.java] + } + binding.viewModel = viewModel + + val uri = args.conferenceUri + Log.i( + "$TAG Looking up for conference with SIP URI [$uri]" + ) + viewModel.findConferenceInfo(uri) + + binding.setBackClickListener { + goBack() + } + + binding.setShareClickListener { + val intent = Intent(Intent.ACTION_EDIT) + intent.type = "vnd.android.cursor.item/event" + intent.putExtra(CalendarContract.Events.TITLE, viewModel.subject.value) + + val description = viewModel.description.value.orEmpty() + if (description.isNotEmpty()) { + intent.putExtra(CalendarContract.Events.DESCRIPTION, description) + } + + intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, viewModel.startTimeStamp.value) + intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, viewModel.endTimeStamp.value) + + intent.putExtra(CalendarContract.Events.CUSTOM_APP_URI, viewModel.sipUri.value) + intent.putExtra( + CalendarContract.Events.CUSTOM_APP_PACKAGE, + requireContext().packageName + ) + + try { + startActivity(intent) + } catch (exception: ActivityNotFoundException) { + Log.e("$TAG No activity found to handle intent: $exception") + } + } + + sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable -> + viewModel.showBackButton.value = slideable + } + + viewModel.conferenceInfoFoundEvent.observe(viewLifecycleOwner) { + it.consume { found -> + if (found) { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + sharedViewModel.openSlidingPaneEvent.value = Event(true) + } + } else { + Log.e("$TAG Failed to find meeting with URI [$uri], going back") + (view.parent as? ViewGroup)?.doOnPreDraw { + goBack() + } + } + } + } } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsFragment.kt index 938e6eeb1..c0f89095d 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsFragment.kt @@ -103,6 +103,14 @@ class MeetingsFragment : GenericFragment() { } } + sharedViewModel.showMeetingEvent.observe(viewLifecycleOwner) { + it.consume { uri -> + Log.i("$TAG Navigating to meeting fragment with URI [$uri]") + val action = MeetingFragmentDirections.actionGlobalMeetingFragment(uri) + binding.meetingsNavContainer.findNavController().navigate(action) + } + } + sharedViewModel.navigateToContactsEvent.observe(viewLifecycleOwner) { it.consume { if (findNavController().currentDestination?.id == R.id.meetingsFragment) { diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt index de4b1f93c..942ca4fbf 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt @@ -24,6 +24,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import org.linphone.R @@ -59,6 +60,7 @@ class MeetingsListFragment : AbstractTopBarFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + postponeEnterTransition() listViewModel = requireActivity().run { ViewModelProvider(this)[MeetingsListViewModel::class.java] @@ -85,13 +87,23 @@ class MeetingsListFragment : AbstractTopBarFragment() { scrollToToday() } + adapter.meetingClickedEvent.observe(viewLifecycleOwner) { + it.consume { model -> + Log.i("$TAG Show conversation with ID [${model.id}]") + sharedViewModel.showMeetingEvent.value = Event(model.id) + } + } + listViewModel.meetings.observe(viewLifecycleOwner) { val currentCount = adapter.itemCount adapter.submitList(it) Log.i("$TAG Meetings list ready with [${it.size}] items") if (currentCount < it.size) { - scrollToToday() + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + scrollToToday() + } } } 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 416bd7754..1b8316f66 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 @@ -73,10 +73,6 @@ class ScheduleMeetingFragment : GenericFragment() { } binding.viewModel = viewModel - sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable -> - viewModel.showBackButton.value = slideable - } - binding.setBackClickListener { goBack() } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt index 5969f3dce..5f4e03142 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/model/MeetingModel.kt @@ -26,7 +26,7 @@ import org.linphone.core.Participant import org.linphone.utils.TimestampUtils class MeetingModel @WorkerThread constructor(conferenceInfo: ConferenceInfo) { - val id = conferenceInfo.uri?.asStringUriOnly() + val id = conferenceInfo.uri?.asStringUriOnly() ?: "" private val timestamp = conferenceInfo.dateTime diff --git a/app/src/main/java/org/linphone/ui/main/meetings/model/ParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/model/ParticipantModel.kt new file mode 100644 index 000000000..caec44af6 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/meetings/model/ParticipantModel.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.main.meetings.model + +import androidx.annotation.WorkerThread +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Address +import org.linphone.ui.main.contacts.model.ContactAvatarModel + +class ParticipantModel @WorkerThread constructor(address: Address, val isOrganizer: Boolean) { + val avatarModel = MutableLiveData() + + init { + val friend = coreContext.contactsManager.findContactByAddress(address) + val avatar = if (friend != null) { + ContactAvatarModel(friend) + } else { + val fakeFriend = coreContext.core.createFriend() + fakeFriend.address = address + ContactAvatarModel(fakeFriend) + } + avatarModel.postValue(avatar) + } + + @WorkerThread + fun destroy() { + avatarModel.value?.destroy() + } +} 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 new file mode 100644 index 000000000..e6013706f --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +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.Locale +import java.util.TimeZone +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.core.ConferenceInfo +import org.linphone.core.Factory +import org.linphone.core.Participant +import org.linphone.core.tools.Log +import org.linphone.ui.main.meetings.model.ParticipantModel +import org.linphone.utils.AppUtils +import org.linphone.utils.Event +import org.linphone.utils.TimestampUtils + +class MeetingViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[Meeting ViewModel]" + } + + val showBackButton = MutableLiveData() + + val isBroadcast = MutableLiveData() + + val isEditable = MutableLiveData() + + val subject = MutableLiveData() + + val sipUri = MutableLiveData() + + val dateTime = MutableLiveData() + + val timezone = MutableLiveData() + + val description = MutableLiveData() + + val speakers = MutableLiveData>() + + val participants = MutableLiveData>() + + val conferenceInfoFoundEvent = MutableLiveData>() + + val startTimeStamp = MutableLiveData() + val endTimeStamp = MutableLiveData() + + private lateinit var conferenceInfo: ConferenceInfo + + init { + } + + override fun onCleared() { + super.onCleared() + + coreContext.postOnCoreThread { + speakers.value.orEmpty().forEach(ParticipantModel::destroy) + participants.value.orEmpty().forEach(ParticipantModel::destroy) + } + } + + @UiThread + fun findConferenceInfo(uri: String) { + coreContext.postOnCoreThread { core -> + val address = Factory.instance().createAddress(uri) + if (address != null) { + val found = core.findConferenceInformationFromUri(address) + if (found != null) { + conferenceInfo = found + configureConferenceInfo() + conferenceInfoFoundEvent.postValue(Event(true)) + } else { + conferenceInfoFoundEvent.postValue(Event(false)) + } + } else { + conferenceInfoFoundEvent.postValue(Event(false)) + } + } + } + + @UiThread + fun join() { + // TODO + } + + @WorkerThread + private fun configureConferenceInfo() { + if (::conferenceInfo.isInitialized) { + subject.postValue(conferenceInfo.subject) + sipUri.postValue(conferenceInfo.uri?.asStringUriOnly() ?: "") + description.postValue(conferenceInfo.description) + + val timestamp = conferenceInfo.dateTime + val duration = conferenceInfo.duration + val date = TimestampUtils.toString( + timestamp, + onlyDate = true, + shortDate = false, + hideYear = false + ) + val startTime = TimestampUtils.timeToString(timestamp) + val end = timestamp + (duration * 60) + val endTime = TimestampUtils.timeToString(end) + startTimeStamp.postValue(timestamp * 1000) + endTimeStamp.postValue(end * 1000) + dateTime.postValue("$date | $startTime - $endTime") + + timezone.postValue( + AppUtils.getFormattedString( + R.string.meeting_schedule_timezone_title, + TimeZone.getDefault().displayName + ) + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + ) + + val organizerAddress = conferenceInfo.organizer + if (organizerAddress != null) { + val localAccount = coreContext.core.accountList.find { account -> + val address = account.params.identityAddress + address != null && organizerAddress.weakEqual(address) + } + isEditable.postValue(localAccount != null) + } else { + isEditable.postValue(false) + Log.e( + "$TAG No organizer SIP URI found for: ${conferenceInfo.uri?.asStringUriOnly()}" + ) + } + + computeParticipantsList() + } + } + + private fun computeParticipantsList() { + speakers.value.orEmpty().forEach(ParticipantModel::destroy) + participants.value.orEmpty().forEach(ParticipantModel::destroy) + + val speakersList = arrayListOf() + val participantsList = arrayListOf() + + var allSpeaker = true + val organizer = conferenceInfo.organizer + var organizerFound = false + for (info in conferenceInfo.participantInfos) { + val participant = info.address + val isOrganizer = organizer?.weakEqual(participant) ?: false + Log.i( + "$TAG Conference [${subject.value}] ${if (isOrganizer) "organizer" else "participant"} [${participant.asStringUriOnly()}] is a [${info.role}]" + ) + if (isOrganizer) { + organizerFound = true + } + + if (info.role == Participant.Role.Listener) { + allSpeaker = false + participantsList.add(ParticipantModel(participant, isOrganizer)) + } else { + speakersList.add(ParticipantModel(participant, isOrganizer)) + } + } + if (!organizerFound && organizer != null) { + Log.i("$TAG Organizer not found in participants list, adding it to participants list") + participantsList.add(ParticipantModel(organizer, true)) + } + + isBroadcast.postValue(!allSpeaker) + speakers.postValue(speakersList) + participants.postValue(participantsList) + } +} 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 f5bf09978..97430080f 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 @@ -35,8 +35,6 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { private const val TAG = "[Schedule Meeting ViewModel]" } - val showBackButton = MutableLiveData() - val isBroadcastSelected = MutableLiveData() val showBroadcastHelp = MutableLiveData() diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt index ce8260b56..8654d1b60 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt @@ -114,4 +114,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() { val showScheduleMeetingEvent: MutableLiveData> by lazy { MutableLiveData>() } + + val showMeetingEvent: MutableLiveData> by lazy { + MutableLiveData>() + } } diff --git a/app/src/main/res/color/list_cell_background_color.xml b/app/src/main/res/color/list_cell_background_color.xml new file mode 100644 index 000000000..1221b511e --- /dev/null +++ b/app/src/main/res/color/list_cell_background_color.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/meeting_fragment.xml b/app/src/main/res/layout/meeting_fragment.xml index 7b54e177a..6892adb61 100644 --- a/app/src/main/res/layout/meeting_fragment.xml +++ b/app/src/main/res/layout/meeting_fragment.xml @@ -7,20 +7,326 @@ + + - + android:layout_height="match_parent" + android:background="@color/white"> - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/meeting_list_cell.xml b/app/src/main/res/layout/meeting_list_cell.xml index 04850d318..34a07c9a8 100644 --- a/app/src/main/res/layout/meeting_list_cell.xml +++ b/app/src/main/res/layout/meeting_list_cell.xml @@ -20,8 +20,6 @@ @@ -66,12 +64,16 @@ app:layout_constraintTop_toBottomOf="@id/header_day"/> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index d5c9e1d31..1952ebb7a 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -24,463 +24,474 @@ type="org.linphone.ui.main.meetings.viewmodel.ScheduleMeetingViewModel" /> - + android:layout_height="match_parent" + android:background="@color/white"> - + - + - + - + - - - - - - - - - + android:layout_height="wrap_content"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/meetings_nav_graph.xml b/app/src/main/res/navigation/meetings_nav_graph.xml index 132f007f3..06af68664 100644 --- a/app/src/main/res/navigation/meetings_nav_graph.xml +++ b/app/src/main/res/navigation/meetings_nav_graph.xml @@ -15,10 +15,15 @@ android:id="@+id/meetingFragment" android:name="org.linphone.ui.main.meetings.fragment.MeetingFragment" android:label="MeetingFragment" - tools:layout="@layout/meeting_fragment"/> + tools:layout="@layout/meeting_fragment"> + + + app:destination="@id/meetingFragment" + app:launchSingleTop="true" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 276bf6b12..10ea19b21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,6 +348,8 @@ Add participants Add speaker Send invitation to participants + Join the meeting now + Organizer Operation in progress, please wait