Skip to content

Commit 224c67e

Browse files
committed
Fix ANR when opening/resuming large files
We calculate an MD5 hash of the contents when a file is shared with a third party application. We now calculate the hash on a separate thread, not the UI thread.
1 parent 4dc41bc commit 224c67e

File tree

2 files changed

+97
-38
lines changed

2 files changed

+97
-38
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.cryptomator.domain.usecases;
2+
3+
import android.content.Context;
4+
import android.net.Uri;
5+
6+
import org.cryptomator.domain.exception.BackendException;
7+
import org.cryptomator.domain.exception.FatalBackendException;
8+
import org.cryptomator.generator.Parameter;
9+
import org.cryptomator.generator.UseCase;
10+
11+
import java.io.FileNotFoundException;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.security.DigestInputStream;
15+
import java.security.MessageDigest;
16+
import java.security.NoSuchAlgorithmException;
17+
18+
@UseCase
19+
public class CalculateFileHash {
20+
21+
private final Context context;
22+
private final Uri uri;
23+
24+
CalculateFileHash(final Context context, @Parameter Uri uri) {
25+
this.context = context;
26+
this.uri = uri;
27+
}
28+
29+
public byte[] execute() throws BackendException, FileNotFoundException {
30+
try {
31+
MessageDigest digest = MessageDigest.getInstance("MD5");
32+
try (InputStream inputStream = context.getContentResolver().openInputStream(uri); //
33+
DigestInputStream dis = new DigestInputStream(inputStream, digest)) {
34+
byte[] buffer = new byte[4096];
35+
while (dis.read(buffer) != -1) {
36+
}
37+
return digest.digest();
38+
} catch (IOException e) {
39+
throw new FatalBackendException(e);
40+
}
41+
} catch (NoSuchAlgorithmException e) {
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
}

presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.cryptomator.domain.exception.FatalBackendException
1919
import org.cryptomator.domain.exception.NoDirFileException
2020
import org.cryptomator.domain.exception.NoSuchCloudFileException
2121
import org.cryptomator.domain.exception.SymLinkException
22+
import org.cryptomator.domain.usecases.CalculateFileHashUseCase
2223
import org.cryptomator.domain.usecases.CloudFolderRecursiveListing
2324
import org.cryptomator.domain.usecases.CloudNodeRecursiveListing
2425
import org.cryptomator.domain.usecases.CopyDataUseCase
@@ -86,8 +87,6 @@ import org.cryptomator.util.file.MimeTypes
8687
import java.io.FileInputStream
8788
import java.io.FileNotFoundException
8889
import java.io.Serializable
89-
import java.security.DigestInputStream
90-
import java.security.MessageDigest
9190
import java.util.function.Supplier
9291
import javax.inject.Inject
9392
import kotlin.reflect.KClass
@@ -111,6 +110,7 @@ class BrowseFilesPresenter @Inject constructor( //
111110
private val moveFoldersUseCase: MoveFoldersUseCase, //
112111
private val getCloudListRecursiveUseCase: GetCloudListRecursiveUseCase, //
113112
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
113+
private val calculateFileHashUseCase: CalculateFileHashUseCase, //
114114
private val contentResolverUtil: ContentResolverUtil, //
115115
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
116116
private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
@@ -541,16 +541,27 @@ class BrowseFilesPresenter @Inject constructor( //
541541
}
542542
openedCloudFile = cloudFile
543543
uriToOpenedFile?.let {
544-
openedCloudFileMd5 = calculateDigestFromUri(it)
545-
viewFileIntent.setDataAndType(it, mimeTypes.fromFilename(cloudFile.name)?.toString())
546-
viewFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
547-
if (sharedPreferencesHandler.keepUnlockedWhileEditing()) {
548-
openWritableFileNotification = OpenWritableFileNotification(context(), it)
549-
openWritableFileNotification?.show()
550-
val cryptomatorApp = activity().application as CryptomatorApp
551-
cryptomatorApp.suspendLock()
552-
}
553-
requestActivityResult(ActivityResultCallbacks.openFileFinished(openFileType), viewFileIntent)
544+
view?.showProgress(ProgressModel.GENERIC)
545+
calculateFileHashUseCase //
546+
.withUri(it) //
547+
.run(object : DefaultResultHandler<ByteArray>() {
548+
override fun onSuccess(hash: ByteArray) {
549+
openedCloudFileMd5 = hash
550+
viewFileIntent.setDataAndType(it, mimeTypes.fromFilename(cloudFile.name)?.toString())
551+
viewFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
552+
if (sharedPreferencesHandler.keepUnlockedWhileEditing()) {
553+
openWritableFileNotification = OpenWritableFileNotification(context(), it)
554+
openWritableFileNotification?.show()
555+
val cryptomatorApp = activity().application as CryptomatorApp
556+
cryptomatorApp.suspendLock()
557+
}
558+
requestActivityResult(ActivityResultCallbacks.openFileFinished(openFileType), viewFileIntent)
559+
}
560+
561+
override fun onFinished() {
562+
view?.showProgress(ProgressModel.COMPLETED)
563+
}
564+
})
554565
}
555566
}
556567

@@ -585,21 +596,34 @@ class BrowseFilesPresenter @Inject constructor( //
585596
context().revokeUriPermission(uriToOpenedFile, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
586597

587598
uriToOpenedFile?.let {
588-
try {
589-
calculateDigestFromUri(it)?.let { hashAfterEdit ->
590-
openedCloudFileMd5?.let { hashBeforeEdit ->
591-
if (hashAfterEdit.contentEquals(hashBeforeEdit)) {
592-
Timber.tag("BrowseFilesPresenter").i("Opened app finished, file not changed")
593-
deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
599+
view?.showProgress(ProgressModel.GENERIC)
600+
calculateFileHashUseCase //
601+
.withUri(it) //
602+
.run(object : DefaultResultHandler<ByteArray>() {
603+
override fun onSuccess(hashAfterEdit: ByteArray) {
604+
openedCloudFileMd5?.let { hashBeforeEdit ->
605+
if (hashAfterEdit.contentEquals(hashBeforeEdit)) {
606+
Timber.tag("BrowseFilesPresenter").i("Opened app finished, file not changed")
607+
deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
608+
} else {
609+
uploadChangedFile(openFileType)
610+
}
611+
} ?: deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
612+
}
613+
614+
override fun onFinished() {
615+
view?.showProgress(ProgressModel.COMPLETED)
616+
}
617+
618+
override fun onError(e: Throwable) {
619+
if (e is FileNotFoundException) {
620+
Timber.tag("BrowseFilesPresenter").e(e, "Failed to read back changes, file isn't present anymore")
621+
Toast.makeText(context(), R.string.error_file_not_found_after_opening_using_3party, Toast.LENGTH_LONG).show()
594622
} else {
595-
uploadChangedFile(openFileType)
623+
super.onError(e)
596624
}
597-
} ?: deleteFileIfMicrosoftWorkaround(openFileType, uriToOpenedFile)
598-
}
599-
} catch (e: FileNotFoundException) {
600-
Timber.tag("BrowseFilesPresenter").e(e, "Failed to read back changes, file isn't present anymore")
601-
Toast.makeText(context(), R.string.error_file_not_found_after_opening_using_3party, Toast.LENGTH_LONG).show()
602-
}
625+
}
626+
})
603627
}
604628
}
605629

@@ -651,18 +675,6 @@ class BrowseFilesPresenter @Inject constructor( //
651675
openWritableFileNotification?.hide() ?: OpenWritableFileNotification(context(), Uri.EMPTY).hide()
652676
}
653677

654-
@Throws(FileNotFoundException::class)
655-
private fun calculateDigestFromUri(uri: Uri): ByteArray? {
656-
val digest = MessageDigest.getInstance("MD5")
657-
DigestInputStream(context().contentResolver.openInputStream(uri), digest).use { dis ->
658-
val buffer = ByteArray(4096)
659-
// Read all bytes:
660-
while (dis.read(buffer) > -1) {
661-
}
662-
}
663-
return digest.digest()
664-
}
665-
666678
private val previewCloudFileNodes: ArrayList<CloudFileModel>
667679
get() {
668680
val previewCloudFiles = ArrayList<CloudFileModel>()
@@ -1322,7 +1334,8 @@ class BrowseFilesPresenter @Inject constructor( //
13221334
copyDataUseCase, //
13231335
moveFilesUseCase, //
13241336
moveFoldersUseCase, //
1325-
getDecryptedCloudForVaultUseCase
1337+
getDecryptedCloudForVaultUseCase, //
1338+
calculateFileHashUseCase
13261339
)
13271340
this.authenticationExceptionHandler = authenticationExceptionHandler
13281341
}

0 commit comments

Comments
 (0)