Improved app fluidity

This commit is contained in:
Sylvain Berfini 2024-04-26 09:43:35 +02:00
parent 41de644945
commit 00b92a61b4
13 changed files with 218 additions and 72 deletions

View file

@ -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<String>()
@ -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
)
]}"

View file

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

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ParticipantModel, RecyclerView.ViewHolder>(
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<ParticipantModel>() {
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
}
}
}

View file

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

View file

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

View file

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

View file

@ -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]"
}

View file

@ -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<ContactAvatarModel>()
val sipUri = address.asStringUriOnly()
init {
val avatar = coreContext.contactsManager.getContactAvatarModelForAddress(address)
avatarModel.postValue(avatar)
}
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address)
}

View file

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

View file

@ -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<SelectedAddressModel>()
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<SelectedAddressModel>()
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<SelectedAddressModel>()

View file

@ -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<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}

View file

@ -305,19 +305,17 @@
app:layout_constraintStart_toStartOf="parent"
app:tint="?attr/color_main2_600"/>
<LinearLayout
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/participants"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
entries="@{viewModel.participants}"
layout="@{@layout/meeting_participant_list_cell}"
android:nestedScrollingEnabled="true"
app:layout_constraintTop_toBottomOf="@id/separator_4"
app:layout_constraintStart_toEndOf="@id/participants_icon"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>