forked from hardcoresushi/DroidFS
Improve file oprations coroutines
This commit is contained in:
parent
f85f9d1c44
commit
2ae41f0f79
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Int, EncryptedVolume>()
|
||||
private val volumesData = HashMap<VolumeData, Int>()
|
||||
private val scopes = HashMap<Int, CoroutineScope>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
@ -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<ExplorerElement>
|
||||
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<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>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<EncryptedVolume>(resultIntent, "volume")!!
|
||||
val srcVolumeId = resultIntent.getIntExtra("volumeId", -1)
|
||||
val srcEncryptedVolume = app.volumeManager.getVolume(srcVolumeId)!!
|
||||
val path = resultIntent.getStringExtra("path")
|
||||
val operationFiles = ArrayList<OperationFile>()
|
||||
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<Uri>, 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<Uri>, 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<OperationFile>)
|
||||
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))
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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<EncryptedVolume>(intent, "volume")!!)
|
||||
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
|
||||
}
|
||||
if (explorerIntent == null) {
|
||||
|
@ -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<Int, Job>()
|
||||
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<T>(val cancelled: Boolean, val failedItem: T?)
|
||||
|
||||
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 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 <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
|
||||
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<OperationFile>,
|
||||
remoteEncryptedVolume: EncryptedVolume = encryptedVolume
|
||||
): String? = coroutineScope {
|
||||
volumeId: Int,
|
||||
items: List<OperationFile>,
|
||||
srcVolumeId: Int = volumeId,
|
||||
): TaskResult<out String?> {
|
||||
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<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 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<String>,
|
||||
uris: List<Uri>,
|
||||
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<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 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<String>,
|
||||
srcUris: ArrayList<Uri>,
|
||||
dstDirs: ArrayList<String>,
|
||||
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<String?>, val uris: List<Uri>)
|
||||
class ImportDirectoryResult(val taskResult: TaskResult<out String?>, val uris: List<Uri>)
|
||||
|
||||
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<Uri>()
|
||||
val task = async {
|
||||
return ImportDirectoryResult(volumeTask(volumeId, notification) { encryptedVolume ->
|
||||
var failedItem: String? = null
|
||||
val dstFiles = arrayListOf<String>()
|
||||
val dstDirs = arrayListOf<String>()
|
||||
|
||||
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<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 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<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 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<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 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<DocumentFile?>?,
|
||||
@ -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<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 dstRootDirectory = ObjRef<DocumentFile?>(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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -261,4 +261,5 @@
|
||||
<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="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>
|
||||
|
Loading…
Reference in New Issue
Block a user