DroidFS/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt

536 lines
24 KiB
Kotlin
Raw Normal View History

2020-07-17 16:35:39 +02:00
package sushi.hardcore.droidfs.explorers
import android.app.Activity
import android.content.Intent
import android.net.Uri
2020-07-17 16:35:39 +02:00
import android.view.Menu
import android.view.MenuItem
2020-07-19 21:46:29 +02:00
import android.widget.Toast
2021-06-07 14:12:40 +02:00
import androidx.activity.result.contract.ActivityResultContracts
2021-08-27 19:47:35 +02:00
import androidx.documentfile.provider.DocumentFile
2022-04-20 15:17:33 +02:00
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
2020-08-05 14:06:54 +02:00
import sushi.hardcore.droidfs.CameraActivity
2021-06-07 14:12:40 +02:00
import sushi.hardcore.droidfs.GocryptfsVolume
2022-03-05 12:51:02 +01:00
import sushi.hardcore.droidfs.MainActivity
2020-07-17 16:35:39 +02:00
import sushi.hardcore.droidfs.R
2020-08-05 14:06:54 +02:00
import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
import sushi.hardcore.droidfs.content_providers.ExternalProvider
2021-06-11 20:23:54 +02:00
import sushi.hardcore.droidfs.databinding.ActivityExplorerBinding
2021-06-07 14:12:40 +02:00
import sushi.hardcore.droidfs.file_operations.OperationFile
2020-12-29 17:05:02 +01:00
import sushi.hardcore.droidfs.util.PathUtils
2021-11-09 11:12:09 +01:00
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
2022-03-24 20:08:23 +01:00
import sushi.hardcore.droidfs.widgets.EditTextDialog
2020-07-17 16:35:39 +02:00
import java.io.File
2020-07-19 21:46:29 +02:00
class ExplorerActivity : BaseExplorerActivity() {
2020-08-09 14:34:42 +02:00
companion object {
private enum class ItemsActions {NONE, COPY, MOVE}
}
2021-06-11 20:23:54 +02:00
2020-07-17 16:35:39 +02:00
private var usf_decrypt = false
private var usf_share = false
2020-08-09 14:34:42 +02:00
private var currentItemAction = ItemsActions.NONE
2020-12-29 17:05:02 +01:00
private val itemsToProcess = ArrayList<OperationFile>()
2021-06-11 20:23:54 +02:00
private lateinit var binding: ActivityExplorerBinding
2021-06-07 14:12:40 +02:00
private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { resultIntent ->
val remoteSessionID = resultIntent.getIntExtra("sessionID", -1)
2021-11-11 15:05:33 +01:00
val remoteGocryptfsVolume = GocryptfsVolume(applicationContext, remoteSessionID)
2021-06-07 14:12:40 +02:00
val path = resultIntent.getStringExtra("path")
val operationFiles = ArrayList<OperationFile>()
if (path == null){ //multiples elements
val paths = resultIntent.getStringArrayListExtra("paths")
val types = resultIntent.getIntegerArrayListExtra("types")
if (types != null && paths != null){
for (i in paths.indices) {
operationFiles.add(
OperationFile.fromExplorerElement(
ExplorerElement(File(paths[i]).name, types[i].toShort(), parentPath = PathUtils.getParentPath(paths[i]))
2021-06-07 14:12:40 +02:00
)
)
if (types[i] == 0){ //directory
remoteGocryptfsVolume.recursiveMapFiles(paths[i]).forEach {
operationFiles.add(OperationFile.fromExplorerElement(it))
}
}
}
}
} else {
operationFiles.add(
OperationFile.fromExplorerElement(
ExplorerElement(File(path).name, 1, parentPath = PathUtils.getParentPath(path))
2021-06-07 14:12:40 +02:00
)
)
}
if (operationFiles.size > 0){
checkPathOverwrite(operationFiles, currentDirectoryPath) { items ->
if (items == null) {
remoteGocryptfsVolume.close()
} else {
2022-04-20 15:17:33 +02:00
lifecycleScope.launch {
val failedItem = fileOperationService.copyElements(items, remoteGocryptfsVolume)
if (failedItem == null) {
Toast.makeText(this@ExplorerActivity, R.string.success_import, Toast.LENGTH_SHORT).show()
} else {
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
2021-06-07 14:12:40 +02:00
}
2022-04-20 15:17:33 +02:00
setCurrentPath(currentDirectoryPath)
2021-06-07 14:12:40 +02:00
remoteGocryptfsVolume.close()
}
}
}
} else {
remoteGocryptfsVolume.close()
}
}
}
}
private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
if (uris != null) {
importFilesFromUris(uris){ failedItem ->
onImportComplete(failedItem, uris)
2021-06-07 14:12:40 +02:00
}
}
}
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
2021-06-07 14:12:40 +02:00
if (uri != null) {
2022-04-20 15:17:33 +02:00
lifecycleScope.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()
2021-06-07 14:12:40 +02:00
} else {
2022-04-20 15:17:33 +02:00
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
2021-06-07 14:12:40 +02:00
.setTitle(R.string.error)
2022-04-20 15:17:33 +02:00
.setMessage(getString(R.string.export_failed, result.failedItem))
2021-06-07 14:12:40 +02:00
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
}
unselectAll()
}
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
rootUri?.let {
2022-04-20 15:17:33 +02:00
val tree = DocumentFile.fromTreeUri(this, it)!! //non-null after Lollipop
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
checkedOperation?.let {
lifecycleScope.launch {
val result = fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree)
if (result.taskResult.cancelled) {
setCurrentPath(currentDirectoryPath)
} else {
onImportComplete(result.taskResult.failedItem, result.uris, tree)
}
}
}
}
}
}
2021-08-27 19:47:35 +02:00
private fun onImportComplete(failedItem: String?, urisToWipe: List<Uri>, rootFile: DocumentFile? = null) {
if (failedItem == null){
2021-11-09 11:12:09 +01:00
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.success_import)
.setMessage("""
${getString(R.string.success_import_msg)}
${getString(R.string.ask_for_wipe)}
""".trimIndent())
.setPositiveButton(R.string.yes) { _, _ ->
2022-04-20 15:17:33 +02:00
lifecycleScope.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, themeValue)
.setTitle(R.string.error)
.setMessage(getString(R.string.wipe_failed, errorMsg))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
.setNegativeButton(R.string.no, null)
.show()
} else {
2021-11-09 11:12:09 +01:00
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
setCurrentPath(currentDirectoryPath)
}
2021-06-07 14:12:40 +02:00
2020-07-17 16:35:39 +02:00
override fun init() {
2021-06-11 20:23:54 +02:00
binding = ActivityExplorerBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.fab.setOnClickListener {
2021-06-07 16:34:50 +02:00
if (currentItemAction != ItemsActions.NONE){
openDialogCreateFolder()
} else {
val adapter = IconTextDialogAdapter(this)
adapter.items = listOf(
listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert),
listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt),
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
2021-06-07 16:34:50 +02:00
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
listOf("createFolder", R.string.mkdir, R.drawable.icon_create_new_folder),
listOf("camera", R.string.camera, R.drawable.icon_photo)
2021-06-07 16:34:50 +02:00
)
2021-11-09 11:12:09 +01:00
CustomAlertDialogBuilder(this, themeValue)
2021-06-07 16:34:50 +02:00
.setSingleChoiceItems(adapter, -1){ thisDialog, which ->
when (adapter.getItem(which)){
"importFromOtherVolumes" -> {
2022-03-05 12:51:02 +01:00
val intent = Intent(this, MainActivity::class.java)
2021-06-07 16:34:50 +02:00
intent.action = "pick"
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
isStartingActivity = true
pickFromOtherVolumes.launch(intent)
}
"importFiles" -> {
isStartingActivity = true
pickFiles.launch(arrayOf("*/*"))
}
"importFolder" -> {
isStartingActivity = true
pickImportDirectory.launch(null)
}
2021-06-07 16:34:50 +02:00
"createFile" -> {
2022-03-24 20:08:23 +01:00
EditTextDialog(this, R.string.enter_file_name) {
createNewFile(it)
}.show()
2021-06-07 16:34:50 +02:00
}
"createFolder" -> {
openDialogCreateFolder()
}
2021-10-03 14:36:06 +02:00
"camera" -> {
2021-06-07 16:34:50 +02:00
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("path", currentDirectoryPath)
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
isStartingActivity = true
startActivity(intent)
}
}
thisDialog.dismiss()
}
2022-03-05 12:51:02 +01:00
.setTitle(getString(R.string.add))
2021-06-07 16:34:50 +02:00
.setNegativeButton(R.string.cancel, null)
.show()
}
}
2020-07-17 16:35:39 +02:00
usf_decrypt = sharedPrefs.getBoolean("usf_decrypt", false)
usf_share = sharedPrefs.getBoolean("usf_share", false)
}
2022-04-17 15:52:34 +02:00
override fun onExplorerElementLongClick(position: Int) {
super.onExplorerElementLongClick(position)
2020-08-09 14:34:42 +02:00
cancelItemAction()
}
2020-07-19 21:46:29 +02:00
private fun createNewFile(fileName: String){
if (fileName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
val filePath = PathUtils.pathJoin(currentDirectoryPath, fileName)
val handleID = gocryptfsVolume.openWriteMode(filePath) //don't check overwrite because openWriteMode open in read-write (doesn't erase content)
2020-12-29 17:05:02 +01:00
if (handleID == -1) {
2021-11-09 11:12:09 +01:00
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
2020-12-29 17:05:02 +01:00
} else {
gocryptfsVolume.closeFile(handleID)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
2020-07-19 21:46:29 +02:00
}
}
}
2020-07-17 16:35:39 +02:00
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.explorer, menu)
2022-05-01 13:50:37 +02:00
val result = super.onCreateOptionsMenu(menu)
2020-08-09 14:34:42 +02:00
if (currentItemAction != ItemsActions.NONE) {
menu.findItem(R.id.validate).isVisible = true
menu.findItem(R.id.close).isVisible = false
2022-05-01 13:50:37 +02:00
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} else {
if (usf_share){
menu.findItem(R.id.share).isVisible = false
2020-07-17 16:35:39 +02:00
}
val anyItemSelected = explorerAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.select_all).isVisible = anyItemSelected
menu.findItem(R.id.delete).isVisible = anyItemSelected
menu.findItem(R.id.copy).isVisible = anyItemSelected
2020-08-09 14:34:42 +02:00
menu.findItem(R.id.cut).isVisible = anyItemSelected
menu.findItem(R.id.decrypt).isVisible = anyItemSelected && usf_decrypt
if (anyItemSelected && usf_share){
var containsDir = false
for (i in explorerAdapter.selectedItems) {
if (explorerElements[i].isDirectory) {
containsDir = true
break
}
}
if (!containsDir) {
menu.findItem(R.id.share).isVisible = true
}
2020-07-17 16:35:39 +02:00
}
}
2022-05-01 13:50:37 +02:00
return result
2020-07-17 16:35:39 +02:00
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
2020-08-09 14:34:42 +02:00
cancelItemAction()
super.onOptionsItemSelected(item)
}
R.id.select_all -> {
explorerAdapter.selectAll()
2020-07-17 16:35:39 +02:00
invalidateOptionsMenu()
true
}
2020-08-09 14:34:42 +02:00
R.id.cut -> {
for (i in explorerAdapter.selectedItems){
2020-12-29 17:05:02 +01:00
itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i]))
2020-08-09 14:34:42 +02:00
}
currentItemAction = ItemsActions.MOVE
unselectAll()
true
}
R.id.copy -> {
for (i in explorerAdapter.selectedItems){
2020-12-29 17:05:02 +01:00
itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i]))
if (explorerElements[i].isDirectory){
gocryptfsVolume.recursiveMapFiles(explorerElements[i].fullPath).forEach {
itemsToProcess.add(OperationFile.fromExplorerElement(it))
}
}
}
2020-08-09 14:34:42 +02:00
currentItemAction = ItemsActions.COPY
unselectAll()
true
}
R.id.validate -> {
2020-08-09 14:34:42 +02:00
if (currentItemAction == ItemsActions.COPY){
2020-12-29 17:05:02 +01:00
checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items ->
items?.let {
2022-04-20 15:17:33 +02:00
lifecycleScope.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 {
2022-04-20 15:17:33 +02:00
CustomAlertDialogBuilder(this@ExplorerActivity, themeValue)
.setTitle(R.string.error)
.setMessage(getString(R.string.copy_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
2020-08-09 14:34:42 +02:00
}
setCurrentPath(currentDirectoryPath)
}
}
}
2020-12-29 17:05:02 +01:00
cancelItemAction()
2022-03-24 20:08:23 +01:00
invalidateOptionsMenu()
}
2020-08-09 14:34:42 +02:00
} else if (currentItemAction == ItemsActions.MOVE){
2022-04-21 14:06:36 +02:00
itemsToProcess.forEach {
it.dstPath = PathUtils.pathJoin(currentDirectoryPath, it.explorerElement.name)
it.overwriteConfirmed = false // reset the field in case of a previous cancelled move
}
val toMove = ArrayList<OperationFile>(itemsToProcess.size)
val toClean = ArrayList<ExplorerElement>()
prepareFilesForMove(
itemsToProcess,
toMove,
toClean,
) {
lifecycleScope.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, themeValue)
.setTitle(R.string.error)
.setMessage(getString(R.string.move_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
2020-08-09 14:34:42 +02:00
}
2022-04-21 14:06:36 +02:00
setCurrentPath(currentDirectoryPath)
2020-08-09 14:34:42 +02:00
}
2020-12-29 17:05:02 +01:00
cancelItemAction()
2022-03-24 20:08:23 +01:00
invalidateOptionsMenu()
}
}
true
}
R.id.delete -> {
val size = explorerAdapter.selectedItems.size
2021-11-09 11:12:09 +01:00
val dialog = CustomAlertDialogBuilder(this, themeValue)
2020-07-17 16:35:39 +02:00
dialog.setTitle(R.string.warning)
dialog.setPositiveButton(R.string.ok) { _, _ -> removeSelectedItems() }
2020-07-17 16:35:39 +02:00
dialog.setNegativeButton(R.string.cancel, null)
if (size > 1) {
dialog.setMessage(getString(R.string.multiple_delete_confirm, explorerAdapter.selectedItems.size.toString()))
2020-07-17 16:35:39 +02:00
} else {
dialog.setMessage(getString(
R.string.single_delete_confirm,
explorerAdapter.explorerElements[explorerAdapter.selectedItems.first()].name
))
2020-07-17 16:35:39 +02:00
}
dialog.show()
true
}
R.id.share -> {
2020-07-17 16:35:39 +02:00
val paths: MutableList<String> = ArrayList()
for (i in explorerAdapter.selectedItems) {
2020-08-05 14:06:54 +02:00
paths.add(explorerElements[i].fullPath)
2020-07-17 16:35:39 +02:00
}
isStartingActivity = true
2021-11-09 11:12:09 +01:00
ExternalProvider.share(this, themeValue, gocryptfsVolume, paths)
2020-07-28 22:25:10 +02:00
unselectAll()
2020-07-17 16:35:39 +02:00
true
}
R.id.decrypt -> {
isStartingActivity = true
pickExportDirectory.launch(null)
2020-07-17 16:35:39 +02:00
true
}
else -> super.onOptionsItemSelected(item)
}
}
2022-04-21 14:06:36 +02:00
/**
* Ask the user what to do if an item would overwrite another item in case of a move.
*
* All [OperationFile] must have a non-null [dstPath][OperationFile.dstPath].
*/
private fun checkMoveOverwrite(items: List<OperationFile>, callback: (List<OperationFile>?) -> Unit) {
for (item in items) {
if (gocryptfsVolume.pathExists(item.dstPath!!) && !item.overwriteConfirmed) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setMessage(
getString(
if (item.explorerElement.isDirectory) {
R.string.dir_overwrite_question
} else {
R.string.file_overwrite_question
},
item.dstPath!!
2020-12-29 17:05:02 +01:00
)
)
2022-04-21 14:06:36 +02:00
.setPositiveButton(R.string.yes) {_, _ ->
item.overwriteConfirmed = true
checkMoveOverwrite(items, callback)
}
.setNegativeButton(R.string.no) { _, _ ->
with(EditTextDialog(this, R.string.enter_new_name) {
item.dstPath = PathUtils.pathJoin(PathUtils.getParentPath(item.dstPath!!), it)
checkMoveOverwrite(items, callback)
}) {
setSelectedText(item.explorerElement.name)
show()
}
}
.show()
return
}
}
callback(items)
}
/**
* Check for destination overwriting in case of a move operation.
*
* If the user decides to merge the content of a folder, the function recursively tests all
* children of the source folder to see if they will overwrite.
*
* The items to be moved are stored in [toMove]. We also need to keep track of the merged
* folders to delete them after the move. These folders are stored in [toClean].
*/
private fun prepareFilesForMove(
items: List<OperationFile>,
toMove: ArrayList<OperationFile>,
toClean: ArrayList<ExplorerElement>,
onReady: () -> Unit
) {
checkMoveOverwrite(items) { checkedItems ->
checkedItems?.let {
for (item in checkedItems) {
if (!item.overwriteConfirmed || !item.explorerElement.isDirectory) {
toMove.add(item)
}
}
val toCheck = mutableListOf<OperationFile>()
for (item in checkedItems) {
if (item.overwriteConfirmed && item.explorerElement.isDirectory) {
val children = gocryptfsVolume.listDir(item.explorerElement.fullPath)
toCheck.addAll(children.map {
OperationFile(it, PathUtils.pathJoin(item.dstPath!!, it.name))
})
toClean.add(item.explorerElement)
}
}
if (toCheck.isEmpty()) {
onReady()
} else {
prepareFilesForMove(toCheck, toMove, toClean, onReady)
2020-12-29 17:05:02 +01:00
}
}
}
}
2020-08-09 14:34:42 +02:00
private fun cancelItemAction() {
if (currentItemAction != ItemsActions.NONE){
currentItemAction = ItemsActions.NONE
itemsToProcess.clear()
}
}
2020-08-04 11:44:29 +02:00
override fun onBackPressed() {
2020-08-09 14:34:42 +02:00
if (currentItemAction != ItemsActions.NONE) {
cancelItemAction()
2020-08-04 11:44:29 +02:00
invalidateOptionsMenu()
} else {
super.onBackPressed()
}
}
private fun removeSelectedItems() {
var failedItem: String? = null
for (i in explorerAdapter.selectedItems) {
val element = explorerAdapter.explorerElements[i]
2020-11-03 17:22:09 +01:00
val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name)
2020-07-17 16:35:39 +02:00
if (element.isDirectory) {
2020-12-29 17:05:02 +01:00
val result = gocryptfsVolume.recursiveRemoveDirectory(fullPath)
result?.let{ failedItem = it }
2020-07-17 16:35:39 +02:00
} else {
2020-08-01 16:43:48 +02:00
if (!gocryptfsVolume.removeFile(fullPath)) {
failedItem = fullPath
2020-07-17 16:35:39 +02:00
}
}
if (failedItem != null) {
2021-11-09 11:12:09 +01:00
CustomAlertDialogBuilder(this, themeValue)
2020-07-17 16:35:39 +02:00
.setTitle(R.string.error)
.setMessage(getString(R.string.remove_failed, failedItem))
2020-07-17 16:35:39 +02:00
.setPositiveButton(R.string.ok, null)
.show()
break
}
}
setCurrentPath(currentDirectoryPath) //refresh
2020-07-17 16:35:39 +02:00
}
}