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