Skip to content

Commit 715a91b

Browse files
committed
Fix ANR when exporting folder with a lot of nodes
Depending on the number of nodes in a folder being exported, an ANR could occur because the initial file/folder creation was done on the UI thread. Writing to it was always done on a separate thread, but not the initial creation.
1 parent 224c67e commit 715a91b

File tree

5 files changed

+142
-82
lines changed

5 files changed

+142
-82
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.cryptomator.domain.exception;
2+
3+
public class IllegalFileNameException extends BackendException {
4+
5+
public IllegalFileNameException() {
6+
}
7+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package org.cryptomator.domain.usecases;
2+
3+
import android.content.ContentResolver;
4+
import android.content.Context;
5+
import android.net.Uri;
6+
import android.provider.DocumentsContract;
7+
8+
import org.cryptomator.domain.CloudFile;
9+
import org.cryptomator.domain.exception.BackendException;
10+
import org.cryptomator.domain.exception.FatalBackendException;
11+
import org.cryptomator.domain.exception.IllegalFileNameException;
12+
import org.cryptomator.domain.exception.NoSuchCloudFileException;
13+
import org.cryptomator.generator.Parameter;
14+
import org.cryptomator.generator.UseCase;
15+
import org.cryptomator.util.file.MimeType;
16+
import org.cryptomator.util.file.MimeTypes;
17+
18+
import java.io.FileNotFoundException;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
@UseCase
23+
public class PrepareDownloadFiles {
24+
25+
private final Context context;
26+
private final MimeTypes mimeTypes;
27+
private final List<CloudFile> filesToExport;
28+
private final Uri parentUri;
29+
private final ContentResolver contentResolver;
30+
private final CloudNodeRecursiveListing cloudNodeRecursiveListing;
31+
32+
private List<DownloadFile> downloadFiles = new ArrayList<>();
33+
34+
PrepareDownloadFiles(Context context, MimeTypes mimeTypes, @Parameter List<CloudFile> filesToExport, @Parameter Uri parentUri, @Parameter CloudNodeRecursiveListing cloudNodeRecursiveListing) {
35+
this.context = context;
36+
this.mimeTypes = mimeTypes;
37+
this.filesToExport = filesToExport;
38+
this.parentUri = parentUri;
39+
this.contentResolver = context.getContentResolver();
40+
this.cloudNodeRecursiveListing = cloudNodeRecursiveListing;
41+
}
42+
43+
List<DownloadFile> execute() throws BackendException, FileNotFoundException {
44+
downloadFiles = prepareFilesForExport(filesToExport, parentUri);
45+
for (CloudFolderRecursiveListing folderRecursiveListing : cloudNodeRecursiveListing.getFoldersContent()) {
46+
prepareFolderContentForExport(folderRecursiveListing, parentUri);
47+
}
48+
return downloadFiles;
49+
}
50+
51+
private List<DownloadFile> prepareFilesForExport(List<CloudFile> filesToExport, Uri parentUri) throws NoSuchCloudFileException, FileNotFoundException, IllegalFileNameException {
52+
List<DownloadFile> downloadFiles = new ArrayList<>();
53+
for (CloudFile cloudFile : filesToExport) {
54+
DownloadFile downloadFile = createDownloadFile(cloudFile, parentUri);
55+
downloadFiles.add(downloadFile);
56+
}
57+
return downloadFiles;
58+
}
59+
60+
private void prepareFolderContentForExport(CloudFolderRecursiveListing folderRecursiveListing, Uri parentUri) throws FileNotFoundException, NoSuchCloudFileException, IllegalFileNameException {
61+
Uri createdFolder = createFolder(parentUri, folderRecursiveListing.getParent().getName());
62+
if (createdFolder != null) {
63+
downloadFiles.addAll(prepareFilesForExport(folderRecursiveListing.getFiles(), createdFolder));
64+
for (CloudFolderRecursiveListing childFolder : folderRecursiveListing.getFolders()) {
65+
prepareFolderContentForExport(childFolder, createdFolder);
66+
}
67+
} else {
68+
throw new FatalBackendException("Failed to create parent folder for export");
69+
}
70+
}
71+
72+
private Uri createFolder(Uri parentUri, String folderName) throws NoSuchCloudFileException {
73+
try {
74+
return DocumentsContract.createDocument(contentResolver, parentUri, DocumentsContract.Document.MIME_TYPE_DIR, folderName);
75+
} catch (FileNotFoundException e) {
76+
throw new NoSuchCloudFileException("Creating folder failed");
77+
}
78+
}
79+
80+
private DownloadFile createDownloadFile(CloudFile file, Uri documentUri) throws NoSuchCloudFileException, IllegalFileNameException {
81+
try {
82+
return new DownloadFile.Builder().setDownloadFile(file).setDataSink(contentResolver.openOutputStream(createNewDocumentUri(documentUri, file.getName()))).build();
83+
} catch (FileNotFoundException e) {
84+
throw new NoSuchCloudFileException(file.getName());
85+
}
86+
}
87+
88+
private Uri createNewDocumentUri(Uri parentUri, String fileName) throws IllegalFileNameException, NoSuchCloudFileException {
89+
MimeType mimeType = mimeTypes.fromFilename(fileName);
90+
if (mimeType == null) {
91+
mimeType = MimeType.APPLICATION_OCTET_STREAM;
92+
}
93+
try {
94+
Uri documentUri = DocumentsContract.createDocument(context.getContentResolver(), parentUri, mimeType.toString(), fileName);
95+
if (documentUri == null) {
96+
throw new IllegalFileNameException();
97+
}
98+
return documentUri;
99+
} catch (FileNotFoundException e) {
100+
throw new NoSuchCloudFileException(fileName);
101+
}
102+
}
103+
104+
}

presentation/src/main/java/org/cryptomator/presentation/exception/ExceptionHandlers.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException
66
import org.cryptomator.domain.di.PerView
77
import org.cryptomator.domain.exception.CloudAlreadyExistsException
88
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
9+
import org.cryptomator.domain.exception.IllegalFileNameException
910
import org.cryptomator.domain.exception.NetworkConnectionException
1011
import org.cryptomator.domain.exception.NoSuchBucketException
1112
import org.cryptomator.domain.exception.NoSuchCloudFileException
@@ -39,7 +40,7 @@ import timber.log.Timber
3940
class ExceptionHandlers @Inject constructor(private val context: Context, defaultExceptionHandler: DefaultExceptionHandler) : Iterable<ExceptionHandler?> {
4041

4142
private val exceptionHandlers: MutableList<ExceptionHandler> = ArrayList()
42-
private val defaultExceptionHandler: ExceptionHandler
43+
private val defaultExceptionHandler: ExceptionHandler = defaultExceptionHandler
4344

4445
private fun setupHandlers() {
4546
staticHandler(AuthenticationException::class.java, R.string.error_authentication_failed)
@@ -122,7 +123,6 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul
122123
}
123124

124125
init {
125-
this.defaultExceptionHandler = defaultExceptionHandler
126126
setupHandlers()
127127
}
128128
}

presentation/src/main/java/org/cryptomator/presentation/exception/IllegalFileNameException.kt

Lines changed: 0 additions & 3 deletions
This file was deleted.

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

Lines changed: 29 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
1717
import org.cryptomator.domain.exception.EmptyDirFileException
1818
import org.cryptomator.domain.exception.FatalBackendException
1919
import org.cryptomator.domain.exception.NoDirFileException
20-
import org.cryptomator.domain.exception.NoSuchCloudFileException
2120
import org.cryptomator.domain.exception.SymLinkException
2221
import org.cryptomator.domain.usecases.CalculateFileHashUseCase
2322
import org.cryptomator.domain.usecases.CloudFolderRecursiveListing
2423
import org.cryptomator.domain.usecases.CloudNodeRecursiveListing
2524
import org.cryptomator.domain.usecases.CopyDataUseCase
2625
import org.cryptomator.domain.usecases.DownloadFile
2726
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase
27+
import org.cryptomator.domain.usecases.PrepareDownloadFilesUseCase
2828
import org.cryptomator.domain.usecases.ResultRenamed
2929
import org.cryptomator.domain.usecases.cloud.CreateFolderUseCase
3030
import org.cryptomator.domain.usecases.cloud.DeleteNodesUseCase
@@ -47,7 +47,6 @@ import org.cryptomator.generator.InstanceState
4747
import org.cryptomator.presentation.CryptomatorApp
4848
import org.cryptomator.presentation.R
4949
import org.cryptomator.presentation.exception.ExceptionHandlers
50-
import org.cryptomator.presentation.exception.IllegalFileNameException
5150
import org.cryptomator.presentation.intent.BrowseFilesIntent
5251
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
5352
import org.cryptomator.presentation.intent.IntentBuilder
@@ -111,6 +110,7 @@ class BrowseFilesPresenter @Inject constructor( //
111110
private val getCloudListRecursiveUseCase: GetCloudListRecursiveUseCase, //
112111
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
113112
private val calculateFileHashUseCase: CalculateFileHashUseCase, //
113+
private val prepareDownloadFilesUseCase: PrepareDownloadFilesUseCase, //
114114
private val contentResolverUtil: ContentResolverUtil, //
115115
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
116116
private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
@@ -982,84 +982,35 @@ class BrowseFilesPresenter @Inject constructor( //
982982
}
983983

984984
private fun prepareExportingOf(parentUri: Uri, exportOperation: ExportOperation, filesToExport: List<CloudFileModel>, cloudNodeRecursiveListing: CloudNodeRecursiveListing) {
985+
view?.showProgress(ProgressModel.GENERIC)
985986
downloadFiles = ArrayList()
986-
downloadFiles.addAll(prepareFilesForExport(cloudFileModelMapper.fromModels(filesToExport), parentUri))
987-
cloudNodeRecursiveListing.foldersContent.forEach { folderRecursiveListing ->
988-
prepareFolderContentForExport(folderRecursiveListing, parentUri)
989-
}
990-
if (downloadFiles.isEmpty()) {
991-
view?.showMessage(R.string.screen_file_browser_nothing_to_export)
992-
view?.closeDialog()
993-
} else {
994-
exportOperation.export(this, downloadFiles)
995-
}
996-
}
997-
998-
private fun prepareFilesForExport(filesToExport: List<CloudFile>, parentUri: Uri): List<DownloadFile> {
999-
return filesToExport.mapTo(ArrayList()) { createDownloadFile(it, parentUri) }
1000-
}
1001-
1002-
private fun prepareFolderContentForExport(cloudFolderRecursiveListing: CloudFolderRecursiveListing, parentUri: Uri) {
1003-
createFolder(parentUri, cloudFolderRecursiveListing.parent.name)?.let {
1004-
downloadFiles.addAll(prepareFilesForExport(cloudFolderRecursiveListing.files, it))
1005-
cloudFolderRecursiveListing.folders.forEach { childFolder ->
1006-
prepareFolderContentForExport(childFolder, it)
1007-
}
1008-
} ?: throw FatalBackendException("Failed to create parent folder for export")
1009-
}
1010-
1011-
private fun createFolder(parentUri: Uri, folderName: String): Uri? {
1012-
return try {
1013-
DocumentsContract.createDocument( //
1014-
context().contentResolver, //
1015-
parentUri, //
1016-
DocumentsContract.Document.MIME_TYPE_DIR, //
1017-
folderName
1018-
)
1019-
} catch (e: FileNotFoundException) {
1020-
Timber.tag("BrowseFilesPresenter").e(e)
1021-
throw IllegalStateException("Creating folder failed")
1022-
}
1023-
}
987+
prepareDownloadFilesUseCase
988+
.withFilesToExport(cloudFileModelMapper.fromModels(filesToExport))
989+
.andParentUri(parentUri)
990+
.andCloudNodeRecursiveListing(cloudNodeRecursiveListing)
991+
.run(object : DefaultResultHandler<List<DownloadFile>>() {
992+
override fun onSuccess(prepareDownloadFiles: List<DownloadFile>) {
993+
view?.showProgress(ProgressModel.COMPLETED)
994+
downloadFiles = prepareDownloadFiles.toMutableList()
995+
if (downloadFiles.isEmpty()) {
996+
view?.showMessage(R.string.screen_file_browser_nothing_to_export)
997+
view?.closeDialog()
998+
} else {
999+
export(exportOperation, downloadFiles)
1000+
}
1001+
}
10241002

1025-
private fun createDownloadFile(file: CloudFile, documentUri: Uri): DownloadFile {
1026-
return try {
1027-
DownloadFile.Builder() //
1028-
.setDownloadFile(file) //
1029-
.setDataSink(
1030-
contentResolverUtil.openOutputStream( //
1031-
createNewDocumentUri(documentUri, file.name)
1032-
)
1033-
) //
1034-
.build()
1035-
} catch (e: FileNotFoundException) {
1036-
showError(e)
1037-
disableSelectionMode()
1038-
throw FatalBackendException(e)
1039-
} catch (e: NoSuchCloudFileException) {
1040-
showError(e)
1041-
disableSelectionMode()
1042-
throw FatalBackendException(e)
1043-
} catch (e: IllegalFileNameException) {
1044-
showError(e)
1045-
disableSelectionMode()
1046-
throw FatalBackendException(e)
1047-
}
1003+
override fun onError(e: Throwable) {
1004+
view?.showProgress(ProgressModel.COMPLETED)
1005+
showError(e)
1006+
disableSelectionMode()
1007+
throw FatalBackendException(e)
1008+
}
1009+
})
10481010
}
10491011

1050-
@Throws(IllegalFileNameException::class, NoSuchCloudFileException::class)
1051-
private fun createNewDocumentUri(parentUri: Uri, fileName: String): Uri {
1052-
val mimeType = mimeTypes.fromFilename(fileName) ?: MimeType.APPLICATION_OCTET_STREAM
1053-
return try {
1054-
DocumentsContract.createDocument( //
1055-
context().contentResolver, //
1056-
parentUri, //
1057-
mimeType.toString(), //
1058-
fileName
1059-
)
1060-
} catch (e: FileNotFoundException) {
1061-
throw NoSuchCloudFileException(fileName)
1062-
} ?: throw IllegalFileNameException()
1012+
private fun export(exportOperation: ExportOperation, downloadFiles: MutableList<DownloadFile>) {
1013+
exportOperation.export(this, downloadFiles)
10631014
}
10641015

10651016
@Callback
@@ -1335,7 +1286,8 @@ class BrowseFilesPresenter @Inject constructor( //
13351286
moveFilesUseCase, //
13361287
moveFoldersUseCase, //
13371288
getDecryptedCloudForVaultUseCase, //
1338-
calculateFileHashUseCase
1289+
calculateFileHashUseCase, //
1290+
prepareDownloadFilesUseCase
13391291
)
13401292
this.authenticationExceptionHandler = authenticationExceptionHandler
13411293
}

0 commit comments

Comments
 (0)