diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 78e6687..baf4b40 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -86,6 +86,8 @@ android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" /> + + 0) { + val written = gocryptfsVolume.writeFile(dstHandleId, offset, ioBuffer, length).toLong() + if (written == length.toLong()) { + offset += written + } else { + success = false + break + } + } + gocryptfsVolume.closeFile(dstHandleId) + } else { + success = false + } + remoteGocryptfsVolume.closeFile(srcHandleId) + } else { + success = false + } + return success + } + + fun copyElements(items: ArrayList, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){ + Thread { + var failedItem: String? = null + for (item in items){ + if (item.explorerElement.isDirectory){ + if (!gocryptfsVolume.pathExists(item.dstPath!!)) { + if (!gocryptfsVolume.mkdir(item.dstPath!!)) { + failedItem = item.explorerElement.fullPath + } + } + } else { + if (!copyFile(item.explorerElement.fullPath, item.dstPath!!, remoteGocryptfsVolume)){ + failedItem = item.explorerElement.fullPath + } + } + if (failedItem != null){ + break + } + } + callback(failedItem) + }.start() + } + + fun moveElements(items: ArrayList, callback: (String?) -> Unit){ + Thread { + val mergedFolders = ArrayList() + var failedItem: String? = null + for (item in items){ + if (item.explorerElement.isDirectory && gocryptfsVolume.pathExists(item.dstPath!!)){ //folder will be merged + mergedFolders.add(item.explorerElement.fullPath) + } else { + if (!gocryptfsVolume.rename(item.explorerElement.fullPath, item.dstPath!!)){ + failedItem = item.explorerElement.fullPath + break + } + } + } + if (failedItem == null){ + for (path in mergedFolders) { + if (!gocryptfsVolume.rmdir(path)){ + failedItem = path + break + } + } + } + callback(failedItem) + }.start() + } + + fun importFilesFromUris(items: ArrayList, uris: List, callback: (String?) -> Unit){ + Thread { + var failedIndex = -1 + for (i in 0 until items.size) { + try { + if (!gocryptfsVolume.importFile(this, uris[i], items[i].dstPath!!)){ + failedIndex = i + } + } catch (e: FileNotFoundException){ + failedIndex = i + } + if (failedIndex != -1){ + callback(uris[failedIndex].toString()) + break + } + } + if (failedIndex == -1){ + callback(null) + } + }.start() + } + + fun wipeUris(uris: List, callback: (String?) -> Unit){ + Thread { + var errorMsg: String? = null + for (uri in uris) { + errorMsg = Wiper.wipe(this, uri) + if (errorMsg != null) { + break + } + } + callback(errorMsg) + }.start() + } + + private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean { + val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let { + contentResolver.openOutputStream(it) + } + return if (outputStream != null){ + gocryptfsVolume.exportFile(srcPath, outputStream) + } else { + false + } + } + + private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? { + treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree -> + val explorerElements = gocryptfsVolume.listDir(plain_directory_path) + for (e in explorerElements) { + val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) + if (e.isDirectory) { + val failedItem = recursiveExportDirectory(fullPath, childTree) + failedItem?.let { return it } + } else { + if (!exportFileInto(fullPath, childTree)){ + return fullPath + } + } + } + return null + } + return treeDocumentFile.name + } + + fun exportFiles(uri: Uri, items: List, callback: (String?) -> Unit){ + Thread { + contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile -> + var failedItem: String? = null + for (element in items) { + failedItem = if (element.isDirectory) { + recursiveExportDirectory(element.fullPath, treeDocumentFile) + } else { + if (exportFileInto(element.fullPath, treeDocumentFile)) null else element.fullPath + } + if (failedItem != null) { + break + } + } + callback(failedItem) + } + }.start() + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt index 6091376..368eb29 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -1,12 +1,12 @@ package sushi.hardcore.droidfs.explorers -import android.content.DialogInterface +import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.content.ServiceConnection import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.Message +import android.os.IBinder import android.view.Menu import android.view.MenuItem import android.view.View @@ -26,10 +26,12 @@ import sushi.hardcore.droidfs.ConstValues.Companion.isAudio import sushi.hardcore.droidfs.ConstValues.Companion.isImage import sushi.hardcore.droidfs.ConstValues.Companion.isText import sushi.hardcore.droidfs.ConstValues.Companion.isVideo +import sushi.hardcore.droidfs.FileOperationService import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter +import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_viewers.AudioPlayer import sushi.hardcore.droidfs.file_viewers.ImageViewer import sushi.hardcore.droidfs.file_viewers.TextEditor @@ -37,11 +39,8 @@ import sushi.hardcore.droidfs.file_viewers.VideoPlayer import sushi.hardcore.droidfs.provider.RestrictedFileProvider import sushi.hardcore.droidfs.util.ExternalProvider import sushi.hardcore.droidfs.util.GocryptfsVolume -import sushi.hardcore.droidfs.util.LoadingTask import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder -import java.io.File -import java.io.FileNotFoundException open class BaseExplorerActivity : BaseActivity() { private lateinit var sortOrderEntries: Array @@ -55,6 +54,7 @@ open class BaseExplorerActivity : BaseActivity() { field = value explorerViewModel.currentDirectoryPath = value } + protected lateinit var fileOperationService: FileOperationService protected lateinit var explorerElements: MutableList protected lateinit var explorerAdapter: ExplorerElementAdapter private var isCreating = true @@ -87,6 +87,7 @@ open class BaseExplorerActivity : BaseActivity() { setCurrentPath(currentDirectoryPath) refresher.isRefreshing = false } + bindFileOperationService() } class ExplorerViewModel: ViewModel() { @@ -97,6 +98,21 @@ open class BaseExplorerActivity : BaseActivity() { setContentView(R.layout.activity_explorer_base) } + protected open fun bindFileOperationService(){ + Intent(this, FileOperationService::class.java).also { + bindService(it, object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as FileOperationService.LocalBinder + fileOperationService = binder.getService() + binder.setGocryptfsVolume(gocryptfsVolume) + } + override fun onServiceDisconnected(arg0: ComponentName) { + + } + }, Context.BIND_AUTO_CREATE) + } + } + private fun startFileViewer(cls: Class<*>, filePath: String, sortOrder: String = ""){ val intent = Intent(this, cls) intent.putExtra("path", filePath) @@ -274,97 +290,97 @@ open class BaseExplorerActivity : BaseActivity() { dialog.show() } - protected fun checkPathOverwrite(path: String, isDirectory: Boolean): String? { - var outputPath: String? = null - if (gocryptfsVolume.pathExists(path)){ - val fileName = File(path).name - val handler = Handler{ msg -> - outputPath = msg.obj as String? - throw RuntimeException() + protected fun checkPathOverwrite(items: ArrayList, dstDirectoryPath: String, callback: (ArrayList?) -> Unit) { + val srcDirectoryPath = items[0].explorerElement.parentPath + var ready = true + for (i in 0 until items.size) { + val testDstPath: String + if (items[i].dstPath == null){ + testDstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.fullPath)) + if (gocryptfsVolume.pathExists(testDstPath)){ + ready = false + } else { + items[i].dstPath = testDstPath + } + } else { + testDstPath = items[i].dstPath!! + if (gocryptfsVolume.pathExists(testDstPath) && !items[i].overwriteConfirmed){ + ready = false + } } - runOnUiThread { - val dialog = ColoredAlertDialogBuilder(this) + if (!ready){ + ColoredAlertDialogBuilder(this) .setTitle(R.string.warning) - .setMessage(getString(if (isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, path)) + .setMessage(getString(if (items[i].explorerElement.isDirectory){R.string.dir_overwrite_question} else {R.string.file_overwrite_question}, testDstPath)) + .setPositiveButton(R.string.yes) {_, _ -> + items[i].dstPath = testDstPath + items[i].overwriteConfirmed = true + checkPathOverwrite(items, dstDirectoryPath, callback) + } .setNegativeButton(R.string.no) { _, _ -> val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null) val dialogEditText = dialogEditTextView.findViewById(R.id.dialog_edit_text) - dialogEditText.setText(fileName) + dialogEditText.setText(items[i].explorerElement.name) dialogEditText.selectAll() val dialog = ColoredAlertDialogBuilder(this) - .setView(dialogEditTextView) - .setTitle(R.string.enter_new_name) - .setPositiveButton(R.string.ok) { _, _ -> - handler.sendMessage(Message().apply { obj = checkPathOverwrite(PathUtils.pathJoin(PathUtils.getParentPath(path), dialogEditText.text.toString()), isDirectory) }) - } - .setNegativeButton(R.string.cancel) { _, _ -> handler.sendMessage(Message().apply { obj = null }) } - .create() + .setView(dialogEditTextView) + .setTitle(R.string.enter_new_name) + .setPositiveButton(R.string.ok) { _, _ -> + items[i].dstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.parentPath), dialogEditText.text.toString()) + checkPathOverwrite(items, dstDirectoryPath, callback) + } + .setOnCancelListener{ + callback(null) + } + .create() dialogEditText.setOnEditorActionListener { _, _, _ -> dialog.dismiss() - handler.sendMessage(Message().apply { obj = checkPathOverwrite(PathUtils.pathJoin(PathUtils.getParentPath(path), dialogEditText.text.toString()), isDirectory) }) + items[i].dstPath = PathUtils.pathJoin(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, items[i].explorerElement.parentPath), dialogEditText.text.toString()) + checkPathOverwrite(items, dstDirectoryPath, callback) true } - dialog.setOnCancelListener { handler.sendMessage(Message().apply { obj = null }) } dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) dialog.show() } - .setPositiveButton(R.string.yes) { _, _ -> handler.sendMessage(Message().apply { obj = path }) } - .create() - dialog.setOnCancelListener { handler.sendMessage(Message().apply { obj = null }) } - dialog.show() + .setOnCancelListener{ + callback(null) + } + .show() + break } - try { Looper.loop() } - catch (e: RuntimeException) {} - } else { - outputPath = path } - return outputPath + if (ready){ + callback(items) + } } - protected fun importFilesFromUris(uris: List, task: LoadingTask, callback: (DialogInterface.OnClickListener)? = null): Boolean { - var success = false + protected fun importFilesFromUris(uris: List, callback: (String?) -> Unit) { + val items = ArrayList() for (uri in uris) { - val fileName = PathUtils.getFilenameFromURI(task.activity, uri) - if (fileName == null){ - task.stopTask { - ColoredAlertDialogBuilder(task.activity) + val fileName = PathUtils.getFilenameFromURI(this, uri) + if (fileName == null) { + ColoredAlertDialogBuilder(this) .setTitle(R.string.error) .setMessage(getString(R.string.error_retrieving_filename, uri)) .setPositiveButton(R.string.ok, null) .show() - } - success = false + items.clear() break } else { - val dstPath = checkPathOverwrite(PathUtils.pathJoin(currentDirectoryPath, fileName), false) - if (dstPath == null){ - break - } else { - var message: String? = null - try { - success = gocryptfsVolume.importFile(task.activity, uri, dstPath) - } catch (e: FileNotFoundException){ - message = if (e.message != null){ - e.message!!+"\n" - } else { - "" + items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, -1, -1, currentDirectoryPath))) + } + } + if (items.size > 0) { + checkPathOverwrite(items, currentDirectoryPath) { checkedItems -> + checkedItems?.let { + fileOperationService.importFilesFromUris(checkedItems, uris){ failedItem -> + runOnUiThread { + callback(failedItem) } } - if (!success || message != null) { - task.stopTask { - ColoredAlertDialogBuilder(task.activity) - .setTitle(R.string.error) - .setMessage((message ?: "")+getString(R.string.import_failed, uri)) - .setCancelable(callback == null) - .setPositiveButton(R.string.ok, callback) - .show() - } - break - } } } } - return success } protected fun rename(old_name: String, new_name: String){ diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt index 5f9070d..9b4a17a 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -3,20 +3,20 @@ package sushi.hardcore.droidfs.explorers import android.app.Activity import android.content.Intent import android.net.Uri -import android.os.Looper import android.view.Menu import android.view.MenuItem import android.view.View import android.view.WindowManager import android.widget.EditText import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.documentfile.provider.DocumentFile import sushi.hardcore.droidfs.CameraActivity import sushi.hardcore.droidfs.OpenActivity import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter -import sushi.hardcore.droidfs.util.* +import sushi.hardcore.droidfs.file_operations.OperationFile +import sushi.hardcore.droidfs.util.ExternalProvider +import sushi.hardcore.droidfs.util.GocryptfsVolume +import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import java.io.File @@ -30,7 +30,7 @@ class ExplorerActivity : BaseExplorerActivity() { private var usf_decrypt = false private var usf_share = false private var currentItemAction = ItemsActions.NONE - private val itemsToProcess = ArrayList() + private val itemsToProcess = ArrayList() override fun init() { setContentView(R.layout.activity_explorer) usf_decrypt = sharedPrefs.getBoolean("usf_decrypt", false) @@ -47,19 +47,17 @@ class ExplorerActivity : BaseExplorerActivity() { if (fileName.isEmpty()) { Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() } else { - checkPathOverwrite(PathUtils.pathJoin(currentDirectoryPath, fileName), false)?.let { - val handleID = gocryptfsVolume.openWriteMode(it) - if (handleID == -1) { - ColoredAlertDialogBuilder(this) + val handleID = gocryptfsVolume.openWriteMode(fileName) //don't check overwrite because openWriteMode open in read-write (doesn't erase content) + if (handleID == -1) { + ColoredAlertDialogBuilder(this) .setTitle(R.string.error) .setMessage(R.string.file_creation_failed) .setPositiveButton(R.string.ok, null) .show() - } else { - gocryptfsVolume.closeFile(handleID) - setCurrentPath(currentDirectoryPath) - invalidateOptionsMenu() - } + } else { + gocryptfsVolume.closeFile(handleID) + setCurrentPath(currentDirectoryPath) + invalidateOptionsMenu() } } } @@ -138,162 +136,137 @@ class ExplorerActivity : BaseExplorerActivity() { super.onActivityResult(requestCode, resultCode, data) if (requestCode == PICK_FILES_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK && data != null) { - object : LoadingTask(this, R.string.loading_msg_import){ - override fun doTask(activity: AppCompatActivity) { - val uris: MutableList = ArrayList() - val singleUri = data.data - if (singleUri == null) { //multiples choices - val clipData = data.clipData - if (clipData != null){ - for (i in 0 until clipData.itemCount) { - uris.add(clipData.getItemAt(i).uri) - } - } - } else { - uris.add(singleUri) + val uris: MutableList = ArrayList() + val singleUri = data.data + if (singleUri == null) { //multiples choices + val clipData = data.clipData + if (clipData != null){ + for (i in 0 until clipData.itemCount) { + uris.add(clipData.getItemAt(i).uri) } - Looper.prepare() - if (importFilesFromUris(uris, this)) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.success_import) - .setMessage(""" + } + } else { + uris.add(singleUri) + } + importFilesFromUris(uris){ failedItem -> + if (failedItem == null){ + ColoredAlertDialogBuilder(this) + .setTitle(R.string.success_import) + .setMessage(""" ${getString(R.string.success_import_msg)} ${getString(R.string.ask_for_wipe)} """.trimIndent()) - .setPositiveButton(R.string.yes) { _, _ -> - object : LoadingTask(activity, R.string.loading_msg_wipe){ - override fun doTask(activity: AppCompatActivity) { - var success = true - for (uri in uris) { - val errorMsg = Wiper.wipe(activity, uri) - if (errorMsg != null) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.error) - .setMessage(getString(R.string.wipe_failed, errorMsg)) - .setPositiveButton(R.string.ok, null) - .show() - } - success = false - break - } - } - if (success) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.wipe_successful) - .setMessage(R.string.wipe_success_msg) - .setPositiveButton(R.string.ok, null) - .show() - } - } + .setPositiveButton(R.string.yes) { _, _ -> + fileOperationService.wipeUris(uris) { errorMsg -> + runOnUiThread { + if (errorMsg == null){ + ColoredAlertDialogBuilder(this) + .setTitle(R.string.wipe_successful) + .setMessage(R.string.wipe_success_msg) + .setPositiveButton(R.string.ok, null) + .show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.wipe_failed, errorMsg)) + .setPositiveButton(R.string.ok, null) + .show() } } + } - .setNegativeButton(R.string.no, null) - .show() - } - } - } - override fun doFinally(activity: AppCompatActivity){ - setCurrentPath(currentDirectoryPath) + } + .setNegativeButton(R.string.no, null) + .show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.import_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() } } } } else if (requestCode == PICK_DIRECTORY_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK && data != null) { - object : LoadingTask(this, R.string.loading_msg_export){ - override fun doTask(activity: AppCompatActivity) { - data.data?.let {uri -> - contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - DocumentFile.fromTreeUri(activity, uri)?.let { treeDocumentFile -> - var failedItem: String? = null - for (i in explorerAdapter.selectedItems) { - val element = explorerAdapter.getItem(i) - val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name) - failedItem = if (element.isDirectory) { - recursiveExportDirectory(fullPath, treeDocumentFile) - } else { - if (exportFileInto(fullPath, treeDocumentFile)) null else fullPath - } - if (failedItem != null) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.error) - .setMessage(getString(R.string.export_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } - break - } - } - if (failedItem == null) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.success_export) - .setMessage(R.string.success_export_msg) - .setPositiveButton(R.string.ok, null) - .show() - } - } + data.data?.let { uri -> + fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem -> + runOnUiThread { + if (failedItem == null){ + ColoredAlertDialogBuilder(this) + .setTitle(R.string.success_export) + .setMessage(R.string.success_export_msg) + .setPositiveButton(R.string.ok, null) + .show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.export_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() } } } - override fun doFinally(activity: AppCompatActivity) { - unselectAll() - } + unselectAll() } } } else if (requestCode == PICK_OTHER_VOLUME_ITEMS_REQUEST_CODE) { if (resultCode == Activity.RESULT_OK && data != null) { - object : LoadingTask(this, R.string.loading_msg_import){ - override fun doTask(activity: AppCompatActivity) { - val remoteSessionID = data.getIntExtra("sessionID", -1) - val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID) - val path = data.getStringExtra("path") - var failedItem: String? = null - Looper.prepare() - if (path == null) { - val paths = data.getStringArrayListExtra("paths") - val types = data.getIntegerArrayListExtra("types") - if (types != null && paths != null){ - for (i in paths.indices) { - failedItem = if (types[i] == 0) { //directory - recursiveImportDirectoryFromOtherVolume(remoteGocryptfsVolume, paths[i], currentDirectoryPath) - } else { - safeImportFileFromOtherVolume(remoteGocryptfsVolume, paths[i], PathUtils.pathJoin(currentDirectoryPath, File(paths[i]).name)) - } - if (failedItem != null) { - break - } + val remoteSessionID = data.getIntExtra("sessionID", -1) + val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID) + val path = data.getStringExtra("path") + val operationFiles = ArrayList() + if (path == null){ //multiples elements + val paths = data.getStringArrayListExtra("paths") + val types = data.getIntegerArrayListExtra("types") + if (types != null && paths != null){ + for (i in paths.indices) { + operationFiles.add( + OperationFile.fromExplorerElement( + ExplorerElement(File(paths[i]).name, types[i].toShort(), -1, -1, PathUtils.getParentPath(paths[i])) + ) + ) + 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, -1, -1, PathUtils.getParentPath(path)) + ) + ) + } + if (operationFiles.size > 0){ + checkPathOverwrite(operationFiles, currentDirectoryPath) { items -> + if (items == null) { + remoteGocryptfsVolume.close() } else { - failedItem = safeImportFileFromOtherVolume(remoteGocryptfsVolume, path, PathUtils.pathJoin(currentDirectoryPath, File(path).name)) - } - if (failedItem == null) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.success_import) - .setMessage(R.string.success_import_msg) - .setPositiveButton(R.string.ok, null) - .show() - } - } else if (failedItem!!.isNotEmpty()){ - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(R.string.error) - .setMessage(getString(R.string.import_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() + fileOperationService.copyElements(items, remoteGocryptfsVolume){ failedItem -> + runOnUiThread { + if (failedItem == null){ + ColoredAlertDialogBuilder(this) + .setTitle(R.string.success_import) + .setMessage(R.string.success_import_msg) + .setPositiveButton(R.string.ok, null) + .show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.import_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() + } + } + remoteGocryptfsVolume.close() } } - remoteGocryptfsVolume.close() - } - override fun doFinally(activity: AppCompatActivity) { - setCurrentPath(currentDirectoryPath) } + } else { + remoteGocryptfsVolume.close() } } } @@ -344,7 +317,7 @@ class ExplorerActivity : BaseExplorerActivity() { } R.id.cut -> { for (i in explorerAdapter.selectedItems){ - itemsToProcess.add(explorerElements[i]) + itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i])) } currentItemAction = ItemsActions.MOVE unselectAll() @@ -352,7 +325,12 @@ class ExplorerActivity : BaseExplorerActivity() { } R.id.copy -> { for (i in explorerAdapter.selectedItems){ - itemsToProcess.add(explorerElements[i]) + itemsToProcess.add(OperationFile.fromExplorerElement(explorerElements[i])) + if (explorerElements[i].isDirectory){ + gocryptfsVolume.recursiveMapFiles(explorerElements[i].fullPath).forEach { + itemsToProcess.add(OperationFile.fromExplorerElement(it)) + } + } } currentItemAction = ItemsActions.COPY unselectAll() @@ -360,81 +338,45 @@ class ExplorerActivity : BaseExplorerActivity() { } 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 itemsToProcess) { - val dstPath = checkPathOverwrite(PathUtils.pathJoin(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){ - if (failedItem.isNotEmpty()) { - stopTask { - ColoredAlertDialogBuilder(activity) + checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> + items?.let { + fileOperationService.copyElements(it.toMutableList() as ArrayList){ failedItem -> + runOnUiThread { + if (failedItem != null){ + ColoredAlertDialogBuilder(this) .setTitle(R.string.error) - .setMessage(getString( - R.string.copy_failed, - failedItem - )) + .setMessage(getString(R.string.copy_failed, failedItem)) .setPositiveButton(R.string.ok, null) .show() - } + } else { + Toast.makeText(this, R.string.copy_success, Toast.LENGTH_SHORT).show() } - break - } - } - if (failedItem == null) { - stopTask { - ColoredAlertDialogBuilder(activity) - .setTitle(getString(R.string.copy_success)) - .setMessage(getString(R.string.copy_success_msg)) - .setPositiveButton(R.string.ok, null) - .show() } } } - override fun doFinally(activity: AppCompatActivity) { - cancelItemAction() - unselectAll() - setCurrentPath(currentDirectoryPath) - } + cancelItemAction() + unselectAll() } } 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() + mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath) + checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> + items?.let { + fileOperationService.moveElements(it.toMutableList() as ArrayList){ failedItem -> + runOnUiThread { + if (failedItem != null){ + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.move_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() + } else { + Toast.makeText(this, R.string.move_success, Toast.LENGTH_SHORT).show() + } } } } - override fun doFinally(activity: AppCompatActivity) { - cancelItemAction() - unselectAll() - setCurrentPath(currentDirectoryPath) - } + cancelItemAction() + unselectAll() } } true @@ -473,6 +415,24 @@ class ExplorerActivity : BaseExplorerActivity() { } } + private fun mapFileForMove(items: ArrayList, srcDirectoryPath: String): ArrayList { + val newItems = ArrayList() + items.forEach { + if (it.explorerElement.isDirectory){ + if (gocryptfsVolume.pathExists(PathUtils.pathJoin(currentDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, it.explorerElement.fullPath)))){ + newItems.addAll( + mapFileForMove( + gocryptfsVolume.listDir(it.explorerElement.fullPath).map { e -> OperationFile.fromExplorerElement(e) } as ArrayList, + srcDirectoryPath + ) + ) + } + } + } + items.addAll(newItems) + return items + } + private fun cancelItemAction() { if (currentItemAction != ItemsActions.NONE){ currentItemAction = ItemsActions.NONE @@ -489,220 +449,13 @@ class ExplorerActivity : BaseExplorerActivity() { } } - private fun copyFile(srcPath: String, dstPath: String): Boolean { - var success = true - val originalHandleId = gocryptfsVolume.openReadMode(srcPath) - if (originalHandleId != -1){ - val newHandleId = gocryptfsVolume.openWriteMode(dstPath) - if (newHandleId != -1){ - var offset: Long = 0 - val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS) - var length: Int - while (gocryptfsVolume.readFile(originalHandleId, offset, ioBuffer).also { length = it } > 0) { - val written = gocryptfsVolume.writeFile(newHandleId, offset, ioBuffer, length).toLong() - if (written == length.toLong()) { - offset += written - } else { - success = false - break - } - } - gocryptfsVolume.closeFile(newHandleId) - } else { - success = false - } - gocryptfsVolume.closeFile(originalHandleId) - } else { - success = false - } - return success - } - - private fun recursiveCopyDirectory(srcDirectoryPath: String, dstDirectoryPath: String): String? { - val mappedElements = gocryptfsVolume.recursiveMapFiles(srcDirectoryPath) - if (!gocryptfsVolume.pathExists(dstDirectoryPath)){ - if (!gocryptfsVolume.mkdir(dstDirectoryPath)) { - return srcDirectoryPath - } - } - for (e in mappedElements) { - val dstPath = checkPathOverwrite(PathUtils.pathJoin(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 { - if (!copyFile(e.fullPath, dstPath)) { - return e.fullPath - } - } - } - } - 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, dstDirectoryPath: String): String? { - for (element in elements){ - val dstPath = checkPathOverwrite(PathUtils.pathJoin(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) - if (srcHandleID != -1) { - val dstHandleID = gocryptfsVolume.openWriteMode(dstPath) - if (dstHandleID != -1) { - var length: Int - val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS) - var offset: Long = 0 - while (remoteGocryptfsVolume.readFile(srcHandleID, offset, ioBuffer).also { length = it } > 0) { - val written = - gocryptfsVolume.writeFile(dstHandleID, offset, ioBuffer, length).toLong() - if (written == length.toLong()) { - offset += length.toLong() - } else { - success = false - break - } - } - gocryptfsVolume.closeFile(dstHandleID) - } - remoteGocryptfsVolume.closeFile(srcHandleID) - } - return success - } - - private fun safeImportFileFromOtherVolume(remoteGocryptfsVolume: GocryptfsVolume, srcPath: String, dstPath: String): String? { - val checkedDstPath = checkPathOverwrite(dstPath, false) - return if (checkedDstPath == null){ - "" - } else { - if (importFileFromOtherVolume(remoteGocryptfsVolume, srcPath, checkedDstPath)) null else srcPath - } - } - - private fun recursiveImportDirectoryFromOtherVolume(remote_gocryptfsVolume: GocryptfsVolume, remote_directory_path: String, outputPath: String): String? { - val mappedElements = remote_gocryptfsVolume.recursiveMapFiles(remote_directory_path) - val dstDirectoryPath = checkPathOverwrite(PathUtils.pathJoin(outputPath, File(remote_directory_path).name), true) - if (dstDirectoryPath == null){ - return "" - } else { - if (!gocryptfsVolume.pathExists(dstDirectoryPath)) { - if (!gocryptfsVolume.mkdir(dstDirectoryPath)) { - return remote_directory_path - } - } - for (e in mappedElements) { - val dstPath = checkPathOverwrite(PathUtils.pathJoin(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 { - if (!importFileFromOtherVolume(remote_gocryptfsVolume, e.fullPath, dstPath)) { - return e.fullPath - } - } - } - } - } - return null - } - - private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean { - val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let { - contentResolver.openOutputStream(it) - } - return if (outputStream != null){ - gocryptfsVolume.exportFile(srcPath, outputStream) - } else { - false - } - } - - private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? { - treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let {childTree -> - val explorerElements = gocryptfsVolume.listDir(plain_directory_path) - for (e in explorerElements) { - val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) - if (e.isDirectory) { - val failedItem = recursiveExportDirectory(fullPath, childTree) - failedItem?.let { return it } - } else { - if (!exportFileInto(fullPath, childTree)){ - return fullPath - } - } - } - return null - } - return treeDocumentFile.name - } - - private fun recursiveRemoveDirectory(plain_directory_path: String): String? { - val explorerElements = gocryptfsVolume.listDir(plain_directory_path) - for (e in explorerElements) { - val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) - if (e.isDirectory) { - val result = recursiveRemoveDirectory(fullPath) - result?.let { return it } - } else { - if (!gocryptfsVolume.removeFile(fullPath)) { - return fullPath - } - } - } - return if (!gocryptfsVolume.rmdir(plain_directory_path)) { - plain_directory_path - } else { - null - } - } - private fun removeSelectedItems() { var failedItem: String? = null for (i in explorerAdapter.selectedItems) { val element = explorerAdapter.getItem(i) val fullPath = PathUtils.pathJoin(currentDirectoryPath, element.name) if (element.isDirectory) { - val result = recursiveRemoveDirectory(fullPath) + val result = gocryptfsVolume.recursiveRemoveDirectory(fullPath) result?.let{ failedItem = it } } else { if (!gocryptfsVolume.removeFile(fullPath)) { diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt index b623dbd..2574854 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityDrop.kt @@ -1,15 +1,11 @@ package sushi.hardcore.droidfs.explorers -import android.content.DialogInterface import android.content.Intent import android.net.Uri -import android.os.Looper import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.appcompat.app.AppCompatActivity import sushi.hardcore.droidfs.R -import sushi.hardcore.droidfs.util.LoadingTask import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder class ExplorerActivityDrop : BaseExplorerActivity() { @@ -31,54 +27,61 @@ class ExplorerActivityDrop : BaseExplorerActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.validate -> { - object : LoadingTask(this, R.string.loading_msg_import) { - override fun doTask(activity: AppCompatActivity) { - val alertDialog = ColoredAlertDialogBuilder(activity) - alertDialog.setCancelable(false) - alertDialog.setPositiveButton(R.string.ok) { _, _ -> finish() } - val errorMsg: String? - val extras = intent.extras - if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)){ - Looper.prepare() - when (intent.action) { - Intent.ACTION_SEND -> { - val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) - errorMsg = if (uri == null){ - getString(R.string.share_intent_parsing_failed) - } else { - if (importFilesFromUris(listOf(uri), this){ _, _ -> finish() }) null else "" - } - } - Intent.ACTION_SEND_MULTIPLE -> { - val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) - errorMsg = if (uris != null){ - if (importFilesFromUris(uris, this){ _, _ -> finish() }) null else "" - } else { - getString(R.string.share_intent_parsing_failed) - } - } - else -> { - errorMsg = getString(R.string.share_intent_parsing_failed) - } + val extras = intent.extras + val errorMsg: String? = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) { + when (intent.action) { + Intent.ACTION_SEND -> { + val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) + if (uri == null) { + getString(R.string.share_intent_parsing_failed) + } else { + importFilesFromUris(listOf(uri), ::onImported) + null } - } else { - errorMsg = getString(R.string.share_intent_parsing_failed) } - if (errorMsg == null || errorMsg.isNotEmpty()){ - if (errorMsg == null) { - alertDialog.setTitle(R.string.success_import) - alertDialog.setMessage(R.string.success_import_msg) - } else if (errorMsg.isNotEmpty()) { - alertDialog.setTitle(R.string.error) - alertDialog.setMessage(errorMsg) + Intent.ACTION_SEND_MULTIPLE -> { + val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + if (uris != null) { + importFilesFromUris(uris, ::onImported) + null + } else { + getString(R.string.share_intent_parsing_failed) } - stopTask { alertDialog.show() } } + else -> getString(R.string.share_intent_parsing_failed) } + } else { + getString(R.string.share_intent_parsing_failed) + } + errorMsg?.let { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(it) + .setPositiveButton(R.string.ok, null) + .show() } true } else -> super.onOptionsItemSelected(item) } } + + private fun onImported(failedItem: String?){ + if (failedItem == null) { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.success_import) + .setMessage(R.string.success_import_msg) + .setCancelable(false) + .setPositiveButton(R.string.ok){_, _ -> + finish() + } + .show() + } else { + ColoredAlertDialogBuilder(this) + .setTitle(R.string.error) + .setMessage(getString(R.string.import_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() + } + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt index 8003a35..85a7bee 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivityPick.kt @@ -17,6 +17,10 @@ class ExplorerActivityPick : BaseExplorerActivity() { resultIntent.putExtra("sessionID", gocryptfsVolume.sessionID) } + override fun bindFileOperationService() { + //don't bind + } + override fun onExplorerItemClick(position: Int) { val wasSelecting = explorerAdapter.selectedItems.isNotEmpty() explorerAdapter.onItemClick(position) diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt index f011bd4..68e8b6e 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt @@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.explorers import sushi.hardcore.droidfs.util.PathUtils import java.util.* -class ExplorerElement(val name: String, val elementType: Short, var size: Long, mtime: Long, parentPath: String) { +class ExplorerElement(val name: String, val elementType: Short, var size: Long, val mtime: Long, val parentPath: String) { val mTime = Date((mtime * 1000).toString().toLong()) val fullPath: String = PathUtils.pathJoin(parentPath, name) diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt new file mode 100644 index 0000000..cec8481 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/OperationFile.kt @@ -0,0 +1,11 @@ +package sushi.hardcore.droidfs.file_operations + +import sushi.hardcore.droidfs.explorers.ExplorerElement + +class OperationFile(val explorerElement: ExplorerElement, var dstPath: String? = null, var overwriteConfirmed: Boolean = false) { + companion object { + fun fromExplorerElement(e: ExplorerElement): OperationFile { + return OperationFile(e, null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/GocryptfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/util/GocryptfsVolume.kt index 6215cde..8b1e93f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/GocryptfsVolume.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/GocryptfsVolume.kt @@ -187,4 +187,24 @@ class GocryptfsVolume(var sessionID: Int) { } return result } + + fun recursiveRemoveDirectory(plain_directory_path: String): String? { + val explorerElements = listDir(plain_directory_path) + for (e in explorerElements) { + val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) + if (e.isDirectory) { + val result = recursiveRemoveDirectory(fullPath) + result?.let { return it } + } else { + if (!removeFile(fullPath)) { + return fullPath + } + } + } + return if (!rmdir(plain_directory_path)) { + plain_directory_path + } else { + null + } + } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt index 949c979..0266c50 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt @@ -43,7 +43,17 @@ object PathUtils { } fun getRelativePath(parentPath: String, childPath: String): String { - return childPath.substring(parentPath.length + 1) + return when { + parentPath.isEmpty() -> { + childPath + } + parentPath.length == childPath.length -> { + "" + } + else -> { + childPath.substring(parentPath.length + 1) + } + } } fun getFilenameFromURI(context: Context, uri: Uri): String? { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e6ab3f..a931ee3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,8 +133,6 @@ Creating volume… Changing password… Opening volume… - Importing selected files… - Wiping original files… Exporting files… Unable to access this file About @@ -144,9 +142,7 @@ The DroidFS repository on the DryCat Gitea instance. Gitea is fully free and self-hosted. Source code, documentation, bug tracker… Share Export/Decrypt - Copying selected items… Copy of %s failed. - The selected items have been successfully copied. Copy successful ! Add Take photo @@ -159,9 +155,7 @@ Reset theme color Reset theme color to the default one Copy - Moving selected items… Move of %s failed. - The selected items have been successfully moved. Move successful ! Enter the timer duration (in s) Please enter a numeric value diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0e42234..1f3fdbc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=11657af6356b7587bfb37287b5992e94a9686d5c8a0a1b60b87b9928a2decde5 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists