Added setting to enable VFS + changes to properly display files

This commit is contained in:
Sylvain Berfini 2021-04-06 13:45:31 +02:00
parent 3f36e4cc74
commit f1ad823364
22 changed files with 178 additions and 80 deletions

View file

@ -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"
} }

View file

@ -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())) {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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)
} }

View file

@ -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")
} }
} }
} }

View file

@ -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)
} }

View file

@ -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")
} }

View file

@ -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
} }

View file

@ -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" />

View file

@ -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"

View file

@ -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>