Implement workaround to open office files in MS apps in write mode

Fixes #150
This commit is contained in:
Julian Raufelder 2022-02-28 22:09:34 +01:00
parent e1a5ff7007
commit 7ce2e8e27c
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
12 changed files with 296 additions and 113 deletions

View File

@ -7,6 +7,7 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import org.cryptomator.data.cloud.crypto.Cryptors import org.cryptomator.data.cloud.crypto.Cryptors
@ -67,6 +68,11 @@ class CryptomatorApp : MultiDexApplication(), HasComponent<ApplicationComponent>
AppCompatDelegate.setDefaultNightMode(SharedPreferencesHandler(applicationContext()).screenStyleMode) AppCompatDelegate.setDefaultNightMode(SharedPreferencesHandler(applicationContext()).screenStyleMode)
cleanupCache() cleanupCache()
if (SharedPreferencesHandler(applicationContext()).microsoftWorkaround()) {
val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder()
StrictMode.setVmPolicy(builder.build())
}
RxJavaPlugins.setErrorHandler { e: Throwable? -> Timber.tag("CryptomatorApp").e(e, "BaseErrorHandler detected a problem") } RxJavaPlugins.setErrorHandler { e: Throwable? -> Timber.tag("CryptomatorApp").e(e, "BaseErrorHandler detected a problem") }
} }

View File

