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