Show files shared by chat message in app if possible

This commit is contained in:
Sylvain Berfini 2021-01-28 21:08:18 +01:00
parent 5b529e8e3a
commit b818bf0155
26 changed files with 965 additions and 21 deletions

View file

@ -23,6 +23,8 @@ import android.annotation.SuppressLint
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Display
import android.view.Surface import android.view.Surface
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@ -73,6 +75,8 @@ abstract class GenericActivity : AppCompatActivity() {
} }
} }
} }
updateScreenSize()
} }
override fun onResume() { override fun onResume() {
@ -102,4 +106,14 @@ abstract class GenericActivity : AppCompatActivity() {
fun isTablet(): Boolean { fun isTablet(): Boolean {
return resources.getBoolean(R.bool.isTablet) return resources.getBoolean(R.bool.isTablet)
} }
private fun updateScreenSize() {
val metrics = DisplayMetrics()
val display: Display = windowManager.defaultDisplay
display.getRealMetrics(metrics)
val screenWidth = metrics.widthPixels.toFloat()
val screenHeight = metrics.heightPixels.toFloat()
coreContext.screenWidth = screenWidth
coreContext.screenHeight = screenHeight
}
} }

View file

@ -348,6 +348,42 @@ internal fun DetailChatRoomFragment.navigateToEphemeralInfo() {
} }
} }
internal fun DetailChatRoomFragment.navigateToTextFileViewer(secure: Boolean) {
val bundle = bundleOf("Secure" to secure)
findMasterNavController().navigate(
R.id.action_global_textViewerFragment,
bundle,
getRightToLeftAnimationNavOptions()
)
}
internal fun DetailChatRoomFragment.navigateToPdfFileViewer(secure: Boolean) {
val bundle = bundleOf("Secure" to secure)
findMasterNavController().navigate(
R.id.action_global_pdfViewerFragment,
bundle,
getRightToLeftAnimationNavOptions()
)
}
internal fun DetailChatRoomFragment.navigateToImageFileViewer(secure: Boolean) {
val bundle = bundleOf("Secure" to secure)
findMasterNavController().navigate(
R.id.action_global_imageViewerFragment,
bundle,
getRightToLeftAnimationNavOptions()
)
}
internal fun DetailChatRoomFragment.navigateToVideoFileViewer(secure: Boolean) {
val bundle = bundleOf("Secure" to secure)
findMasterNavController().navigate(
R.id.action_global_videoViewerFragment,
bundle,
getRightToLeftAnimationNavOptions()
)
}
internal fun ChatRoomCreationFragment.navigateToGroupInfo() { internal fun ChatRoomCreationFragment.navigateToGroupInfo() {
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
findNavController().navigate( findNavController().navigate(

View file

@ -24,8 +24,6 @@ import android.animation.ValueAnimator
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Display
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.flexbox.FlexboxLayout import com.google.android.flexbox.FlexboxLayout
@ -179,10 +177,7 @@ class OutgoingCallActivity : ProximitySensorActivity() {
} }
private fun initNumpadLayout() { private fun initNumpadLayout() {
val metrics = DisplayMetrics() val screenWidth = coreContext.screenWidth
val display: Display = windowManager.defaultDisplay
display.getRealMetrics(metrics)
val screenWidth = metrics.widthPixels.toFloat()
numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply { numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply {
addUpdateListener { addUpdateListener {
val value = it.animatedValue as Float val value = it.animatedValue as Float

View file

@ -27,8 +27,6 @@ import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.util.DisplayMetrics
import android.view.Display
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.flexbox.FlexboxLayout import com.google.android.flexbox.FlexboxLayout
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
@ -242,10 +240,7 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
} }
private fun initNumpadLayout() { private fun initNumpadLayout() {
val metrics = DisplayMetrics() val screenWidth = coreContext.screenWidth
val display: Display = requireActivity().windowManager.defaultDisplay
display.getRealMetrics(metrics)
val screenWidth = metrics.widthPixels.toFloat()
numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply { numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply {
addUpdateListener { addUpdateListener {
val value = it.animatedValue as Float val value = it.animatedValue as Float

View file

@ -70,6 +70,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
private lateinit var tabsFragment: FragmentContainerView private lateinit var tabsFragment: FragmentContainerView
private lateinit var statusFragment: FragmentContainerView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -110,6 +111,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
tabsFragment = findViewById(R.id.tabs_fragment) tabsFragment = findViewById(R.id.tabs_fragment)
statusFragment = findViewById(R.id.status_fragment)
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
@ -154,6 +156,9 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
arguments: Bundle? arguments: Bundle?
) { ) {
currentFocus?.hideKeyboard() currentFocus?.hideKeyboard()
if (statusFragment.visibility == View.GONE) {
statusFragment.visibility = View.VISIBLE
}
val motionLayout: MotionLayout = binding.content as MotionLayout val motionLayout: MotionLayout = binding.content as MotionLayout
if (corePreferences.enableAnimations) { if (corePreferences.enableAnimations) {
@ -171,6 +176,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
} }
fun hideStatusFragment() {
statusFragment.visibility = View.GONE
}
private fun View.hideKeyboard() { private fun View.hideKeyboard() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0) imm.hideSoftInputFromWindow(windowToken, 0)

View file

@ -41,6 +41,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.io.File
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
@ -225,7 +226,19 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
adapter.openContentEvent.observe(viewLifecycleOwner, { adapter.openContentEvent.observe(viewLifecycleOwner, {
it.consume { path -> it.consume { path ->
openFile(path) if (!File(path).exists()) {
(requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
} else {
Log.i("[Chat Message] Opening file: $path")
sharedViewModel.fileToOpen.value = path
when {
FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(viewModel.chatRoom.currentParams.encryptionEnabled())
FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer(viewModel.chatRoom.currentParams.encryptionEnabled())
FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer(viewModel.chatRoom.currentParams.encryptionEnabled())
FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer(viewModel.chatRoom.currentParams.encryptionEnabled())
else -> openFile(path)
}
}
} }
}) })
@ -581,14 +594,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
dialogViewModel.showOkButton({ dialogViewModel.showOkButton({
dialog.dismiss() dialog.dismiss()
intent.setDataAndType(contentUri, "text/plain") navigateToTextFileViewer(viewModel.chatRoom.currentParams.encryptionEnabled())
try {
startActivity(intent)
} catch (anfe: ActivityNotFoundException) {
Log.e("[Chat Message] Couldn't find an activity to handle text/plain MIME type")
val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.chat_room_cant_open_file_no_app_found)
}
}) })
dialog.show() dialog.show()

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.activities.main.files.viewmodels.ImageFileViewModel
import org.linphone.activities.main.files.viewmodels.ImageFileViewModelFactory
import org.linphone.activities.main.fragments.SecureFragment
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.databinding.ImageViewerFragmentBinding
class ImageViewerFragment : SecureFragment<ImageViewerFragmentBinding>() {
private lateinit var viewModel: ImageFileViewModel
private lateinit var sharedViewModel: SharedMainViewModel
private lateinit var gestureDetector: GestureDetector
private lateinit var scaleGestureDetector: ScaleGestureDetector
private var scaleFactor = 1.0f
override fun getLayoutId(): Int = R.layout.image_viewer_fragment
@SuppressLint("ClickableViewAccessibility")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this
sharedViewModel = requireActivity().run {
ViewModelProvider(this).get(SharedMainViewModel::class.java)
}
val filePath = sharedViewModel.fileToOpen.value
filePath ?: return
viewModel = ViewModelProvider(
this,
ImageFileViewModelFactory(filePath)
)[ImageFileViewModel::class.java]
binding.viewModel = viewModel
isSecure = arguments?.getBoolean("Secure") ?: false
gestureDetector = GestureDetector(requireContext(), object :
GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent?): Boolean {
scaleFactor = if (scaleFactor == 1.0f) 2.0f else 1.0f
binding.imageView.scaleX = scaleFactor
binding.imageView.scaleY = scaleFactor
return true
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
if (scaleFactor <= 1.0f) return false
val scrollX = binding.imageView.scrollX + distanceX.toInt()
binding.imageView.scrollX = scrollX
val scrollY = binding.imageView.scrollY + distanceY.toInt()
binding.imageView.scrollY = scrollY
return true
}
})
scaleGestureDetector = ScaleGestureDetector(requireContext(), object :
ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.1f, 5.0f)
binding.imageView.scaleX = scaleFactor
binding.imageView.scaleY = scaleFactor
return false
}
})
binding.imageView.setOnTouchListener { _, event ->
val previousScaleFactor = scaleFactor
scaleGestureDetector.onTouchEvent(event)
if (previousScaleFactor != scaleFactor) {
// Prevent touch event from going further
return@setOnTouchListener true
}
gestureDetector.onTouchEvent(event)
return@setOnTouchListener true
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.fragments
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.activities.main.files.viewmodels.PdfFileViewModel
import org.linphone.activities.main.files.viewmodels.PdfFileViewModelFactory
import org.linphone.activities.main.fragments.SecureFragment
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.databinding.PdfViewerFragmentBinding
class PdfViewerFragment : SecureFragment<PdfViewerFragmentBinding>() {
private lateinit var viewModel: PdfFileViewModel
private lateinit var sharedViewModel: SharedMainViewModel
override fun getLayoutId(): Int = R.layout.pdf_viewer_fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this
sharedViewModel = requireActivity().run {
ViewModelProvider(this).get(SharedMainViewModel::class.java)
}
val filePath = sharedViewModel.fileToOpen.value
filePath ?: return
viewModel = ViewModelProvider(
this,
PdfFileViewModelFactory(filePath)
)[PdfFileViewModel::class.java]
binding.viewModel = viewModel
isSecure = arguments?.getBoolean("Secure") ?: false
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.fragments
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.activities.main.files.viewmodels.TextFileViewModel
import org.linphone.activities.main.files.viewmodels.TextFileViewModelFactory
import org.linphone.activities.main.fragments.SecureFragment
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.databinding.TextViewerFragmentBinding
class TextViewerFragment : SecureFragment<TextViewerFragmentBinding>() {
private lateinit var viewModel: TextFileViewModel
private lateinit var sharedViewModel: SharedMainViewModel
override fun getLayoutId(): Int = R.layout.text_viewer_fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this
sharedViewModel = requireActivity().run {
ViewModelProvider(this).get(SharedMainViewModel::class.java)
}
val filePath = sharedViewModel.fileToOpen.value
filePath ?: return
viewModel = ViewModelProvider(
this,
TextFileViewModelFactory(filePath)
)[TextFileViewModel::class.java]
binding.viewModel = viewModel
isSecure = arguments?.getBoolean("Secure") ?: false
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.fragments
import android.os.Bundle
import androidx.navigation.fragment.findNavController
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.databinding.FileViewerTopBarFragmentBinding
class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
override fun getLayoutId(): Int = R.layout.file_viewer_top_bar_fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this
binding.setBackClickListener {
findNavController().popBackStack()
}
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.fragments
import android.os.Bundle
import android.widget.MediaController
import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.activities.main.files.viewmodels.VideoFileViewModel
import org.linphone.activities.main.files.viewmodels.VideoFileViewModelFactory
import org.linphone.activities.main.fragments.SecureFragment
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.databinding.VideoViewerFragmentBinding
class VideoViewerFragment : SecureFragment<VideoViewerFragmentBinding>() {
private lateinit var viewModel: VideoFileViewModel
private lateinit var sharedViewModel: SharedMainViewModel
override fun getLayoutId(): Int = R.layout.video_viewer_fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this
sharedViewModel = requireActivity().run {
ViewModelProvider(this).get(SharedMainViewModel::class.java)
}
val filePath = sharedViewModel.fileToOpen.value
filePath ?: return
viewModel = ViewModelProvider(
this,
VideoFileViewModelFactory(filePath)
)[VideoFileViewModel::class.java]
binding.viewModel = viewModel
initVideoControls()
isSecure = arguments?.getBoolean("Secure") ?: false
}
private fun initVideoControls() {
val mediaController = MediaController(requireContext())
viewModel.initMediaController(mediaController, binding.videoView)
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class ImageFileViewModelFactory(private val filePath: String) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ImageFileViewModel(filePath) as T
}
}
class ImageFileViewModel(val filePath: String) : ViewModel()

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.viewmodels
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
class PdfFileViewModelFactory(private val filePath: String) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return PdfFileViewModel(filePath) as T
}
}
class PdfFileViewModel(filePath: String) : ViewModel() {
val operationInProgress = MutableLiveData<Boolean>()
val pages = MutableLiveData<ArrayList<PdfPageViewModel>>()
private val pdfRenderer: PdfRenderer
init {
operationInProgress.value = false
val input = ParcelFileDescriptor.open(File(filePath), ParcelFileDescriptor.MODE_READ_ONLY)
pdfRenderer = PdfRenderer(input)
Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath")
loadPdf()
}
override fun onCleared() {
for (page in pages.value.orEmpty()) {
page.destroy()
}
pages.value?.clear()
super.onCleared()
}
private fun loadPdf() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
operationInProgress.postValue(true)
for (index in 0 until pdfRenderer.pageCount) {
val page: PdfRenderer.Page = pdfRenderer.openPage(index)
val width = if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight
val bm = Bitmap.createBitmap(
width.toInt(),
(width / page.width * page.height).toInt(),
Bitmap.Config.ARGB_8888
)
page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
page.close()
val pageViewModel = PdfPageViewModel(bm)
val list = arrayListOf<PdfPageViewModel>()
list.addAll(pages.value.orEmpty())
list.add(pageViewModel)
pages.postValue(list)
}
operationInProgress.postValue(false)
} catch (e: Exception) {
Log.e("[PDF Viewer] Exception: $e")
operationInProgress.postValue(false)
}
}
}
}
class PdfPageViewModel(val bitmap: Bitmap) {
fun destroy() {
bitmap.recycle()
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import java.io.BufferedReader
import java.io.FileReader
import java.lang.StringBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.core.tools.Log
class TextFileViewModelFactory(private val filePath: String) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return TextFileViewModel(filePath) as T
}
}
class TextFileViewModel(filePath: String) : ViewModel() {
val operationInProgress = MutableLiveData<Boolean>()
val text = MutableLiveData<String>()
init {
operationInProgress.value = false
openFile(filePath)
}
private fun openFile(filePath: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
operationInProgress.postValue(true)
try {
val br = BufferedReader(FileReader(filePath))
var line: String?
var textBuilder = StringBuilder()
while (br.readLine().also { line = it } != null) {
textBuilder.append(line)
textBuilder.append('\n')
}
br.close()
text.postValue(textBuilder.toString())
operationInProgress.postValue(false)
} catch (e: Exception) {
Log.e("[Text Viewer] Exception: $e")
operationInProgress.postValue(false)
}
}
}
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2021 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.activities.main.files.viewmodels
import android.widget.MediaController
import android.widget.VideoView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.linphone.core.tools.Log
class VideoFileViewModelFactory(private val filePath: String) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return VideoFileViewModel(filePath) as T
}
}
class VideoFileViewModel(val filePath: String) : ViewModel() {
fun initMediaController(mediaController: MediaController, videoView: VideoView) {
videoView.setOnPreparedListener { mediaPlayer ->
mediaPlayer.setOnVideoSizeChangedListener { _, _, _ ->
videoView.setMediaController(mediaController)
// The following will make the video controls above the video
// mediaController.setAnchorView(videoView)
}
}
videoView.setOnCompletionListener { mediaPlayer ->
mediaPlayer.release()
}
videoView.setOnErrorListener { _, what, extra ->
Log.e("[Video Viewer] Error: $what ($extra)")
false
}
videoView.setVideoPath(filePath)
videoView.start()
}
}

View file

@ -48,6 +48,8 @@ class SharedMainViewModel : ViewModel() {
MutableLiveData<Event<ChatMessage>>() MutableLiveData<Event<ChatMessage>>()
} }
val fileToOpen = MutableLiveData<String>()
var createEncryptedChatRoom: Boolean = false var createEncryptedChatRoom: Boolean = false
val chatRoomParticipants = MutableLiveData<ArrayList<Address>>() val chatRoomParticipants = MutableLiveData<ArrayList<Address>>()

View file

@ -56,6 +56,9 @@ class CoreContext(val context: Context, coreConfig: Config) {
val core: Core val core: Core
val handler: Handler = Handler(Looper.getMainLooper()) val handler: Handler = Handler(Looper.getMainLooper())
var screenWidth: Float = 0f
var screenHeight: Float = 0f
val appVersion: String by lazy { val appVersion: String by lazy {
val appVersion = org.linphone.BuildConfig.VERSION_NAME val appVersion = org.linphone.BuildConfig.VERSION_NAME
val appBranch = context.getString(R.string.linphone_app_branch) val appBranch = context.getString(R.string.linphone_app_branch)

View file

@ -59,6 +59,18 @@ class FileUtils {
return extension return extension
} }
fun isPlainTextFile(path: String): Boolean {
val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("text/plain") ?: false
}
fun isExtensionPdf(path: String): Boolean {
val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("application/pdf") ?: false
}
fun isExtensionImage(path: String): Boolean { fun isExtensionImage(path: String): Boolean {
val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault()) val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="backClickListener"
type="android.view.View.OnClickListener"/>
</data>
<LinearLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:background="?attr/lightToolbarBackgroundColor"
android:orientation="horizontal">
<ImageView
android:id="@+id/back"
android:onClick="@{backClickListener}"
android:contentDescription="@string/content_description_go_back"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.2"
android:background="?attr/button_background_drawable"
android:padding="18dp"
android:src="@drawable/back" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:visibility="invisible" />
</LinearLayout>
</layout>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="org.linphone.activities.main.files.viewmodels.ImageFileViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_color">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.main.files.fragments.TopBarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
tools:layout="@layout/file_viewer_top_bar_fragment" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_bar_fragment"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
glidePath="@{viewModel.filePath}"/>
</LinearLayout>
</RelativeLayout>
</layout>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="data"
type="org.linphone.activities.main.files.viewmodels.PdfFileViewModel.PdfPageViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@{data.bitmap}" />
</RelativeLayout>
</layout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="org.linphone.activities.main.files.viewmodels.PdfFileViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/backgroundColor">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.main.files.fragments.TopBarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
tools:layout="@layout/file_viewer_top_bar_fragment" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_bar_fragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
bind:entries="@{viewModel.pages}"
bind:layout="@{@layout/pdf_viewer_cell}"/>
</ScrollView>
<include
layout="@layout/wait_layout"
bind:visibility="@{viewModel.operationInProgress}"/>
</RelativeLayout>
</layout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="org.linphone.activities.main.files.viewmodels.TextFileViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/backgroundColor">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.main.files.fragments.TopBarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
tools:layout="@layout/file_viewer_top_bar_fragment" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_bar_fragment">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:singleLine="false"
android:text="@{viewModel.text}"/>
</ScrollView>
<include
layout="@layout/wait_layout"
bind:visibility="@{viewModel.operationInProgress}"/>
</RelativeLayout>
</layout>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="org.linphone.activities.main.files.viewmodels.VideoFileViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_color">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.main.files.fragments.TopBarFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
tools:layout="@layout/file_viewer_top_bar_fragment" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_bar_fragment">
<VideoView
android:id="@+id/video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>
</RelativeLayout>
</layout>

View file

@ -320,5 +320,25 @@
android:name="IsLinking" android:name="IsLinking"
app:argType="boolean" /> app:argType="boolean" />
</fragment> </fragment>
<fragment
android:id="@+id/textViewerFragment"
android:name="org.linphone.activities.main.files.fragments.TextViewerFragment"
android:label="TextViewerFragment" />
<action android:id="@+id/action_global_textViewerFragment" app:destination="@id/textViewerFragment" />
<fragment
android:id="@+id/pdfViewerFragment"
android:name="org.linphone.activities.main.files.fragments.PdfViewerFragment"
android:label="PdfViewerFragment" />
<action android:id="@+id/action_global_pdfViewerFragment" app:destination="@id/pdfViewerFragment" />
<fragment
android:id="@+id/imageViewerFragment"
android:name="org.linphone.activities.main.files.fragments.ImageViewerFragment"
android:label="ImageViewerFragment" />
<action android:id="@+id/action_global_imageViewerFragment" app:destination="@id/imageViewerFragment"/>
<fragment
android:id="@+id/videoViewerFragment"
android:name="org.linphone.activities.main.files.fragments.VideoViewerFragment"
android:label="VideoViewerFragment" />
<action android:id="@+id/action_global_videoViewerFragment" app:destination="@id/videoViewerFragment"/>
</navigation> </navigation>

View file

@ -173,6 +173,7 @@
<string name="chat_room_dummy_subject" translatable="false">dummy subject</string> <string name="chat_room_dummy_subject" translatable="false">dummy subject</string>
<string name="chat_room_toast_choose_for_sharing">Select a conversation or create a new one</string> <string name="chat_room_toast_choose_for_sharing">Select a conversation or create a new one</string>
<string name="chat_room_cant_open_file_no_app_found">Can\'t open file, no application available for this format.</string> <string name="chat_room_cant_open_file_no_app_found">Can\'t open file, no application available for this format.</string>
<string name="chat_room_file_not_found">File not found</string>
<string name="chat_room_creation_failed">Chat room creation failed</string> <string name="chat_room_creation_failed">Chat room creation failed</string>
<string name="chat_room_context_menu_group_info">Group info</string> <string name="chat_room_context_menu_group_info">Group info</string>
<string name="chat_room_context_menu_participants_devices">Conversation\'s devices</string> <string name="chat_room_context_menu_participants_devices">Conversation\'s devices</string>