From 56ec0f891149f19edc4fb40bf55d60fc72c35289 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 27 Oct 2023 16:18:37 +0200 Subject: [PATCH] Added calls list while in conference + started conference participants list --- .../ConferenceParticipantsListAdapter.kt | 89 ++++++++++ .../fragment/ActiveConferenceCallFragment.kt | 13 ++ .../ConferenceParticipantsListFragment.kt | 78 +++++++++ .../org/linphone/ui/call/model/CallModel.kt | 13 +- .../linphone/ui/call/model/ConferenceModel.kt | 159 ++++++++++++++---- .../call/model/ConferenceParticipantModel.kt | 43 +++++ .../call_active_conference_fragment.xml | 4 + .../call_conference_participant_list_cell.xml | 94 +++++++++++ ..._conference_participants_list_fragment.xml | 57 +++++++ .../layout/call_extra_conference_actions.xml | 5 + .../main/res/layout/calls_list_fragment.xml | 5 +- .../res/layout/chat_conversation_fragment.xml | 2 + .../res/layout/meeting_schedule_fragment.xml | 17 +- .../main/res/navigation/call_nav_graph.xml | 18 ++ app/src/main/res/values/strings.xml | 2 + 15 files changed, 559 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt create mode 100644 app/src/main/java/org/linphone/ui/call/fragment/ConferenceParticipantsListFragment.kt create mode 100644 app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantModel.kt create mode 100644 app/src/main/res/layout/call_conference_participant_list_cell.xml create mode 100644 app/src/main/res/layout/call_conference_participants_list_fragment.xml diff --git a/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt b/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt new file mode 100644 index 000000000..da330cd0c --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt @@ -0,0 +1,89 @@ +/* + * 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.call.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.UiThread +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.linphone.R +import org.linphone.databinding.CallConferenceParticipantListCellBinding +import org.linphone.ui.call.model.ConferenceParticipantModel + +class ConferenceParticipantsListAdapter(private val viewLifecycleOwner: LifecycleOwner) : + ListAdapter(ParticipantDiffCallback()) { + var selectedAdapterPosition = -1 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val binding: CallConferenceParticipantListCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.call_conference_participant_list_cell, + parent, + false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as ViewHolder).bind(getItem(position)) + } + + fun resetSelection() { + notifyItemChanged(selectedAdapterPosition) + selectedAdapterPosition = -1 + } + + inner class ViewHolder( + val binding: CallConferenceParticipantListCellBinding + ) : RecyclerView.ViewHolder(binding.root) { + @UiThread + fun bind(participantModel: ConferenceParticipantModel) { + with(binding) { + model = participantModel + + lifecycleOwner = viewLifecycleOwner + + binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition + + executePendingBindings() + } + } + } + + private class ParticipantDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ConferenceParticipantModel, + newItem: ConferenceParticipantModel + ): Boolean { + return oldItem.sipUri == newItem.sipUri + } + + override fun areContentsTheSame( + oldItem: ConferenceParticipantModel, + newItem: ConferenceParticipantModel + ): Boolean { + return false + } + } +} diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt index 447727959..b096c1ebe 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveConferenceCallFragment.kt @@ -28,6 +28,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController import com.google.android.material.bottomsheet.BottomSheetBehavior import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.tools.Log @@ -113,6 +114,18 @@ class ActiveConferenceCallFragment : GenericCallFragment() { override fun onSlide(bottomSheet: View, slideOffset: Float) {} }) + binding.setCallsListClickListener { + Log.i("$TAG Going to calls list fragment") + val action = ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToCallsListFragment() + findNavController().navigate(action) + } + + binding.setParticipantsListClickListener { + Log.i("$TAG Going to conference participants list fragment") + val action = ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToConferenceParticipantsListFragment() + findNavController().navigate(action) + } + binding.setShareConferenceClickListener { val sipUri = callViewModel.conferenceModel.sipUri.value.orEmpty() if (sipUri.isNotEmpty()) { diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ConferenceParticipantsListFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ConferenceParticipantsListFragment.kt new file mode 100644 index 000000000..4c5f461cb --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/fragment/ConferenceParticipantsListFragment.kt @@ -0,0 +1,78 @@ +/* + * 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.call.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.core.tools.Log +import org.linphone.databinding.CallConferenceParticipantsListFragmentBinding +import org.linphone.ui.call.adapter.ConferenceParticipantsListAdapter +import org.linphone.ui.call.viewmodel.CurrentCallViewModel + +class ConferenceParticipantsListFragment : GenericCallFragment() { + companion object { + private const val TAG = "[Conference Participants List Fragment]" + } + + private lateinit var binding: CallConferenceParticipantsListFragmentBinding + + private lateinit var viewModel: CurrentCallViewModel + + private lateinit var adapter: ConferenceParticipantsListAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = CallConferenceParticipantsListFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel = requireActivity().run { + ViewModelProvider(this)[CurrentCallViewModel::class.java] + } + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + + adapter = ConferenceParticipantsListAdapter(viewLifecycleOwner) + binding.participantsList.setHasFixedSize(true) + binding.participantsList.adapter = adapter + binding.participantsList.layoutManager = LinearLayoutManager(requireContext()) + + binding.setBackClickListener { + findNavController().popBackStack() + } + + viewModel.conferenceModel.participants.observe(viewLifecycleOwner) { + Log.i("$TAG participants list updated with [${it.size}] items") + adapter.submitList(it) + } + } +} diff --git a/app/src/main/java/org/linphone/ui/call/model/CallModel.kt b/app/src/main/java/org/linphone/ui/call/model/CallModel.kt index fe1a63d73..973b31be0 100644 --- a/app/src/main/java/org/linphone/ui/call/model/CallModel.kt +++ b/app/src/main/java/org/linphone/ui/call/model/CallModel.kt @@ -56,10 +56,17 @@ class CallModel @WorkerThread constructor(val call: Call) { init { call.addListener(callListener) - displayName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)) - contact.postValue( - coreContext.contactsManager.getContactAvatarModelForAddress(call.remoteAddress) + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + call.remoteAddress ) + val conferenceInfo = coreContext.core.findConferenceInformationFromUri(call.remoteAddress) + if (conferenceInfo != null) { + displayName.postValue(conferenceInfo.subject) + avatarModel.showConferenceIcon.postValue(true) + } else { + displayName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)) + } + contact.postValue(avatarModel) state.postValue(LinphoneUtils.callStateToString(call.state)) isPaused.postValue(LinphoneUtils.isCallPaused(call.state)) diff --git a/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt b/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt index 7d41ba4d9..0dd434812 100644 --- a/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt +++ b/app/src/main/java/org/linphone/ui/call/model/ConferenceModel.kt @@ -21,12 +21,14 @@ package org.linphone.ui.call.model import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData +import org.linphone.R import org.linphone.core.Call import org.linphone.core.Conference import org.linphone.core.ConferenceListenerStub import org.linphone.core.Participant import org.linphone.core.ParticipantDevice import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils class ConferenceModel { companion object { @@ -37,13 +39,48 @@ class ConferenceModel { val sipUri = MutableLiveData() + val participants = MutableLiveData>() + val participantDevices = MutableLiveData>() + val participantsLabel = MutableLiveData() + private lateinit var conference: Conference val isCurrentCallInConference = MutableLiveData() private val conferenceListener = object : ConferenceListenerStub() { + @WorkerThread + override fun onParticipantAdded(conference: Conference, participant: Participant) { + Log.i( + "$TAG Participant added: ${participant.address.asStringUriOnly()}" + ) + addParticipant(participant) + } + + @WorkerThread + override fun onParticipantRemoved(conference: Conference, participant: Participant) { + Log.i( + "$TAG Participant removed: ${participant.address.asStringUriOnly()}" + ) + removeParticipant(participant) + } + + @WorkerThread + override fun onParticipantAdminStatusChanged( + conference: Conference, + participant: Participant + ) { + val newAdminStatus = participant.isAdmin + Log.i( + "$TAG Participant [${participant.address.asStringUriOnly()}] is [${if (newAdminStatus) "now admin" else "no longer admin"}]" + ) + val participantModel = participants.value.orEmpty().find { + it.participant.address.weakEqual(participant.address) + } + participantModel?.isAdmin?.postValue(newAdminStatus) + } + @WorkerThread override fun onParticipantDeviceAdded( conference: Conference, @@ -52,14 +89,7 @@ class ConferenceModel { Log.i( "$TAG Participant device added: ${participantDevice.address.asStringUriOnly()}" ) - - val list = arrayListOf() - list.addAll(participantDevices.value.orEmpty()) - - val newModel = ConferenceParticipantDeviceModel(participantDevice) - list.add(newModel) - - participantDevices.postValue(sortParticipantDevicesList(list)) + addParticipantDevice(participantDevice) } @WorkerThread @@ -70,19 +100,7 @@ class ConferenceModel { Log.i( "$TAG Participant device removed: ${participantDevice.address.asStringUriOnly()}" ) - - val list = arrayListOf() - list.addAll(participantDevices.value.orEmpty()) - - val toRemove = list.find { - participantDevice.address.weakEqual(it.device.address) - } - if (toRemove != null) { - toRemove.destroy() - list.remove(toRemove) - } - - participantDevices.postValue(list) + removeParticipantDevice(participantDevice) } @WorkerThread @@ -100,7 +118,7 @@ class ConferenceModel { override fun onStateChanged(conference: Conference, state: Conference.State) { Log.i("$TAG State changed [$state]") if (conference.state == Conference.State.Created) { - computeParticipantsDevices() + computeParticipants() } } } @@ -132,46 +150,63 @@ class ConferenceModel { subject.postValue(conference.subject) if (conference.state == Conference.State.Created) { - computeParticipantsDevices() + computeParticipants() } } @WorkerThread - private fun computeParticipantsDevices() { + private fun computeParticipants() { + participants.value.orEmpty().forEach(ConferenceParticipantModel::destroy) participantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceModel::destroy) - val list = arrayListOf() - val participants = conference.participantList - Log.i("$TAG [${participants.size}] participant in conference") + val participantsList = arrayListOf() + val devicesList = arrayListOf() - for (participant in participants) { + val conferenceParticipants = conference.participantList + Log.i("$TAG [${conferenceParticipants.size}] participant in conference") + + val meParticipant = conference.me + val meParticipantModel = ConferenceParticipantModel(meParticipant) + participantsList.add(meParticipantModel) + + for (participant in conferenceParticipants) { val devices = participant.devices val role = participant.role Log.i( "$TAG Participant [${participant.address.asStringUriOnly()}] has [${devices.size}] devices and role [${role.name}]" ) + val participantModel = ConferenceParticipantModel(participant) + participantsList.add(participantModel) + if (role == Participant.Role.Listener) { continue } for (device in participant.devices) { val model = ConferenceParticipantDeviceModel(device) - list.add(model) + devicesList.add(model) } } Log.i( - "$TAG [${list.size}] participant devices will be displayed (not counting ourselves)" + "$TAG [${devicesList.size}] participant devices for [${participantsList.size}] participants will be displayed (not counting ourselves)" ) val ourDevices = conference.me.devices Log.i("$TAG We have [${ourDevices.size}] devices") for (device in ourDevices) { val model = ConferenceParticipantDeviceModel(device, true) - list.add(model) + devicesList.add(model) } - participantDevices.postValue(sortParticipantDevicesList(list)) + participantDevices.postValue(sortParticipantDevicesList(devicesList)) + participants.postValue(participantsList) + participantsLabel.postValue( + AppUtils.getFormattedString( + R.string.conference_participants_list_title, + participantsList.size + ) + ) } private fun sortParticipantDevicesList(devices: List): ArrayList { @@ -195,4 +230,64 @@ class ConferenceModel { return sortedList } + + @WorkerThread + private fun addParticipant(participant: Participant) { + val list = arrayListOf() + list.addAll(participants.value.orEmpty()) + + val newModel = ConferenceParticipantModel(participant) + list.add(newModel) + + participants.postValue(list) + participantsLabel.postValue( + AppUtils.getFormattedString(R.string.conference_participants_list_title, list.size) + ) + } + + @WorkerThread + private fun addParticipantDevice(participantDevice: ParticipantDevice) { + val list = arrayListOf() + list.addAll(participantDevices.value.orEmpty()) + + val newModel = ConferenceParticipantDeviceModel(participantDevice) + list.add(newModel) + + participantDevices.postValue(sortParticipantDevicesList(list)) + } + + @WorkerThread + private fun removeParticipant(participant: Participant) { + val list = arrayListOf() + list.addAll(participants.value.orEmpty()) + + val toRemove = list.find { + participant.address.weakEqual(it.participant.address) + } + if (toRemove != null) { + toRemove.destroy() + list.remove(toRemove) + } + + participants.postValue(list) + participantsLabel.postValue( + AppUtils.getFormattedString(R.string.conference_participants_list_title, list.size) + ) + } + + @WorkerThread + private fun removeParticipantDevice(participantDevice: ParticipantDevice) { + val list = arrayListOf() + list.addAll(participantDevices.value.orEmpty()) + + val toRemove = list.find { + participantDevice.address.weakEqual(it.device.address) + } + if (toRemove != null) { + toRemove.destroy() + list.remove(toRemove) + } + + participantDevices.postValue(list) + } } diff --git a/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantModel.kt b/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantModel.kt new file mode 100644 index 000000000..26c236666 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/model/ConferenceParticipantModel.kt @@ -0,0 +1,43 @@ +/* + * 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.call.model + +import androidx.annotation.WorkerThread +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Participant + +class ConferenceParticipantModel @WorkerThread constructor(val participant: Participant) { + val sipUri = participant.address.asStringUriOnly() + + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + participant.address + ) + + val isAdmin = MutableLiveData() + + init { + isAdmin.postValue(participant.isAdmin) + } + + @WorkerThread + fun destroy() { + } +} diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml index 477885e25..e57a386a3 100644 --- a/app/src/main/res/layout/call_active_conference_fragment.xml +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -12,6 +12,9 @@ + @@ -208,6 +211,7 @@ layout="@layout/call_extra_conference_actions" bind:viewModel="@{viewModel}" bind:callsViewModel="@{callsViewModel}" + bind:participantsListClickListener="@{participantsListClickListener}" bind:callsListClickListener="@{callsListClickListener}"/> diff --git a/app/src/main/res/layout/call_conference_participant_list_cell.xml b/app/src/main/res/layout/call_conference_participant_list_cell.xml new file mode 100644 index 000000000..e9fb70269 --- /dev/null +++ b/app/src/main/res/layout/call_conference_participant_list_cell.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_conference_participants_list_fragment.xml b/app/src/main/res/layout/call_conference_participants_list_fragment.xml new file mode 100644 index 000000000..966223b7a --- /dev/null +++ b/app/src/main/res/layout/call_conference_participants_list_fragment.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_extra_conference_actions.xml b/app/src/main/res/layout/call_extra_conference_actions.xml index 41b393914..80298ce04 100644 --- a/app/src/main/res/layout/call_extra_conference_actions.xml +++ b/app/src/main/res/layout/call_extra_conference_actions.xml @@ -8,6 +8,9 @@ + @@ -50,6 +53,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> @@ -47,7 +46,7 @@ android:id="@+id/calls_list" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginTop="20dp" + android:layout_marginTop="10dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/title" diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index c8b793db1..68f1c9ac3 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -72,6 +72,7 @@ + + + + + + \ 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 67cdd6c43..a207d9f7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -390,6 +390,7 @@ One time Add description Add participants + Click to add more participants Add speaker Send invitation to participants Join the meeting now @@ -433,6 +434,7 @@ Waiting for other participants… Screen share Participants + %s participants Account connection error