diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt index c242608..1f6cbdd 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt @@ -24,6 +24,7 @@ import sushi.hardcore.droidfs.databinding.ActivityMainBinding import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding import sushi.hardcore.droidfs.explorers.ExplorerRouter import sushi.hardcore.droidfs.file_operations.FileOperationService +import sushi.hardcore.droidfs.file_operations.TaskResult import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder @@ -418,11 +419,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> VolumeData?) { lifecycleScope.launch { val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile) - when { - result.taskResult.cancelled -> { + when (result.taskResult.state) { + TaskResult.State.CANCELLED -> { result.dstRootDirectory?.delete() } - result.taskResult.failedItem == null -> { + TaskResult.State.SUCCESS -> { result.dstRootDirectory?.let { getResultVolume(it)?.let { volume -> volumeDatabase.saveVolume(volume) @@ -435,13 +436,14 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener { } } } - else -> { + TaskResult.State.FAILED -> { CustomAlertDialogBuilder(this@MainActivity, theme) .setTitle(R.string.error) - .setMessage(getString(R.string.copy_failed, result.taskResult.failedItem.name)) + .setMessage(getString(R.string.copy_failed, result.taskResult.failedItem!!.name)) .setPositiveButton(R.string.ok, null) .show() } + TaskResult.State.ERROR -> result.taskResult.showErrorAlertDialog(this@MainActivity, theme) } } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt index bfb7957..4bb26dc 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt @@ -1,11 +1,16 @@ package sushi.hardcore.droidfs +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import sushi.hardcore.droidfs.filesystems.EncryptedVolume class VolumeManager { private var id = 0 private val volumes = HashMap() private val volumesData = HashMap() + private val scopes = HashMap() fun insert(volume: EncryptedVolume, data: VolumeData): Int { volumes[id] = volume @@ -25,8 +30,13 @@ class VolumeManager { return volumes[id] } + fun getCoroutineScope(volumeId: Int): CoroutineScope { + return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it } + } + fun closeVolume(id: Int) { volumes.remove(id)?.let { volume -> + scopes[id]?.cancel() volume.close() volumesData.filter { it.value == id }.forEach { volumesData.remove(it.key) @@ -35,7 +45,10 @@ class VolumeManager { } fun closeAll() { - volumes.forEach { it.value.close() } + volumes.forEach { + scopes[it.key]?.cancel() + it.value.close() + } volumes.clear() volumesData.clear() } 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 05c8384..d2ca695 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -29,6 +29,7 @@ import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter import sushi.hardcore.droidfs.content_providers.ExternalProvider import sushi.hardcore.droidfs.file_operations.FileOperationService import sushi.hardcore.droidfs.file_operations.OperationFile +import sushi.hardcore.droidfs.file_operations.TaskResult import sushi.hardcore.droidfs.file_viewers.* import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.Stat @@ -42,7 +43,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene private var foldersFirst = true private var mapFolders = true private var currentSortOrderIndex = 0 - private var volumeId = -1 + protected var volumeId = -1 protected lateinit var encryptedVolume: EncryptedVolume private lateinit var volumeName: String private lateinit var explorerViewModel: ExplorerViewModel @@ -52,7 +53,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene explorerViewModel.currentDirectoryPath = value } protected lateinit var fileOperationService: FileOperationService - protected val taskScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + protected val activityScope = MainScope() protected lateinit var explorerElements: MutableList protected lateinit var explorerAdapter: ExplorerElementAdapter protected lateinit var app: VolumeManagerApp @@ -171,11 +172,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene override fun onServiceConnected(className: ComponentName, service: IBinder) { val binder = service as FileOperationService.LocalBinder fileOperationService = binder.getService() - binder.setEncryptedVolume(encryptedVolume) - } - override fun onServiceDisconnected(arg0: ComponentName) { - } + override fun onServiceDisconnected(arg0: ComponentName) {} }, Context.BIND_AUTO_CREATE) } } @@ -439,7 +437,33 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene } } - protected fun importFilesFromUris(uris: List, callback: (String?) -> Unit) { + protected fun onTaskResult( + result: TaskResult, + failedErrorMessage: Int, + successMessage: Int = -1, + onSuccess: (() -> Unit)? = null, + ) { + when (result.state) { + TaskResult.State.SUCCESS -> { + if (onSuccess == null) { + Toast.makeText(this, successMessage, Toast.LENGTH_SHORT).show() + } else { + onSuccess() + } + } + TaskResult.State.FAILED -> { + CustomAlertDialogBuilder(this, theme) + .setTitle(R.string.error) + .setMessage(getString(failedErrorMessage, result.failedItem)) + .setPositiveButton(R.string.ok, null) + .show() + } + TaskResult.State.ERROR -> result.showErrorAlertDialog(this, theme) + TaskResult.State.CANCELLED -> {} + } + } + + protected fun importFilesFromUris(uris: List, callback: () -> Unit) { val items = ArrayList() for (uri in uris) { val fileName = PathUtils.getFilenameFromURI(this, uri) @@ -458,13 +482,10 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene if (items.size > 0) { checkPathOverwrite(items, currentDirectoryPath) { checkedItems -> checkedItems?.let { - taskScope.launch { - val taskResult = fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris) - if (taskResult.cancelled) { - setCurrentPath(currentDirectoryPath) - } else { - callback(taskResult.failedItem) - } + activityScope.launch { + val result = fileOperationService.importFilesFromUris(volumeId, checkedItems.map { it.dstPath!! }, uris) + onTaskResult(result, R.string.import_failed, onSuccess = callback) + setCurrentPath(currentDirectoryPath) } } } @@ -589,7 +610,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene override fun onDestroy() { super.onDestroy() if (!isChangingConfigurations) { //activity won't be recreated - taskScope.cancel() + activityScope.cancel() } } 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 50c1c26..5dd8b05 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -17,9 +17,7 @@ import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter import sushi.hardcore.droidfs.content_providers.ExternalProvider import sushi.hardcore.droidfs.file_operations.OperationFile -import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.Stat -import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.EditTextDialog @@ -36,7 +34,8 @@ class ExplorerActivity : BaseExplorerActivity() { private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.let { resultIntent -> - val remoteEncryptedVolume = IntentUtils.getParcelableExtra(resultIntent, "volume")!! + val srcVolumeId = resultIntent.getIntExtra("volumeId", -1) + val srcEncryptedVolume = app.volumeManager.getVolume(srcVolumeId)!! val path = resultIntent.getStringExtra("path") val operationFiles = ArrayList() if (path == null){ //multiples elements @@ -48,7 +47,7 @@ class ExplorerActivity : BaseExplorerActivity() { OperationFile(paths[i], types[i]) ) if (types[i] == Stat.S_IFDIR) { - remoteEncryptedVolume.recursiveMapFiles(paths[i])?.forEach { + srcEncryptedVolume.recursiveMapFiles(paths[i])?.forEach { operationFiles.add(OperationFile.fromExplorerElement(it)) } } @@ -64,17 +63,14 @@ class ExplorerActivity : BaseExplorerActivity() { if (items != null) { // stop loading thumbnails while writing files explorerAdapter.loadThumbnails = false - taskScope.launch { - val failedItem = fileOperationService.copyElements(items, remoteEncryptedVolume) - if (failedItem == null) { - Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show() - } else { - CustomAlertDialogBuilder(this@ExplorerActivity, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.import_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } + activityScope.launch { + onTaskResult( + fileOperationService.copyElements( + volumeId, + items, + srcVolumeId + ), R.string.import_failed, R.string.success_import + ) explorerAdapter.loadThumbnails = true setCurrentPath(currentDirectoryPath) } @@ -86,81 +82,62 @@ class ExplorerActivity : BaseExplorerActivity() { } private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> if (uris != null) { - importFilesFromUris(uris){ failedItem -> - onImportComplete(failedItem, uris) + for (uri in uris) { + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + importFilesFromUris(uris) { + onImportComplete(uris) } } } private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> if (uri != null) { - taskScope.launch { - val result = fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }) - if (!result.cancelled) { - if (result.failedItem == null) { - Toast.makeText(this@ExplorerActivity, R.string.success_export, Toast.LENGTH_SHORT).show() - } else { - CustomAlertDialogBuilder(this@ExplorerActivity, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.export_failed, result.failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } - } + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + val items = explorerAdapter.selectedItems.map { i -> explorerElements[i] } + activityScope.launch { + val result = fileOperationService.exportFiles(volumeId, items, uri) + onTaskResult(result, R.string.export_failed, R.string.success_export) } } unselectAll() } private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri -> rootUri?.let { + contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION) val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop val operation = OperationFile(PathUtils.pathJoin(currentDirectoryPath, tree.name!!), Stat.S_IFDIR) checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation -> checkedOperation?.let { - taskScope.launch { - val result = fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree) - if (result.taskResult.cancelled) { - setCurrentPath(currentDirectoryPath) - } else { - onImportComplete(result.taskResult.failedItem, result.uris, tree) + activityScope.launch { + val result = fileOperationService.importDirectory(volumeId, checkedOperation[0].dstPath!!, tree) + onTaskResult(result.taskResult, R.string.import_failed) { + onImportComplete(result.uris, tree) } + setCurrentPath(currentDirectoryPath) } } } } } - private fun onImportComplete(failedItem: String?, urisToWipe: List, rootFile: DocumentFile? = null) { - if (failedItem == null){ - CustomAlertDialogBuilder(this, theme) - .setTitle(R.string.success_import) - .setMessage(""" - ${getString(R.string.success_import_msg)} - ${getString(R.string.ask_for_wipe)} - """.trimIndent()) - .setPositiveButton(R.string.yes) { _, _ -> - taskScope.launch { - val errorMsg = fileOperationService.wipeUris(urisToWipe, rootFile) - if (errorMsg == null) { - Toast.makeText(this@ExplorerActivity, R.string.wipe_successful, Toast.LENGTH_SHORT).show() - } else { - CustomAlertDialogBuilder(this@ExplorerActivity, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.wipe_failed, errorMsg)) - .setPositiveButton(R.string.ok, null) - .show() - } - } + private fun onImportComplete(urisToWipe: List, rootFile: DocumentFile? = null) { + CustomAlertDialogBuilder(this, theme) + .setTitle(R.string.success_import) + .setMessage(""" + ${getString(R.string.success_import_msg)} + ${getString(R.string.ask_for_wipe)} + """.trimIndent()) + .setPositiveButton(R.string.yes) { _, _ -> + activityScope.launch { + onTaskResult( + fileOperationService.wipeUris(urisToWipe, rootFile), + R.string.wipe_failed, + R.string.wipe_successful, + ) } - .setNegativeButton(R.string.no, null) - .show() - } else { - CustomAlertDialogBuilder(this, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.import_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } - setCurrentPath(currentDirectoryPath) + } + .setNegativeButton(R.string.no, null) + .show() } override fun init() { @@ -324,22 +301,16 @@ class ExplorerActivity : BaseExplorerActivity() { } R.id.validate -> { if (currentItemAction == ItemsActions.COPY){ - checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> - items?.let { - taskScope.launch { - val failedItem = fileOperationService.copyElements(it.toMutableList() as ArrayList) - if (!isFinishing) { - if (failedItem == null) { - Toast.makeText(this@ExplorerActivity, R.string.copy_success, Toast.LENGTH_SHORT).show() - } else { - CustomAlertDialogBuilder(this@ExplorerActivity, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.copy_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } - setCurrentPath(currentDirectoryPath) - } + checkPathOverwrite(itemsToProcess, currentDirectoryPath) { + // copying before being cleared + it?.toMutableList()?.let { items -> + activityScope.launch { + onTaskResult( + fileOperationService.copyElements(volumeId, items), + R.string.copy_failed, + R.string.copy_success, + ) + setCurrentPath(currentDirectoryPath) } } cancelItemAction() @@ -357,17 +328,12 @@ class ExplorerActivity : BaseExplorerActivity() { toMove, toClean, ) { - taskScope.launch { - val failedItem = fileOperationService.moveElements(toMove, toClean) - if (failedItem == null) { - Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show() - } else { - CustomAlertDialogBuilder(this@ExplorerActivity, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.move_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } + activityScope.launch { + onTaskResult( + fileOperationService.moveElements(volumeId, toMove, toClean), + R.string.move_success, + R.string.move_failed, + ) setCurrentPath(currentDirectoryPath) } cancelItemAction() @@ -381,8 +347,9 @@ class ExplorerActivity : BaseExplorerActivity() { val dialog = CustomAlertDialogBuilder(this, theme) dialog.setTitle(R.string.warning) dialog.setPositiveButton(R.string.ok) { _, _ -> - taskScope.launch { - fileOperationService.removeElements(explorerAdapter.selectedItems.map { i -> explorerElements[i] })?.let { failedItem -> + val items = explorerAdapter.selectedItems.map { i -> explorerElements[i] } + activityScope.launch { + fileOperationService.removeElements(volumeId, items)?.let { failedItem -> CustomAlertDialogBuilder(this@ExplorerActivity, theme) .setTitle(R.string.error) .setMessage(getString(R.string.remove_failed, failedItem)) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt index 047ec1f..c2d9080 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt @@ -73,23 +73,15 @@ class ExplorerActivityDrop : BaseExplorerActivity() { } } - private fun onImported(failedItem: String?){ + private fun onImported() { setCurrentPath(currentDirectoryPath) - if (failedItem == null) { - CustomAlertDialogBuilder(this, theme) - .setTitle(R.string.success_import) - .setMessage(R.string.success_import_msg) - .setCancelable(false) - .setPositiveButton(R.string.ok){_, _ -> - finish() - } - .show() - } else { - CustomAlertDialogBuilder(this, theme) - .setTitle(R.string.error) - .setMessage(getString(R.string.import_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } + CustomAlertDialogBuilder(this, theme) + .setTitle(R.string.success_import) + .setMessage(R.string.success_import_msg) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> + finish() + } + .show() } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt index 281c552..0628250 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt @@ -14,7 +14,7 @@ class ExplorerActivityPick : BaseExplorerActivity() { private var isFinishingIntentionally = false override fun init() { setContentView(R.layout.activity_explorer_pick) - resultIntent.putExtra("volume", encryptedVolume) + resultIntent.putExtra("volumeId", volumeId) } override fun bindFileOperationService() { diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerRouter.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerRouter.kt index e3114e4..e287b7c 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerRouter.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerRouter.kt @@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.explorers import android.content.Context import android.content.Intent -import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.util.IntentUtils class ExplorerRouter(private val context: Context, private val intent: Intent) { @@ -16,7 +15,6 @@ class ExplorerRouter(private val context: Context, private val intent: Intent) { IntentUtils.forwardIntent(intent, explorerIntent) } else if (pickMode) { explorerIntent = Intent(context, ExplorerActivityPick::class.java) - explorerIntent.putExtra("destinationVolume", IntentUtils.getParcelableExtra(intent, "volume")!!) explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT } if (explorerIntent == null) { 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 e5e3413..a8f38cb 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 @@ -13,9 +13,18 @@ import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.documentfile.provider.DocumentFile -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.async +import kotlinx.coroutines.yield import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.R +import sushi.hardcore.droidfs.VolumeManager +import sushi.hardcore.droidfs.VolumeManagerApp import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.util.ObjRef @@ -31,19 +40,18 @@ class FileOperationService : Service() { } private val binder = LocalBinder() - private lateinit var encryptedVolume: EncryptedVolume + private lateinit var volumeManger: VolumeManager + private var serviceScope = MainScope() private lateinit var notificationManager: NotificationManagerCompat private val tasks = HashMap() private var lastNotificationId = 0 inner class LocalBinder : Binder() { fun getService(): FileOperationService = this@FileOperationService - fun setEncryptedVolume(volume: EncryptedVolume) { - encryptedVolume = volume - } } override fun onBind(p0: Intent?): IBinder { + volumeManger = (application as VolumeManagerApp).volumeManager return binder } @@ -110,29 +118,55 @@ class FileOperationService : Service() { tasks[notificationId]?.cancel() } - class TaskResult(val cancelled: Boolean, val failedItem: T?) - - private suspend fun waitForTask(notification: FileOperationNotification, task: Deferred): TaskResult { - tasks[notification.notificationId] = task - return try { - TaskResult(false, task.await()) - } catch (e: CancellationException) { - TaskResult(true, null) - } finally { - cancelNotification(notification) - } + private fun getEncryptedVolume(volumeId: Int): EncryptedVolume { + return volumeManger.getVolume(volumeId) ?: throw IllegalArgumentException("Invalid volumeId: $volumeId") } - private fun copyFile(srcPath: String, dstPath: String, remoteEncryptedVolume: EncryptedVolume = encryptedVolume): Boolean { + private suspend fun waitForTask(notification: FileOperationNotification, task: Deferred): TaskResult { + tasks[notification.notificationId] = task + return serviceScope.async { + try { + TaskResult.completed(task.await()) + } catch (e: CancellationException) { + TaskResult.cancelled() + } catch (e: Throwable) { + e.printStackTrace() + TaskResult.error(e.localizedMessage) + } finally { + cancelNotification(notification) + } + }.await() + } + + private suspend fun volumeTask( + volumeId: Int, + notification: FileOperationNotification, + task: suspend (encryptedVolume: EncryptedVolume) -> T + ): TaskResult { + return waitForTask( + notification, + volumeManger.getCoroutineScope(volumeId).async { + task(getEncryptedVolume(volumeId)) + } + ) + } + + private suspend fun copyFile( + encryptedVolume: EncryptedVolume, + srcPath: String, + dstPath: String, + srcEncryptedVolume: EncryptedVolume = encryptedVolume, + ): Boolean { var success = true - val srcFileHandle = remoteEncryptedVolume.openFileReadMode(srcPath) + val srcFileHandle = srcEncryptedVolume.openFileReadMode(srcPath) if (srcFileHandle != -1L) { val dstFileHandle = encryptedVolume.openFileWriteMode(dstPath) if (dstFileHandle != -1L) { var offset: Long = 0 val ioBuffer = ByteArray(Constants.IO_BUFF_SIZE) var length: Long - while (remoteEncryptedVolume.read(srcFileHandle, offset, ioBuffer, 0, ioBuffer.size.toLong()).also { length = it.toLong() } > 0) { + while (srcEncryptedVolume.read(srcFileHandle, offset, ioBuffer, 0, ioBuffer.size.toLong()).also { length = it.toLong() } > 0) { + yield() val written = encryptedVolume.write(dstFileHandle, offset, ioBuffer, 0, length).toLong() if (written == length) { offset += written @@ -146,7 +180,7 @@ class FileOperationService : Service() { } else { success = false } - remoteEncryptedVolume.closeFile(srcFileHandle) + srcEncryptedVolume.closeFile(srcFileHandle) } else { success = false } @@ -154,25 +188,24 @@ class FileOperationService : Service() { } suspend fun copyElements( - items: ArrayList, - remoteEncryptedVolume: EncryptedVolume = encryptedVolume - ): String? = coroutineScope { + volumeId: Int, + items: List, + srcVolumeId: Int = volumeId, + ): TaskResult { val notification = showNotification(R.string.file_op_copy_msg, items.size) - val task = async { + val srcEncryptedVolume = getEncryptedVolume(srcVolumeId) + return volumeTask(volumeId, notification) { encryptedVolume -> var failedItem: String? = null - for (i in 0 until items.size) { - withContext(Dispatchers.IO) { - if (items[i].isDirectory) { - if (!encryptedVolume.pathExists(items[i].dstPath!!)) { - if (!encryptedVolume.mkdir(items[i].dstPath!!)) { - failedItem = items[i].srcPath - } - } - } else { - if (!copyFile(items[i].srcPath, items[i].dstPath!!, remoteEncryptedVolume)) { + for (i in items.indices) { + yield() + if (items[i].isDirectory) { + if (!encryptedVolume.pathExists(items[i].dstPath!!)) { + if (!encryptedVolume.mkdir(items[i].dstPath!!)) { failedItem = items[i].srcPath } } + } else if (!copyFile(encryptedVolume, items[i].srcPath, items[i].dstPath!!, srcEncryptedVolume)) { + failedItem = items[i].srcPath } if (failedItem == null) { updateNotificationProgress(notification, i+1, items.size) @@ -182,13 +215,11 @@ class FileOperationService : Service() { } failedItem } - // treat cancellation as success - waitForTask(notification, task).failedItem } - suspend fun moveElements(toMove: List, toClean: List): String? = coroutineScope { + suspend fun moveElements(volumeId: Int, toMove: List, toClean: List): TaskResult { val notification = showNotification(R.string.file_op_move_msg, toMove.size) - val task = async(Dispatchers.IO) { + return volumeTask(volumeId, notification) { encryptedVolume -> val total = toMove.size+toClean.size var failedItem: String? = null for ((i, item) in toMove.withIndex()) { @@ -211,25 +242,23 @@ class FileOperationService : Service() { } failedItem } - // treat cancellation as success - waitForTask(notification, task).failedItem } private suspend fun importFilesFromUris( + encryptedVolume: EncryptedVolume, dstPaths: List, uris: List, notification: FileOperationNotification, ): String? { var failedIndex = -1 for (i in dstPaths.indices) { - withContext(Dispatchers.IO) { - try { - if (!encryptedVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) { - failedIndex = i - } - } catch (e: FileNotFoundException) { + yield() + try { + if (!encryptedVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) { failedIndex = i } + } catch (e: FileNotFoundException) { + failedIndex = i } if (failedIndex == -1) { updateNotificationProgress(notification, i+1, dstPaths.size) @@ -240,12 +269,11 @@ class FileOperationService : Service() { return null } - suspend fun importFilesFromUris(dstPaths: List, uris: List): TaskResult = coroutineScope { + suspend fun importFilesFromUris(volumeId: Int, dstPaths: List, uris: List): TaskResult { val notification = showNotification(R.string.file_op_import_msg, dstPaths.size) - val task = async { - importFilesFromUris(dstPaths, uris, notification) + return volumeTask(volumeId, notification) { encryptedVolume -> + importFilesFromUris(encryptedVolume, dstPaths, uris, notification) } - waitForTask(notification, task) } /** @@ -255,77 +283,63 @@ class FileOperationService : Service() { * * @return false if cancelled early, true otherwise. */ - private fun recursiveMapDirectoryForImport( + private suspend fun recursiveMapDirectoryForImport( rootSrcDir: DocumentFile, rootDstPath: String, dstFiles: ArrayList, srcUris: ArrayList, dstDirs: ArrayList, - scope: CoroutineScope, - ): Boolean { + ) { dstDirs.add(rootDstPath) for (child in rootSrcDir.listFiles()) { - if (!scope.isActive) { - return false - } + yield() child.name?.let { name -> val subPath = PathUtils.pathJoin(rootDstPath, name) if (child.isDirectory) { - if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) { - return false - } - } - else if (child.isFile) { + recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs) + } else if (child.isFile) { srcUris.add(child.uri) dstFiles.add(subPath) } } } - return true } - class ImportDirectoryResult(val taskResult: TaskResult, val uris: List) + class ImportDirectoryResult(val taskResult: TaskResult, val uris: List) suspend fun importDirectory( + volumeId: Int, rootDstPath: String, rootSrcDir: DocumentFile, - ): ImportDirectoryResult = coroutineScope { + ): ImportDirectoryResult { val notification = showNotification(R.string.file_op_import_msg, null) val srcUris = arrayListOf() - val task = async { + return ImportDirectoryResult(volumeTask(volumeId, notification) { encryptedVolume -> var failedItem: String? = null val dstFiles = arrayListOf() val dstDirs = arrayListOf() - - withContext(Dispatchers.IO) { - if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) { - return@withContext - } - - // create destination folders so the new files can use them - for (dir in dstDirs) { - if (!encryptedVolume.mkdir(dir)) { - failedItem = dir - break - } + recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs) + // create destination folders so the new files can use them + for (dir in dstDirs) { + if (!encryptedVolume.mkdir(dir)) { + failedItem = dir + break } } if (failedItem == null) { - failedItem = importFilesFromUris(dstFiles, srcUris, notification) + failedItem = importFilesFromUris(encryptedVolume, dstFiles, srcUris, notification) } failedItem - } - ImportDirectoryResult(waitForTask(notification, task), srcUris) + }, srcUris) } - suspend fun wipeUris(uris: List, rootFile: DocumentFile? = null): String? = coroutineScope { + suspend fun wipeUris(uris: List, rootFile: DocumentFile? = null): TaskResult { val notification = showNotification(R.string.file_op_wiping_msg, uris.size) - val task = async { + val task = serviceScope.async(Dispatchers.IO) { var errorMsg: String? = null for (i in uris.indices) { - withContext(Dispatchers.IO) { - errorMsg = Wiper.wipe(this@FileOperationService, uris[i]) - } + yield() + errorMsg = Wiper.wipe(this@FileOperationService, uris[i]) if (errorMsg == null) { updateNotificationProgress(notification, i+1, uris.size) } else { @@ -337,11 +351,10 @@ class FileOperationService : Service() { } errorMsg } - // treat cancellation as success - waitForTask(notification, task).failedItem + return waitForTask(notification, task) } - private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean { + private fun exportFileInto(encryptedVolume: EncryptedVolume, srcPath: String, treeDocumentFile: DocumentFile): Boolean { val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let { contentResolver.openOutputStream(it) } @@ -352,25 +365,20 @@ class FileOperationService : Service() { } } - private fun recursiveExportDirectory( + private suspend fun recursiveExportDirectory( + encryptedVolume: EncryptedVolume, plain_directory_path: String, treeDocumentFile: DocumentFile, - scope: CoroutineScope ): String? { treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree -> val explorerElements = encryptedVolume.readDir(plain_directory_path) ?: return null for (e in explorerElements) { - if (!scope.isActive) { - return null - } + yield() val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) if (e.isDirectory) { - val failedItem = recursiveExportDirectory(fullPath, childTree, scope) - failedItem?.let { return it } - } else { - if (!exportFileInto(fullPath, childTree)){ - return fullPath - } + recursiveExportDirectory(encryptedVolume, fullPath, childTree)?.let { return it } + } else if (!exportFileInto(encryptedVolume, fullPath, childTree)) { + return fullPath } } return null @@ -378,18 +386,20 @@ class FileOperationService : Service() { return treeDocumentFile.name } - suspend fun exportFiles(uri: Uri, items: List): TaskResult = coroutineScope { + suspend fun exportFiles(volumeId: Int, items: List, uri: Uri): TaskResult { val notification = showNotification(R.string.file_op_export_msg, items.size) - val task = async { - contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + return volumeTask(volumeId, notification) { encryptedVolume -> val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!! var failedItem: String? = null for (i in items.indices) { - withContext(Dispatchers.IO) { - failedItem = if (items[i].isDirectory) { - recursiveExportDirectory(items[i].fullPath, treeDocumentFile, this) + yield() + failedItem = if (items[i].isDirectory) { + recursiveExportDirectory(encryptedVolume, items[i].fullPath, treeDocumentFile) + } else { + if (exportFileInto(encryptedVolume, items[i].fullPath, treeDocumentFile)) { + null } else { - if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath + items[i].fullPath } } if (failedItem == null) { @@ -400,21 +410,37 @@ class FileOperationService : Service() { } failedItem } - waitForTask(notification, task) } - suspend fun removeElements(items: List): String? = coroutineScope { + private suspend fun recursiveRemoveDirectory(encryptedVolume: EncryptedVolume, path: String): String? { + encryptedVolume.readDir(path)?.let { elements -> + for (e in elements) { + yield() + val fullPath = PathUtils.pathJoin(path, e.name) + if (e.isDirectory) { + recursiveRemoveDirectory(encryptedVolume, fullPath)?.let { return it } + } else if (!encryptedVolume.deleteFile(fullPath)) { + return fullPath + } + } + } + return if (!encryptedVolume.rmdir(path)) { + path + } else { + null + } + } + + suspend fun removeElements(volumeId: Int, items: List): String? { val notification = showNotification(R.string.file_op_delete_msg, items.size) - val task = async(Dispatchers.IO) { + return volumeTask(volumeId, notification) { encryptedVolume -> var failedItem: String? = null for ((i, element) in items.withIndex()) { + yield() if (element.isDirectory) { - val result = encryptedVolume.recursiveRemoveDirectory(element.fullPath) - result?.let { failedItem = it } - } else { - if (!encryptedVolume.deleteFile(element.fullPath)) { - failedItem = element.fullPath - } + recursiveRemoveDirectory(encryptedVolume, element.fullPath)?.let { failedItem = it } + } else if (!encryptedVolume.deleteFile(element.fullPath)) { + failedItem = element.fullPath } if (failedItem == null) { updateNotificationProgress(notification, i + 1, items.size) @@ -423,14 +449,11 @@ class FileOperationService : Service() { } } failedItem - } - waitForTask(notification, task).failedItem + }.failedItem // treat cancellation as success } - private fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int { - if (!scope.isActive) { - return 0 - } + private suspend fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int { + yield() val children = rootDirectory.listFiles() var count = children.size for (child in children) { @@ -441,7 +464,7 @@ class FileOperationService : Service() { return count } - private fun recursiveCopyVolume( + private suspend fun recursiveCopyVolume( src: DocumentFile, dst: DocumentFile, dstRootDirectory: ObjRef?, @@ -453,9 +476,7 @@ class FileOperationService : Service() { val dstDir = dst.createDirectory(src.name ?: return src) ?: return src dstRootDirectory?.let { it.value = dstDir } for (child in src.listFiles()) { - if (!scope.isActive) { - return null - } + yield() if (child.isFile) { val dstFile = dstDir.createFile("", child.name ?: return child) ?: return child val outputStream = contentResolver.openOutputStream(dstFile.uri) @@ -474,21 +495,16 @@ class FileOperationService : Service() { return null } - class CopyVolumeResult(val taskResult: TaskResult, val dstRootDirectory: DocumentFile?) + class CopyVolumeResult(val taskResult: TaskResult, val dstRootDirectory: DocumentFile?) - suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult = coroutineScope { + suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult { val notification = showNotification(R.string.copy_volume_notification, null) val dstRootDirectory = ObjRef(null) - val task = async(Dispatchers.IO) { + val task = serviceScope.async(Dispatchers.IO) { val total = recursiveCountChildElements(src, this) - if (isActive) { - updateNotificationProgress(notification, 0, total) - recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this) - } else { - null - } + updateNotificationProgress(notification, 0, total) + recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this) } - // treat cancellation as success - CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value) + return CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value) } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/TaskResult.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/TaskResult.kt new file mode 100644 index 0000000..7cfd5cd --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/TaskResult.kt @@ -0,0 +1,47 @@ +package sushi.hardcore.droidfs.file_operations + +import android.content.Context +import sushi.hardcore.droidfs.R +import sushi.hardcore.droidfs.Theme +import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder + +class TaskResult private constructor(val state: State, val failedItem: T?, val errorMessage: String?) { + enum class State { + SUCCESS, + /** + * Task completed but failed + */ + FAILED, + /** + * Task thrown an exception + */ + ERROR, + CANCELLED, + } + + fun showErrorAlertDialog(context: Context, theme: Theme) { + CustomAlertDialogBuilder(context, theme) + .setTitle(R.string.error) + .setMessage(context.getString(R.string.task_failed, errorMessage)) + .setPositiveButton(R.string.ok, null) + .show() + } + + companion object { + fun completed(failedItem: T?): TaskResult { + return if (failedItem == null) { + TaskResult(State.SUCCESS, null, null) + } else { + TaskResult(State.FAILED, failedItem, null) + } + } + + fun error(errorMessage: String?): TaskResult { + return TaskResult(State.ERROR, null, errorMessage) + } + + fun cancelled(): TaskResult { + return TaskResult(State.CANCELLED, null, null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt index a03244e..200e374 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt @@ -218,25 +218,4 @@ abstract class EncryptedVolume: Parcelable { } return result } - - fun recursiveRemoveDirectory(path: String): String? { - readDir(path)?.let { elements -> - for (e in elements) { - val fullPath = PathUtils.pathJoin(path, e.name) - if (e.isDirectory) { - val result = recursiveRemoveDirectory(fullPath) - result?.let { return it } - } else { - if (!deleteFile(fullPath)) { - return fullPath - } - } - } - } - return if (!rmdir(path)) { - path - } else { - null - } - } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d2081f..12d9f79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -261,4 +261,5 @@ The configuration file cannot be loaded. Make sure the volume is accessible. The configuration file cannot be decrypted. Please check your password. The filesystem id in the config file is different to the last time we opened this volume. This could mean an attacker replaced the filesystem with a different one. + The task failed: %s