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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,51 +82,45 @@ 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("""
|
||||||
@ -138,29 +128,16 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
${getString(R.string.ask_for_wipe)}
|
${getString(R.string.ask_for_wipe)}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
.setPositiveButton(R.string.yes) { _, _ ->
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
taskScope.launch {
|
activityScope.launch {
|
||||||
val errorMsg = fileOperationService.wipeUris(urisToWipe, rootFile)
|
onTaskResult(
|
||||||
if (errorMsg == null) {
|
fileOperationService.wipeUris(urisToWipe, rootFile),
|
||||||
Toast.makeText(this@ExplorerActivity, R.string.wipe_successful, Toast.LENGTH_SHORT).show()
|
R.string.wipe_failed,
|
||||||
} else {
|
R.string.wipe_successful,
|
||||||
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)
|
.setNegativeButton(R.string.no, null)
|
||||||
.show()
|
.show()
|
||||||
} else {
|
|
||||||
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,24 +301,18 @@ 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)
|
|
||||||
.setMessage(getString(R.string.copy_failed, failedItem))
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
setCurrentPath(currentDirectoryPath)
|
setCurrentPath(currentDirectoryPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
cancelItemAction()
|
cancelItemAction()
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
@ -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))
|
||||||
|
@ -73,9 +73,8 @@ 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)
|
||||||
@ -84,12 +83,5 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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> {
|
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<out T> {
|
||||||
tasks[notification.notificationId] = task
|
tasks[notification.notificationId] = task
|
||||||
return try {
|
return serviceScope.async {
|
||||||
TaskResult(false, task.await())
|
try {
|
||||||
|
TaskResult.completed(task.await())
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
TaskResult(true, null)
|
TaskResult.cancelled()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
e.printStackTrace()
|
||||||
|
TaskResult.error(e.localizedMessage)
|
||||||
} finally {
|
} finally {
|
||||||
cancelNotification(notification)
|
cancelNotification(notification)
|
||||||
}
|
}
|
||||||
|
}.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyFile(srcPath: String, dstPath: String, remoteEncryptedVolume: EncryptedVolume = encryptedVolume): Boolean {
|
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,26 +188,25 @@ 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
|
failedItem = items[i].srcPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!copyFile(encryptedVolume, items[i].srcPath, items[i].dstPath!!, srcEncryptedVolume)) {
|
||||||
if (!copyFile(items[i].srcPath, items[i].dstPath!!, remoteEncryptedVolume)) {
|
|
||||||
failedItem = items[i].srcPath
|
failedItem = items[i].srcPath
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failedItem == null) {
|
if (failedItem == null) {
|
||||||
updateNotificationProgress(notification, i+1, items.size)
|
updateNotificationProgress(notification, i+1, items.size)
|
||||||
} else {
|
} else {
|
||||||
@ -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,18 +242,17 @@ 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
|
failedIndex = i
|
||||||
@ -230,7 +260,6 @@ class FileOperationService : Service() {
|
|||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
failedIndex = i
|
failedIndex = i
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (failedIndex == -1) {
|
if (failedIndex == -1) {
|
||||||
updateNotificationProgress(notification, i+1, dstPaths.size)
|
updateNotificationProgress(notification, i+1, dstPaths.size)
|
||||||
} else {
|
} else {
|
||||||
@ -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,53 +283,42 @@ 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) {
|
|
||||||
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) {
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// create destination folders so the new files can use them
|
// create destination folders so the new files can use them
|
||||||
for (dir in dstDirs) {
|
for (dir in dstDirs) {
|
||||||
if (!encryptedVolume.mkdir(dir)) {
|
if (!encryptedVolume.mkdir(dir)) {
|
||||||
@ -309,23 +326,20 @@ class FileOperationService : Service() {
|
|||||||
break
|
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,44 +365,41 @@ 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 {
|
|
||||||
if (!exportFileInto(fullPath, childTree)){
|
|
||||||
return fullPath
|
return fullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
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 {
|
} else {
|
||||||
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath
|
if (exportFileInto(encryptedVolume, items[i].fullPath, treeDocumentFile)) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
items[i].fullPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failedItem == null) {
|
if (failedItem == null) {
|
||||||
@ -400,22 +410,38 @@ 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 {
|
|
||||||
if (!encryptedVolume.deleteFile(element.fullPath)) {
|
|
||||||
failedItem = element.fullPath
|
failedItem = element.fullPath
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (failedItem == null) {
|
if (failedItem == null) {
|
||||||
updateNotificationProgress(notification, i + 1, items.size)
|
updateNotificationProgress(notification, i + 1, items.size)
|
||||||
} else {
|
} else {
|
||||||
@ -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
|
return CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
|
||||||
}
|
|
||||||
}
|
|
||||||
// treat cancellation as success
|
|
||||||
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
|
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="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>
|
||||||
|
Loading…
Reference in New Issue
Block a user