From 5cc9abfd76a1a608e1180d5e3fe5d5e442674cd4 Mon Sep 17 00:00:00 2001 From: Anon7250 <74374523+Anon7250@users.noreply.github.com> Date: Thu, 19 Aug 2021 14:13:32 -0700 Subject: [PATCH] Adding an 'Import/Encrypt Folder' button --- .../droidfs/explorers/BaseExplorerActivity.kt | 21 ++- .../droidfs/explorers/ExplorerActivity.kt | 89 ++++++++----- .../droidfs/explorers/ExplorerElement.kt | 2 +- .../file_operations/FileOperationService.kt | 126 ++++++++++++++---- .../res/drawable/icon_create_new_folder.xml | 5 + .../main/res/drawable/icon_import_folder.xml | 5 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 184 insertions(+), 66 deletions(-) create mode 100644 app/src/main/res/drawable/icon_create_new_folder.xml create mode 100644 app/src/main/res/drawable/icon_import_folder.xml diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt index 3ee7e56..26faf80 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -17,6 +17,7 @@ import android.widget.EditText import android.widget.ListView import android.widget.TextView import android.widget.Toast +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.swiperefreshlayout.widget.SwipeRefreshLayout @@ -254,7 +255,7 @@ open class BaseExplorerActivity : BaseActivity() { synchronized(this) { explorerElements.add( 0, - ExplorerElement("..", (-1).toShort(), -1, -1, currentDirectoryPath) + ExplorerElement("..", (-1).toShort(), parentPath = currentDirectoryPath) ) } } @@ -408,13 +409,13 @@ open class BaseExplorerActivity : BaseActivity() { items.clear() break } else { - items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, -1, -1, currentDirectoryPath))) + items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, parentPath = currentDirectoryPath))) } } if (items.size > 0) { checkPathOverwrite(items, currentDirectoryPath) { checkedItems -> checkedItems?.let { - fileOperationService.importFilesFromUris(checkedItems, uris){ failedItem -> + fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris){ failedItem -> runOnUiThread { callback(failedItem) } @@ -424,6 +425,20 @@ open class BaseExplorerActivity : BaseActivity() { } } + fun importDirectory(sourceUri: Uri, callback: (String?, List) -> Unit) { + val tree = DocumentFile.fromTreeUri(this, sourceUri)!! //non-null after Lollipop + val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath)) + checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation -> + checkedOperation?.let { + fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree) { failedItem, uris -> + runOnUiThread { + callback(failedItem, uris) + } + } + } + } + } + protected fun rename(old_name: String, new_name: String){ if (new_name.isEmpty()) { Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt index 7c73a42..9c116b9 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -2,6 +2,7 @@ package sushi.hardcore.droidfs.explorers import android.app.Activity import android.content.Intent +import android.net.Uri import android.view.Menu import android.view.MenuItem import android.view.WindowManager @@ -44,7 +45,7 @@ class ExplorerActivity : BaseExplorerActivity() { for (i in paths.indices) { operationFiles.add( OperationFile.fromExplorerElement( - ExplorerElement(File(paths[i]).name, types[i].toShort(), -1, -1, PathUtils.getParentPath(paths[i])) + ExplorerElement(File(paths[i]).name, types[i].toShort(), parentPath = PathUtils.getParentPath(paths[i])) ) ) if (types[i] == 0){ //directory @@ -57,7 +58,7 @@ class ExplorerActivity : BaseExplorerActivity() { } else { operationFiles.add( OperationFile.fromExplorerElement( - ExplorerElement(File(path).name, 1, -1, -1, PathUtils.getParentPath(path)) + ExplorerElement(File(path).name, 1, parentPath = PathUtils.getParentPath(path)) ) ) } @@ -92,42 +93,11 @@ class ExplorerActivity : BaseExplorerActivity() { private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> if (uris != null) { importFilesFromUris(uris){ failedItem -> - if (failedItem == null){ - ColoredAlertDialogBuilder(this) - .setTitle(R.string.success_import) - .setMessage(""" - ${getString(R.string.success_import_msg)} - ${getString(R.string.ask_for_wipe)} - """.trimIndent()) - .setPositiveButton(R.string.yes) { _, _ -> - fileOperationService.wipeUris(uris) { errorMsg -> - runOnUiThread { - if (errorMsg == null){ - Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).show() - } else { - ColoredAlertDialogBuilder(this) - .setTitle(R.string.error) - .setMessage(getString(R.string.wipe_failed, errorMsg)) - .setPositiveButton(R.string.ok, null) - .show() - } - } - } - } - .setNegativeButton(R.string.no, null) - .show() - } else { - ColoredAlertDialogBuilder(this) - .setTitle(R.string.error) - .setMessage(getString(R.string.import_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } - setCurrentPath(currentDirectoryPath) + onImportComplete(failedItem, uris) } } } - private val pickDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> if (uri != null) { fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem -> runOnUiThread { @@ -145,6 +115,46 @@ class ExplorerActivity : BaseExplorerActivity() { } unselectAll() } + private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri -> + rootUri?.let { + importDirectory(it, ::onImportComplete) + } + } + + private fun onImportComplete(failedItem: String?, uris: List) { + if (failedItem == null){ + ColoredAlertDialogBuilder(this) + .setTitle(R.string.success_import) + .setMessage(""" + ${getString(R.string.success_import_msg)} + ${getString(R.string.ask_for_wipe)} + """.trimIndent()) + .setPositiveButton(R.string.yes) { _, _ -> + fileOperationService.wipeUris(uris) { errorMsg -> + runOnUiThread { + if (errorMsg == null){ + Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.wipe_failed, errorMsg)) + .setPositiveButton(R.string.ok, null) + .show() + } + } + } + } + .setNegativeButton(R.string.no, null) + .show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.import_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() + } + setCurrentPath(currentDirectoryPath) + } override fun init() { binding = ActivityExplorerBinding.inflate(layoutInflater) @@ -157,8 +167,9 @@ class ExplorerActivity : BaseExplorerActivity() { adapter.items = listOf( listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert), listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt), + listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder), listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown), - listOf("createFolder", R.string.mkdir, R.drawable.icon_folder), + listOf("createFolder", R.string.mkdir, R.drawable.icon_create_new_folder), listOf("takePhoto", R.string.take_photo, R.drawable.icon_camera) ) ColoredAlertDialogBuilder(this) @@ -175,6 +186,10 @@ class ExplorerActivity : BaseExplorerActivity() { isStartingActivity = true pickFiles.launch(arrayOf("*/*")) } + "importFolder" -> { + isStartingActivity = true + pickImportDirectory.launch(null) + } "createFile" -> { val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null) val dialogEditText = dialogEditTextView.findViewById(R.id.dialog_edit_text) @@ -381,7 +396,7 @@ class ExplorerActivity : BaseExplorerActivity() { } R.id.decrypt -> { isStartingActivity = true - pickDirectory.launch(null) + pickExportDirectory.launch(null) true } else -> super.onOptionsItemSelected(item) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt index cf2c884..27e3d7c 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt @@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.explorers import sushi.hardcore.droidfs.util.PathUtils import java.util.* -class ExplorerElement(val name: String, val elementType: Short, var size: Long, mTime: Long, val parentPath: String) { +class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) { val mTime = Date((mTime * 1000).toString().toLong()) val fullPath: String = PathUtils.pathJoin(parentPath, name) diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt index a65e271..67d4663 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt @@ -38,7 +38,7 @@ class FileOperationService : Service() { return binder } - private fun showNotification(message: Int, total: Int): FileOperationNotification { + private fun showNotification(message: Int, total: Int?): FileOperationNotification { ++lastNotificationId if (!::notificationManager.isInitialized){ notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -73,11 +73,18 @@ class FileOperationService : Service() { } notificationBuilder .setContentTitle(getString(message)) - .setContentText("0/$total") .setSmallIcon(R.mipmap.icon_launcher) .setOngoing(true) - .setProgress(total, 0, false) .addAction(notificationAction.build()) + if (total != null) { + notificationBuilder + .setContentText("0/$total") + .setProgress(total, 0, false) + } else { + notificationBuilder + .setContentText(getString(R.string.discovering_files)) + .setProgress(0, 0, true) + } notifications[lastNotificationId] = false notificationManager.notify(lastNotificationId, notificationBuilder.build()) return FileOperationNotification(notificationBuilder, lastNotificationId) @@ -198,33 +205,102 @@ class FileOperationService : Service() { }.start() } - fun importFilesFromUris(items: ArrayList, uris: List, callback: (String?) -> Unit){ + private fun importFilesFromUris(dstPaths: List, uris: List, reuseNotification: FileOperationNotification? = null, callback: (String?) -> Unit){ + val notification = reuseNotification ?: showNotification(R.string.file_op_import_msg, dstPaths.size) + var failedIndex = -1 + for (i in dstPaths.indices) { + if (notifications[notification.notificationId]!!){ + cancelNotification(notification) + return + } + try { + if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) { + failedIndex = i + } + } catch (e: FileNotFoundException){ + failedIndex = i + } + if (failedIndex == -1) { + updateNotificationProgress(notification, i, dstPaths.size) + } else { + cancelNotification(notification) + callback(uris[failedIndex].toString()) + break + } + } + if (failedIndex == -1){ + cancelNotification(notification) + callback(null) + } + } + + fun importFilesFromUris(dstPaths: List, uris: List, callback: (String?) -> Unit) { Thread { - val notification = showNotification(R.string.file_op_import_msg, items.size) - var failedIndex = -1 - for (i in 0 until items.size) { - if (notifications[notification.notificationId]!!){ + importFilesFromUris(dstPaths, uris, null, callback) + }.start() + } + + /** + * Map the content of an unencrypted directory to prepare its import + * + * Contents of dstFiles and srcUris, at the same index, will match each other + * + * @return false if cancelled early, true otherwise. + */ + private fun recursiveMapDirectoryForImport( + rootSrcDir: DocumentFile, + rootDstPath: String, + dstFiles: ArrayList, + srcUris: ArrayList, + dstDirs: ArrayList, + notification: FileOperationNotification + ): Boolean { + dstDirs.add(rootDstPath) + for (child in rootSrcDir.listFiles()) { + if (notifications[notification.notificationId]!!) { + cancelNotification(notification) + return false + } + child.name?.let { name -> + val subPath = PathUtils.pathJoin(rootDstPath, name) + if (child.isDirectory) { + if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, notification)) { + return false + } + } + else if (child.isFile) { + srcUris.add(child.uri) + dstFiles.add(subPath) + } + } + } + return true + } + + fun importDirectory(rootDstPath: String, rootSrcDir: DocumentFile, callback: (String?, List) -> Unit) { + Thread { + val notification = showNotification(R.string.file_op_import_msg, null) + + val dstFiles = arrayListOf() + val srcUris = arrayListOf() + val dstDirs = arrayListOf() + if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) { + return@Thread + } + + updateNotificationProgress(notification, 0, dstDirs.size) + + // create destination folders so the new files can use them + for (mkdir in dstDirs) { + if (notifications[notification.notificationId]!!) { cancelNotification(notification) return@Thread } - try { - if (!gocryptfsVolume.importFile(this, uris[i], items[i].dstPath!!)){ - failedIndex = i - } - } catch (e: FileNotFoundException){ - failedIndex = i - } - if (failedIndex == -1) { - updateNotificationProgress(notification, i, items.size) - } else { - cancelNotification(notification) - callback(uris[failedIndex].toString()) - break - } + gocryptfsVolume.mkdir(mkdir) } - if (failedIndex == -1){ - cancelNotification(notification) - callback(null) + + importFilesFromUris(dstFiles, srcUris, notification) { failedItem -> + callback(failedItem, srcUris) } }.start() } diff --git a/app/src/main/res/drawable/icon_create_new_folder.xml b/app/src/main/res/drawable/icon_create_new_folder.xml new file mode 100644 index 0000000..9720363 --- /dev/null +++ b/app/src/main/res/drawable/icon_create_new_folder.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/icon_import_folder.xml b/app/src/main/res/drawable/icon_import_folder.xml new file mode 100644 index 0000000..74484f0 --- /dev/null +++ b/app/src/main/res/drawable/icon_import_folder.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fc890f..cc782c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ Volume Path: Volume Name: Import/Encrypt files + Import/Encrypt folder + Discovering files… Create folder Directory Empty Warning !