@ -6,6 +6,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.widget.Toast import android.widget.Toast
import androidx.core.net.toFile
import org.cryptomator.data.cloud.crypto.CryptoFolder import org.cryptomator.data.cloud.crypto.CryptoFolder
import org.cryptomator.domain.Cloud import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile import org.cryptomator.domain.CloudFile
@ -232,8 +233,8 @@ class BrowseFilesPresenter @Inject constructor( //
@Callback(dispatchResultOkOnly = false) @Callback(dispatchResultOkOnly = false)
fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) { fun getCloudListAfterAuthentication(result: ActivityResult, cloudFolderModel: CloudFolderModel) {
if(result.isResultOk) { if (result.isResultOk) {
val cloudModel = result.getSingleResult(CloudModel::class.java) // FIXME update other vaults using this cloud as well val cloudModel = result.getSingleResult(CloudModel::class.java)
val cloudNode = cloudFolderModel.toCloudNode() val cloudNode = cloudFolderModel.toCloudNode()
if (cloudNode is CryptoFolder) { if (cloudNode is CryptoFolder) {
updatedDecryptedCloudFor(Vault.aCopyOf(cloudFolderModel.vault()!!.toVault()).withCloud(cloudModel.toCloud()).build(), cloudFolderModel) updatedDecryptedCloudFor(Vault.aCopyOf(cloudFolderModel.vault()!!.toVault()).withCloud(cloudModel.toCloud()).build(), cloudFolderModel)
@ -263,6 +264,7 @@ class BrowseFilesPresenter @Inject constructor( //
view?.updateActiveFolderDueToAuthenticationProblem(folder) view?.updateActiveFolderDueToAuthenticationProblem(folder)
getCloudList(folder) getCloudList(folder)
} }
override fun onFinished() { override fun onFinished() {
resumedAfterAuthentication = false resumedAfterAuthentication = false
} }
@ -529,14 +531,16 @@ class BrowseFilesPresenter @Inject constructor( //
private fun viewExternalFile(cloudFile: CloudFileModel) { private fun viewExternalFile(cloudFile: CloudFileModel) {
val viewFileIntent = Intent(Intent.ACTION_VIEW) val viewFileIntent = Intent(Intent.ACTION_VIEW)
fileUtil.contentUriFor(cloudFile).let { var openFileType = OpenFileType.DEFAULT
uriToOpenedFile = it uriToOpenedFile = if (useMicrosoftWorkaround(cloudFile)) {
openFileType = OpenFileType.MICROSOFT_WORKAROUND
Uri.fromFile(fileUtil.getLegacyFileForMicrosoftWorkaround(cloudFile))
} else {
fileUtil.contentUriFor(cloudFile)
}.also {
openedCloudFile = cloudFile openedCloudFile = cloudFile
openedCloudFileMd5 = calculateDigestFromUri(it) openedCloudFileMd5 = calculateDigestFromUri(it)
viewFileIntent.setDataAndType( // viewFileIntent.setDataAndType(uriToOpenedFile, mimeTypes.fromFilename(cloudFile.name)?.toString())
uriToOpenedFile, //
mimeTypes.fromFilename(cloudFile.name)?.toString()
)
viewFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) viewFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
if (sharedPreferencesHandler.keepUnlockedWhileEditing()) { if (sharedPreferencesHandler.keepUnlockedWhileEditing()) {
openWritableFileNotification = OpenWritableFileNotification(context(), it) openWritableFileNotification = OpenWritableFileNotification(context(), it)
@ -544,10 +548,119 @@ class BrowseFilesPresenter @Inject constructor( //
val cryptomatorApp = activity().application as CryptomatorApp val cryptomatorApp = activity().application as CryptomatorApp
cryptomatorApp.suspendLock() cryptomatorApp.suspendLock()
} }
activity().startActivityForResult(viewFileIntent, OPEN_FILE_FINISHED) requestActivityResult(ActivityResultCallbacks.openFileFinished(openFileType), viewFileIntent)
} }
} }
enum class OpenFileType {
DEFAULT, MICROSOFT_WORKAROUND
}
private fun useMicrosoftWorkaround(cloudFile: CloudFileModel): Boolean {
return sharedPreferencesHandler.microsoftWorkaround()
&& cloudFile.name.contains(".")
&& microsoftExtensions().contains(cloudFile.name.substring(cloudFile.name.lastIndexOf(".") + 1, cloudFile.name.length))
}
private fun microsoftExtensions(): Array<out String> {
return context().resources.getStringArray(R.array.microsoft_extensions);
}
@Callback(dispatchResultOkOnly = false)
fun openFileFinished(result: ActivityResult, openFileType: OpenFileType) {
try {
// necessary see https://community.cryptomator.org/t/android-tabelle-nach-upload-unlesbar/6550
Thread.sleep(500)
} catch (e: InterruptedException) {
Timber.tag("BrowseFilesPresenter").e(e, "Failed to sleep after resuming editing, necessary for google office apps")
}
if (sharedPreferencesHandler.keepUnlockedWhileEditing()) {
val cryptomatorApp = activity().application as CryptomatorApp
cryptomatorApp.unSuspendLock()
}
hideWritableNotification()
context().revokeUriPermission(uriToOpenedFile, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
uriToOpenedFile?.let {
try {
calculateDigestFromUri(it)?.let { hashAfterEdit ->
openedCloudFileMd5?.let { hashBeforeEdit ->
if (hashAfterEdit.contentEquals(hashBeforeEdit)) {
Timber.tag("BrowseFilesPresenter").i("Opened app finished, file not changed")
deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
} else {
uploadChangedFile(openFileType)
}
} ?: deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
}
} catch (e: FileNotFoundException) {
Timber.tag("BrowseFilesPresenter").e(e, "Failed to read back changes, file isn't present anymore")
Toast.makeText(context(), R.string.error_file_not_found_after_opening_using_3party, Toast.LENGTH_LONG).show()
}
}
}
private fun uploadChangedFile(openFileType: OpenFileType) {
view?.showUploadDialog(1)
openedCloudFile?.let { openedCloudFile ->
openedCloudFile.parent?.let { openedCloudFilesParent ->
uriToOpenedFile?.let { uriToOpenedFile ->
uploadFilesUseCase //
.withParent(openedCloudFilesParent.toCloudNode()) //
.andFiles(listOf(createUploadFile(openedCloudFile.name, uriToOpenedFile, true))) //
.run(object : DefaultProgressAwareResultHandler<List<CloudFile>, UploadState>() {
override fun onProgress(progress: Progress<UploadState>) {
view?.showProgress(progressModelMapper.toModel(progress))
}
override fun onSuccess(files: List<CloudFile>) {
files.forEach { file ->
view?.addOrUpdateCloudNode(cloudFileModelMapper.toModel(file))
}
deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
onFileUploadCompleted()
}
override fun onError(e: Throwable) {
onFileUploadError()
if (ExceptionUtil.contains(e, CloudNodeAlreadyExistsException::class.java)) {
ExceptionUtil.extract(e, CloudNodeAlreadyExistsException::class.java).get().message?.let {
onCloudNodeAlreadyExists(it)
} ?: super.onError(e)
} else {
super.onError(e)
}
}
})
}
}
}
}
private fun deleteFileIfMicrosoftWorkaround(openFileType: OpenFileType, uriToOpenedFile: Uri?) {
if (openFileType == OpenFileType.MICROSOFT_WORKAROUND) {
uriToOpenedFile?.toFile()?.delete()
}
}
private fun hideWritableNotification() {
// openWritableFileNotification can not be made serializable because of this, can be null after Activity resumed
openWritableFileNotification?.hide() ?: OpenWritableFileNotification(context(), Uri.EMPTY).hide()
}
@Throws(FileNotFoundException::class)
private fun calculateDigestFromUri(uri: Uri): ByteArray? {
val digest = MessageDigest.getInstance("MD5")
DigestInputStream(context().contentResolver.openInputStream(uri), digest).use { dis ->
val buffer = ByteArray(4096)
// Read all bytes:
while (dis.read(buffer) > -1) {
}
}
return digest.digest()
}
private val previewCloudFileNodes: ArrayList<CloudFileModel> private val previewCloudFileNodes: ArrayList<CloudFileModel>
get() { get() {
val previewCloudFiles = ArrayList<CloudFileModel>() val previewCloudFiles = ArrayList<CloudFileModel>()
@ -1154,7 +1267,7 @@ class BrowseFilesPresenter @Inject constructor( //
} }
fun onFolderReloadContent(folder: CloudFolderModel) { fun onFolderReloadContent(folder: CloudFolderModel) {
if(!resumedAfterAuthentication) { if (!resumedAfterAuthentication) {
getCloudList(folder) getCloudList(folder)
} }
} }
@ -1174,92 +1287,6 @@ class BrowseFilesPresenter @Inject constructor( //
activity().invalidateOptionsMenu() activity().invalidateOptionsMenu()
} }
fun openFileFinished() {
try {
// necessary see https://community.cryptomator.org/t/android-tabelle-nach-upload-unlesbar/6550
Thread.sleep(500)
} catch (e: InterruptedException) {
Timber.tag("BrowseFilesPresenter").e(e, "Failed to sleep after resuming editing, necessary for google office apps")
}
if (sharedPreferencesHandler.keepUnlockedWhileEditing()) {
val cryptomatorApp = activity().application as CryptomatorApp
cryptomatorApp.unSuspendLock()
}
hideWritableNotification()
context().revokeUriPermission(uriToOpenedFile, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
uriToOpenedFile?.let {
try {
calculateDigestFromUri(it)?.let { hashAfterEdit ->
openedCloudFileMd5?.let { hashBeforeEdit ->
if (hashAfterEdit.contentEquals(hashBeforeEdit)) {
Timber.tag("BrowseFilesPresenter").i("Opened app finished, file not changed")
} else {
uploadChangedFile()
}
}
}
} catch (e: FileNotFoundException) {
Timber.tag("BrowseFilesPresenter").e(e, "Failed to read back changes, file isn't present anymore")
Toast.makeText(context(), R.string.error_file_not_found_after_opening_using_3party, Toast.LENGTH_LONG).show()
}
}
}
private fun uploadChangedFile() {
view?.showUploadDialog(1)
openedCloudFile?.let { openedCloudFile ->
openedCloudFile.parent?.let { openedCloudFilesParent ->
uriToOpenedFile?.let { uriToOpenedFile ->
uploadFilesUseCase //
.withParent(openedCloudFilesParent.toCloudNode()) //
.andFiles(listOf(createUploadFile(openedCloudFile.name, uriToOpenedFile, true))) //
.run(object : DefaultProgressAwareResultHandler<List<CloudFile>, UploadState>() {
override fun onProgress(progress: Progress<UploadState>) {
view?.showProgress(progressModelMapper.toModel(progress))
}
override fun onSuccess(files: List<CloudFile>) {
files.forEach { file ->
view?.addOrUpdateCloudNode(cloudFileModelMapper.toModel(file))
}
onFileUploadCompleted()
}
override fun onError(e: Throwable) {
onFileUploadError()
if (ExceptionUtil.contains(e, CloudNodeAlreadyExistsException::class.java)) {
ExceptionUtil.extract(e, CloudNodeAlreadyExistsException::class.java).get().message?.let {
onCloudNodeAlreadyExists(it)
} ?: super.onError(e)
} else {
super.onError(e)
}
}
})
}
}
}
}
private fun hideWritableNotification() {
// openWritableFileNotification can not be made serializable because of this, can be null after Activity resumed
openWritableFileNotification?.hide() ?: OpenWritableFileNotification(context(), Uri.EMPTY).hide()
}
@Throws(FileNotFoundException::class)
private fun calculateDigestFromUri(uri: Uri): ByteArray? {
val digest = MessageDigest.getInstance("MD5")
DigestInputStream(context().contentResolver.openInputStream(uri), digest).use { dis ->
val buffer = ByteArray(8192)
// Read all bytes:
while (dis.read(buffer) > -1) {
}
}
return digest.digest()
}
interface ExportOperation : Serializable { interface ExportOperation : Serializable {
fun export(presenter: BrowseFilesPresenter, downloadFiles: List<DownloadFile>) fun export(presenter: BrowseFilesPresenter, downloadFiles: List<DownloadFile>)

View File

@ -32,7 +32,6 @@ import org.cryptomator.presentation.model.comparator.CloudNodeModelNameZACompara
import org.cryptomator.presentation.model.comparator.CloudNodeModelSizeBiggestFirstComparator import org.cryptomator.presentation.model.comparator.CloudNodeModelSizeBiggestFirstComparator
import org.cryptomator.presentation.model.comparator.CloudNodeModelSizeSmallestFirstComparator import org.cryptomator.presentation.model.comparator.CloudNodeModelSizeSmallestFirstComparator
import org.cryptomator.presentation.presenter.BrowseFilesPresenter import org.cryptomator.presentation.presenter.BrowseFilesPresenter
import org.cryptomator.presentation.presenter.BrowseFilesPresenter.Companion.OPEN_FILE_FINISHED
import org.cryptomator.presentation.service.CryptorsService import org.cryptomator.presentation.service.CryptorsService
import org.cryptomator.presentation.ui.activity.view.BrowseFilesView import org.cryptomator.presentation.ui.activity.view.BrowseFilesView
import org.cryptomator.presentation.ui.bottomsheet.FileSettingsBottomSheet import org.cryptomator.presentation.ui.bottomsheet.FileSettingsBottomSheet
@ -249,14 +248,6 @@ class BrowseFilesActivity : BaseActivity(), //
else -> super.onMenuItemSelected(itemId) else -> super.onMenuItemSelected(itemId)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
if (requestCode == OPEN_FILE_FINISHED) {
browseFilesPresenter.openFileFinished()
}
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean { override fun onPrepareOptionsMenu(menu: Menu): Boolean {
if (isNavigationMode(SELECT_ITEMS)) { if (isNavigationMode(SELECT_ITEMS)) {
menu.findItem(R.id.action_delete_items).isEnabled = enableGeneralSelectionActions menu.findItem(R.id.action_delete_items).isEnabled = enableGeneralSelectionActions

View File

@ -11,6 +11,7 @@ import org.cryptomator.presentation.ui.activity.view.SettingsView
import org.cryptomator.presentation.ui.dialog.DebugModeDisclaimerDialog import org.cryptomator.presentation.ui.dialog.DebugModeDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.DisableAppWhenObscuredDisclaimerDialog import org.cryptomator.presentation.ui.dialog.DisableAppWhenObscuredDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.DisableSecureScreenDisclaimerDialog import org.cryptomator.presentation.ui.dialog.DisableSecureScreenDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.MicrosoftWorkaroundDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog
import org.cryptomator.presentation.ui.dialog.UpdateAppDialog import org.cryptomator.presentation.ui.dialog.UpdateAppDialog
import org.cryptomator.presentation.ui.fragment.SettingsFragment import org.cryptomator.presentation.ui.fragment.SettingsFragment
@ -23,8 +24,9 @@ class SettingsActivity : BaseActivity(),
DebugModeDisclaimerDialog.Callback, DebugModeDisclaimerDialog.Callback,
DisableAppWhenObscuredDisclaimerDialog.Callback, DisableAppWhenObscuredDisclaimerDialog.Callback,
DisableSecureScreenDisclaimerDialog.Callback, DisableSecureScreenDisclaimerDialog.Callback,
UpdateAppAvailableDialog.Callback, // UpdateAppAvailableDialog.Callback,
UpdateAppDialog.Callback { UpdateAppDialog.Callback,
MicrosoftWorkaroundDisclaimerDialog.Callback {
@Inject @Inject
lateinit var presenter: SettingsPresenter lateinit var presenter: SettingsPresenter
@ -101,4 +103,12 @@ class SettingsActivity : BaseActivity(),
override fun onUpdateAppDialogLoaded() { override fun onUpdateAppDialogLoaded() {
showProgress(ProgressModel.GENERIC) showProgress(ProgressModel.GENERIC)
} }
override fun onMicrosoftDisclaimerAccepted() {
presenter.onDebugModeChanged(accepted())
}
override fun onMicrosoftDisclaimerRejected() {
settingsFragment().deactivateMicrosoftWorkaround()
}
} }

View File

@ -0,0 +1,41 @@
package org.cryptomator.presentation.ui.dialog
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import org.cryptomator.generator.Dialog
import org.cryptomator.presentation.R
@Dialog(R.layout.dialog_microsoft_workaround_disclaimer)
class MicrosoftWorkaroundDisclaimerDialog : BaseDialog<MicrosoftWorkaroundDisclaimerDialog.Callback>() {
interface Callback {
fun onMicrosoftDisclaimerAccepted()
fun onMicrosoftDisclaimerRejected()
}
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
builder //
.setTitle(R.string.dialog_microsoft_workaround_disclaimer_title) //
.setPositiveButton(getString(R.string.dialog_microsoft_workaround_positive_button)) { _: DialogInterface, _: Int -> callback?.onMicrosoftDisclaimerAccepted() } //
.setNegativeButton(getString(R.string.dialog_microsoft_workaround_negative_button)) { _: DialogInterface, _: Int -> callback?.onMicrosoftDisclaimerRejected() }
return builder.create()
}
public override fun setupView() {
// empty
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
callback?.onMicrosoftDisclaimerRejected()
}
companion object {
fun newInstance(): DialogFragment {
return MicrosoftWorkaroundDisclaimerDialog()
}
}
}

View File

@ -19,6 +19,7 @@ import org.cryptomator.presentation.ui.activity.SettingsActivity
import org.cryptomator.presentation.ui.dialog.DebugModeDisclaimerDialog import org.cryptomator.presentation.ui.dialog.DebugModeDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.DisableAppWhenObscuredDisclaimerDialog import org.cryptomator.presentation.ui.dialog.DisableAppWhenObscuredDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.DisableSecureScreenDisclaimerDialog import org.cryptomator.presentation.ui.dialog.DisableSecureScreenDisclaimerDialog
import org.cryptomator.presentation.ui.dialog.MicrosoftWorkaroundDisclaimerDialog
import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.file.LruFileCacheUtil import org.cryptomator.util.file.LruFileCacheUtil
import java.lang.Boolean.FALSE import java.lang.Boolean.FALSE
@ -95,6 +96,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
true true
} }
private val microsoftWorkaroundChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
onMicrosoftWorkaroundChanged(TRUE == newValue)
true
}
private fun activity(): SettingsActivity = this.activity as SettingsActivity private fun activity(): SettingsActivity = this.activity as SettingsActivity
private fun isBiometricAuthenticationNotAvailableRemovePreference() { private fun isBiometricAuthenticationNotAvailableRemovePreference() {
@ -206,6 +212,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
(findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as Preference?)?.onPreferenceChangeListener = useAutoPhotoUploadChangedListener (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as Preference?)?.onPreferenceChangeListener = useAutoPhotoUploadChangedListener
(findPreference(SharedPreferencesHandler.USE_LRU_CACHE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener (findPreference(SharedPreferencesHandler.USE_LRU_CACHE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener
(findPreference(SharedPreferencesHandler.LRU_CACHE_SIZE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener (findPreference(SharedPreferencesHandler.LRU_CACHE_SIZE) as Preference?)?.onPreferenceChangeListener = useLruChangedListener
(findPreference(SharedPreferencesHandler.MICROSOFT_WORKAROUND) as Preference?)?.onPreferenceChangeListener = microsoftWorkaroundChangeListener
if (BuildConfig.FLAVOR == "apkstore") { if (BuildConfig.FLAVOR == "apkstore") {
(findPreference(UPDATE_CHECK_ITEM_KEY) as Preference?)?.onPreferenceClickListener = updateCheckClickListener (findPreference(UPDATE_CHECK_ITEM_KEY) as Preference?)?.onPreferenceClickListener = updateCheckClickListener
} }
@ -263,14 +270,24 @@ class SettingsFragment : PreferenceFragmentCompat() {
(findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as SwitchPreferenceCompat?)?.isChecked = enabled (findPreference(SharedPreferencesHandler.PHOTO_UPLOAD) as SwitchPreferenceCompat?)?.isChecked = enabled
} }
fun rootView(): View {
return activity().findViewById(R.id.activityRootView)
}
fun disableAutoUpload() { fun disableAutoUpload() {
onUseAutoPhotoUploadChanged(false) onUseAutoPhotoUploadChanged(false)
} }
private fun onMicrosoftWorkaroundChanged(enabled: Boolean) {
if (enabled) {
activity().showDialog(MicrosoftWorkaroundDisclaimerDialog.newInstance())
}
}
fun deactivateMicrosoftWorkaround() {
sharedPreferencesHandler.setMicrosoftWorkaround(false)
}
fun rootView(): View {
return activity().findViewById(R.id.activityRootView)
}
companion object { companion object {
private const val APP_VERSION_ITEM_KEY = "appVersion" private const val APP_VERSION_ITEM_KEY = "appVersion"

View File

@ -20,8 +20,6 @@ import java.io.InvalidClassException
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.ArrayList
import java.util.HashSet
import javax.inject.Inject import javax.inject.Inject
import timber.log.Timber import timber.log.Timber
@ -114,6 +112,29 @@ class FileUtil @Inject constructor(private val context: Context, private val mim
return FileInfo(name, mimeTypes) return FileInfo(name, mimeTypes)
} }
fun getLegacyFileForMicrosoftWorkaround(cloudFile: CloudFileModel): File {
return getPublicDecryptedFileStorage()?.let {
val publicOfficeFile = File(it, fileNameLowerCaseExtension(cloudFile))
fileFor(cloudFile).copyTo(publicOfficeFile, true)
} ?: fileFor(cloudFile)
}
private fun getPublicDecryptedFileStorage(): File? {
return try {
val mediaDir = context.getExternalMediaDirs().first { dir -> dir.startsWith("/storage/emulated/0") }
val publicDecryptedFileStorage = File(mediaDir, "decrypted")
if (publicDecryptedFileStorage.canWrite()) {
publicDecryptedFileStorage
} else {
Timber.tag("FileUtil").e("Media storage isn't writable")
null
}
} catch (e: NoSuchElementException) {
Timber.tag("FileUtil").e("Media storage isn't available")
null
}
}
fun storeImagePreviewFiles(imagePreviewFilesStore: ImagePreviewFilesStore?): String? { fun storeImagePreviewFiles(imagePreviewFilesStore: ImagePreviewFilesStore?): String? {
decryptedFileStorage.mkdir() decryptedFileStorage.mkdir()
val file = File(decryptedFileStorage, IMAGE_PREVIEW__FILE_NAMES) val file = File(decryptedFileStorage, IMAGE_PREVIEW__FILE_NAMES)

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_vertical_margin">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_microsoft_workaround_disclaimer_hint" />
</RelativeLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -54,4 +54,34 @@
<item>30</item> <item>30</item>
<item>Never</item> <item>Never</item>
</string-array> </string-array>
<string-array name="microsoft_extensions">
<item>doc</item>
<item>dot</item>
<item>docx</item>
<item>dotx</item>
<item>docm</item>
<item>dotm</item>
<item>xls</item>
<item>xlt</item>
<item>xla</item>
<item>xlsx</item>
<item>xltx</item>
<item>xlsm</item>
<item>xltm</item>
<item>xlam</item>
<item>xlsb</item>
<item>ppt</item>
<item>pot</item>
<item>pps</item>
<item>ppa</item>
<item>pptx</item>
<item>potx</item>
<item>ppsx</item>
<item>ppam</item>
<item>pptm</item>
<item>potm</item>
<item>ppsm</item>
<item>mdb</item>
</string-array>
</resources> </resources>

View File

@ -275,6 +275,8 @@
<string name="screen_settings_section_version">Version</string> <string name="screen_settings_section_version">Version</string>
<string name="screen_settings_advanced_settings">Advanced Settings</string> <string name="screen_settings_advanced_settings">Advanced Settings</string>
<string name="screen_settings_microsoft_apps_workaround_label">Workaround opening Microsoft files</string>
<string name="screen_settings_microsoft_apps_workaround_summary">Open files in Microsoft apps with write permission</string>
<string name="screen_settings_background_unlock_preparation_label">Accelerate unlock</string> <string name="screen_settings_background_unlock_preparation_label">Accelerate unlock</string>
<string name="screen_settings_background_unlock_preparation_label_summary">Download vault config in the background while prompted to enter the password or biometric auth</string> <string name="screen_settings_background_unlock_preparation_label_summary">Download vault config in the background while prompted to enter the password or biometric auth</string>
<string name="screen_settings_keep_unlocked_while_editing_files">Keep unlocked</string> <string name="screen_settings_keep_unlocked_while_editing_files">Keep unlocked</string>
@ -409,6 +411,11 @@
<string name="dialog_debug_mode_positive_button">Enable</string> <string name="dialog_debug_mode_positive_button">Enable</string>
<string name="dialog_debug_mode_negative_button" translatable="false">@string/dialog_button_cancel</string> <string name="dialog_debug_mode_negative_button" translatable="false">@string/dialog_button_cancel</string>
<string name="dialog_microsoft_workaround_disclaimer_hint">Due to a bug in Microsoft apps, we need to share the file with them in a public media folder. We only use this using those file types. After resuming to Cryptomator, we delete the public available file again but we can not influence what happens to this file in the meantime. So just enable it if this isn\'t a problem for you.\n\nIf enabled, please restart Cryptomator to apply this change.</string>
<string name="dialog_microsoft_workaround_disclaimer_title" translatable="false">@string/dialog_debug_mode_disclaimer_title</string>
<string name="dialog_microsoft_workaround_positive_button" translatable="false">@string/dialog_debug_mode_positive_button</string>
<string name="dialog_microsoft_workaround_negative_button" translatable="false">@string/dialog_button_cancel</string>
<string name="dialog_disable_app_obscured_disclaimer_hint">This setting is a security feature and prevents other apps from tricking users into doing things they do not wan\'t to do.\n\nBy disabling, you confirm that you are <a href="https://docs.cryptomator.org/en/1.5/android/settings/#block-app-when-obscured">aware of the risks</a>.</string> <string name="dialog_disable_app_obscured_disclaimer_hint">This setting is a security feature and prevents other apps from tricking users into doing things they do not wan\'t to do.\n\nBy disabling, you confirm that you are <a href="https://docs.cryptomator.org/en/1.5/android/settings/#block-app-when-obscured">aware of the risks</a>.</string>
<string name="dialog_disable_app_obscured_disclaimer_title">Attention</string> <string name="dialog_disable_app_obscured_disclaimer_title">Attention</string>
<string name="dialog_disable_app_obscured_positive_button">Disable</string> <string name="dialog_disable_app_obscured_positive_button">Disable</string>

View File

@ -181,6 +181,12 @@
<PreferenceCategory android:title="@string/screen_settings_advanced_settings"> <PreferenceCategory android:title="@string/screen_settings_advanced_settings">
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="shareOfficeFilePublicly"
android:summary="@string/screen_settings_microsoft_apps_workaround_summary"
android:title="@string/screen_settings_microsoft_apps_workaround_label" />
<androidx.preference.SwitchPreferenceCompat <androidx.preference.SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="backgroundUnlockPreparation" android:key="backgroundUnlockPreparation"

View File

@ -261,6 +261,14 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
} }
} }
fun setMicrosoftWorkaround(enabled: Boolean) {
defaultSharedPreferences.setValue(MICROSOFT_WORKAROUND, enabled)
}
fun microsoftWorkaround(): Boolean {
return defaultSharedPreferences.getBoolean(MICROSOFT_WORKAROUND, false)
}
companion object { companion object {
private const val SCREEN_LOCK_DIALOG_SHOWN = "askForScreenLockDialogShown" private const val SCREEN_LOCK_DIALOG_SHOWN = "askForScreenLockDialogShown"
@ -287,6 +295,7 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
const val PHOTO_UPLOAD_INCLUDING_VIDEOS = "photoUploadIncludingVideos" const val PHOTO_UPLOAD_INCLUDING_VIDEOS = "photoUploadIncludingVideos"
const val USE_LRU_CACHE = "lruCache" const val USE_LRU_CACHE = "lruCache"
const val LRU_CACHE_SIZE = "lruCacheSize" const val LRU_CACHE_SIZE = "lruCacheSize"
const val MICROSOFT_WORKAROUND = "shareOfficeFilePublicly"
const val MAIL = "mail" const val MAIL = "mail"
const val UPDATE_INTERVAL = "updateInterval" const val UPDATE_INTERVAL = "updateInterval"
private const val LAST_UPDATE_CHECK = "lastUpdateCheck" private const val LAST_UPDATE_CHECK = "lastUpdateCheck"