package sushi.hardcore.droidfs.explorers import android.app.Activity import android.content.Intent import android.net.Uri 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 com.github.clans.fab.FloatingActionButton import com.github.clans.fab.FloatingActionMenu import kotlinx.android.synthetic.main.activity_explorer.* import sushi.hardcore.droidfs.OpenActivity import sushi.hardcore.droidfs.R 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 private var usf_decrypt = false private var usf_share = false private var modeSelectLocation = false private val filesToCopy = ArrayList() override fun init() { setContentView(R.layout.activity_explorer) usf_decrypt = sharedPrefs.getBoolean("usf_decrypt", false) usf_share = sharedPrefs.getBoolean("usf_share", false) } override fun onExplorerItemLongClick(position: Int) { cancelCopy() explorerAdapter.onItemLongClick(position) invalidateOptionsMenu() } private fun createNewFile(fileName: String){ if (fileName.isEmpty()) { Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() } else { val handleID = gocryptfsVolume.openWriteMode(PathUtils.path_join(currentDirectoryPath, fileName)) 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() } } } fun onClickCreateFile(view: View) { findViewById(R.id.fam_explorer).close(true) val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null) val dialogEditText = dialogEditTextView.findViewById(R.id.dialog_edit_text) val dialog = ColoredAlertDialogBuilder(this) .setView(dialogEditTextView) .setTitle(getString(R.string.enter_file_name)) .setPositiveButton(R.string.ok) { _, _ -> val fileName = dialogEditText.text.toString() createNewFile(fileName) } .setNegativeButton(R.string.cancel, null) .create() dialogEditText.setOnEditorActionListener { _, _, _ -> val fileName = dialogEditText.text.toString() dialog.dismiss() createNewFile(fileName) true } dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) dialog.show() } fun onClickAddFile(view: View?) { fam_explorer.close(true) val i = Intent(Intent.ACTION_OPEN_DOCUMENT) i.type = "*/*" i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) i.addCategory(Intent.CATEGORY_OPENABLE) startActivityForResult(i, PICK_FILES_REQUEST_CODE) } fun onClickAddFileFromOtherVolume(view: View?) { fam_explorer.close(true) val intent = Intent(this, OpenActivity::class.java) intent.action = "pick" startActivityForResult(intent, PICK_OTHER_VOLUME_ITEMS_REQUEST_CODE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 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) } var success = true for (uri in uris) { val dstPath = PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(activity, uri)) contentResolver.openInputStream(uri)?.let { success = gocryptfsVolume.importFile(it, dstPath) } if (!success) { stopTask { ColoredAlertDialogBuilder(activity) .setTitle(R.string.error) .setMessage(getString(R.string.import_failed, uri)) .setPositiveButton(R.string.ok, null) .show() } break } } if (success) { stopTask { ColoredAlertDialogBuilder(activity) .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) { 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() } } } } } .setNegativeButton(getString(R.string.no), null) .show() } } } override fun doFinally(activity: AppCompatActivity){ setCurrentPath(currentDirectoryPath) } } } } 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) { val uri = data.data val outputDir = PathUtils.getFullPathFromTreeUri(uri, activity) var failedItem: String? = null for (i in explorerAdapter.selectedItems) { val element = explorerAdapter.getItem(i) val fullPath = PathUtils.path_join(currentDirectoryPath, element.name) failedItem = if (element.isDirectory) { recursiveExportDirectory(fullPath, outputDir) } else { if (gocryptfsVolume.exportFile(fullPath, PathUtils.path_join(outputDir, element.name))) 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() } } } override fun doFinally(activity: AppCompatActivity) { 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 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 { if (importFileFromOtherVolume(remoteGocryptfsVolume, paths[i], PathUtils.path_join(currentDirectoryPath, File(paths[i]).name))) null else paths[i] } if (failedItem != null) { break } } } } else { failedItem = if (importFileFromOtherVolume(remoteGocryptfsVolume, path, PathUtils.path_join(currentDirectoryPath, File(path).name))) null else path } if (failedItem == null) { stopTask { ColoredAlertDialogBuilder(activity) .setTitle(R.string.success_import) .setMessage(R.string.success_import_msg) .setPositiveButton(R.string.ok, null) .show() } } else { stopTask { ColoredAlertDialogBuilder(activity) .setTitle(R.string.error) .setMessage(getString(R.string.import_failed, failedItem)) .setPositiveButton(R.string.ok, null) .show() } } remoteGocryptfsVolume.close() } override fun doFinally(activity: AppCompatActivity) { setCurrentPath(currentDirectoryPath) } } } } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.explorer, menu) if (modeSelectLocation) { menu.findItem(R.id.validate).isVisible = true menu.findItem(R.id.close).isVisible = false } else { handleMenuItems(menu) if (usf_share){ menu.findItem(R.id.share).isVisible = false } 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 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 } } } return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { cancelCopy() super.onOptionsItemSelected(item) } R.id.select_all -> { explorerAdapter.selectAll() invalidateOptionsMenu() true } R.id.copy -> { for (i in explorerAdapter.selectedItems){ filesToCopy.add(explorerElements[i]) } modeSelectLocation = true unselectAll() findViewById(R.id.fab_add_file).visibility = View.GONE findViewById(R.id.fab_import_file).visibility = View.GONE findViewById(R.id.fab_import_file_from_other_volume).visibility = View.GONE true } R.id.validate -> { object : LoadingTask(this, R.string.loading_msg_copy){ override fun doTask(activity: AppCompatActivity) { var failedItem: String? = null for (element in filesToCopy) { val originalPath = element.getFullPath() failedItem = if (element.isDirectory) { recursiveCopyDirectory(originalPath, currentDirectoryPath) } else { if (copyFile(originalPath, PathUtils.path_join(currentDirectoryPath, element.name))) null else originalPath } if (failedItem != null) { stopTask { ColoredAlertDialogBuilder(activity) .setTitle(R.string.error) .setMessage(getString(R.string.copy_failed, failedItem)) .setPositiveButton(R.string.ok, null) .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) { cancelCopy() unselectAll() setCurrentPath(currentDirectoryPath) } } true } R.id.delete -> { val size = explorerAdapter.selectedItems.size val dialog = ColoredAlertDialogBuilder(this) dialog.setTitle(R.string.warning) dialog.setPositiveButton(R.string.ok) { _, _ -> removeSelectedItems() } dialog.setNegativeButton(R.string.cancel, null) if (size > 1) { dialog.setMessage(getString(R.string.multiple_delete_confirm, explorerAdapter.selectedItems.size.toString())) } else { dialog.setMessage(getString(R.string.single_delete_confirm, explorerAdapter.getItem(explorerAdapter.selectedItems[0]).name)) } dialog.show() true } R.id.share -> { val paths: MutableList = ArrayList() for (i in explorerAdapter.selectedItems) { paths.add(explorerElements[i].getFullPath()) } ExternalProvider.share(this, gocryptfsVolume, paths) unselectAll() true } R.id.decrypt -> { val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(i, PICK_DIRECTORY_REQUEST_CODE) true } else -> super.onOptionsItemSelected(item) } } private fun cancelCopy() { if (modeSelectLocation){ modeSelectLocation = false findViewById(R.id.fab_add_file).visibility = View.VISIBLE findViewById(R.id.fab_import_file).visibility = View.VISIBLE findViewById(R.id.fab_import_file_from_other_volume).visibility = View.VISIBLE filesToCopy.clear() } } 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, outputPath: String): String? { val mappedElements = gocryptfsVolume.recursiveMapFiles(srcDirectoryPath) val dstDirectoryPath = PathUtils.path_join(outputPath, File(srcDirectoryPath).name) if (!gocryptfsVolume.pathExists(dstDirectoryPath)) { if (!gocryptfsVolume.mkdir(dstDirectoryPath)) { return dstDirectoryPath } } for (e in mappedElements) { val srcPath = e.getFullPath() val dstPath = PathUtils.path_join(dstDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, srcPath)) if (e.isDirectory) { if (!gocryptfsVolume.mkdir(dstPath)){ return srcPath } } else { if (!copyFile(srcPath, dstPath)) { return srcPath } } } return null } private fun importFileFromOtherVolume(remote_gocryptfsVolume: GocryptfsVolume, srcPath: String, dstPath: String): Boolean { var success = true val srcHandleID = remote_gocryptfsVolume.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 (remote_gocryptfsVolume.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) } remote_gocryptfsVolume.closeFile(srcHandleID) } return success } 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) if (!gocryptfsVolume.pathExists(dstDirectoryPath)) { if (!gocryptfsVolume.mkdir(dstDirectoryPath)) { return dstDirectoryPath } } for (e in mappedElements) { val srcPath = e.getFullPath() val dstPath = PathUtils.path_join(dstDirectoryPath, PathUtils.getRelativePath(remote_directory_path, srcPath)) if (e.isDirectory) { if (!gocryptfsVolume.mkdir(dstPath)){ return srcPath } } else { if (!importFileFromOtherVolume(remote_gocryptfsVolume, srcPath, dstPath)) { return srcPath } } } return null } private fun recursiveExportDirectory(plain_directory_path: String, output_dir: String?): String? { if (File(PathUtils.path_join(output_dir, plain_directory_path)).mkdir()) { val explorerElements = gocryptfsVolume.listDir(plain_directory_path) for (e in explorerElements) { val fullPath = PathUtils.path_join(plain_directory_path, e.name) if (e.isDirectory) { val failedItem = recursiveExportDirectory(fullPath, output_dir) failedItem?.let { return it } } else { if (!gocryptfsVolume.exportFile(fullPath, PathUtils.path_join(output_dir, fullPath))) { return fullPath } } } return null } return output_dir } private fun recursiveRemoveDirectory(plain_directory_path: String): String? { val explorerElements = gocryptfsVolume.listDir(plain_directory_path) for (e in explorerElements) { val fullPath = PathUtils.path_join(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.path_join(currentDirectoryPath, element.name) if (element.isDirectory) { val result = recursiveRemoveDirectory(fullPath) result?.let{ failedItem = it } } else { if (!gocryptfsVolume.removeFile(fullPath)) { failedItem = fullPath } } if (failedItem != null) { ColoredAlertDialogBuilder(this) .setTitle(R.string.error) .setMessage(getString(R.string.remove_failed, failedItem)) .setPositiveButton(R.string.ok, null) .show() break } } unselectAll() setCurrentPath(currentDirectoryPath) //refresh } }