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