diff --git a/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt b/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt index f7429873f..fffae9745 100644 --- a/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt +++ b/app/src/main/java/org/linphone/ui/call/model/ZrtpSasConfirmationDialogModel.kt @@ -34,7 +34,7 @@ class ZrtpSasConfirmationDialogModel @UiThread constructor( ) : ViewModel() { companion object { private const val TAG = "[ZRTP SAS Confirmation Dialog]" - private const val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" } val message = MutableLiveData() @@ -55,24 +55,24 @@ class ZrtpSasConfirmationDialogModel @UiThread constructor( // TODO: improve algo? val rnd = Random() - val randomLetters1 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[ + val randomLetters1 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ rnd.nextInt( - alphabet.length + ALPHABET.length ) ]}" - val randomLetters2 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[ + val randomLetters2 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ rnd.nextInt( - alphabet.length + ALPHABET.length ) ]}" - val randomLetters3 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[ + val randomLetters3 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ rnd.nextInt( - alphabet.length + ALPHABET.length ) ]}" - val randomLetters4 = "${alphabet[rnd.nextInt(alphabet.length)]}${alphabet[ + val randomLetters4 = "${ALPHABET[rnd.nextInt(ALPHABET.length)]}${ALPHABET[ rnd.nextInt( - alphabet.length + ALPHABET.length ) ]}" diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index 118b6ed4f..52b0bfb50 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -125,6 +125,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { } } } + viewModel.participants.observe(viewLifecycleOwner) { items -> adapter.submitList(items) Log.i("$TAG Participants list updated with [${items.size}] items") diff --git a/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt index 7436e8e2e..b63671182 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt @@ -20,7 +20,9 @@ package org.linphone.ui.main.fragment import android.content.res.Configuration +import android.os.Bundle import android.view.View +import android.view.ViewGroup import androidx.annotation.IdRes import androidx.annotation.UiThread import androidx.core.view.doOnPreDraw @@ -53,7 +55,16 @@ abstract class AbstractMainFragment : GenericFragment() { abstract fun onDefaultAccountChanged() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + postponeEnterTransition() + super.onViewCreated(view, savedInstanceState) + } + fun setViewModel(abstractMainViewModel: AbstractMainViewModel) { + (view?.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + } + viewModel = abstractMainViewModel viewModel.openDrawerMenuEvent.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingParticipantsAdapter.kt b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingParticipantsAdapter.kt new file mode 100644 index 000000000..7cc0033b8 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingParticipantsAdapter.kt @@ -0,0 +1,73 @@ +/* + * 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.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.UiThread +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.linphone.R +import org.linphone.databinding.MeetingParticipantListCellBinding +import org.linphone.ui.main.meetings.model.ParticipantModel + +class MeetingParticipantsAdapter : ListAdapter( + MeetingParticipantDiffCallback() +) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val binding: MeetingParticipantListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.meeting_participant_list_cell, + parent, + false + ) + binding.lifecycleOwner = parent.findViewTreeLifecycleOwner() + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as ViewHolder).bind(getItem(position)) + } + + inner class ViewHolder( + val binding: MeetingParticipantListCellBinding + ) : RecyclerView.ViewHolder(binding.root) { + @UiThread + fun bind(participantModel: ParticipantModel) { + with(binding) { + model = participantModel + executePendingBindings() + } + } + } + + private class MeetingParticipantDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ParticipantModel, newItem: ParticipantModel): Boolean { + return oldItem.sipUri == newItem.sipUri + } + + override fun areContentsTheSame(oldItem: ParticipantModel, newItem: ParticipantModel): Boolean { + return oldItem.avatarModel.id == newItem.avatarModel.id + } + } +} diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/EditMeetingFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/EditMeetingFragment.kt index 08137c3b5..f77b954dd 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/EditMeetingFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/EditMeetingFragment.kt @@ -76,7 +76,8 @@ class EditMeetingFragment : SlidingPaneChildFragment() { val conferenceUri = args.conferenceUri Log.i("$TAG Found conference URI [$conferenceUri] in arguments") - viewModel.loadExistingConferenceInfoFromUri(conferenceUri) + val conferenceInfo = sharedViewModel.displayedMeeting + viewModel.findConferenceInfo(conferenceInfo, conferenceUri) binding.setBackClickListener { goBack() 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 9bb31f7a2..8ed8573be 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 @@ -37,6 +37,7 @@ import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.MeetingFragmentBinding @@ -44,6 +45,7 @@ import org.linphone.databinding.MeetingPopupMenuBinding import org.linphone.ui.GenericActivity import org.linphone.ui.main.fragment.SlidingPaneChildFragment import org.linphone.ui.main.history.model.ConfirmationDialogModel +import org.linphone.ui.main.meetings.adapter.MeetingParticipantsAdapter import org.linphone.ui.main.meetings.viewmodel.MeetingViewModel import org.linphone.utils.DialogUtils import org.linphone.utils.Event @@ -56,10 +58,18 @@ class MeetingFragment : SlidingPaneChildFragment() { private lateinit var binding: MeetingFragmentBinding + private lateinit var adapter: MeetingParticipantsAdapter + private lateinit var viewModel: MeetingViewModel private val args: MeetingFragmentArgs by navArgs() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + adapter = MeetingParticipantsAdapter() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -91,7 +101,16 @@ class MeetingFragment : SlidingPaneChildFragment() { Log.i( "$TAG Looking up for conference with SIP URI [$uri]" ) - viewModel.findConferenceInfo(uri) + val conferenceInfo = sharedViewModel.displayedMeeting + viewModel.findConferenceInfo(conferenceInfo, uri) + + binding.participants.isNestedScrollingEnabled = false + binding.participants.setHasFixedSize(false) + binding.participants.layoutManager = LinearLayoutManager(requireContext()) + + if (binding.participants.adapter != adapter) { + binding.participants.adapter = adapter + } binding.setBackClickListener { goBack() @@ -144,6 +163,11 @@ class MeetingFragment : SlidingPaneChildFragment() { } } + viewModel.participants.observe(viewLifecycleOwner) { items -> + adapter.submitList(items) + Log.i("$TAG Participants list updated with [${items.size}] items") + } + viewModel.conferenceInfoDeletedEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Meeting info has been deleted successfully") 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 083b99066..8a7782b9f 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 @@ -126,6 +126,7 @@ class MeetingsListFragment : AbstractMainFragment() { adapter.meetingClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> Log.i("$TAG Show conversation with ID [${model.id}]") + sharedViewModel.displayedMeeting = model.conferenceInfo val action = MeetingFragmentDirections.actionGlobalMeetingFragment(model.id) binding.meetingsNavContainer.findNavController().navigate(action) } 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 221ed6888..c795e67ee 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 @@ -28,7 +28,7 @@ import org.linphone.core.Participant import org.linphone.core.tools.Log import org.linphone.utils.TimestampUtils -class MeetingModel @WorkerThread constructor(private val conferenceInfo: ConferenceInfo) { +class MeetingModel @WorkerThread constructor(val conferenceInfo: ConferenceInfo) { companion object { private const val TAG = "[Meeting Model]" } 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 index abd90a29b..546cd0d0b 100644 --- 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 @@ -20,16 +20,11 @@ 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() + val sipUri = address.asStringUriOnly() - init { - val avatar = coreContext.contactsManager.getContactAvatarModelForAddress(address) - avatarModel.postValue(avatar) - } + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address) } 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 6092f3085..0f178da60 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 @@ -130,9 +130,26 @@ class MeetingViewModel @UiThread constructor() : ViewModel() { } @UiThread - fun findConferenceInfo(uri: String) { + fun findConferenceInfo(meeting: ConferenceInfo?, uri: String) { coreContext.postOnCoreThread { core -> + if (meeting != null && ::conferenceInfo.isInitialized && meeting == conferenceInfo) { + Log.i("$TAG ConferenceInfo object already in memory, skipping") + conferenceInfoFoundEvent.postValue(Event(true)) + return@postOnCoreThread + } + val address = Factory.instance().createAddress(uri) + + if (meeting != null && (!::conferenceInfo.isInitialized || conferenceInfo != meeting)) { + if (address != null && meeting.uri?.equal(address) == true) { + Log.i("$TAG ConferenceInfo object available in sharedViewModel, using it") + conferenceInfo = meeting + configureConferenceInfo() + conferenceInfoFoundEvent.postValue(Event(true)) + return@postOnCoreThread + } + } + if (address != null) { val found = core.findConferenceInformationFromUri(address) if (found != null) { 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 0f73a2e97..2ef3abfa6 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 @@ -90,7 +90,7 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { private lateinit var conferenceScheduler: ConferenceScheduler - private lateinit var conferenceInfoToEdit: ConferenceInfo + private lateinit var conferenceInfo: ConferenceInfo private val conferenceSchedulerListener = object : ConferenceSchedulerListenerStub() { @WorkerThread @@ -106,9 +106,9 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { } ConferenceScheduler.State.Ready -> { val conferenceAddress = conferenceScheduler.info?.uri - if (::conferenceInfoToEdit.isInitialized) { + if (::conferenceInfo.isInitialized) { Log.i( - "$TAG Conference info [${conferenceInfoToEdit.uri?.asStringUriOnly()}] has been updated" + "$TAG Conference info [${conferenceInfo.uri?.asStringUriOnly()}] has been updated" ) } else { Log.i( @@ -210,61 +210,42 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { } @UiThread - fun loadExistingConferenceInfoFromUri(conferenceUri: String) { + fun findConferenceInfo(meeting: ConferenceInfo?, uri: String) { coreContext.postOnCoreThread { core -> - val conferenceAddress = core.interpretUrl(conferenceUri, false) - if (conferenceAddress == null) { - Log.e("$TAG Failed to parse conference URI [$conferenceUri], abort") + if (meeting != null && ::conferenceInfo.isInitialized && meeting == conferenceInfo) { + Log.i("$TAG ConferenceInfo object already in memory, skipping") + configureConferenceInfo() + } + + val address = Factory.instance().createAddress(uri) + + if (meeting != null && (!::conferenceInfo.isInitialized || conferenceInfo != meeting)) { + if (address != null && meeting.uri?.equal(address) == true) { + Log.i("$TAG ConferenceInfo object available in sharedViewModel, using it") + conferenceInfo = meeting + configureConferenceInfo() + return@postOnCoreThread + } + } + + if (address == null) { + Log.e("$TAG Failed to parse conference URI [$address], abort") return@postOnCoreThread } - val conferenceInfo = core.findConferenceInformationFromUri(conferenceAddress) + val conferenceInfo = core.findConferenceInformationFromUri(address) if (conferenceInfo == null) { Log.e( - "$TAG Failed to find a conference info matching URI [${conferenceAddress.asString()}], abort" + "$TAG Failed to find a conference info matching URI [${address.asString()}], abort" ) return@postOnCoreThread } - - conferenceInfoToEdit = conferenceInfo + this.conferenceInfo = conferenceInfo Log.i( "$TAG Found conference info matching URI [${conferenceInfo.uri?.asString()}] with subject [${conferenceInfo.subject}]" ) - subject.postValue(conferenceInfo.subject) - description.postValue(conferenceInfo.description) - isBroadcastSelected.postValue(false) // TODO FIXME: not implemented yet - - startHour = 0 - startMinutes = 0 - endHour = 0 - endMinutes = 0 - startTimestamp = conferenceInfo.dateTime * 1000 /* Linphone timestamps are in seconds */ - endTimestamp = (conferenceInfo.dateTime + conferenceInfo.duration) * 1000 /* Linphone timestamps are in seconds */ - Log.i( - "$TAG Loaded start date is [$startTimestamp], loaded end date is [$endTimestamp]" - ) - computeDateLabels() - computeTimeLabels() - updateTimezone() - - val list = arrayListOf() - for (participant in conferenceInfo.participantInfos) { - val address = participant.address - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( - address - ) - val model = SelectedAddressModel(address, avatarModel) { model -> - // onRemoveFromSelection - removeModelFromSelection(model) - } - list.add(model) - Log.i("$TAG Loaded participant [${address.asStringUriOnly()}]") - } - Log.i( - "$TAG [${list.size}] participants loaded from found conference info" - ) - participants.postValue(list) + configureConferenceInfo() } } @@ -433,14 +414,13 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { Log.i( "$TAG Updating ${if (isBroadcastSelected.value == true) "broadcast" else "meeting"}" ) - if (!::conferenceInfoToEdit.isInitialized) { + if (!::conferenceInfo.isInitialized) { Log.e("No conference info to edit found!") return@postOnCoreThread } operationInProgress.postValue(true) - val conferenceInfo = conferenceInfoToEdit conferenceInfo.subject = subject.value conferenceInfo.description = description.value @@ -479,6 +459,48 @@ class ScheduleMeetingViewModel @UiThread constructor() : ViewModel() { } } + @WorkerThread + private fun configureConferenceInfo() { + if (::conferenceInfo.isInitialized) { + subject.postValue(conferenceInfo.subject) + description.postValue(conferenceInfo.description) + + isBroadcastSelected.postValue(false) // TODO FIXME: not implemented yet + + startHour = 0 + startMinutes = 0 + endHour = 0 + endMinutes = 0 + startTimestamp = conferenceInfo.dateTime * 1000 /* Linphone timestamps are in seconds */ + endTimestamp = + (conferenceInfo.dateTime + conferenceInfo.duration) * 1000 /* Linphone timestamps are in seconds */ + Log.i( + "$TAG Loaded start date is [$startTimestamp], loaded end date is [$endTimestamp]" + ) + computeDateLabels() + computeTimeLabels() + updateTimezone() + + val list = arrayListOf() + for (participant in conferenceInfo.participantInfos) { + val address = participant.address + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + address + ) + val model = SelectedAddressModel(address, avatarModel) { model -> + // onRemoveFromSelection + removeModelFromSelection(model) + } + list.add(model) + Log.i("$TAG Loaded participant [${address.asStringUriOnly()}]") + } + Log.i( + "$TAG [${list.size}] participants loaded from found conference info" + ) + participants.postValue(list) + } + } + @UiThread private fun removeModelFromSelection(model: SelectedAddressModel) { val newList = arrayListOf() 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 0b77aee72..58e736098 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 @@ -25,6 +25,7 @@ import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.core.ChatRoom +import org.linphone.core.ConferenceInfo import org.linphone.core.Friend import org.linphone.ui.main.chat.model.MessageModel import org.linphone.utils.Event @@ -137,6 +138,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() { /* Meetings related */ + var displayedMeeting: ConferenceInfo? = null // Prevents the need to go look for the conference info + val forceRefreshMeetingsListEvent: MutableLiveData> by lazy { MutableLiveData>() } diff --git a/app/src/main/res/layout/meeting_fragment.xml b/app/src/main/res/layout/meeting_fragment.xml index 6733ce486..d03234c4c 100644 --- a/app/src/main/res/layout/meeting_fragment.xml +++ b/app/src/main/res/layout/meeting_fragment.xml @@ -305,19 +305,17 @@ app:layout_constraintStart_toStartOf="parent" app:tint="?attr/color_main2_600"/> - + app:layout_constraintEnd_toEndOf="parent"/>