Added setting to enable VFS + changes to properly display files
This commit is contained in:
parent
3f36e4cc74
commit
f1ad823364
22 changed files with 178 additions and 80 deletions
|
@ -227,6 +227,7 @@ dependencies {
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
|
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
implementation 'com.google.android:flexbox:2.0.0'
|
implementation 'com.google.android:flexbox:2.0.0'
|
||||||
|
@ -253,7 +254,6 @@ dependencies {
|
||||||
|
|
||||||
// Only enable leak canary prior to release
|
// Only enable leak canary prior to release
|
||||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||||
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,8 @@ class ChatMessageContentViewModel(
|
||||||
|
|
||||||
val fileName = MutableLiveData<String>()
|
val fileName = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val filePath = MutableLiveData<String>()
|
||||||
|
|
||||||
val fileSize = MutableLiveData<String>()
|
val fileSize = MutableLiveData<String>()
|
||||||
|
|
||||||
val downloadable = MutableLiveData<Boolean>()
|
val downloadable = MutableLiveData<Boolean>()
|
||||||
|
@ -88,6 +90,7 @@ class ChatMessageContentViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
filePath.value = ""
|
||||||
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
|
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
|
||||||
FileUtils.getNameFromFilePath(content.filePath!!)
|
FileUtils.getNameFromFilePath(content.filePath!!)
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,19 +99,20 @@ class ChatMessageContentViewModel(
|
||||||
fileSize.value = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
fileSize.value = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
||||||
|
|
||||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||||
val filePath = content.filePath ?: ""
|
val path = if (content.isFileEncrypted) content.plainFilePath else content.filePath ?: ""
|
||||||
downloadable.value = filePath.isEmpty()
|
downloadable.value = content.filePath.orEmpty().isEmpty()
|
||||||
|
|
||||||
if (filePath.isNotEmpty()) {
|
if (path.isNotEmpty()) {
|
||||||
Log.i("[Content] Found displayable content: $filePath")
|
Log.i("[Content] Found displayable content: $path")
|
||||||
isImage.value = FileUtils.isExtensionImage(filePath)
|
filePath.value = path
|
||||||
isVideo.value = FileUtils.isExtensionVideo(filePath)
|
isImage.value = FileUtils.isExtensionImage(path)
|
||||||
isAudio.value = FileUtils.isExtensionAudio(filePath)
|
isVideo.value = FileUtils.isExtensionVideo(path)
|
||||||
|
isAudio.value = FileUtils.isExtensionAudio(path)
|
||||||
|
|
||||||
if (isVideo.value == true) {
|
if (isVideo.value == true) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
videoPreview.postValue(ImageUtils.getVideoPreview(filePath))
|
videoPreview.postValue(ImageUtils.getVideoPreview(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,6 +134,17 @@ class ChatMessageContentViewModel(
|
||||||
chatMessage.addListener(chatMessageListener)
|
chatMessage.addListener(chatMessageListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
val path = filePath.value.orEmpty()
|
||||||
|
if (content.isFileEncrypted && path.isNotEmpty()) {
|
||||||
|
Log.i("[Content] Deleting file used for preview: $path")
|
||||||
|
FileUtils.deleteFile(path)
|
||||||
|
filePath.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
fun download() {
|
fun download() {
|
||||||
val filePath = content.filePath
|
val filePath = content.filePath
|
||||||
if (content.isFileTransfer && (filePath == null || filePath.isEmpty())) {
|
if (content.isFileTransfer && (filePath == null || filePath.isEmpty())) {
|
||||||
|
|
|
@ -52,15 +52,13 @@ class AudioViewerFragment : SecureFragment<FileAudioViewerFragmentBinding>() {
|
||||||
|
|
||||||
val content = sharedViewModel.contentToOpen.value
|
val content = sharedViewModel.contentToOpen.value
|
||||||
content ?: return
|
content ?: return
|
||||||
val filePath = content.filePath
|
|
||||||
filePath ?: return
|
|
||||||
|
|
||||||
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
||||||
?.setContent(content)
|
?.setContent(content)
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
AudioFileViewModelFactory(filePath)
|
AudioFileViewModelFactory(content)
|
||||||
)[AudioFileViewModel::class.java]
|
)[AudioFileViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
|
|
@ -47,15 +47,13 @@ class ImageViewerFragment : SecureFragment<FileImageViewerFragmentBinding>() {
|
||||||
|
|
||||||
val content = sharedViewModel.contentToOpen.value
|
val content = sharedViewModel.contentToOpen.value
|
||||||
content ?: return
|
content ?: return
|
||||||
val filePath = content.filePath
|
|
||||||
filePath ?: return
|
|
||||||
|
|
||||||
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
||||||
?.setContent(content)
|
?.setContent(content)
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
ImageFileViewModelFactory(filePath)
|
ImageFileViewModelFactory(content)
|
||||||
)[ImageFileViewModel::class.java]
|
)[ImageFileViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
|
|
@ -47,15 +47,13 @@ class PdfViewerFragment : SecureFragment<FilePdfViewerFragmentBinding>() {
|
||||||
|
|
||||||
val content = sharedViewModel.contentToOpen.value
|
val content = sharedViewModel.contentToOpen.value
|
||||||
content ?: return
|
content ?: return
|
||||||
val filePath = content.filePath
|
|
||||||
filePath ?: return
|
|
||||||
|
|
||||||
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
||||||
?.setContent(content)
|
?.setContent(content)
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
PdfFileViewModelFactory(filePath)
|
PdfFileViewModelFactory(content)
|
||||||
)[PdfFileViewModel::class.java]
|
)[PdfFileViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
|
|
@ -45,15 +45,13 @@ class TextViewerFragment : SecureFragment<FileTextViewerFragmentBinding>() {
|
||||||
|
|
||||||
val content = sharedViewModel.contentToOpen.value
|
val content = sharedViewModel.contentToOpen.value
|
||||||
content ?: return
|
content ?: return
|
||||||
val filePath = content.filePath
|
|
||||||
filePath ?: return
|
|
||||||
|
|
||||||
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
||||||
?.setContent(content)
|
?.setContent(content)
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
TextFileViewModelFactory(filePath)
|
TextFileViewModelFactory(content)
|
||||||
)[TextFileViewModel::class.java]
|
)[TextFileViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
val filePath = content?.plainFilePath.orEmpty()
|
val filePath = content?.plainFilePath.orEmpty()
|
||||||
plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath
|
plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath
|
||||||
Log.i("[File Viewer] Plain file path is: $filePath")
|
Log.i("[File Viewer] Plain file path is: $plainFilePath")
|
||||||
if (plainFilePath.isNotEmpty()) {
|
if (plainFilePath.isNotEmpty()) {
|
||||||
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
|
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
|
||||||
(requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
|
(requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
|
||||||
|
|
|
@ -51,15 +51,13 @@ class VideoViewerFragment : SecureFragment<FileVideoViewerFragmentBinding>() {
|
||||||
|
|
||||||
val content = sharedViewModel.contentToOpen.value
|
val content = sharedViewModel.contentToOpen.value
|
||||||
content ?: return
|
content ?: return
|
||||||
val filePath = content.filePath
|
|
||||||
filePath ?: return
|
|
||||||
|
|
||||||
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
||||||
?.setContent(content)
|
?.setContent(content)
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
VideoFileViewModelFactory(filePath)
|
VideoFileViewModelFactory(content)
|
||||||
)[VideoFileViewModel::class.java]
|
)[VideoFileViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
|
|
@ -25,17 +25,18 @@ import android.widget.MediaController
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import java.lang.IllegalStateException
|
import java.lang.IllegalStateException
|
||||||
|
import org.linphone.core.Content
|
||||||
|
|
||||||
class AudioFileViewModelFactory(private val filePath: String) :
|
class AudioFileViewModelFactory(private val content: Content) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return AudioFileViewModel(filePath) as T
|
return AudioFileViewModel(content) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioFileViewModel(val filePath: String) : ViewModel(), MediaController.MediaPlayerControl {
|
class AudioFileViewModel(content: Content) : FileViewerViewModel(content), MediaController.MediaPlayerControl {
|
||||||
val mediaPlayer = MediaPlayer()
|
val mediaPlayer = MediaPlayer()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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 org.linphone.core.Content
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.FileUtils
|
||||||
|
|
||||||
|
open class FileViewerViewModel(val content: Content) : ViewModel() {
|
||||||
|
val filePath: String
|
||||||
|
private val deleteAfterUse: Boolean = content.isFileEncrypted
|
||||||
|
|
||||||
|
init {
|
||||||
|
filePath = if (deleteAfterUse) content.plainFilePath else content.filePath.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
if (deleteAfterUse) {
|
||||||
|
Log.i("[File Viewer] Deleting temporary plain file: $filePath")
|
||||||
|
FileUtils.deleteFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,14 +21,15 @@ package org.linphone.activities.main.files.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.core.Content
|
||||||
|
|
||||||
class ImageFileViewModelFactory(private val filePath: String) :
|
class ImageFileViewModelFactory(private val content: Content) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return ImageFileViewModel(filePath) as T
|
return ImageFileViewModel(content) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImageFileViewModel(val filePath: String) : ViewModel()
|
class ImageFileViewModel(content: Content) : FileViewerViewModel(content)
|
||||||
|
|
|
@ -28,18 +28,19 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.core.Content
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
class PdfFileViewModelFactory(private val filePath: String) :
|
class PdfFileViewModelFactory(private val content: Content) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return PdfFileViewModel(filePath) as T
|
return PdfFileViewModel(content) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PdfFileViewModel(filePath: String) : ViewModel() {
|
class PdfFileViewModel(content: Content) : FileViewerViewModel(content) {
|
||||||
val operationInProgress = MutableLiveData<Boolean>()
|
val operationInProgress = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
private val pdfRenderer: PdfRenderer
|
private val pdfRenderer: PdfRenderer
|
||||||
|
|
|
@ -29,18 +29,19 @@ import java.lang.StringBuilder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.linphone.core.Content
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
class TextFileViewModelFactory(private val filePath: String) :
|
class TextFileViewModelFactory(private val content: Content) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return TextFileViewModel(filePath) as T
|
return TextFileViewModel(content) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextFileViewModel(filePath: String) : ViewModel() {
|
class TextFileViewModel(content: Content) : FileViewerViewModel(content) {
|
||||||
val operationInProgress = MutableLiveData<Boolean>()
|
val operationInProgress = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val text = MutableLiveData<String>()
|
val text = MutableLiveData<String>()
|
||||||
|
@ -48,18 +49,18 @@ class TextFileViewModel(filePath: String) : ViewModel() {
|
||||||
init {
|
init {
|
||||||
operationInProgress.value = false
|
operationInProgress.value = false
|
||||||
|
|
||||||
openFile(filePath)
|
openFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openFile(filePath: String) {
|
private fun openFile() {
|
||||||
|
operationInProgress.value = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
operationInProgress.postValue(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val br = BufferedReader(FileReader(filePath))
|
val br = BufferedReader(FileReader(filePath))
|
||||||
var line: String?
|
var line: String?
|
||||||
var textBuilder = StringBuilder()
|
val textBuilder = StringBuilder()
|
||||||
while (br.readLine().also { line = it } != null) {
|
while (br.readLine().also { line = it } != null) {
|
||||||
textBuilder.append(line)
|
textBuilder.append(line)
|
||||||
textBuilder.append('\n')
|
textBuilder.append('\n')
|
||||||
|
|
|
@ -21,14 +21,15 @@ package org.linphone.activities.main.files.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.core.Content
|
||||||
|
|
||||||
class VideoFileViewModelFactory(private val filePath: String) :
|
class VideoFileViewModelFactory(private val content: Content) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return VideoFileViewModel(filePath) as T
|
return VideoFileViewModel(content) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoFileViewModel(val filePath: String) : ViewModel()
|
class VideoFileViewModel(content: Content) : FileViewerViewModel(content)
|
||||||
|
|
|
@ -38,6 +38,13 @@ class AdvancedSettingsViewModel : GenericSettingsViewModel() {
|
||||||
}
|
}
|
||||||
val debugMode = MutableLiveData<Boolean>()
|
val debugMode = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val logsServerUrlListener = object : SettingListenerStub() {
|
||||||
|
override fun onTextValueChanged(newValue: String) {
|
||||||
|
core.logCollectionUploadServerUrl = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val logsServerUrl = MutableLiveData<String>()
|
||||||
|
|
||||||
val backgroundModeListener = object : SettingListenerStub() {
|
val backgroundModeListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
prefs.keepServiceAlive = newValue
|
prefs.keepServiceAlive = newValue
|
||||||
|
@ -97,12 +104,13 @@ class AdvancedSettingsViewModel : GenericSettingsViewModel() {
|
||||||
}
|
}
|
||||||
val remoteProvisioningUrl = MutableLiveData<String>()
|
val remoteProvisioningUrl = MutableLiveData<String>()
|
||||||
|
|
||||||
val logsServerUrlListener = object : SettingListenerStub() {
|
val vfsListener = object : SettingListenerStub() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.logCollectionUploadServerUrl = newValue
|
prefs.vfsEnabled = newValue
|
||||||
|
if (newValue) coreContext.setupVFS()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val logsServerUrl = MutableLiveData<String>()
|
val vfs = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val goToBatterySettingsListener = object : SettingListenerStub() {
|
val goToBatterySettingsListener = object : SettingListenerStub() {
|
||||||
override fun onClicked() {
|
override fun onClicked() {
|
||||||
|
@ -129,6 +137,7 @@ class AdvancedSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
debugMode.value = prefs.debugLogs
|
debugMode.value = prefs.debugLogs
|
||||||
|
logsServerUrl.value = core.logCollectionUploadServerUrl
|
||||||
backgroundMode.value = prefs.keepServiceAlive
|
backgroundMode.value = prefs.keepServiceAlive
|
||||||
autoStart.value = prefs.autoStart
|
autoStart.value = prefs.autoStart
|
||||||
|
|
||||||
|
@ -142,7 +151,7 @@ class AdvancedSettingsViewModel : GenericSettingsViewModel() {
|
||||||
animations.value = prefs.enableAnimations
|
animations.value = prefs.enableAnimations
|
||||||
deviceName.value = prefs.deviceName
|
deviceName.value = prefs.deviceName
|
||||||
remoteProvisioningUrl.value = core.provisioningUri
|
remoteProvisioningUrl.value = core.provisioningUri
|
||||||
logsServerUrl.value = core.logCollectionUploadServerUrl
|
vfs.value = prefs.vfsEnabled
|
||||||
|
|
||||||
batterySettingsVisibility.value = Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)
|
batterySettingsVisibility.value = Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)
|
||||||
}
|
}
|
||||||
|
|
|
@ -292,7 +292,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (corePreferences.vfsEnabled) {
|
if (corePreferences.vfsEnabled) {
|
||||||
setupVFS(context)
|
setupVFS()
|
||||||
}
|
}
|
||||||
core = Factory.instance().createCoreWithConfig(coreConfig, context)
|
core = Factory.instance().createCoreWithConfig(coreConfig, context)
|
||||||
|
|
||||||
|
@ -645,16 +645,19 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
/* VFS */
|
/* VFS */
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TRANSFORMATION = "AES/GCM/NoPadding"
|
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||||
private val ANDROID_KEY_STORE = "AndroidKeyStore"
|
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||||
private val ALIAS = "vfs"
|
private const val ALIAS = "vfs"
|
||||||
private val LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256 = 2
|
private const val LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256 = 2
|
||||||
|
private const val VFS_FILE = "vfs.prefs"
|
||||||
|
private const val VFS_IV = "vfsiv"
|
||||||
|
private const val VFS_KEY = "vfskey"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(java.lang.Exception::class)
|
@Throws(java.lang.Exception::class)
|
||||||
private fun generateSecretKey() {
|
private fun generateSecretKey() {
|
||||||
val keyGenerator: KeyGenerator
|
val keyGenerator =
|
||||||
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
|
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
|
||||||
keyGenerator.init(
|
keyGenerator.init(
|
||||||
KeyGenParameterSpec.Builder(
|
KeyGenParameterSpec.Builder(
|
||||||
ALIAS,
|
ALIAS,
|
||||||
|
@ -701,7 +704,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(java.lang.Exception::class)
|
@Throws(java.lang.Exception::class)
|
||||||
fun encryptToken(string_to_encrypt: String): Pair<String?, String?>? {
|
fun encryptToken(string_to_encrypt: String): Pair<String?, String?> {
|
||||||
val encryptedData = encryptData(string_to_encrypt)
|
val encryptedData = encryptData(string_to_encrypt)
|
||||||
return Pair<String?, String?>(
|
return Pair<String?, String?>(
|
||||||
Base64.encodeToString(encryptedData.first, Base64.DEFAULT),
|
Base64.encodeToString(encryptedData.first, Base64.DEFAULT),
|
||||||
|
@ -710,7 +713,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(java.lang.Exception::class)
|
@Throws(java.lang.Exception::class)
|
||||||
fun sha512(input: String): String? {
|
fun sha512(input: String): String {
|
||||||
val md = MessageDigest.getInstance("SHA-512")
|
val md = MessageDigest.getInstance("SHA-512")
|
||||||
val messageDigest = md.digest(input.toByteArray())
|
val messageDigest = md.digest(input.toByteArray())
|
||||||
val no = BigInteger(1, messageDigest)
|
val no = BigInteger(1, messageDigest)
|
||||||
|
@ -722,41 +725,48 @@ class CoreContext(val context: Context, coreConfig: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(java.lang.Exception::class)
|
@Throws(java.lang.Exception::class)
|
||||||
fun getVfsKey(sharedPreferences: SharedPreferences): String? {
|
fun getVfsKey(sharedPreferences: SharedPreferences): String {
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
|
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
|
||||||
keyStore.load(null)
|
keyStore.load(null)
|
||||||
return decryptData(
|
return decryptData(
|
||||||
sharedPreferences.getString("vfskey", null),
|
sharedPreferences.getString(VFS_KEY, null),
|
||||||
Base64.decode(sharedPreferences.getString("vfsiv", null), Base64.DEFAULT)
|
Base64.decode(sharedPreferences.getString(VFS_IV, null), Base64.DEFAULT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupVFS(c: Context) {
|
fun setupVFS() {
|
||||||
try {
|
try {
|
||||||
|
Log.i("[Context] Enabling VFS")
|
||||||
|
|
||||||
val masterKey: MasterKey = Builder(
|
val masterKey: MasterKey = Builder(
|
||||||
c,
|
context,
|
||||||
MasterKey.DEFAULT_MASTER_KEY_ALIAS
|
MasterKey.DEFAULT_MASTER_KEY_ALIAS
|
||||||
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||||
val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
|
val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
|
||||||
c, "vfs.prefs", masterKey,
|
context, VFS_FILE, masterKey,
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
)
|
)
|
||||||
if (sharedPreferences.getString("vfsiv", null) == null) {
|
|
||||||
|
if (sharedPreferences.getString(VFS_IV, null) == null) {
|
||||||
generateSecretKey()
|
generateSecretKey()
|
||||||
generateToken()?.let { encryptToken(it) }?.let { data ->
|
generateToken()?.let { encryptToken(it) }?.let { data ->
|
||||||
sharedPreferences.edit().putString("vfsiv", data.first)
|
sharedPreferences
|
||||||
.putString("vfskey", data.second).commit()
|
.edit()
|
||||||
|
.putString(VFS_IV, data.first)
|
||||||
|
.putString(VFS_KEY, data.second)
|
||||||
|
.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Factory.instance().setVfsEncryption(
|
Factory.instance().setVfsEncryption(
|
||||||
LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256,
|
LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256,
|
||||||
Arrays.copyOfRange(getVfsKey(sharedPreferences)?.toByteArray(), 0, 32),
|
getVfsKey(sharedPreferences).toByteArray().copyOfRange(0, 32),
|
||||||
32
|
32
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Log.i("[Context] VFS enabled")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
Log.f("[Context] Unable to setup VFS encryption: $e")
|
||||||
throw RuntimeException("Unable to setup VFS encryption")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ class CorePreferences constructor(private val context: Context) {
|
||||||
get() = config.getBool("app", "vfs", false)
|
get() = config.getBool("app", "vfs", false)
|
||||||
set(value) {
|
set(value) {
|
||||||
if (!value && config.getBool("app", "vfs", false)) {
|
if (!value && config.getBool("app", "vfs", false)) {
|
||||||
Log.w("[VFS] It is not possible to deactivate VFS after it has been activated")
|
Log.w("[VFS] It is not possible to disable VFS once it has been enabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.setBool("app", "vfs", value)
|
config.setBool("app", "vfs", value)
|
||||||
|
@ -141,7 +141,7 @@ class CorePreferences constructor(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var useInAppFileViewerForNonEncryptedFiles: Boolean
|
var useInAppFileViewerForNonEncryptedFiles: Boolean
|
||||||
get() = config.getBool("app", "use_in_app_file_viewer_for_non_encrypted_files", true)
|
get() = config.getBool("app", "use_in_app_file_viewer_for_non_encrypted_files", false)
|
||||||
set(value) {
|
set(value) {
|
||||||
config.setBool("app", "use_in_app_file_viewer_for_non_encrypted_files", value)
|
config.setBool("app", "use_in_app_file_viewer_for_non_encrypted_files", value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.constraintlayout.widget.Guideline
|
import androidx.constraintlayout.widget.Guideline
|
||||||
import androidx.databinding.*
|
import androidx.databinding.*
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
@ -320,7 +321,15 @@ fun loadAvatarWithGlideFallback(imageView: ImageView, path: String?) {
|
||||||
@BindingAdapter("glidePath")
|
@BindingAdapter("glidePath")
|
||||||
fun loadImageWithGlide(imageView: ImageView, path: String) {
|
fun loadImageWithGlide(imageView: ImageView, path: String) {
|
||||||
if (path.isNotEmpty() && FileUtils.isExtensionImage(path)) {
|
if (path.isNotEmpty() && FileUtils.isExtensionImage(path)) {
|
||||||
GlideApp.with(imageView).load(path).into(imageView)
|
if (corePreferences.vfsEnabled) {
|
||||||
|
GlideApp.with(imageView)
|
||||||
|
.load(path)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.into(imageView)
|
||||||
|
} else {
|
||||||
|
GlideApp.with(imageView).load(path).into(imageView)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w("[Data Binding] [Glide] Can't load $path")
|
Log.w("[Data Binding] [Glide] Can't load $path")
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,8 @@ import org.linphone.core.tools.Log
|
||||||
|
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val VFS_PLAIN_FILE_EXTENSION = ".bctbx_evfs_plain"
|
||||||
|
|
||||||
fun getNameFromFilePath(filePath: String): String {
|
fun getNameFromFilePath(filePath: String): String {
|
||||||
var name = filePath
|
var name = filePath
|
||||||
val i = filePath.lastIndexOf('/')
|
val i = filePath.lastIndexOf('/')
|
||||||
|
@ -53,13 +55,18 @@ class FileUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionFromFileName(fileName: String): String {
|
fun getExtensionFromFileName(fileName: String): String {
|
||||||
var extension = MimeTypeMap.getFileExtensionFromUrl(fileName)
|
val realFileName = if (fileName.endsWith(VFS_PLAIN_FILE_EXTENSION)) {
|
||||||
if (extension == null || extension.isEmpty()) {
|
fileName.substring(0, fileName.length - VFS_PLAIN_FILE_EXTENSION.length)
|
||||||
val i = fileName.lastIndexOf('.')
|
} else fileName
|
||||||
|
|
||||||
|
var extension = MimeTypeMap.getFileExtensionFromUrl(realFileName)
|
||||||
|
if (extension.isNullOrEmpty()) {
|
||||||
|
val i = realFileName.lastIndexOf('.')
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
extension = fileName.substring(i + 1)
|
extension = realFileName.substring(i + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extension
|
return extension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:maxHeight="@{data.alone ? @dimen/chat_message_bubble_image_height_big : @dimen/chat_message_bubble_image_height_small}"
|
android:maxHeight="@{data.alone ? @dimen/chat_message_bubble_image_height_big : @dimen/chat_message_bubble_image_height_small}"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
app:glidePath="@{data.content.filePath}"
|
app:glidePath="@{data.filePath}"
|
||||||
android:visibility="@{data.image ? View.VISIBLE : View.GONE}"
|
android:visibility="@{data.image ? View.VISIBLE : View.GONE}"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,14 @@
|
||||||
linphone:defaultValue="@{viewModel.remoteProvisioningUrl}"
|
linphone:defaultValue="@{viewModel.remoteProvisioningUrl}"
|
||||||
linphone:inputType="@{InputType.TYPE_TEXT_VARIATION_URI}"/>
|
linphone:inputType="@{InputType.TYPE_TEXT_VARIATION_URI}"/>
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/settings_widget_switch"
|
||||||
|
linphone:title="@{@string/advanced_settings_vfs_title}"
|
||||||
|
linphone:subtitle="@{@string/advanced_settings_vfs_summary}"
|
||||||
|
linphone:listener="@{viewModel.vfsListener}"
|
||||||
|
linphone:checked="@={viewModel.vfs}"
|
||||||
|
linphone:enabled="@{!viewModel.vfs}"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/settings_category_font"
|
style="@style/settings_category_font"
|
||||||
android:id="@+id/pref_video_codecs_header"
|
android:id="@+id/pref_video_codecs_header"
|
||||||
|
|
|
@ -485,6 +485,8 @@
|
||||||
<string name="advanced_settings_go_to_battery_optimization_settings">Battery optimization settings</string>
|
<string name="advanced_settings_go_to_battery_optimization_settings">Battery optimization settings</string>
|
||||||
<string name="advanced_settings_go_to_power_manager_settings">Power manager settings</string>
|
<string name="advanced_settings_go_to_power_manager_settings">Power manager settings</string>
|
||||||
<string name="advanced_settings_go_to_android_app_settings">Android app settings</string>
|
<string name="advanced_settings_go_to_android_app_settings">Android app settings</string>
|
||||||
|
<string name="advanced_settings_vfs_title">Encrypt everything</string>
|
||||||
|
<string name="advanced_settings_vfs_summary">Once enabled it can\'t be disabled!</string>
|
||||||
|
|
||||||
<!-- Tunnel settings -->
|
<!-- Tunnel settings -->
|
||||||
<string name="tunnel_settings_hostname_url_title">Hostname</string>
|
<string name="tunnel_settings_hostname_url_title">Hostname</string>
|
||||||
|
|
Loading…
Reference in a new issue