Moved file & media viewer in separated activities

This commit is contained in:
Sylvain Berfini 2024-05-12 09:30:57 +02:00
parent eb0748df7f
commit ccd53b74db
36 changed files with 654 additions and 620 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.DS_Store
/build
/captures

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View file

@ -104,6 +104,16 @@
</activity>
<activity
android:name=".ui.file_viewer.MediaViewerActivity"
android:launchMode="singleTask"
android:resizeableActivity="true" />
<activity
android:name=".ui.file_viewer.FileViewerActivity"
android:launchMode="singleTask"
android:resizeableActivity="true" />
<activity
android:name=".ui.welcome.WelcomeActivity"
android:launchMode="singleTask"

View file

@ -1,121 +1,79 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.fragment
package org.linphone.ui.file_viewer
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.content.FileProvider
import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import androidx.viewpager2.widget.ViewPager2
import java.io.File
import kotlinx.coroutines.launch
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerFragmentBinding
import org.linphone.ui.main.file_media_viewer.adapter.PdfPagesListAdapter
import org.linphone.ui.main.file_media_viewer.viewmodel.FileViewModel
import org.linphone.ui.main.fragment.GenericMainFragment
import org.linphone.databinding.FileViewerActivityBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.file_viewer.adapter.PdfPagesListAdapter
import org.linphone.ui.file_viewer.viewmodel.FileViewModel
import org.linphone.utils.FileUtils
@UiThread
class FileViewerFragment : GenericMainFragment() {
class FileViewerActivity : GenericActivity() {
companion object {
private const val TAG = "[File Viewer Fragment]"
private const val TAG = "[File Viewer Activity]"
private const val EXPORT_FILE_AS_DOCUMENT = 10
}
private lateinit var binding: FileViewerFragmentBinding
private lateinit var binding: FileViewerActivityBinding
private lateinit var viewModel: FileViewModel
private lateinit var adapter: PdfPagesListAdapter
private val args: FileViewerFragmentArgs by navArgs()
private val pageChangedListener = object : OnPageChangeCallback() {
private val pageChangedListener = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
viewModel.pdfCurrentPage.value = (position + 1).toString()
}
}
private var navBarDefaultColor: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.navigationBarColor = getColor(R.color.gray_900)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FileViewerFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun goBack(): Boolean {
return findNavController().popBackStack()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
super.onViewCreated(view, savedInstanceState)
navBarDefaultColor = requireActivity().window.navigationBarColor
binding = DataBindingUtil.setContentView(this, R.layout.file_viewer_activity)
binding.lifecycleOwner = this
setUpToastsArea(binding.toastsArea)
viewModel = ViewModelProvider(this)[FileViewModel::class.java]
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
val path = args.path
val preLoadedContent = args.content
val args = intent.extras
val path = args?.getString("path")
if (path.isNullOrEmpty()) {
finish()
return
}
val preLoadedContent = args.getString("content")
Log.i(
"$TAG Path argument is [$path], pre loaded text content is ${if (preLoadedContent.isNullOrEmpty()) "not available" else "available, using it"}"
)
viewModel.loadFile(path, preLoadedContent)
binding.setBackClickListener {
goBack()
finish()
}
viewModel.fileReadyEvent.observe(viewLifecycleOwner) {
viewModel.fileReadyEvent.observe(this) {
it.consume { done ->
if (done) {
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
} else {
(view.parent as? ViewGroup)?.doOnPreDraw {
Log.e("$TAG Failed to open file, going back")
goBack()
}
if (!done) {
finish()
Log.e("$TAG Failed to open file, going back")
}
}
}
@ -124,7 +82,7 @@ class FileViewerFragment : GenericMainFragment() {
shareFile()
}
viewModel.pdfRendererReadyEvent.observe(viewLifecycleOwner) {
viewModel.pdfRendererReadyEvent.observe(this) {
it.consume {
Log.i("$TAG PDF renderer is ready, attaching adapter to ViewPager")
if (viewModel.screenWidth == 0 || viewModel.screenHeight == 0) {
@ -136,7 +94,7 @@ class FileViewerFragment : GenericMainFragment() {
}
}
viewModel.exportPlainTextFileEvent.observe(viewLifecycleOwner) {
viewModel.exportPlainTextFileEvent.observe(this) {
it.consume { name ->
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@ -147,7 +105,7 @@ class FileViewerFragment : GenericMainFragment() {
}
}
viewModel.exportPdfEvent.observe(viewLifecycleOwner) {
viewModel.exportPdfEvent.observe(this) {
it.consume { name ->
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@ -160,9 +118,6 @@ class FileViewerFragment : GenericMainFragment() {
}
override fun onResume() {
// Force this navigation bar color
requireActivity().window.navigationBarColor = requireContext().getColor(R.color.gray_900)
super.onResume()
updateScreenSize()
@ -174,13 +129,6 @@ class FileViewerFragment : GenericMainFragment() {
super.onPause()
}
override fun onDestroy() {
// Reset default navigation bar color
requireActivity().window.navigationBarColor = navBarDefaultColor
super.onDestroy()
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == EXPORT_FILE_AS_DOCUMENT && resultCode == Activity.RESULT_OK) {
@ -194,7 +142,7 @@ class FileViewerFragment : GenericMainFragment() {
private fun updateScreenSize() {
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
windowManager.defaultDisplay.getMetrics(displayMetrics)
viewModel.screenHeight = displayMetrics.heightPixels
viewModel.screenWidth = displayMetrics.widthPixels
Log.i(
@ -206,15 +154,15 @@ class FileViewerFragment : GenericMainFragment() {
lifecycleScope.launch {
val filePath = FileUtils.getProperFilePath(viewModel.getFilePath())
val copy = FileUtils.getFilePath(
requireContext(),
baseContext,
Uri.parse(filePath),
overrideExisting = true,
copyToCache = true
)
if (!copy.isNullOrEmpty()) {
val publicUri = FileProvider.getUriForFile(
requireContext(),
requireContext().getString(R.string.file_provider),
baseContext,
getString(R.string.file_provider),
File(copy)
)
Log.i("$TAG Public URI for file is [$publicUri], starting intent chooser")

View file

@ -0,0 +1,252 @@
package org.linphone.ui.file_viewer
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.annotation.UiThread
import androidx.core.content.FileProvider
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.FileMediaViewerActivityBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.file_viewer.adapter.MediaListAdapter
import org.linphone.ui.file_viewer.viewmodel.MediaListViewModel
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.utils.AppUtils
import org.linphone.utils.FileUtils
@UiThread
class MediaViewerActivity : GenericActivity() {
companion object {
private const val TAG = "[Media Viewer Activity]"
}
private lateinit var binding: FileMediaViewerActivityBinding
private lateinit var adapter: MediaListAdapter
private lateinit var viewModel: MediaListViewModel
private lateinit var viewPager: ViewPager2
private val pageListener = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val list = viewModel.mediaList.value.orEmpty()
if (position >= 0 && position < list.size) {
val model = list[position]
viewModel.currentlyDisplayedFileName.value = "${model.fileName}\n${model.dateTime}"
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.navigationBarColor = getColor(R.color.gray_900)
binding = DataBindingUtil.setContentView(this, R.layout.file_media_viewer_activity)
binding.lifecycleOwner = this
setUpToastsArea(binding.toastsArea)
viewModel = ViewModelProvider(this)[MediaListViewModel::class.java]
binding.viewModel = viewModel
adapter = MediaListAdapter(this, viewModel) { fullScreen ->
viewModel.fullScreenMode.value = fullScreen
}
viewPager = binding.mediaViewPager
viewPager.adapter = adapter
viewPager.registerOnPageChangeCallback(pageListener)
val args = intent.extras
if (args == null) {
finish()
return
}
val path = args.getString("path", "")
if (path.isNullOrEmpty()) {
finish()
return
}
val timestamp = args.getLong("timestamp", -1)
val isEncrypted = args.getBoolean("isEncrypted", false)
val originalPath = args.getString("originalPath", "")
viewModel.initTempModel(path, timestamp, isEncrypted, originalPath)
val localSipUri = args.getString("localSipUri").orEmpty()
val remoteSipUri = args.getString("remoteSipUri").orEmpty()
Log.i(
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri] trying to display file [$path]"
)
viewModel.findChatRoom(null, localSipUri, remoteSipUri)
viewModel.mediaList.observe(this) {
updateMediaList(path, it)
}
binding.setBackClickListener {
finish()
}
binding.setShareClickListener {
shareFile()
}
binding.setExportClickListener {
exportFile()
}
}
override fun onDestroy() {
if (::viewPager.isInitialized) {
viewPager.unregisterOnPageChangeCallback(pageListener)
}
super.onDestroy()
}
private fun updateMediaList(path: String, list: List<FileModel>) {
val count = list.size
Log.i("$TAG Found [$count] media for conversation")
var index = list.indexOfFirst { model ->
model.path == path
}
if (index == -1) {
Log.d(
"$TAG Path [$path] not found in media list (expected if VFS is enabled), trying using file name"
)
val fileName = FileUtils.getNameFromFilePath(path)
val underscore = fileName.indexOf("_")
val originalFileName = if (underscore != -1 && underscore < 2) {
fileName.subSequence(underscore, fileName.length)
} else {
fileName
}
index = list.indexOfFirst { model ->
model.path.endsWith(originalFileName)
}
if (index == -1) {
Log.w(
"$TAG Path [$path] not found in media list using either path or filename [$originalFileName]"
)
}
}
val position = if (index == -1) {
Log.e(
"$TAG File [$path] not found, using most recent one instead!"
)
val message = getString(R.string.conversation_media_not_found_toast)
val icon = R.drawable.warning_circle
showRedToast(message, icon)
0
} else {
index
}
viewPager.setCurrentItem(position, false)
viewPager.offscreenPageLimit = 1
}
private fun exportFile() {
val list = viewModel.mediaList.value.orEmpty()
val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != null) {
val filePath = model.path
lifecycleScope.launch {
withContext(Dispatchers.IO) {
Log.i(
"$TAG Export file [$filePath] to Android's MediaStore"
)
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
if (mediaStorePath.isNotEmpty()) {
Log.i(
"$TAG File [$filePath] has been successfully exported to MediaStore"
)
val message = AppUtils.getString(
R.string.toast_file_successfully_exported_to_media_store
)
showGreenToast(
message,
R.drawable.check
)
} else {
Log.e(
"$TAG Failed to export file [$filePath] to MediaStore!"
)
val message = AppUtils.getString(
R.string.toast_export_file_to_media_store_error
)
showRedToast(
message,
R.drawable.warning_circle
)
}
}
}
} else {
Log.e(
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
)
}
}
private fun shareFile() {
val list = viewModel.mediaList.value.orEmpty()
val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != null) {
lifecycleScope.launch {
val filePath = FileUtils.getProperFilePath(model.path)
val copy = FileUtils.getFilePath(
baseContext,
Uri.parse(filePath),
overrideExisting = true,
copyToCache = true
)
if (!copy.isNullOrEmpty()) {
val publicUri = FileProvider.getUriForFile(
baseContext,
getString(R.string.file_provider),
File(copy)
)
Log.i(
"$TAG Public URI for file is [$publicUri], starting intent chooser"
)
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, publicUri)
putExtra(Intent.EXTRA_SUBJECT, model.fileName)
type = model.mimeTypeString
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
} else {
Log.e(
"$TAG Failed to copy file [$filePath] to share!"
)
}
}
} else {
Log.e(
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
)
}
}
}

View file

@ -17,18 +17,22 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.adapter
package org.linphone.ui.file_viewer.adapter
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
import org.linphone.ui.main.file_media_viewer.fragment.MediaViewerFragment
import org.linphone.ui.file_viewer.fragment.MediaViewerFragment
import org.linphone.ui.file_viewer.viewmodel.MediaListViewModel
class MediaListAdapter(fragment: Fragment, private val viewModel: ConversationMediaListViewModel) : FragmentStateAdapter(
fragment
) {
class MediaListAdapter(
fragmentActivity: FragmentActivity,
private val viewModel: MediaListViewModel,
private val lambda: ((fullScreen: Boolean) -> Unit)
) :
FragmentStateAdapter(fragmentActivity) {
companion object {
private const val TAG = "[Media List Adapter]"
}
@ -37,12 +41,22 @@ class MediaListAdapter(fragment: Fragment, private val viewModel: ConversationMe
return viewModel.mediaList.value.orEmpty().size
}
override fun getItemId(position: Int): Long {
return viewModel.mediaList.value.orEmpty().getOrNull(position)?.originalPath.hashCode().toLong()
}
override fun containsItem(itemId: Long): Boolean {
return viewModel.mediaList.value.orEmpty().any { it.originalPath.hashCode().toLong() == itemId }
}
override fun createFragment(position: Int): Fragment {
val fragment = MediaViewerFragment()
fragment.fullScreenChanged = lambda
fragment.arguments = Bundle().apply {
val path = viewModel.mediaList.value.orEmpty().getOrNull(position)?.file
Log.i("$TAG Path is [$path] for position [$position]")
val path = viewModel.mediaList.value.orEmpty().getOrNull(position)?.path
Log.d("$TAG Path is [$path] for position [$position]")
putString("path", path)
putBoolean("fullScreen", viewModel.fullScreenMode.value == true)
}
return fragment
}

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.adapter
package org.linphone.ui.file_viewer.adapter
import android.view.LayoutInflater
import android.view.View
@ -25,7 +25,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.ui.main.file_media_viewer.viewmodel.FileViewModel
import org.linphone.ui.file_viewer.viewmodel.FileViewModel
class PdfPagesListAdapter(private val viewModel: FileViewModel) : RecyclerView.Adapter<PdfPagesListAdapter.PdfPageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder {

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.fragment
package org.linphone.ui.file_viewer.fragment
import android.os.Bundle
import android.view.LayoutInflater
@ -27,9 +27,10 @@ import androidx.annotation.UiThread
import androidx.lifecycle.ViewModelProvider
import org.linphone.core.tools.Log
import org.linphone.databinding.FileMediaViewerChildFragmentBinding
import org.linphone.ui.main.file_media_viewer.viewmodel.MediaViewModel
import org.linphone.ui.file_viewer.viewmodel.MediaViewModel
import org.linphone.ui.main.fragment.GenericMainFragment
import org.linphone.ui.main.viewmodel.SharedMainViewModel
import org.linphone.utils.FileUtils
@UiThread
class MediaViewerFragment : GenericMainFragment() {
@ -41,6 +42,8 @@ class MediaViewerFragment : GenericMainFragment() {
private lateinit var viewModel: MediaViewModel
var fullScreenChanged: ((fullScreen: Boolean) -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -58,6 +61,7 @@ class MediaViewerFragment : GenericMainFragment() {
}
viewModel = ViewModelProvider(this)[MediaViewModel::class.java]
viewModel.fullScreenMode.value = arguments?.getBoolean("fullScreen", true) ?: true
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
@ -73,7 +77,8 @@ class MediaViewerFragment : GenericMainFragment() {
return
}
Log.i("$TAG Path argument is [$path]")
val exists = FileUtils.doesFileExist(path)
Log.i("$TAG Path argument is [$path], it ${if (exists) "exists" else "doesn't exist"}")
viewModel.loadFile(path)
viewModel.isVideo.observe(viewLifecycleOwner) { isVideo ->
@ -94,18 +99,15 @@ class MediaViewerFragment : GenericMainFragment() {
}
}
viewModel.fullScreenMode.observe(viewLifecycleOwner) {
if (it != sharedViewModel.mediaViewerFullScreenMode.value) {
sharedViewModel.mediaViewerFullScreenMode.value = it
}
binding.setToggleFullScreenModeClickListener {
viewModel.toggleFullScreen()
fullScreenChanged?.invoke(viewModel.fullScreenMode.value == true)
}
}
override fun onResume() {
super.onResume()
viewModel.fullScreenMode.value = sharedViewModel.mediaViewerFullScreenMode.value
if (viewModel.isVideo.value == true) {
Log.i("$TAG Resumed, starting video player")
binding.videoPlayer.start()

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.viewmodel
package org.linphone.ui.file_viewer.viewmodel
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
@ -119,14 +119,15 @@ class FileViewModel @UiThread constructor() : GenericViewModel() {
mimeType.postValue(mime)
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Pdf -> {
Log.i("$TAG File [$file] seems to be a PDF")
Log.d("$TAG File [$file] seems to be a PDF")
loadPdf()
}
FileUtils.MimeType.PlainText -> {
Log.i("$TAG File [$file] seems to be plain text")
Log.d("$TAG File [$file] seems to be plain text")
loadPlainText()
}
else -> {
Log.e("$TAG Unexpected MIME type [$mime] for file at [$file]")
fileReadyEvent.value = Event(false)
}
}
@ -158,7 +159,7 @@ class FileViewModel @UiThread constructor() : GenericViewModel() {
val page: PdfRenderer.Page = pdfRenderer.openPage(index)
currentPdfPage = page
Log.i(
Log.d(
"$TAG Page size is ${page.width}/${page.height}, screen size is $screenWidth/$screenHeight"
)
val bm = Bitmap.createBitmap(

View file

@ -0,0 +1,96 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.file_viewer.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.viewmodel.AbstractConversationViewModel
import org.linphone.utils.FileUtils
import org.linphone.utils.LinphoneUtils
class MediaListViewModel @UiThread constructor() : AbstractConversationViewModel() {
companion object {
private const val TAG = "[Media List ViewModel]"
}
val mediaList = MutableLiveData<List<FileModel>>()
val fullScreenMode = MutableLiveData<Boolean>()
val currentlyDisplayedFileName = MutableLiveData<String>()
override fun beforeNotifyingChatRoomFound(sameOne: Boolean) {
loadMediaList()
}
init {
fullScreenMode.value = true
}
override fun onCleared() {
super.onCleared()
mediaList.value.orEmpty().forEach(FileModel::destroy)
}
@UiThread
fun initTempModel(path: String, timestamp: Long, isEncrypted: Boolean, originalPath: String) {
val name = FileUtils.getNameFromFilePath(path)
val model = FileModel(path, name, 0, timestamp, isEncrypted, originalPath)
mediaList.postValue(arrayListOf(model))
}
@WorkerThread
private fun loadMediaList() {
val list = arrayListOf<FileModel>()
val chatRoomId = LinphoneUtils.getChatRoomId(chatRoom)
Log.i("$TAG Loading media contents for conversation [$chatRoomId]")
val media = chatRoom.mediaContents
Log.i("$TAG [${media.size}] media have been fetched")
for (mediaContent in media) {
// Do not display voice recordings here, even if they are media file
if (mediaContent.isVoiceRecording) continue
val isEncrypted = mediaContent.isFileEncrypted
val originalPath = mediaContent.filePath.orEmpty()
val path = if (isEncrypted) {
Log.d(
"$TAG [VFS] Content is encrypted, requesting plain file path for file [${mediaContent.filePath}]"
)
mediaContent.exportPlainFile()
} else {
originalPath
}
val name = mediaContent.name.orEmpty()
val size = mediaContent.size.toLong()
val timestamp = mediaContent.creationTimestamp
if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath)
list.add(model)
}
}
Log.i("$TAG [${list.size}] media have been processed")
mediaList.postValue(list)
}
}

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.viewmodel
package org.linphone.ui.file_viewer.viewmodel
import android.media.AudioAttributes
import android.media.MediaPlayer
@ -75,22 +75,24 @@ class MediaViewModel @UiThread constructor() : GenericViewModel() {
val mime = FileUtils.getMimeTypeFromExtension(extension)
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image -> {
Log.i("$TAG File [$file] seems to be an image")
Log.d("$TAG File [$file] seems to be an image")
isImage.value = true
path.value = file
}
FileUtils.MimeType.Video -> {
Log.i("$TAG File [$file] seems to be a video")
Log.d("$TAG File [$file] seems to be a video")
isVideo.value = true
isVideoPlaying.value = false
}
FileUtils.MimeType.Audio -> {
Log.i("$TAG File [$file] seems to be an audio file")
Log.d("$TAG File [$file] seems to be an audio file")
isAudio.value = true
initMediaPlayer()
}
else -> { }
else -> {
Log.e("$TAG Unexpected MIME type [$mime] for file at [$file]")
}
}
}

View file

@ -136,7 +136,7 @@ class ConversationsFilesAdapter :
private class FilesDiffCallback : DiffUtil.ItemCallback<FileModel>() {
override fun areItemsTheSame(oldItem: FileModel, newItem: FileModel): Boolean {
return oldItem.file == newItem.file && oldItem.fileName == newItem.fileName
return oldItem.path == newItem.path && oldItem.fileName == newItem.fileName
}
override fun areContentsTheSame(oldItem: FileModel, newItem: FileModel): Boolean {

View file

@ -37,6 +37,7 @@ import org.linphone.core.tools.Log
import org.linphone.databinding.ChatDocumentsFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.viewmodel.ConversationDocumentsListViewModel
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.utils.Event
@ -126,13 +127,14 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
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)
Log.i("$TAG User clicked on file [${model.path}], let's display it in file viewer")
goToFileViewer(model)
}
}
}
private fun goToFileViewer(path: String) {
private fun goToFileViewer(fileModel: FileModel) {
val path = fileModel.path
Log.i("$TAG Navigating to file viewer fragment with path [$path]")
val extension = FileUtils.getExtensionFromFileName(path)
val mime = FileUtils.getMimeTypeFromExtension(extension)
@ -142,6 +144,9 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
putString("localSipUri", viewModel.localSipUri)
putString("remoteSipUri", viewModel.remoteSipUri)
putString("path", path)
putBoolean("isEncrypted", fileModel.isEncrypted)
putLong("timestamp", fileModel.fileCreationTimestamp)
putString("originalPath", fileModel.originalPath)
}
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> {

View file

@ -80,6 +80,7 @@ import org.linphone.ui.main.MainActivity
import org.linphone.ui.main.chat.ConversationScrollListener
import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.model.MessageDeliveryModel
import org.linphone.ui.main.chat.model.MessageModel
import org.linphone.ui.main.chat.model.MessageReactionsModel
@ -614,10 +615,10 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
viewModel.fileToDisplayEvent.observe(viewLifecycleOwner) {
it.consume { file ->
it.consume { model ->
if (messageLongPressDialog != null) return@consume
Log.i("$TAG User clicked on file [$file], let's display it in file viewer")
goToFileViewer(file)
Log.i("$TAG User clicked on file [${model.path}], let's display it in file viewer")
goToFileViewer(model)
}
}
@ -892,7 +893,8 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
}
private fun goToFileViewer(path: String) {
private fun goToFileViewer(fileModel: FileModel) {
val path = fileModel.path
Log.i("$TAG Navigating to file viewer fragment with path [$path]")
val extension = FileUtils.getExtensionFromFileName(path)
val mime = FileUtils.getMimeTypeFromExtension(extension)
@ -902,6 +904,9 @@ class ConversationFragment : SlidingPaneChildFragment() {
putString("localSipUri", viewModel.localSipUri)
putString("remoteSipUri", viewModel.remoteSipUri)
putString("path", path)
putBoolean("isEncrypted", fileModel.isEncrypted)
putLong("timestamp", fileModel.fileCreationTimestamp)
putString("originalPath", fileModel.originalPath)
}
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {

View file

@ -38,6 +38,7 @@ import org.linphone.core.tools.Log
import org.linphone.databinding.ChatMediaFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
import org.linphone.utils.Event
@ -83,7 +84,9 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[ConversationMediaListViewModel::class.java]
if (!::viewModel.isInitialized) {
viewModel = ViewModelProvider(this)[ConversationMediaListViewModel::class.java]
}
binding.viewModel = viewModel
observeToastEvents(viewModel)
@ -126,8 +129,6 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
return 1
}
}
// This isn't supported by GridLayoutManager, it will crash
// layoutManager.stackFromEnd = true
binding.mediaList.layoutManager = layoutManager
if (binding.mediaList.adapter != adapter) {
@ -155,13 +156,14 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
viewModel.openMediaEvent.observe(viewLifecycleOwner) {
it.consume { model ->
Log.i("$TAG User clicked on file [${model.file}], let's display it in file viewer")
goToFileViewer(model.file)
Log.i("$TAG User clicked on file [${model.path}], let's display it in file viewer")
goToFileViewer(model)
}
}
}
private fun goToFileViewer(path: String) {
private fun goToFileViewer(fileModel: FileModel) {
val path = fileModel.path
Log.i("$TAG Navigating to file viewer fragment with path [$path]")
val extension = FileUtils.getExtensionFromFileName(path)
val mime = FileUtils.getMimeTypeFromExtension(extension)
@ -171,6 +173,9 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
putString("localSipUri", viewModel.localSipUri)
putString("remoteSipUri", viewModel.remoteSipUri)
putString("path", path)
putBoolean("isEncrypted", fileModel.isEncrypted)
putLong("timestamp", fileModel.fileCreationTimestamp)
putString("originalPath", fileModel.originalPath)
}
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {

View file

@ -19,6 +19,7 @@
*/
package org.linphone.ui.main.chat.fragment
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -36,6 +37,8 @@ import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatListFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.file_viewer.FileViewerActivity
import org.linphone.ui.file_viewer.MediaViewerActivity
import org.linphone.ui.main.chat.adapter.ConversationsListAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel
import org.linphone.ui.main.fragment.AbstractMainFragment
@ -85,9 +88,7 @@ class ConversationsListFragment : AbstractMainFragment() {
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (
findNavController().currentDestination?.id == R.id.startConversationFragment ||
findNavController().currentDestination?.id == R.id.meetingWaitingRoomFragment ||
findNavController().currentDestination?.id == R.id.fileViewerFragment ||
findNavController().currentDestination?.id == R.id.mediaListViewerFragment
findNavController().currentDestination?.id == R.id.meetingWaitingRoomFragment
) {
// Holds fragment in place while new fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
@ -232,24 +233,23 @@ class ConversationsListFragment : AbstractMainFragment() {
if (findNavController().currentDestination?.id == R.id.conversationsListFragment) {
val path = bundle.getString("path", "")
val isMedia = bundle.getBoolean("isMedia", false)
if (path.isEmpty()) {
Log.e("$TAG Can't navigate to file viewer for empty path!")
return@consume
}
Log.i(
"$TAG Navigating to ${if (isMedia) "media" else "file"} viewer fragment with path [$path]"
)
val action = if (isMedia) {
val localSipUri = bundle.getString("localSipUri", "")
val remoteSipUri = bundle.getString("remoteSipUri", "")
ConversationsListFragmentDirections.actionConversationsListFragmentToMediaListViewerFragment(
localSipUri = localSipUri,
remoteSipUri = remoteSipUri,
path = path
)
if (isMedia) {
val intent = Intent(requireActivity(), MediaViewerActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
} else {
ConversationsListFragmentDirections.actionConversationsListFragmentToFileViewerFragment(
path,
null
)
val intent = Intent(requireActivity(), FileViewerActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
findNavController().navigate(action)
}
}
}

View file

@ -32,7 +32,7 @@ class EventLogModel @WorkerThread constructor(
isFromGroup: Boolean = false,
isGroupedWithPreviousOne: Boolean = false,
isGroupedWithNextOne: Boolean = false,
onContentClicked: ((file: String) -> Unit)? = null,
onContentClicked: ((fileModel: FileModel) -> Unit)? = null,
onJoinConferenceClicked: ((uri: String) -> Unit)? = null,
onWebUrlClicked: ((url: String) -> Unit)? = null,
onContactClicked: ((friendRefKey: String) -> Unit)? = null,

View file

@ -35,11 +35,12 @@ import org.linphone.utils.FileUtils
import org.linphone.utils.TimestampUtils
class FileModel @AnyThread constructor(
val file: String,
val path: String,
val fileName: String,
val fileSize: Long,
private val fileCreationTimestamp: Long,
private val isEncrypted: Boolean,
val fileCreationTimestamp: Long,
val isEncrypted: Boolean,
val originalPath: String,
val isWaitingToBeDownloaded: Boolean = false,
private val onClicked: ((model: FileModel) -> Unit)? = null
) {
@ -82,7 +83,7 @@ class FileModel @AnyThread constructor(
formattedFileSize.postValue(FileUtils.bytesToDisplayableSize(fileSize))
if (!isWaitingToBeDownloaded) {
val extension = FileUtils.getExtensionFromFileName(file)
val extension = FileUtils.getExtensionFromFileName(path)
isPdf = extension == "pdf"
val mime = FileUtils.getMimeTypeFromExtension(extension)
@ -113,9 +114,9 @@ class FileModel @AnyThread constructor(
@AnyThread
fun destroy() {
if (isEncrypted) {
Log.i("$TAG [VFS] Deleting plain file in cache: $file")
Log.i("$TAG [VFS] Deleting plain file in cache: $path")
scope.launch {
FileUtils.deleteFile(file)
FileUtils.deleteFile(path)
}
}
}
@ -127,22 +128,22 @@ class FileModel @AnyThread constructor(
@AnyThread
suspend fun deleteFile() {
Log.i("$TAG Deleting file [$file]")
FileUtils.deleteFile(file)
Log.i("$TAG Deleting file [$path]")
FileUtils.deleteFile(path)
}
private fun getDuration() {
try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(coreContext.context, Uri.parse(file))
retriever.setDataSource(coreContext.context, Uri.parse(path))
val durationInMs = retriever.extractMetadata(METADATA_KEY_DURATION)?.toInt() ?: 0
val seconds = durationInMs / 1000
val duration = TimestampUtils.durationToString(seconds)
Log.d("$TAG Duration for file [$file] is $duration")
Log.d("$TAG Duration for file [$path] is $duration")
audioVideoDuration.postValue(duration)
retriever.release()
} catch (e: Exception) {
Log.e("$TAG Failed to get duration for file [$file]: $e")
Log.e("$TAG Failed to get duration for file [$path]: $e")
}
}
}

View file

@ -73,7 +73,7 @@ class MessageModel @WorkerThread constructor(
val isForward: Boolean,
isGroupedWithPreviousOne: Boolean,
isGroupedWithNextOne: Boolean,
private val onContentClicked: ((file: String) -> Unit)? = null,
private val onContentClicked: ((fileModel: FileModel) -> Unit)? = null,
private val onJoinConferenceClicked: ((uri: String) -> Unit)? = null,
private val onWebUrlClicked: ((url: String) -> Unit)? = null,
private val onContactClicked: ((friendRefKey: String) -> Unit)? = null,
@ -355,13 +355,14 @@ class MessageModel @WorkerThread constructor(
checkAndRepairFilePathIfNeeded(content)
val originalPath = content.filePath.orEmpty()
val path = if (isFileEncrypted) {
Log.i(
Log.d(
"$TAG [VFS] Content is encrypted, requesting plain file path for file [${content.filePath}]"
)
content.exportPlainFile()
} else {
content.filePath ?: ""
originalPath
}
val name = content.name ?: ""
if (path.isNotEmpty()) {
@ -378,9 +379,10 @@ class MessageModel @WorkerThread constructor(
name,
fileSize,
timestamp,
isFileEncrypted
isFileEncrypted,
originalPath
) { model ->
onContentClicked?.invoke(model.file)
onContentClicked?.invoke(model)
}
filesPath.add(fileModel)
@ -392,9 +394,10 @@ class MessageModel @WorkerThread constructor(
name,
fileSize,
timestamp,
isFileEncrypted
isFileEncrypted,
originalPath
) { model ->
onContentClicked?.invoke(model.file)
onContentClicked?.invoke(model)
}
filesPath.add(fileModel)
@ -414,16 +417,17 @@ class MessageModel @WorkerThread constructor(
val timestamp = content.creationTimestamp
if (name.isNotEmpty()) {
val fileModel = if (isOutgoing && chatMessage.isFileTransferInProgress) {
val path = content.filePath ?: ""
val path = content.filePath.orEmpty()
FileModel(
path,
name,
content.fileSize.toLong(),
timestamp,
isFileEncrypted,
path,
false
) { model ->
onContentClicked?.invoke(model.file)
onContentClicked?.invoke(model)
}
} else {
FileModel(
@ -432,6 +436,7 @@ class MessageModel @WorkerThread constructor(
content.fileSize.toLong(),
timestamp,
isFileEncrypted,
name,
true
) { model ->
downloadContent(model, content)

View file

@ -45,6 +45,8 @@ class ConversationDocumentsListViewModel @UiThread constructor() : AbstractConve
}
override fun onCleared() {
super.onCleared()
documentsList.value.orEmpty().forEach(FileModel::destroy)
}
@ -62,19 +64,20 @@ class ConversationDocumentsListViewModel @UiThread constructor() : AbstractConve
Log.i("$TAG [${documents.size}] documents have been fetched")
for (documentContent in documents) {
val isEncrypted = documentContent.isFileEncrypted
val originalPath = documentContent.filePath.orEmpty()
val path = if (isEncrypted) {
Log.i(
Log.d(
"$TAG [VFS] Content is encrypted, requesting plain file path for file [${documentContent.filePath}]"
)
documentContent.exportPlainFile()
} else {
documentContent.filePath.orEmpty()
originalPath
}
val name = documentContent.name.orEmpty()
val size = documentContent.size.toLong()
val timestamp = documentContent.creationTimestamp
if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size, timestamp, isEncrypted) {
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) {
openDocumentEvent.postValue(Event(it))
}
list.add(model)

View file

@ -34,12 +34,6 @@ class ConversationMediaListViewModel @UiThread constructor() : AbstractConversat
val mediaList = MutableLiveData<List<FileModel>>()
val fullScreenMode = MutableLiveData<Boolean>()
val currentlyDisplayedFileName = MutableLiveData<String>()
val operationInProgress = MutableLiveData<Boolean>()
val openMediaEvent: MutableLiveData<Event<FileModel>> by lazy {
MutableLiveData<Event<FileModel>>()
}
@ -49,13 +43,13 @@ class ConversationMediaListViewModel @UiThread constructor() : AbstractConversat
}
override fun onCleared() {
super.onCleared()
mediaList.value.orEmpty().forEach(FileModel::destroy)
}
@WorkerThread
private fun loadMediaList() {
operationInProgress.postValue(true)
val list = arrayListOf<FileModel>()
Log.i(
"$TAG Loading media contents for conversation [${LinphoneUtils.getChatRoomId(
@ -69,19 +63,20 @@ class ConversationMediaListViewModel @UiThread constructor() : AbstractConversat
if (mediaContent.isVoiceRecording) continue
val isEncrypted = mediaContent.isFileEncrypted
val originalPath = mediaContent.filePath.orEmpty()
val path = if (isEncrypted) {
Log.i(
Log.d(
"$TAG [VFS] Content is encrypted, requesting plain file path for file [${mediaContent.filePath}]"
)
mediaContent.exportPlainFile()
} else {
mediaContent.filePath.orEmpty()
originalPath
}
val name = mediaContent.name.orEmpty()
val size = mediaContent.size.toLong()
val timestamp = mediaContent.creationTimestamp
if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size, timestamp, isEncrypted) {
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) {
openMediaEvent.postValue(Event(it))
}
list.add(model)
@ -89,6 +84,5 @@ class ConversationMediaListViewModel @UiThread constructor() : AbstractConversat
}
Log.i("$TAG [${media.size}] media have been processed")
mediaList.postValue(list)
operationInProgress.postValue(false)
}
}

View file

@ -43,6 +43,7 @@ import org.linphone.core.Participant
import org.linphone.core.ParticipantInfo
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.EventLogModel
import org.linphone.ui.main.chat.model.FileModel
import org.linphone.ui.main.chat.model.MessageModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
@ -100,8 +101,8 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
MutableLiveData<Event<Boolean>>()
}
val fileToDisplayEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
val fileToDisplayEvent: MutableLiveData<Event<FileModel>> by lazy {
MutableLiveData<Event<FileModel>>()
}
val conferenceToJoinEvent: MutableLiveData<Event<String>> by lazy {
@ -664,8 +665,8 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
groupChatRoom,
index > 0,
index != groupedEventLogs.size - 1,
{ file ->
fileToDisplayEvent.postValue(Event(file))
{ fileModel ->
fileToDisplayEvent.postValue(Event(fileModel))
},
{ conferenceUri ->
conferenceToJoinEvent.postValue(Event(conferenceUri))

View file

@ -264,7 +264,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : GenericViewMo
}
content.name = attachment.fileName
// Let the file body handler take care of the upload
content.filePath = attachment.file
content.filePath = attachment.path
message.addFileContent(content)
}
@ -320,8 +320,8 @@ class SendMessageInConversationViewModel @UiThread constructor() : GenericViewMo
val fileName = FileUtils.getNameFromFilePath(file)
val timestamp = System.currentTimeMillis() / 1000
val model = FileModel(file, fileName, 0, timestamp, isEncrypted = false) { model ->
removeAttachment(model.file)
val model = FileModel(file, fileName, 0, timestamp, false, file) { model ->
removeAttachment(model.path)
}
list.add(model)
@ -340,7 +340,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : GenericViewMo
val list = arrayListOf<FileModel>()
list.addAll(attachments.value.orEmpty())
val found = list.find {
it.file == file
it.path == file
}
if (found != null) {
if (delete) {

View file

@ -1,279 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.file_media_viewer.fragment
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.core.content.FileProvider
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import androidx.viewpager2.widget.ViewPager2
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.FileMediaViewerFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel
import org.linphone.ui.main.file_media_viewer.adapter.MediaListAdapter
import org.linphone.ui.main.fragment.GenericMainFragment
import org.linphone.utils.AppUtils
import org.linphone.utils.FileUtils
class MediaListViewerFragment : GenericMainFragment() {
companion object {
private const val TAG = "[Media List Viewer]"
}
private lateinit var binding: FileMediaViewerFragmentBinding
private lateinit var adapter: MediaListAdapter
private lateinit var viewModel: ConversationMediaListViewModel
private lateinit var viewPager: ViewPager2
private val args: MediaListViewerFragmentArgs by navArgs()
private var navBarDefaultColor: Int = -1
private val pageListener = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val list = viewModel.mediaList.value.orEmpty()
if (position >= 0 && position < list.size) {
val model = list[position]
viewModel.currentlyDisplayedFileName.value = "${model.fileName}\n${model.dateTime}"
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FileMediaViewerFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
super.onViewCreated(view, savedInstanceState)
navBarDefaultColor = requireActivity().window.navigationBarColor
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[ConversationMediaListViewModel::class.java]
binding.viewModel = viewModel
observeToastEvents(viewModel)
// Consider full screen mode the default
sharedViewModel.mediaViewerFullScreenMode.value = true
val localSipUri = args.localSipUri
val remoteSipUri = args.remoteSipUri
val path = args.path
Log.i(
"$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri] trying to display file [$path]"
)
val chatRoom = sharedViewModel.displayedChatRoom
viewModel.findChatRoom(chatRoom, localSipUri, remoteSipUri)
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]"
)
adapter = MediaListAdapter(this, viewModel)
viewPager = binding.mediaViewPager
viewPager.adapter = adapter
viewPager.registerOnPageChangeCallback(pageListener)
var index = it.indexOfFirst { model ->
model.file == path
}
if (index == -1) {
Log.i(
"$TAG Path [$path] not found in media list (expected if VFS is enabled), trying using file name"
)
val fileName = File(path).name
val underscore = fileName.indexOf("_")
val originalFileName = if (underscore != -1 && underscore < 2) {
fileName.subSequence(underscore, fileName.length)
} else {
fileName
}
index = it.indexOfFirst { model ->
model.file.endsWith(originalFileName)
}
if (index == -1) {
Log.w(
"$TAG Path [$path] not found either using filename [$originalFileName] match"
)
}
}
val position = if (index == -1) {
Log.e("$TAG File [$path] not found, using latest one available instead!")
val message = getString(R.string.conversation_media_not_found_toast)
val icon = R.drawable.warning_circle
(requireActivity() as GenericActivity).showRedToast(message, icon)
count - 1
} else {
index
}
viewPager.setCurrentItem(position, false)
viewPager.offscreenPageLimit = 1
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
binding.setBackClickListener {
goBack()
}
binding.setShareClickListener {
shareFile()
}
binding.setExportClickListener {
exportFile()
}
sharedViewModel.mediaViewerFullScreenMode.observe(viewLifecycleOwner) {
if (it != viewModel.fullScreenMode.value) {
viewModel.fullScreenMode.value = it
}
}
}
override fun onResume() {
// Force this navigation bar color
requireActivity().window.navigationBarColor = requireContext().getColor(R.color.gray_900)
super.onResume()
}
override fun onDestroy() {
// Reset default navigation bar color
requireActivity().window.navigationBarColor = navBarDefaultColor
if (::viewPager.isInitialized) {
viewPager.unregisterOnPageChangeCallback(pageListener)
}
super.onDestroy()
}
private fun exportFile() {
val list = viewModel.mediaList.value.orEmpty()
val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != null) {
val filePath = model.file
lifecycleScope.launch {
withContext(Dispatchers.IO) {
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
if (mediaStorePath.isNotEmpty()) {
Log.i(
"$TAG File [$filePath] has been successfully exported to MediaStore"
)
val message = AppUtils.getString(
R.string.toast_file_successfully_exported_to_media_store
)
(requireActivity() as GenericActivity).showGreenToast(
message,
R.drawable.check
)
} else {
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
val message = AppUtils.getString(
R.string.toast_export_file_to_media_store_error
)
(requireActivity() as GenericActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
}
}
} else {
Log.e(
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
)
}
}
private fun shareFile() {
val list = viewModel.mediaList.value.orEmpty()
val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != null) {
lifecycleScope.launch {
val filePath = FileUtils.getProperFilePath(model.file)
val copy = FileUtils.getFilePath(
requireContext(),
Uri.parse(filePath),
overrideExisting = true,
copyToCache = true
)
if (!copy.isNullOrEmpty()) {
val publicUri = FileProvider.getUriForFile(
requireContext(),
requireContext().getString(R.string.file_provider),
File(copy)
)
Log.i("$TAG Public URI for file is [$publicUri], starting intent chooser")
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, publicUri)
putExtra(Intent.EXTRA_SUBJECT, model.fileName)
type = model.mimeTypeString
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
} else {
Log.e("$TAG Failed to copy file [$filePath] to share!")
}
}
} else {
Log.e(
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
)
}
}
}

View file

@ -32,6 +32,7 @@ import org.linphone.core.CorePreferences
import org.linphone.core.tools.Log
import org.linphone.databinding.HelpDebugFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.file_viewer.FileViewerActivity
import org.linphone.ui.main.fragment.GenericMainFragment
import org.linphone.ui.main.help.viewmodel.HelpViewModel
@ -117,11 +118,12 @@ class DebugFragment : GenericMainFragment() {
viewModel.showConfigFileEvent.observe(viewLifecycleOwner) {
it.consume { content ->
if (findNavController().currentDestination?.id == R.id.debugFragment) {
val action = DebugFragmentDirections.actionDebugFragmentToFileViewerFragment(
CorePreferences.CONFIG_FILE_NAME,
content
)
findNavController().navigate(action)
val intent = Intent(requireActivity(), FileViewerActivity::class.java)
val bundle = Bundle()
bundle.putString("path", CorePreferences.CONFIG_FILE_NAME)
bundle.putString("content", content)
intent.putExtras(bundle)
startActivity(intent)
}
}
}

View file

@ -47,6 +47,7 @@ class SingleSignOnViewModel : GenericViewModel() {
private const val CLIENT_ID = "linphone"
private const val REDIRECT_URI = "org.linphone:/openidcallback"
private const val OPEN_ID_WELL_KNOWN = ".well-known/openid-configuration"
}
val singleSignOnProcessCompletedEvent = MutableLiveData<Event<Boolean>>()
@ -106,12 +107,12 @@ class SingleSignOnViewModel : GenericViewModel() {
Log.e(
"$TAG Failed to fetch configuration on [$singleSignOnUrl]: ${ex.errorDescription}"
)
if (!singleSignOnUrl.endsWith(".well-known/openid-configuration")) {
Log.w("$TAG Trying again appending .well-known/openid-configuration to URL")
if (!singleSignOnUrl.endsWith(OPEN_ID_WELL_KNOWN)) {
Log.w("$TAG Trying again appending [$OPEN_ID_WELL_KNOWN] to URL")
singleSignOnUrl = if (singleSignOnUrl.endsWith("/")) {
"$singleSignOnUrl.well-known/openid-configuration"
"$singleSignOnUrl$OPEN_ID_WELL_KNOWN"
} else {
"$singleSignOnUrl/.well-known/openid-configuration"
"$singleSignOnUrl/$OPEN_ID_WELL_KNOWN"
}
singleSignOn()
return@RetrieveConfigurationCallback

View file

@ -124,8 +124,6 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
// When using keyboard to share gif or other, see RichContentReceiver & RichEditText classes
val richContentUri = MutableLiveData<Event<Uri>>()
val mediaViewerFullScreenMode = MutableLiveData<Boolean>()
val displayFileEvent: MutableLiveData<Event<Bundle>> by lazy {
MutableLiveData<Event<Bundle>>()
}

View file

@ -118,7 +118,7 @@ class FileUtils {
type.startsWith("application/json") -> MimeType.PlainText
else -> MimeType.Unknown
}
Log.i("$TAG MIME type for [$type] is [$mime]")
Log.d("$TAG MIME type for [$type] is [$mime]")
return mime
}
@ -388,6 +388,11 @@ class FileUtils {
}
}
fun doesFileExist(path: String): Boolean {
val file = File(path)
return file.exists()
}
@AnyThread
suspend fun dumpStringToFile(data: String, to: File): Boolean {
try {

View file

@ -36,7 +36,7 @@
android:scaleType="centerCrop"
android:contentDescription="@{model.isVideoPreview ? @string/content_description_chat_bubble_video : @string/content_description_chat_bubble_image}"
android:visibility="@{model.isImage || model.isVideoPreview ? View.VISIBLE : View.GONE}"
coilBubbleGrid="@{model.file}"
coilBubbleGrid="@{model.path}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

View file

@ -30,7 +30,7 @@
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:contentDescription="@{model.isVideoPreview ? @string/content_description_chat_bubble_video : @string/content_description_chat_bubble_image}"
coilBubble="@{model.file}"
coilBubble="@{model.path}"
app:layout_constraintHeight_max="@dimen/chat_bubble_big_image_max_size"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

View file

@ -22,7 +22,7 @@
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@{model.isVideoPreview ? @string/content_description_chat_bubble_video : @string/content_description_chat_bubble_image}"
coilBubbleGrid="@{model.file}"
coilBubbleGrid="@{model.path}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
@ -13,71 +12,61 @@
type="org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:padding="15dp"
android:onClick="@{backClickListener}"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"/>
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:padding="15dp"
android:onClick="@{backClickListener}"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/main_page_title_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/conversation_media_list_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/main_page_title_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="@string/conversation_media_list_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mediaList"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/color_grey_100"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mediaList"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/color_grey_100"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -15,7 +15,7 @@
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel" />
type="org.linphone.ui.file_viewer.viewmodel.MediaListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -23,6 +23,12 @@
android:layout_height="match_parent"
android:background="@color/black">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:constraint_referenced_ids="back, title, share, save, file_name"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/media_view_pager"
android:layout_width="match_parent"
@ -39,7 +45,6 @@
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
@ -53,7 +58,6 @@
android:background="?attr/color_main2_000"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/share"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
@ -68,7 +72,6 @@
android:padding="15dp"
android:src="@drawable/share_network"
android:contentDescription="@string/content_description_share_file"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/save"
@ -84,7 +87,6 @@
android:padding="15dp"
android:src="@drawable/download_simple"
android:contentDescription="@string/content_description_save_file"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"
@ -102,11 +104,23 @@
android:textSize="12sp"
android:textColor="?attr/color_main2_600"
android:textAlignment="center"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout
android:id="@+id/toasts_area"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="@dimen/toast_top_margin"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
app:layout_constraintWidth_max="@dimen/toast_max_width"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -5,13 +5,16 @@
<data>
<import type="android.view.View" />
<variable
name="toggleFullScreenModeClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.file_media_viewer.viewmodel.MediaViewModel" />
type="org.linphone.ui.file_viewer.viewmodel.MediaViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:onClick="@{toggleFullScreenModeClickListener}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
@ -58,7 +61,7 @@
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:onClick="@{toggleFullScreenModeClickListener}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/illu"

View file

@ -13,7 +13,7 @@
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.file_media_viewer.viewmodel.FileViewModel" />
type="org.linphone.ui.file_viewer.viewmodel.FileViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -21,6 +21,12 @@
android:layout_height="match_parent"
android:background="@color/black">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:constraint_referenced_ids="back, title, share, save, file_name"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pdf_view_pager"
android:layout_width="match_parent"
@ -79,7 +85,6 @@
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
@ -94,7 +99,6 @@
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text=""
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toStartOf="@id/share"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="parent"/>
@ -109,7 +113,6 @@
android:padding="15dp"
android:src="@drawable/share_network"
android:contentDescription="@string/content_description_share_file"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/save"
@ -125,7 +128,6 @@
android:padding="15dp"
android:src="@drawable/download_simple"
android:contentDescription="@string/content_description_save_file"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:tint="?attr/color_main1_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"
@ -143,11 +145,23 @@
android:textSize="12sp"
android:textColor="?attr/color_main2_600"
android:textAlignment="center"
android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<LinearLayout
android:id="@+id/toasts_area"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="@dimen/toast_top_margin"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
app:layout_constraintWidth_max="@dimen/toast_max_width"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -216,12 +216,6 @@
app:launchSingleTop="true"
app:popUpTo="@id/helpFragment"
app:popUpToInclusive="true"/>
<action
android:id="@+id/action_debugFragment_to_fileViewerFragment"
app:destination="@id/fileViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
</fragment>
<fragment
@ -269,18 +263,6 @@
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"
app:launchSingleTop="true" />
<action
android:id="@+id/action_conversationsListFragment_to_mediaListViewerFragment"
app:destination="@id/mediaListViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<action
android:id="@+id/action_conversationsListFragment_to_fileViewerFragment"
app:destination="@id/fileViewerFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<action
android:id="@+id/action_conversationsListFragment_to_accountProfileFragment"
app:destination="@id/accountProfileFragment"
@ -379,36 +361,6 @@
app:argType="string" />
</fragment>
<fragment
android:id="@+id/fileViewerFragment"
android:name="org.linphone.ui.main.file_media_viewer.fragment.FileViewerFragment"
android:label="FileViewerFragment"
tools:layout="@layout/file_viewer_fragment">
<argument
android:name="path"
app:argType="string" />
<argument
android:name="content"
app:argType="string"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/mediaListViewerFragment"
android:name="org.linphone.ui.main.file_media_viewer.fragment.MediaListViewerFragment"
android:label="MediaListViewerFragment"
tools:layout="@layout/file_media_viewer_fragment">
<argument
android:name="localSipUri"
app:argType="string" />
<argument
android:name="remoteSipUri"
app:argType="string" />
<argument
android:name="path"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/ldapServerConfigurationFragment"
android:name="org.linphone.ui.main.settings.fragment.LdapServerConfigurationFragment"