From 598ac6cbd3ca609aee8a4fc79304ab829b2791ea Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 Feb 2024 16:41:31 +0100 Subject: [PATCH] Added documents menu (like media) --- .../ConversationDocumentsListFragment.kt | 149 ++++++++++++++++++ .../chat/fragment/ConversationFragment.kt | 14 +- .../fragment/ConversationMediaListFragment.kt | 17 +- .../ConversationDocumentsListViewModel.kt | 128 +++++++++++++++ .../ConversationMediaListViewModel.kt | 47 +++--- .../layout/chat_conversation_popup_menu.xml | 33 +++- .../chat_document_content_list_cell.xml | 67 ++++++++ .../res/layout/chat_documents_fragment.xml | 94 +++++++++++ .../main/res/layout/chat_media_fragment.xml | 133 +++++++++------- .../main/res/navigation/chat_nav_graph.xml | 21 +++ app/src/main/res/values/strings.xml | 5 +- 11 files changed, 615 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt create mode 100644 app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt create mode 100644 app/src/main/res/layout/chat_document_content_list_cell.xml create mode 100644 app/src/main/res/layout/chat_documents_fragment.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt new file mode 100644 index 000000000..be2cc0d81 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt @@ -0,0 +1,149 @@ +/* + * 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.chat.fragment + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +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.R +import org.linphone.core.tools.Log +import org.linphone.databinding.ChatDocumentsFragmentBinding +import org.linphone.ui.main.MainActivity +import org.linphone.ui.main.chat.viewmodel.ConversationDocumentsListViewModel +import org.linphone.ui.main.fragment.SlidingPaneChildFragment +import org.linphone.utils.Event +import org.linphone.utils.FileUtils + +@UiThread +class ConversationDocumentsListFragment : SlidingPaneChildFragment() { + companion object { + private const val TAG = "[Conversation Documents List Fragment]" + } + + private lateinit var binding: ChatDocumentsFragmentBinding + + private lateinit var viewModel: ConversationDocumentsListViewModel + + private val args: ConversationMediaListFragmentArgs by navArgs() + + override fun goBack(): Boolean { + return findNavController().popBackStack() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ChatDocumentsFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + postponeEnterTransition() + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + viewModel = ViewModelProvider(this)[ConversationDocumentsListViewModel::class.java] + binding.viewModel = viewModel + + val localSipUri = args.localSipUri + val remoteSipUri = args.remoteSipUri + Log.i( + "$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]" + ) + val chatRoom = sharedViewModel.displayedChatRoom + viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri) + + binding.setBackClickListener { + goBack() + } + + viewModel.chatRoomFoundEvent.observe(viewLifecycleOwner) { + it.consume { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + viewModel.loadDocumentsList() + } + } + } + + viewModel.documentsList.observe(viewLifecycleOwner) { + val count = it.size + Log.i( + "$TAG Found [$count] documents for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]" + ) + // TODO: FIXME: use Adapter + } + + viewModel.openDocumentEvent.observe(viewLifecycleOwner) { + it.consume { model -> + Log.i("$TAG User clicked on file [${model.file}], let's display it in file viewer") + goToFileViewer(model.file) + } + } + } + + private fun goToFileViewer(path: String) { + Log.i("$TAG Navigating to file viewer fragment with path [$path]") + val extension = FileUtils.getExtensionFromFileName(path) + val mime = FileUtils.getMimeTypeFromExtension(extension) + + val bundle = Bundle() + bundle.apply { + putString("localSipUri", viewModel.localSipUri) + putString("remoteSipUri", viewModel.remoteSipUri) + putString("path", path) + } + when (FileUtils.getMimeType(mime)) { + FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> { + bundle.putBoolean("isMedia", false) + sharedViewModel.displayFileEvent.value = Event(bundle) + } + else -> { + val intent = Intent(Intent.ACTION_VIEW) + val contentUri: Uri = + FileUtils.getPublicFilePath(requireContext(), path) + intent.setDataAndType(contentUri, "file/$mime") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + try { + requireContext().startActivity(intent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Can't open file [$path] in third party app: $anfe") + val message = getString( + R.string.toast_no_app_registered_to_handle_content_type_error + ) + val icon = R.drawable.file + (requireActivity() as MainActivity).showRedToast(message, icon) + } + } + } + } +} diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 10b327404..7fe6682d9 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -880,7 +880,7 @@ class ConversationFragment : SlidingPaneChildFragment() { popupWindow.dismiss() } - popupView.setMediasClickListener { + popupView.setMediaClickListener { if (findNavController().currentDestination?.id == R.id.conversationFragment) { val action = ConversationFragmentDirections.actionConversationFragmentToConversationMediaListFragment( @@ -892,6 +892,18 @@ class ConversationFragment : SlidingPaneChildFragment() { popupWindow.dismiss() } + popupView.setDocumentsClickListener { + if (findNavController().currentDestination?.id == R.id.conversationFragment) { + val action = + ConversationFragmentDirections.actionConversationFragmentToConversationDocumentsListFragment( + localSipUri = viewModel.localSipUri, + remoteSipUri = viewModel.remoteSipUri + ) + findNavController().navigate(action) + } + popupWindow.dismiss() + } + // Elevation is for showing a shadow around the popup popupWindow.elevation = 20f popupWindow.showAsDropDown(view, 0, 0, Gravity.BOTTOM) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt index 05705bdc8..c1f370bab 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt @@ -86,14 +86,21 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() { goBack() } + viewModel.chatRoomFoundEvent.observe(viewLifecycleOwner) { + it.consume { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + viewModel.loadMediaList() + } + } + } + viewModel.mediaList.observe(viewLifecycleOwner) { val count = it.size Log.i( "$TAG Found [$count] media for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]" ) - (view.parent as? ViewGroup)?.doOnPreDraw { - startPostponedEnterTransition() - } + // TODO: FIXME: use Adapter } viewModel.openMediaEvent.observe(viewLifecycleOwner) { @@ -120,10 +127,6 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() { bundle.putBoolean("isMedia", true) sharedViewModel.displayFileEvent.value = Event(bundle) } - FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> { - bundle.putBoolean("isMedia", false) - sharedViewModel.displayFileEvent.value = Event(bundle) - } else -> { val intent = Intent(Intent.ACTION_VIEW) val contentUri: Uri = diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt new file mode 100644 index 000000000..ee079c940 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt @@ -0,0 +1,128 @@ +/* + * 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.chat.viewmodel + +import androidx.annotation.UiThread +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.ChatRoom +import org.linphone.core.Factory +import org.linphone.core.tools.Log +import org.linphone.ui.main.chat.model.FileModel +import org.linphone.utils.Event + +class ConversationDocumentsListViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[Conversation Documents List ViewModel]" + } + + val documentsList = MutableLiveData>() + + val operationInProgress = MutableLiveData() + + val chatRoomFoundEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val openDocumentEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private lateinit var chatRoom: ChatRoom + + lateinit var localSipUri: String + + lateinit var remoteSipUri: String + + @UiThread + fun findChatRoom(room: ChatRoom?, localSipUri: String, remoteSipUri: String) { + this.localSipUri = localSipUri + this.remoteSipUri = remoteSipUri + + coreContext.postOnCoreThread { core -> + Log.i( + "$TAG Looking for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]" + ) + if (room != null && ::chatRoom.isInitialized && chatRoom == room) { + Log.i("$TAG Conversation object already in memory, skipping") + chatRoomFoundEvent.postValue(Event(true)) + return@postOnCoreThread + } + + val localAddress = Factory.instance().createAddress(localSipUri) + val remoteAddress = Factory.instance().createAddress(remoteSipUri) + + if (room != null && (!::chatRoom.isInitialized || chatRoom != room)) { + if (localAddress?.weakEqual(room.localAddress) == true && remoteAddress?.weakEqual( + room.peerAddress + ) == true + ) { + Log.i("$TAG Conversation object available in sharedViewModel, using it") + chatRoom = room + chatRoomFoundEvent.postValue(Event(true)) + return@postOnCoreThread + } + } + + if (localAddress != null && remoteAddress != null) { + Log.i("$TAG Searching for conversation in Core using local & peer SIP addresses") + val found = core.searchChatRoom( + null, + localAddress, + remoteAddress, + arrayOfNulls( + 0 + ) + ) + if (found != null) { + chatRoom = found + chatRoomFoundEvent.postValue(Event(true)) + } + } + } + } + + @UiThread + fun loadDocumentsList() { + operationInProgress.value = true + + coreContext.postOnCoreThread { + val list = arrayListOf() + if (::chatRoom.isInitialized) { + val documents = chatRoom.documentContents + for (documentContent in documents) { + val path = documentContent.filePath.orEmpty() + val name = documentContent.name.orEmpty() + val size = documentContent.size.toLong() + if (path.isNotEmpty() && name.isNotEmpty()) { + val model = FileModel(path, name, size) { + openDocumentEvent.postValue(Event(it)) + } + list.add(model) + } + } + } + documentsList.postValue(list) + + operationInProgress.postValue(false) + } + } +} diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt index 042724e0a..6ab9f7809 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt @@ -20,7 +20,6 @@ package org.linphone.ui.main.chat.viewmodel import androidx.annotation.UiThread -import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext @@ -41,6 +40,12 @@ class ConversationMediaListViewModel @UiThread constructor() : ViewModel() { val currentlyDisplayedFileName = MutableLiveData() + val operationInProgress = MutableLiveData() + + val chatRoomFoundEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val openMediaEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -62,7 +67,7 @@ class ConversationMediaListViewModel @UiThread constructor() : ViewModel() { ) if (room != null && ::chatRoom.isInitialized && chatRoom == room) { Log.i("$TAG Conversation object already in memory, skipping") - loadMediaList() + chatRoomFoundEvent.postValue(Event(true)) return@postOnCoreThread } @@ -76,7 +81,7 @@ class ConversationMediaListViewModel @UiThread constructor() : ViewModel() { ) { Log.i("$TAG Conversation object available in sharedViewModel, using it") chatRoom = room - loadMediaList() + chatRoomFoundEvent.postValue(Event(true)) return@postOnCoreThread } } @@ -93,29 +98,35 @@ class ConversationMediaListViewModel @UiThread constructor() : ViewModel() { ) if (found != null) { chatRoom = found - loadMediaList() + chatRoomFoundEvent.postValue(Event(true)) } } } } - @WorkerThread - private fun loadMediaList() { - val list = arrayListOf() - if (::chatRoom.isInitialized) { - val media = chatRoom.mediaContents - for (mediaContent in media) { - val path = mediaContent.filePath.orEmpty() - val name = mediaContent.name.orEmpty() - val size = mediaContent.size.toLong() - if (path.isNotEmpty() && name.isNotEmpty()) { - val model = FileModel(path, name, size) { - openMediaEvent.postValue(Event(it)) + @UiThread + fun loadMediaList() { + operationInProgress.value = true + + coreContext.postOnCoreThread { + val list = arrayListOf() + if (::chatRoom.isInitialized) { + val media = chatRoom.mediaContents + for (mediaContent in media) { + val path = mediaContent.filePath.orEmpty() + val name = mediaContent.name.orEmpty() + val size = mediaContent.size.toLong() + if (path.isNotEmpty() && name.isNotEmpty()) { + val model = FileModel(path, name, size) { + openMediaEvent.postValue(Event(it)) + } + list.add(model) } - list.add(model) } } + mediaList.postValue(list) + + operationInProgress.postValue(false) } - mediaList.postValue(list) } } diff --git a/app/src/main/res/layout/chat_conversation_popup_menu.xml b/app/src/main/res/layout/chat_conversation_popup_menu.xml index 9433dc13c..1203b800b 100644 --- a/app/src/main/res/layout/chat_conversation_popup_menu.xml +++ b/app/src/main/res/layout/chat_conversation_popup_menu.xml @@ -22,7 +22,10 @@ name="configureEphemeralMessagesClickListener" type="View.OnClickListener" /> + + app:layout_constraintBottom_toTopOf="@id/media"/> + + diff --git a/app/src/main/res/layout/chat_document_content_list_cell.xml b/app/src/main/res/layout/chat_document_content_list_cell.xml new file mode 100644 index 000000000..465499c7f --- /dev/null +++ b/app/src/main/res/layout/chat_document_content_list_cell.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_documents_fragment.xml b/app/src/main/res/layout/chat_documents_fragment.xml new file mode 100644 index 000000000..7c3a375a0 --- /dev/null +++ b/app/src/main/res/layout/chat_documents_fragment.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_media_fragment.xml b/app/src/main/res/layout/chat_media_fragment.xml index 2503fc5f7..d84881875 100644 --- a/app/src/main/res/layout/chat_media_fragment.xml +++ b/app/src/main/res/layout/chat_media_fragment.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/tools"> @@ -12,74 +13,84 @@ type="org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel" /> - + android:layout_height="match_parent"> - + - + - + - + + + + + + + android:text="@string/conversation_no_media_found" + android:textColor="?attr/color_main2_600" + android:textSize="16sp" + android:visibility="@{viewModel.mediaList.empty ? View.VISIBLE : View.GONE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> - + - + - + \ No newline at end of file diff --git a/app/src/main/res/navigation/chat_nav_graph.xml b/app/src/main/res/navigation/chat_nav_graph.xml index 46527f271..767dac75c 100644 --- a/app/src/main/res/navigation/chat_nav_graph.xml +++ b/app/src/main/res/navigation/chat_nav_graph.xml @@ -43,6 +43,14 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> + + + + + + \ 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 c2c53e79f..b4e322727 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -428,8 +428,10 @@ Search Conversation info Ephemeral messages - Medias + Media + Documents No media found + No document found No matching result End-to-end encrypted conversation Messages in this conversation are e2e encrypted. Only your correspondent can decrypt them. @@ -462,6 +464,7 @@ Ephemeral lifetime is now %s Shared media + Shared documents Read %s Received %s