Move feature

This commit is contained in:
Hardcore Sushi 2020-08-09 14:34:42 +02:00
parent 35a2e35bdc
commit cac264043f
6 changed files with 208 additions and 105 deletions

View File

@ -250,7 +250,7 @@ open class BaseExplorerActivity : BaseActivity() {
dialog.show()
}
protected fun checkFileOverwrite(path: String): String? {
protected fun checkPathOverwrite(path: String, isDirectory: Boolean): String? {
var outputPath: String? = null
if (gocryptfsVolume.pathExists(path)){
val fileName = File(path).name
@ -261,7 +261,7 @@ open class BaseExplorerActivity : BaseActivity() {
runOnUiThread {
val dialog = ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(getString(R.string.file_overwrite_question, fileName))
.setMessage(getString(if (isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, path))
.setNegativeButton(R.string.no) { _, _ ->
val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null)
val dialogEditText = dialogEditTextView.findViewById<EditText>(R.id.dialog_edit_text)
@ -269,15 +269,15 @@ open class BaseExplorerActivity : BaseActivity() {
dialogEditText.selectAll()
val dialog = ColoredAlertDialogBuilder(this)
.setView(dialogEditTextView)
.setTitle(getString(R.string.enter_new_filename))
.setTitle(getString(R.string.enter_new_name))
.setPositiveButton(R.string.ok) { _, _ ->
handler.sendMessage(Message().apply { obj = checkFileOverwrite(PathUtils.path_join(PathUtils.getParentPath(path), dialogEditText.text.toString())) })
handler.sendMessage(Message().apply { obj = checkPathOverwrite(PathUtils.path_join(PathUtils.getParentPath(path), dialogEditText.text.toString()), isDirectory) })
}
.setNegativeButton(R.string.cancel) { _, _ -> handler.sendMessage(Message().apply { obj = null }) }
.create()
dialogEditText.setOnEditorActionListener { _, _, _ ->
dialog.dismiss()
handler.sendMessage(Message().apply { obj = checkFileOverwrite(PathUtils.path_join(PathUtils.getParentPath(path), dialogEditText.text.toString())) })
handler.sendMessage(Message().apply { obj = checkPathOverwrite(PathUtils.path_join(PathUtils.getParentPath(path), dialogEditText.text.toString()), isDirectory) })
true
}
dialog.setOnCancelListener { handler.sendMessage(Message().apply { obj = null }) }

View File

@ -18,16 +18,18 @@ import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
import sushi.hardcore.droidfs.util.*
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File
import kotlin.collections.ArrayList
class ExplorerActivity : BaseExplorerActivity() {
private val PICK_DIRECTORY_REQUEST_CODE = 1
private val PICK_FILES_REQUEST_CODE = 2
private val PICK_OTHER_VOLUME_ITEMS_REQUEST_CODE = 3
companion object {
private const val PICK_DIRECTORY_REQUEST_CODE = 1
private const val PICK_FILES_REQUEST_CODE = 2
private const val PICK_OTHER_VOLUME_ITEMS_REQUEST_CODE = 3
private enum class ItemsActions {NONE, COPY, MOVE}
}
private var usf_decrypt = false
private var usf_share = false
private var modeSelectLocation = false
private val filesToCopy = ArrayList<ExplorerElement>()
private var currentItemAction = ItemsActions.NONE
private val itemsToProcess = ArrayList<ExplorerElement>()
override fun init() {
setContentView(R.layout.activity_explorer)
usf_decrypt = sharedPrefs.getBoolean("usf_decrypt", false)
@ -35,7 +37,7 @@ class ExplorerActivity : BaseExplorerActivity() {
}
override fun onExplorerItemLongClick(position: Int) {
cancelCopy()
cancelItemAction()
explorerAdapter.onItemLongClick(position)
invalidateOptionsMenu()
}
@ -44,7 +46,7 @@ class ExplorerActivity : BaseExplorerActivity() {
if (fileName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
checkFileOverwrite(PathUtils.path_join(currentDirectoryPath, fileName))?.let {
checkPathOverwrite(PathUtils.path_join(currentDirectoryPath, fileName), false)?.let {
val handleID = gocryptfsVolume.openWriteMode(it)
if (handleID == -1) {
ColoredAlertDialogBuilder(this)
@ -62,7 +64,7 @@ class ExplorerActivity : BaseExplorerActivity() {
}
fun onClickFAB(view: View) {
if (modeSelectLocation){
if (currentItemAction != ItemsActions.NONE){
openDialogCreateFolder()
} else {
val adapter = IconTextDialogAdapter(this)
@ -152,7 +154,7 @@ class ExplorerActivity : BaseExplorerActivity() {
Looper.prepare()
var success = false
for (uri in uris) {
val dstPath = checkFileOverwrite(PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri)))
val dstPath = checkPathOverwrite(PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri)), false)
if (dstPath == null){
break
} else {
@ -316,7 +318,7 @@ class ExplorerActivity : BaseExplorerActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.explorer, menu)
if (modeSelectLocation) {
if (currentItemAction != ItemsActions.NONE) {
menu.findItem(R.id.validate).isVisible = true
menu.findItem(R.id.close).isVisible = false
} else {
@ -328,6 +330,7 @@ class ExplorerActivity : BaseExplorerActivity() {
menu.findItem(R.id.select_all).isVisible = anyItemSelected
menu.findItem(R.id.delete).isVisible = anyItemSelected
menu.findItem(R.id.copy).isVisible = anyItemSelected
menu.findItem(R.id.cut).isVisible = anyItemSelected
menu.findItem(R.id.decrypt).isVisible = anyItemSelected && usf_decrypt
if (anyItemSelected && usf_share){
var containsDir = false
@ -348,7 +351,7 @@ class ExplorerActivity : BaseExplorerActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
cancelCopy()
cancelItemAction()
super.onOptionsItemSelected(item)
}
R.id.select_all -> {
@ -356,38 +359,52 @@ class ExplorerActivity : BaseExplorerActivity() {
invalidateOptionsMenu()
true
}
R.id.cut -> {
for (i in explorerAdapter.selectedItems){
itemsToProcess.add(explorerElements[i])
}
currentItemAction = ItemsActions.MOVE
unselectAll()
true
}
R.id.copy -> {
for (i in explorerAdapter.selectedItems){
filesToCopy.add(explorerElements[i])
itemsToProcess.add(explorerElements[i])
}
modeSelectLocation = true
currentItemAction = ItemsActions.COPY
unselectAll()
true
}
R.id.validate -> {
if (currentItemAction == ItemsActions.COPY){
object : LoadingTask(this, R.string.loading_msg_copy){
override fun doTask(activity: AppCompatActivity) {
var failedItem: String? = null
Looper.prepare()
for (element in filesToCopy) {
failedItem = if (element.isDirectory) {
recursiveCopyDirectory(element.fullPath, currentDirectoryPath)
} else {
val dstPath = checkFileOverwrite(PathUtils.path_join(currentDirectoryPath, element.name))
if (dstPath == null){
for (element in itemsToProcess) {
val dstPath = checkPathOverwrite(PathUtils.path_join(currentDirectoryPath, element.name), element.isDirectory)
failedItem = if (dstPath == null){
""
} else {
if (element.isDirectory) {
recursiveCopyDirectory(element.fullPath, dstPath)
} else {
if (copyFile(element.fullPath, dstPath)) null else element.fullPath
}
}
if (failedItem != null && failedItem.isNotEmpty()) {
if (failedItem != null){
if (failedItem.isNotEmpty()) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(getString(R.string.copy_failed, failedItem))
.setMessage(getString(
R.string.copy_failed,
failedItem
))
.setPositiveButton(R.string.ok, null)
.show()
}
}
break
}
}
@ -402,11 +419,41 @@ class ExplorerActivity : BaseExplorerActivity() {
}
}
override fun doFinally(activity: AppCompatActivity) {
cancelCopy()
cancelItemAction()
unselectAll()
setCurrentPath(currentDirectoryPath)
}
}
} else if (currentItemAction == ItemsActions.MOVE){
object : LoadingTask(this, R.string.loading_msg_move){
override fun doTask(activity: AppCompatActivity) {
Looper.prepare()
val failedItem = moveElements(itemsToProcess, currentDirectoryPath)
if (failedItem == null) {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(getString(R.string.move_success))
.setMessage(getString(R.string.move_success_msg))
.setPositiveButton(R.string.ok, null)
.show()
}
} else if (failedItem.isNotEmpty()){
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(getString(R.string.move_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
override fun doFinally(activity: AppCompatActivity) {
cancelItemAction()
unselectAll()
setCurrentPath(currentDirectoryPath)
}
}
}
true
}
R.id.delete -> {
@ -443,16 +490,16 @@ class ExplorerActivity : BaseExplorerActivity() {
}
}
private fun cancelCopy() {
if (modeSelectLocation){
modeSelectLocation = false
filesToCopy.clear()
private fun cancelItemAction() {
if (currentItemAction != ItemsActions.NONE){
currentItemAction = ItemsActions.NONE
itemsToProcess.clear()
}
}
override fun onBackPressed() {
if (modeSelectLocation) {
cancelCopy()
if (currentItemAction != ItemsActions.NONE) {
cancelItemAction()
invalidateOptionsMenu()
} else {
super.onBackPressed()
@ -488,24 +535,24 @@ class ExplorerActivity : BaseExplorerActivity() {
return success
}
private fun recursiveCopyDirectory(srcDirectoryPath: String, outputPath: String): String? {
private fun recursiveCopyDirectory(srcDirectoryPath: String, dstDirectoryPath: String): String? {
val mappedElements = gocryptfsVolume.recursiveMapFiles(srcDirectoryPath)
val dstDirectoryPath = PathUtils.path_join(outputPath, File(srcDirectoryPath).name)
if (!gocryptfsVolume.pathExists(dstDirectoryPath)) {
if (!gocryptfsVolume.pathExists(dstDirectoryPath)){
if (!gocryptfsVolume.mkdir(dstDirectoryPath)) {
return dstDirectoryPath
return srcDirectoryPath
}
}
for (e in mappedElements) {
val dstPath = PathUtils.path_join(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, e.fullPath))
val dstPath = checkPathOverwrite(PathUtils.path_join(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, e.fullPath)), e.isDirectory)
if (dstPath == null){
return ""
} else {
if (e.isDirectory) {
if (!gocryptfsVolume.pathExists(dstPath)){
if (!gocryptfsVolume.mkdir(dstPath)){
return e.fullPath
}
} else {
val checkedDstPath = checkFileOverwrite(dstPath)
if (checkedDstPath == null){
return ""
}
} else {
if (!copyFile(e.fullPath, dstPath)) {
return e.fullPath
@ -516,6 +563,38 @@ class ExplorerActivity : BaseExplorerActivity() {
return null
}
private fun moveDirectory(srcDirectoryPath: String, dstDirectoryPath: String): String? {
if (!gocryptfsVolume.pathExists(dstDirectoryPath)) {
if (!gocryptfsVolume.rename(srcDirectoryPath, dstDirectoryPath)) {
return srcDirectoryPath
}
} else {
moveElements(gocryptfsVolume.listDir(srcDirectoryPath), dstDirectoryPath)
gocryptfsVolume.rmdir(srcDirectoryPath)
}
return null
}
private fun moveElements(elements: List<ExplorerElement>, dstDirectoryPath: String): String? {
for (element in elements){
val dstPath = checkPathOverwrite(PathUtils.path_join(dstDirectoryPath, element.name), element.isDirectory)
if (dstPath == null){
return ""
} else {
if (element.isDirectory){
moveDirectory(element.fullPath, dstPath)?.let{
return it
}
} else {
if (!gocryptfsVolume.rename(element.fullPath, dstPath)){
return element.fullPath
}
}
}
}
return null
}
private fun importFileFromOtherVolume(remoteGocryptfsVolume: GocryptfsVolume, srcPath: String, dstPath: String): Boolean {
var success = true
val srcHandleID = remoteGocryptfsVolume.openReadMode(srcPath)
@ -525,9 +604,7 @@ class ExplorerActivity : BaseExplorerActivity() {
var length: Int
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var offset: Long = 0
while (remoteGocryptfsVolume.readFile(srcHandleID, offset, ioBuffer)
.also { length = it } > 0
) {
while (remoteGocryptfsVolume.readFile(srcHandleID, offset, ioBuffer).also { length = it } > 0) {
val written =
gocryptfsVolume.writeFile(dstHandleID, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
@ -545,39 +622,44 @@ class ExplorerActivity : BaseExplorerActivity() {
}
private fun safeImportFileFromOtherVolume(remoteGocryptfsVolume: GocryptfsVolume, srcPath: String, dstPath: String): String? {
val checkedDstPath = checkFileOverwrite(PathUtils.path_join(currentDirectoryPath, File(dstPath).name))
val checkedDstPath = checkPathOverwrite(dstPath, false)
return if (checkedDstPath == null){
""
} else {
if (importFileFromOtherVolume(remoteGocryptfsVolume, srcPath, checkedDstPath)) null else dstPath
if (importFileFromOtherVolume(remoteGocryptfsVolume, srcPath, checkedDstPath)) null else srcPath
}
}
private fun recursiveImportDirectoryFromOtherVolume(remote_gocryptfsVolume: GocryptfsVolume, remote_directory_path: String, outputPath: String): String? {
val mappedElements = gocryptfsVolume.recursiveMapFiles(remote_directory_path)
val dstDirectoryPath = PathUtils.path_join(outputPath, File(remote_directory_path).name)
val dstDirectoryPath = checkPathOverwrite(PathUtils.path_join(outputPath, File(remote_directory_path).name), true)
if (dstDirectoryPath == null){
return ""
} else {
if (!gocryptfsVolume.pathExists(dstDirectoryPath)) {
if (!gocryptfsVolume.mkdir(dstDirectoryPath)) {
return dstDirectoryPath
return remote_directory_path
}
}
for (e in mappedElements) {
val dstPath = PathUtils.path_join(dstDirectoryPath, PathUtils.getRelativePath(remote_directory_path, e.fullPath))
val dstPath = checkPathOverwrite(PathUtils.path_join(dstDirectoryPath, PathUtils.getRelativePath(remote_directory_path, e.fullPath)), e.isDirectory)
if (dstPath == null){
return ""
} else {
if (e.isDirectory) {
if (!gocryptfsVolume.pathExists(dstPath)){
if (!gocryptfsVolume.mkdir(dstPath)){
return e.fullPath
}
}
} else {
val checkedDstPath = checkFileOverwrite(dstPath)
if (checkedDstPath == null){
return ""
} else {
if (!importFileFromOtherVolume(remote_gocryptfsVolume, e.fullPath, checkedDstPath)) {
if (!importFileFromOtherVolume(remote_gocryptfsVolume, e.fullPath, dstPath)) {
return e.fullPath
}
}
}
}
}
return null
}

View File

@ -45,25 +45,24 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
errorMsg = if (uri == null){
getString(R.string.share_intent_parsing_failed)
} else {
val outputPathTest = PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri))
val outputPath = checkFileOverwrite(outputPathTest)
val outputPath = checkPathOverwrite(PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri)), false)
if (outputPath == null) {
""
} else {
if (gocryptfsVolume.importFile(activity, uri, outputPath)) null else getString(R.string.import_failed, outputPath)
if (gocryptfsVolume.importFile(activity, uri, outputPath)) null else getString(R.string.import_failed, uri)
}
}
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (uris != null){
for (uri in uris) {
val outputPath = checkFileOverwrite(PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri)))
val outputPath = checkPathOverwrite(PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri)), false)
if (outputPath == null){
errorMsg = ""
break
} else {
if (!gocryptfsVolume.importFile(activity, uri, outputPath)) {
errorMsg = getString(R.string.import_failed, outputPath)
errorMsg = getString(R.string.import_failed, uri)
break
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FFFFFF"
android:pathData="M19.28,15.28c0.45,-1 0.72,-2.11 0.72,-3.28 0,-4.42 -3.58,-8 -8,-8s-8,3.58 -8,8 3.58,8 8,8c1.17,0 2.28,-0.27 3.28,-0.72l4.72,4.72 -4.72,4.72c-1,-0.45 -2.11,-0.72 -3.28,-0.72 -4.42,0 -8,3.58 -8,8s3.58,8 8,8 8,-3.58 8,-8c0,-1.17 -0.27,-2.28 -0.72,-3.28l4.72,-4.72 14,14h6v-2l-24.72,-24.72zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM12,40c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM24,25c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM38,6l-12,12 4,4 14,-14v-2z"/>
</vector>

View File

@ -9,10 +9,10 @@
android:icon="@drawable/icon_select_all"/>
<item
android:id="@+id/copy"
android:id="@+id/cut"
app:showAsAction="always"
android:visible="false"
android:icon="@drawable/icon_copy"/>
android:icon="@drawable/icon_cut"/>
<item
android:id="@+id/delete"
@ -20,6 +20,13 @@
android:visible="false"
android:icon="@drawable/icon_delete" />
<item
android:id="@+id/copy"
app:showAsAction="ifRoom"
android:visible="false"
android:icon="@drawable/icon_copy"
android:title="@string/copy_menu_title"/>
<item
android:id="@+id/decrypt"
app:showAsAction="ifRoom"

View File

@ -19,11 +19,11 @@
<string name="error_mkdir">Folder creation failed.</string>
<string name="success_import">Import successful !</string>
<string name="success_import_msg">The selected files have been successfully imported.</string>
<string name="import_failed">Import of %1$s failed.</string>
<string name="export_failed">Export of %1$s failed.</string>
<string name="import_failed">Import of %s failed.</string>
<string name="export_failed">Export of %s failed.</string>
<string name="success_export">Export successful !</string>
<string name="success_export_msg">The selected files have been successfully exported.</string>
<string name="remove_failed">Deletion of %1$s failed</string>
<string name="remove_failed">Deletion of %s failed</string>
<string name="passwords_mismatch">Passwords don\'t match</string>
<string name="dir_not_empty">The selected directory isn\'t empty</string>
<string name="success_volume_create">Volume successfully created !</string>
@ -40,24 +40,24 @@
<string name="parent_folder">Parent Folder</string>
<string name="enter_volume_path">Please enter the volume path</string>
<string name="external_open">Open with external app</string>
<string name="single_delete_confirm">Are you sure you want to delete %1$s ?</string>
<string name="multiple_delete_confirm">Are you sure you want to delete these %1$s items ?</string>
<string name="location">Location: /%1$s</string>
<string name="total_size">Total size: %1$s</string>
<string name="single_delete_confirm">Are you sure you want to delete %s ?</string>
<string name="multiple_delete_confirm">Are you sure you want to delete these %s items ?</string>
<string name="location">Location: /%s</string>
<string name="total_size">Total size: %s</string>
<string name="import_from_other_volume">Import from another volume</string>
<string name="read_file_failed">Failed to open this file.</string>
<string name="volume">Volume: %1$s</string>
<string name="volume">Volume: %s</string>
<string name="yes">YES</string>
<string name="no">NO</string>
<string name="ask_for_wipe">Do you want to wipe the original files ?</string>
<string name="wipe_failed">Wiping failed: %1$s</string>
<string name="wipe_failed">Wiping failed: %s</string>
<string name="wipe_successful">Files successfully wiped !</string>
<string name="wipe_success_msg">The imported files have been successfully wiped from their original locations.</string>
<string name="create_password_warning">Warning !\nThis password will be the only way to decrypt the volume and access the files inside.\nChoose a very strong password (not \"123456\" or \"password\"), do not lose it and keep it secure (preferably only in your mind).\n\nDroidFS cannot protect you from screen recording apps, keyloggers, apk backdooring, compromised root accesses, memory dumps etc.\nDo not type passwords in insecure environments.</string>
<string name="open_activity_warning">Warning !\nOpening volumes in insecure environments can lead to data leaks.\nDroidFS cannot protect you from screen recording apps, keyloggers, apk backdooring, compromised root accesses, memory dumps etc.\nDo not open volumes containing sensitive data unless you know exactly what you are doing.</string>
<string name="rename">Rename</string>
<string name="rename_title">New name:</string>
<string name="rename_failed">Failed to rename %1$s</string>
<string name="rename_failed">Failed to rename %s</string>
<string name="remember_volume_path">Remember volume path</string>
<string name="sort_order">Sort order:</string>
<string name="old_password">Old password:</string>
@ -137,16 +137,22 @@
<string name="share">Share</string>
<string name="decrypt">Decrypt</string>
<string name="loading_msg_copy">Copying selected items…</string>
<string name="copy_failed">Copy of %1$s failed.</string>
<string name="copy_failed">Copy of %s failed.</string>
<string name="copy_success_msg">The selected items have been successfully copied.</string>
<string name="copy_success">Copy successful !</string>
<string name="fab_dialog_title">Add</string>
<string name="take_photo">Take photo</string>
<string name="picture_save_success">Picture saved to %1$s</string>
<string name="picture_save_success">Picture saved to %s</string>
<string name="picture_save_failed">Failed to save this picture.</string>
<string name="default_total_size">N//A</string>
<string name="file_overwrite_question">%s already exists, do you want to overwrite it ?</string>
<string name="enter_new_filename">Enter new filename</string>
<string name="dir_overwrite_question">%s already exists, do you want to merge its content ?</string>
<string name="enter_new_name">Enter new name</string>
<string name="reset_theme_color">Reset theme color</string>
<string name="reset_theme_color_summary">Reset theme color to the default one</string>
<string name="copy_menu_title">Copy</string>
<string name="loading_msg_move">Moving selected items…</string>
<string name="move_failed">Move of %s failed.</string>
<string name="move_success_msg">The selected items have been successfully moved.</string>
<string name="move_success">Move successful !</string>
</resources>