Improve file oprations coroutines

This commit is contained in:
Matéo Duparc 2023-05-09 23:13:46 +02:00
parent f85f9d1c44
commit 2ae41f0f79
Signed by untrusted user: hardcoresushi
GPG Key ID: AFE384344A45E13A
11 changed files with 330 additions and 294 deletions

View File

@ -24,6 +24,7 @@ import sushi.hardcore.droidfs.databinding.ActivityMainBinding
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerRouter import sushi.hardcore.droidfs.explorers.ExplorerRouter
import sushi.hardcore.droidfs.file_operations.FileOperationService 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.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder 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?) { private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> VolumeData?) {
lifecycleScope.launch { lifecycleScope.launch {
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile) val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
when { when (result.taskResult.state) {
result.taskResult.cancelled -> { TaskResult.State.CANCELLED -> {
result.dstRootDirectory?.delete() result.dstRootDirectory?.delete()
} }
result.taskResult.failedItem == null -> { TaskResult.State.SUCCESS -> {
result.dstRootDirectory?.let { result.dstRootDirectory?.let {
getResultVolume(it)?.let { volume -> getResultVolume(it)?.let { volume ->
volumeDatabase.saveVolume(volume) volumeDatabase.saveVolume(volume)
@ -435,13 +436,14 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
} }
else -> { TaskResult.State.FAILED -> {
CustomAlertDialogBuilder(this@MainActivity, theme) CustomAlertDialogBuilder(this@MainActivity, theme)
.setTitle(R.string.error) .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) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
TaskResult.State.ERROR -> result.taskResult.showErrorAlertDialog(this@MainActivity, theme)
} }
} }
} }

View File

@ -1,11 +1,16 @@
package sushi.hardcore.droidfs 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 import sushi.hardcore.droidfs.filesystems.EncryptedVolume
class VolumeManager { class VolumeManager {
private var id = 0 private var id = 0
private val volumes = HashMap<Int, EncryptedVolume>() private val volumes = HashMap<Int, EncryptedVolume>()
private val volumesData = HashMap<VolumeData, Int>() private val volumesData = HashMap<VolumeData, Int>()
private val scopes = HashMap<Int, CoroutineScope>()
fun insert(volume: EncryptedVolume, data: VolumeData): Int { fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume volumes[id] = volume
@ -25,8 +30,13 @@ class VolumeManager {
return volumes[id] return volumes[id]
} }
fun getCoroutineScope(volumeId: Int): CoroutineScope {
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it }
}
fun closeVolume(id: Int) { fun closeVolume(id: Int) {
volumes.remove(id)?.let { volume -> volumes.remove(id)?.let { volume ->
scopes[id]?.cancel()
volume.close() volume.close()
volumesData.filter { it.value == id }.forEach { volumesData.filter { it.value == id }.forEach {
volumesData.remove(it.key) volumesData.remove(it.key)
@ -35,7 +45,10 @@ class VolumeManager {
} }
fun closeAll() { fun closeAll() {
volumes.forEach { it.value.close() } volumes.forEach {
scopes[it.key]?.cancel()
it.value.close()
}
volumes.clear() volumes.clear()
volumesData.clear() volumesData.clear()
} }

View File

@ -29,6 +29,7 @@ import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
import sushi.hardcore.droidfs.content_providers.ExternalProvider import sushi.hardcore.droidfs.content_providers.ExternalProvider
import sushi.hardcore.droidfs.file_operations.FileOperationService import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_operations.TaskResult
import sushi.hardcore.droidfs.file_viewers.* import sushi.hardcore.droidfs.file_viewers.*
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
@ -42,7 +43,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private var foldersFirst = true private var foldersFirst = true
private var mapFolders = true private var mapFolders = true
private var currentSortOrderIndex = 0 private var currentSortOrderIndex = 0
private var volumeId = -1 protected var volumeId = -1
protected lateinit var encryptedVolume: EncryptedVolume protected lateinit var encryptedVolume: EncryptedVolume
private lateinit var volumeName: String private lateinit var volumeName: String
private lateinit var explorerViewModel: ExplorerViewModel private lateinit var explorerViewModel: ExplorerViewModel
@ -52,7 +53,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
explorerViewModel.currentDirectoryPath = value explorerViewModel.currentDirectoryPath = value
} }
protected lateinit var fileOperationService: FileOperationService protected lateinit var fileOperationService: FileOperationService
protected val taskScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) protected val activityScope = MainScope()
protected lateinit var explorerElements: MutableList<ExplorerElement> protected lateinit var explorerElements: MutableList<ExplorerElement>
protected lateinit var explorerAdapter: ExplorerElementAdapter protected lateinit var explorerAdapter: ExplorerElementAdapter
protected lateinit var app: VolumeManagerApp protected lateinit var app: VolumeManagerApp
@ -171,11 +172,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as FileOperationService.LocalBinder val binder = service as FileOperationService.LocalBinder
fileOperationService = binder.getService() fileOperationService = binder.getService()
binder.setEncryptedVolume(encryptedVolume)
}
override fun onServiceDisconnected(arg0: ComponentName) {
} }
override fun onServiceDisconnected(arg0: ComponentName) {}
}, Context.BIND_AUTO_CREATE) }, Context.BIND_AUTO_CREATE)
} }
} }
@ -439,7 +437,33 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
} }
protected fun importFilesFromUris(uris: List<Uri>, callback: (String?) -> Unit) { protected fun onTaskResult(
result: TaskResult<out String?>,
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<Uri>, callback: () -> Unit) {
val items = ArrayList<OperationFile>() val items = ArrayList<OperationFile>()
for (uri in uris) { for (uri in uris) {
val fileName = PathUtils.getFilenameFromURI(this, uri) val fileName = PathUtils.getFilenameFromURI(this, uri)
@ -458,13 +482,10 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (items.size > 0) { if (items.size > 0) {
checkPathOverwrite(items, currentDirectoryPath) { checkedItems -> checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
checkedItems?.let { checkedItems?.let {
taskScope.launch { activityScope.launch {
val taskResult = fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris) val result = fileOperationService.importFilesFromUris(volumeId, checkedItems.map { it.dstPath!! }, uris)
if (taskResult.cancelled) { onTaskResult(result, R.string.import_failed, onSuccess = callback)
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} else {
callback(taskResult.failedItem)
}
} }
} }
} }
@ -589,7 +610,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if (!isChangingConfigurations) { //activity won't be recreated if (!isChangingConfigurations) { //activity won't be recreated
taskScope.cancel() activityScope.cancel()
} }
} }

View File

@ -17,9 +17,7 @@ import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
import sushi.hardcore.droidfs.content_providers.ExternalProvider import sushi.hardcore.droidfs.content_providers.ExternalProvider
import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -36,7 +34,8 @@ class ExplorerActivity : BaseExplorerActivity() {
private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { resultIntent -> result.data?.let { resultIntent ->
val remoteEncryptedVolume = IntentUtils.getParcelableExtra<EncryptedVolume>(resultIntent, "volume")!! val srcVolumeId = resultIntent.getIntExtra("volumeId", -1)
val srcEncryptedVolume = app.volumeManager.getVolume(srcVolumeId)!!
val path = resultIntent.getStringExtra("path") val path = resultIntent.getStringExtra("path")
val operationFiles = ArrayList<OperationFile>() val operationFiles = ArrayList<OperationFile>()
if (path == null){ //multiples elements if (path == null){ //multiples elements
@ -48,7 +47,7 @@ class ExplorerActivity : BaseExplorerActivity() {
OperationFile(paths[i], types[i]) OperationFile(paths[i], types[i])
) )
if (types[i] == Stat.S_IFDIR) { if (types[i] == Stat.S_IFDIR) {
remoteEncryptedVolume.recursiveMapFiles(paths[i])?.forEach { srcEncryptedVolume.recursiveMapFiles(paths[i])?.forEach {
operationFiles.add(OperationFile.fromExplorerElement(it)) operationFiles.add(OperationFile.fromExplorerElement(it))
} }
} }
@ -64,17 +63,14 @@ class ExplorerActivity : BaseExplorerActivity() {
if (items != null) { if (items != null) {
// stop loading thumbnails while writing files // stop loading thumbnails while writing files
explorerAdapter.loadThumbnails = false explorerAdapter.loadThumbnails = false
taskScope.launch { activityScope.launch {
val failedItem = fileOperationService.copyElements(items, remoteEncryptedVolume) onTaskResult(
if (failedItem == null) { fileOperationService.copyElements(
Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show() volumeId,
} else { items,
CustomAlertDialogBuilder(this@ExplorerActivity, theme) srcVolumeId
.setTitle(R.string.error) ), R.string.import_failed, R.string.success_import
.setMessage(getString(R.string.import_failed, failedItem)) )
.setPositiveButton(R.string.ok, null)
.show()
}
explorerAdapter.loadThumbnails = true explorerAdapter.loadThumbnails = true
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} }
@ -86,81 +82,62 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
if (uris != null) { if (uris != null) {
importFilesFromUris(uris){ failedItem -> for (uri in uris) {
onImportComplete(failedItem, uris) contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
importFilesFromUris(uris) {
onImportComplete(uris)
} }
} }
} }
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
if (uri != null) { if (uri != null) {
taskScope.launch { contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val result = fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }) val items = explorerAdapter.selectedItems.map { i -> explorerElements[i] }
if (!result.cancelled) { activityScope.launch {
if (result.failedItem == null) { val result = fileOperationService.exportFiles(volumeId, items, uri)
Toast.makeText(this@ExplorerActivity, R.string.success_export, Toast.LENGTH_SHORT).show() onTaskResult(result, R.string.export_failed, R.string.success_export)
} else {
CustomAlertDialogBuilder(this@ExplorerActivity, theme)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, result.failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
}
} }
} }
unselectAll() unselectAll()
} }
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri -> private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
rootUri?.let { rootUri?.let {
contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop
val operation = OperationFile(PathUtils.pathJoin(currentDirectoryPath, tree.name!!), Stat.S_IFDIR) val operation = OperationFile(PathUtils.pathJoin(currentDirectoryPath, tree.name!!), Stat.S_IFDIR)
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation -> checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
checkedOperation?.let { checkedOperation?.let {
taskScope.launch { activityScope.launch {
val result = fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree) val result = fileOperationService.importDirectory(volumeId, checkedOperation[0].dstPath!!, tree)
if (result.taskResult.cancelled) { onTaskResult(result.taskResult, R.string.import_failed) {
setCurrentPath(currentDirectoryPath) onImportComplete(result.uris, tree)
} else {
onImportComplete(result.taskResult.failedItem, result.uris, tree)
} }
setCurrentPath(currentDirectoryPath)
} }
} }
} }
} }
} }
private fun onImportComplete(failedItem: String?, urisToWipe: List<Uri>, rootFile: DocumentFile? = null) { private fun onImportComplete(urisToWipe: List<Uri>, rootFile: DocumentFile? = null) {
if (failedItem == null){ CustomAlertDialogBuilder(this, theme)
CustomAlertDialogBuilder(this, theme) .setTitle(R.string.success_import)
.setTitle(R.string.success_import) .setMessage("""
.setMessage(""" ${getString(R.string.success_import_msg)}
${getString(R.string.success_import_msg)} ${getString(R.string.ask_for_wipe)}
${getString(R.string.ask_for_wipe)} """.trimIndent())
""".trimIndent()) .setPositiveButton(R.string.yes) { _, _ ->
.setPositiveButton(R.string.yes) { _, _ -> activityScope.launch {
taskScope.launch { onTaskResult(
val errorMsg = fileOperationService.wipeUris(urisToWipe, rootFile) fileOperationService.wipeUris(urisToWipe, rootFile),
if (errorMsg == null) { R.string.wipe_failed,
Toast.makeText(this@ExplorerActivity, R.string.wipe_successful, Toast.LENGTH_SHORT).show() R.string.wipe_successful,
} else { )
CustomAlertDialogBuilder(this@ExplorerActivity, theme)
.setTitle(R.string.error)
.setMessage(getString(R.string.wipe_failed, errorMsg))
.setPositiveButton(R.string.ok, null)
.show()
}
}
} }
.setNegativeButton(R.string.no, null) }
.show() .setNegativeButton(R.string.no, null)
} else { .show()
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
setCurrentPath(currentDirectoryPath)
} }
override fun init() { override fun init() {
@ -324,22 +301,16 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
R.id.validate -> { R.id.validate -> {
if (currentItemAction == ItemsActions.COPY){ if (currentItemAction == ItemsActions.COPY){
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> checkPathOverwrite(itemsToProcess, currentDirectoryPath) {
items?.let { // copying before being cleared
taskScope.launch { it?.toMutableList()?.let { items ->
val failedItem = fileOperationService.copyElements(it.toMutableList() as ArrayList<OperationFile>) activityScope.launch {
if (!isFinishing) { onTaskResult(
if (failedItem == null) { fileOperationService.copyElements(volumeId, items),
Toast.makeText(this@ExplorerActivity, R.string.copy_success, Toast.LENGTH_SHORT).show() R.string.copy_failed,
} else { R.string.copy_success,
CustomAlertDialogBuilder(this@ExplorerActivity, theme) )
.setTitle(R.string.error) setCurrentPath(currentDirectoryPath)
.setMessage(getString(R.string.copy_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
setCurrentPath(currentDirectoryPath)
}
} }
} }
cancelItemAction() cancelItemAction()
@ -357,17 +328,12 @@ class ExplorerActivity : BaseExplorerActivity() {
toMove, toMove,
toClean, toClean,
) { ) {
taskScope.launch { activityScope.launch {
val failedItem = fileOperationService.moveElements(toMove, toClean) onTaskResult(
if (failedItem == null) { fileOperationService.moveElements(volumeId, toMove, toClean),
Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show() R.string.move_success,
} else { R.string.move_failed,
CustomAlertDialogBuilder(this@ExplorerActivity, theme) )
.setTitle(R.string.error)
.setMessage(getString(R.string.move_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} }
cancelItemAction() cancelItemAction()
@ -381,8 +347,9 @@ class ExplorerActivity : BaseExplorerActivity() {
val dialog = CustomAlertDialogBuilder(this, theme) val dialog = CustomAlertDialogBuilder(this, theme)
dialog.setTitle(R.string.warning) dialog.setTitle(R.string.warning)
dialog.setPositiveButton(R.string.ok) { _, _ -> dialog.setPositiveButton(R.string.ok) { _, _ ->
taskScope.launch { val items = explorerAdapter.selectedItems.map { i -> explorerElements[i] }
fileOperationService.removeElements(explorerAdapter.selectedItems.map { i -> explorerElements[i] })?.let { failedItem -> activityScope.launch {
fileOperationService.removeElements(volumeId, items)?.let { failedItem ->
CustomAlertDialogBuilder(this@ExplorerActivity, theme) CustomAlertDialogBuilder(this@ExplorerActivity, theme)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(getString(R.string.remove_failed, failedItem)) .setMessage(getString(R.string.remove_failed, failedItem))

View File

@ -73,23 +73,15 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
} }
} }
private fun onImported(failedItem: String?){ private fun onImported() {
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
if (failedItem == null) { CustomAlertDialogBuilder(this, theme)
CustomAlertDialogBuilder(this, theme) .setTitle(R.string.success_import)
.setTitle(R.string.success_import) .setMessage(R.string.success_import_msg)
.setMessage(R.string.success_import_msg) .setCancelable(false)
.setCancelable(false) .setPositiveButton(R.string.ok) { _, _ ->
.setPositiveButton(R.string.ok){_, _ -> finish()
finish() }
} .show()
.show()
} else {
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
} }
} }

View File

@ -14,7 +14,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
private var isFinishingIntentionally = false private var isFinishingIntentionally = false
override fun init() { override fun init() {
setContentView(R.layout.activity_explorer_pick) setContentView(R.layout.activity_explorer_pick)
resultIntent.putExtra("volume", encryptedVolume) resultIntent.putExtra("volumeId", volumeId)
} }
override fun bindFileOperationService() { override fun bindFileOperationService() {

View File

@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.explorers
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.IntentUtils
class ExplorerRouter(private val context: Context, private val intent: Intent) { 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) IntentUtils.forwardIntent(intent, explorerIntent)
} else if (pickMode) { } else if (pickMode) {
explorerIntent = Intent(context, ExplorerActivityPick::class.java) explorerIntent = Intent(context, ExplorerActivityPick::class.java)
explorerIntent.putExtra("destinationVolume", IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")!!)
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
} }
if (explorerIntent == null) { if (explorerIntent == null) {

View File

@ -13,9 +13,18 @@ import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.documentfile.provider.DocumentFile 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.Constants
import sushi.hardcore.droidfs.R 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.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
@ -31,19 +40,18 @@ class FileOperationService : Service() {
} }
private val binder = LocalBinder() private val binder = LocalBinder()
private lateinit var encryptedVolume: EncryptedVolume private lateinit var volumeManger: VolumeManager
private var serviceScope = MainScope()
private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationManager: NotificationManagerCompat
private val tasks = HashMap<Int, Job>() private val tasks = HashMap<Int, Job>()
private var lastNotificationId = 0 private var lastNotificationId = 0
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getService(): FileOperationService = this@FileOperationService fun getService(): FileOperationService = this@FileOperationService
fun setEncryptedVolume(volume: EncryptedVolume) {
encryptedVolume = volume
}
} }
override fun onBind(p0: Intent?): IBinder { override fun onBind(p0: Intent?): IBinder {
volumeManger = (application as VolumeManagerApp).volumeManager
return binder return binder
} }
@ -110,29 +118,55 @@ class FileOperationService : Service() {
tasks[notificationId]?.cancel() tasks[notificationId]?.cancel()
} }
class TaskResult<T>(val cancelled: Boolean, val failedItem: T?) private fun getEncryptedVolume(volumeId: Int): EncryptedVolume {
return volumeManger.getVolume(volumeId) ?: throw IllegalArgumentException("Invalid volumeId: $volumeId")
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<T> {
tasks[notification.notificationId] = task
return try {
TaskResult(false, task.await())
} catch (e: CancellationException) {
TaskResult(true, null)
} finally {
cancelNotification(notification)
}
} }
private fun copyFile(srcPath: String, dstPath: String, remoteEncryptedVolume: EncryptedVolume = encryptedVolume): Boolean { private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<out T> {
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 <T> volumeTask(
volumeId: Int,
notification: FileOperationNotification,
task: suspend (encryptedVolume: EncryptedVolume) -> T
): TaskResult<out T> {
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 var success = true
val srcFileHandle = remoteEncryptedVolume.openFileReadMode(srcPath) val srcFileHandle = srcEncryptedVolume.openFileReadMode(srcPath)
if (srcFileHandle != -1L) { if (srcFileHandle != -1L) {
val dstFileHandle = encryptedVolume.openFileWriteMode(dstPath) val dstFileHandle = encryptedVolume.openFileWriteMode(dstPath)
if (dstFileHandle != -1L) { if (dstFileHandle != -1L) {
var offset: Long = 0 var offset: Long = 0
val ioBuffer = ByteArray(Constants.IO_BUFF_SIZE) val ioBuffer = ByteArray(Constants.IO_BUFF_SIZE)
var length: Long 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() val written = encryptedVolume.write(dstFileHandle, offset, ioBuffer, 0, length).toLong()
if (written == length) { if (written == length) {
offset += written offset += written
@ -146,7 +180,7 @@ class FileOperationService : Service() {
} else { } else {
success = false success = false
} }
remoteEncryptedVolume.closeFile(srcFileHandle) srcEncryptedVolume.closeFile(srcFileHandle)
} else { } else {
success = false success = false
} }
@ -154,25 +188,24 @@ class FileOperationService : Service() {
} }
suspend fun copyElements( suspend fun copyElements(
items: ArrayList<OperationFile>, volumeId: Int,
remoteEncryptedVolume: EncryptedVolume = encryptedVolume items: List<OperationFile>,
): String? = coroutineScope { srcVolumeId: Int = volumeId,
): TaskResult<out String?> {
val notification = showNotification(R.string.file_op_copy_msg, items.size) 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 var failedItem: String? = null
for (i in 0 until items.size) { for (i in items.indices) {
withContext(Dispatchers.IO) { yield()
if (items[i].isDirectory) { if (items[i].isDirectory) {
if (!encryptedVolume.pathExists(items[i].dstPath!!)) { if (!encryptedVolume.pathExists(items[i].dstPath!!)) {
if (!encryptedVolume.mkdir(items[i].dstPath!!)) { if (!encryptedVolume.mkdir(items[i].dstPath!!)) {
failedItem = items[i].srcPath
}
}
} else {
if (!copyFile(items[i].srcPath, items[i].dstPath!!, remoteEncryptedVolume)) {
failedItem = items[i].srcPath failedItem = items[i].srcPath
} }
} }
} else if (!copyFile(encryptedVolume, items[i].srcPath, items[i].dstPath!!, srcEncryptedVolume)) {
failedItem = items[i].srcPath
} }
if (failedItem == null) { if (failedItem == null) {
updateNotificationProgress(notification, i+1, items.size) updateNotificationProgress(notification, i+1, items.size)
@ -182,13 +215,11 @@ class FileOperationService : Service() {
} }
failedItem failedItem
} }
// treat cancellation as success
waitForTask(notification, task).failedItem
} }
suspend fun moveElements(toMove: List<OperationFile>, toClean: List<String>): String? = coroutineScope { suspend fun moveElements(volumeId: Int, toMove: List<OperationFile>, toClean: List<String>): TaskResult<out String?> {
val notification = showNotification(R.string.file_op_move_msg, toMove.size) 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 val total = toMove.size+toClean.size
var failedItem: String? = null var failedItem: String? = null
for ((i, item) in toMove.withIndex()) { for ((i, item) in toMove.withIndex()) {
@ -211,25 +242,23 @@ class FileOperationService : Service() {
} }
failedItem failedItem
} }
// treat cancellation as success
waitForTask(notification, task).failedItem
} }
private suspend fun importFilesFromUris( private suspend fun importFilesFromUris(
encryptedVolume: EncryptedVolume,
dstPaths: List<String>, dstPaths: List<String>,
uris: List<Uri>, uris: List<Uri>,
notification: FileOperationNotification, notification: FileOperationNotification,
): String? { ): String? {
var failedIndex = -1 var failedIndex = -1
for (i in dstPaths.indices) { for (i in dstPaths.indices) {
withContext(Dispatchers.IO) { yield()
try { try {
if (!encryptedVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) { if (!encryptedVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
failedIndex = i
}
} catch (e: FileNotFoundException) {
failedIndex = i failedIndex = i
} }
} catch (e: FileNotFoundException) {
failedIndex = i
} }
if (failedIndex == -1) { if (failedIndex == -1) {
updateNotificationProgress(notification, i+1, dstPaths.size) updateNotificationProgress(notification, i+1, dstPaths.size)
@ -240,12 +269,11 @@ class FileOperationService : Service() {
return null return null
} }
suspend fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>): TaskResult<String?> = coroutineScope { suspend fun importFilesFromUris(volumeId: Int, dstPaths: List<String>, uris: List<Uri>): TaskResult<out String?> {
val notification = showNotification(R.string.file_op_import_msg, dstPaths.size) val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
val task = async { return volumeTask(volumeId, notification) { encryptedVolume ->
importFilesFromUris(dstPaths, uris, notification) importFilesFromUris(encryptedVolume, dstPaths, uris, notification)
} }
waitForTask(notification, task)
} }
/** /**
@ -255,77 +283,63 @@ class FileOperationService : Service() {
* *
* @return false if cancelled early, true otherwise. * @return false if cancelled early, true otherwise.
*/ */
private fun recursiveMapDirectoryForImport( private suspend fun recursiveMapDirectoryForImport(
rootSrcDir: DocumentFile, rootSrcDir: DocumentFile,
rootDstPath: String, rootDstPath: String,
dstFiles: ArrayList<String>, dstFiles: ArrayList<String>,
srcUris: ArrayList<Uri>, srcUris: ArrayList<Uri>,
dstDirs: ArrayList<String>, dstDirs: ArrayList<String>,
scope: CoroutineScope, ) {
): Boolean {
dstDirs.add(rootDstPath) dstDirs.add(rootDstPath)
for (child in rootSrcDir.listFiles()) { for (child in rootSrcDir.listFiles()) {
if (!scope.isActive) { yield()
return false
}
child.name?.let { name -> child.name?.let { name ->
val subPath = PathUtils.pathJoin(rootDstPath, name) val subPath = PathUtils.pathJoin(rootDstPath, name)
if (child.isDirectory) { if (child.isDirectory) {
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) { recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs)
return false } else if (child.isFile) {
}
}
else if (child.isFile) {
srcUris.add(child.uri) srcUris.add(child.uri)
dstFiles.add(subPath) dstFiles.add(subPath)
} }
} }
} }
return true
} }
class ImportDirectoryResult(val taskResult: TaskResult<String?>, val uris: List<Uri>) class ImportDirectoryResult(val taskResult: TaskResult<out String?>, val uris: List<Uri>)
suspend fun importDirectory( suspend fun importDirectory(
volumeId: Int,
rootDstPath: String, rootDstPath: String,
rootSrcDir: DocumentFile, rootSrcDir: DocumentFile,
): ImportDirectoryResult = coroutineScope { ): ImportDirectoryResult {
val notification = showNotification(R.string.file_op_import_msg, null) val notification = showNotification(R.string.file_op_import_msg, null)
val srcUris = arrayListOf<Uri>() val srcUris = arrayListOf<Uri>()
val task = async { return ImportDirectoryResult(volumeTask(volumeId, notification) { encryptedVolume ->
var failedItem: String? = null var failedItem: String? = null
val dstFiles = arrayListOf<String>() val dstFiles = arrayListOf<String>()
val dstDirs = arrayListOf<String>() val dstDirs = arrayListOf<String>()
recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs)
withContext(Dispatchers.IO) { // create destination folders so the new files can use them
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) { for (dir in dstDirs) {
return@withContext if (!encryptedVolume.mkdir(dir)) {
} failedItem = dir
break
// create destination folders so the new files can use them
for (dir in dstDirs) {
if (!encryptedVolume.mkdir(dir)) {
failedItem = dir
break
}
} }
} }
if (failedItem == null) { if (failedItem == null) {
failedItem = importFilesFromUris(dstFiles, srcUris, notification) failedItem = importFilesFromUris(encryptedVolume, dstFiles, srcUris, notification)
} }
failedItem failedItem
} }, srcUris)
ImportDirectoryResult(waitForTask(notification, task), srcUris)
} }
suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): String? = coroutineScope { suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): TaskResult<out String?> {
val notification = showNotification(R.string.file_op_wiping_msg, uris.size) val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
val task = async { val task = serviceScope.async(Dispatchers.IO) {
var errorMsg: String? = null var errorMsg: String? = null
for (i in uris.indices) { for (i in uris.indices) {
withContext(Dispatchers.IO) { yield()
errorMsg = Wiper.wipe(this@FileOperationService, uris[i]) errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
}
if (errorMsg == null) { if (errorMsg == null) {
updateNotificationProgress(notification, i+1, uris.size) updateNotificationProgress(notification, i+1, uris.size)
} else { } else {
@ -337,11 +351,10 @@ class FileOperationService : Service() {
} }
errorMsg errorMsg
} }
// treat cancellation as success return waitForTask(notification, task)
waitForTask(notification, task).failedItem
} }
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 { val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let {
contentResolver.openOutputStream(it) contentResolver.openOutputStream(it)
} }
@ -352,25 +365,20 @@ class FileOperationService : Service() {
} }
} }
private fun recursiveExportDirectory( private suspend fun recursiveExportDirectory(
encryptedVolume: EncryptedVolume,
plain_directory_path: String, plain_directory_path: String,
treeDocumentFile: DocumentFile, treeDocumentFile: DocumentFile,
scope: CoroutineScope
): String? { ): String? {
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree -> treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
val explorerElements = encryptedVolume.readDir(plain_directory_path) ?: return null val explorerElements = encryptedVolume.readDir(plain_directory_path) ?: return null
for (e in explorerElements) { for (e in explorerElements) {
if (!scope.isActive) { yield()
return null
}
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) { if (e.isDirectory) {
val failedItem = recursiveExportDirectory(fullPath, childTree, scope) recursiveExportDirectory(encryptedVolume, fullPath, childTree)?.let { return it }
failedItem?.let { return it } } else if (!exportFileInto(encryptedVolume, fullPath, childTree)) {
} else { return fullPath
if (!exportFileInto(fullPath, childTree)){
return fullPath
}
} }
} }
return null return null
@ -378,18 +386,20 @@ class FileOperationService : Service() {
return treeDocumentFile.name return treeDocumentFile.name
} }
suspend fun exportFiles(uri: Uri, items: List<ExplorerElement>): TaskResult<String?> = coroutineScope { suspend fun exportFiles(volumeId: Int, items: List<ExplorerElement>, uri: Uri): TaskResult<out String?> {
val notification = showNotification(R.string.file_op_export_msg, items.size) val notification = showNotification(R.string.file_op_export_msg, items.size)
val task = async { return volumeTask(volumeId, notification) { encryptedVolume ->
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!! val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
var failedItem: String? = null var failedItem: String? = null
for (i in items.indices) { for (i in items.indices) {
withContext(Dispatchers.IO) { yield()
failedItem = if (items[i].isDirectory) { failedItem = if (items[i].isDirectory) {
recursiveExportDirectory(items[i].fullPath, treeDocumentFile, this) recursiveExportDirectory(encryptedVolume, items[i].fullPath, treeDocumentFile)
} else {
if (exportFileInto(encryptedVolume, items[i].fullPath, treeDocumentFile)) {
null
} else { } else {
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath items[i].fullPath
} }
} }
if (failedItem == null) { if (failedItem == null) {
@ -400,21 +410,37 @@ class FileOperationService : Service() {
} }
failedItem failedItem
} }
waitForTask(notification, task)
} }
suspend fun removeElements(items: List<ExplorerElement>): 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<ExplorerElement>): String? {
val notification = showNotification(R.string.file_op_delete_msg, items.size) 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 var failedItem: String? = null
for ((i, element) in items.withIndex()) { for ((i, element) in items.withIndex()) {
yield()
if (element.isDirectory) { if (element.isDirectory) {
val result = encryptedVolume.recursiveRemoveDirectory(element.fullPath) recursiveRemoveDirectory(encryptedVolume, element.fullPath)?.let { failedItem = it }
result?.let { failedItem = it } } else if (!encryptedVolume.deleteFile(element.fullPath)) {
} else { failedItem = element.fullPath
if (!encryptedVolume.deleteFile(element.fullPath)) {
failedItem = element.fullPath
}
} }
if (failedItem == null) { if (failedItem == null) {
updateNotificationProgress(notification, i + 1, items.size) updateNotificationProgress(notification, i + 1, items.size)
@ -423,14 +449,11 @@ class FileOperationService : Service() {
} }
} }
failedItem failedItem
} }.failedItem // treat cancellation as success
waitForTask(notification, task).failedItem
} }
private fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int { private suspend fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int {
if (!scope.isActive) { yield()
return 0
}
val children = rootDirectory.listFiles() val children = rootDirectory.listFiles()
var count = children.size var count = children.size
for (child in children) { for (child in children) {
@ -441,7 +464,7 @@ class FileOperationService : Service() {
return count return count
} }
private fun recursiveCopyVolume( private suspend fun recursiveCopyVolume(
src: DocumentFile, src: DocumentFile,
dst: DocumentFile, dst: DocumentFile,
dstRootDirectory: ObjRef<DocumentFile?>?, dstRootDirectory: ObjRef<DocumentFile?>?,
@ -453,9 +476,7 @@ class FileOperationService : Service() {
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
dstRootDirectory?.let { it.value = dstDir } dstRootDirectory?.let { it.value = dstDir }
for (child in src.listFiles()) { for (child in src.listFiles()) {
if (!scope.isActive) { yield()
return null
}
if (child.isFile) { if (child.isFile) {
val dstFile = dstDir.createFile("", child.name ?: return child) ?: return child val dstFile = dstDir.createFile("", child.name ?: return child) ?: return child
val outputStream = contentResolver.openOutputStream(dstFile.uri) val outputStream = contentResolver.openOutputStream(dstFile.uri)
@ -474,21 +495,16 @@ class FileOperationService : Service() {
return null return null
} }
class CopyVolumeResult(val taskResult: TaskResult<DocumentFile?>, val dstRootDirectory: DocumentFile?) class CopyVolumeResult(val taskResult: TaskResult<out DocumentFile?>, 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 notification = showNotification(R.string.copy_volume_notification, null)
val dstRootDirectory = ObjRef<DocumentFile?>(null) val dstRootDirectory = ObjRef<DocumentFile?>(null)
val task = async(Dispatchers.IO) { val task = serviceScope.async(Dispatchers.IO) {
val total = recursiveCountChildElements(src, this) val total = recursiveCountChildElements(src, this)
if (isActive) { updateNotificationProgress(notification, 0, total)
updateNotificationProgress(notification, 0, total) recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
} else {
null
}
} }
// treat cancellation as success return CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
} }
} }

View File

@ -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<T> 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 <T> completed(failedItem: T?): TaskResult<T> {
return if (failedItem == null) {
TaskResult(State.SUCCESS, null, null)
} else {
TaskResult(State.FAILED, failedItem, null)
}
}
fun <T> error(errorMessage: String?): TaskResult<T> {
return TaskResult(State.ERROR, null, errorMessage)
}
fun <T> cancelled(): TaskResult<T> {
return TaskResult(State.CANCELLED, null, null)
}
}
}

View File

@ -218,25 +218,4 @@ abstract class EncryptedVolume: Parcelable {
} }
return result 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
}
}
} }

View File

@ -261,4 +261,5 @@
<string name="config_load_error">The configuration file cannot be loaded. Make sure the volume is accessible.</string> <string name="config_load_error">The configuration file cannot be loaded. Make sure the volume is accessible.</string>
<string name="wrong_password">The configuration file cannot be decrypted. Please check your password.</string> <string name="wrong_password">The configuration file cannot be decrypted. Please check your password.</string>
<string name="filesystem_id_changed">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.</string> <string name="filesystem_id_changed">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.</string>
<string name="task_failed">The task failed: %s</string>
</resources> </resources>