Adding an 'Import/Encrypt Folder' button

This commit is contained in:
Anon7250 2021-08-19 14:13:32 -07:00 committed by Hardcore Sushi
parent 60ba9531be
commit 5cc9abfd76
Signed by untrusted user: hardcoresushi
GPG Key ID: 007F84120107191E
7 changed files with 184 additions and 66 deletions

View File

@ -17,6 +17,7 @@ import android.widget.EditText
import android.widget.ListView import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -254,7 +255,7 @@ open class BaseExplorerActivity : BaseActivity() {
synchronized(this) { synchronized(this) {
explorerElements.add( explorerElements.add(
0, 0,
ExplorerElement("..", (-1).toShort(), -1, -1, currentDirectoryPath) ExplorerElement("..", (-1).toShort(), parentPath = currentDirectoryPath)
) )
} }
} }
@ -408,13 +409,13 @@ open class BaseExplorerActivity : BaseActivity() {
items.clear() items.clear()
break break
} else { } else {
items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, -1, -1, currentDirectoryPath))) items.add(OperationFile.fromExplorerElement(ExplorerElement(fileName, 1, parentPath = currentDirectoryPath)))
} }
} }
if (items.size > 0) { if (items.size > 0) {
checkPathOverwrite(items, currentDirectoryPath) { checkedItems -> checkPathOverwrite(items, currentDirectoryPath) { checkedItems ->
checkedItems?.let { checkedItems?.let {
fileOperationService.importFilesFromUris(checkedItems, uris){ failedItem -> fileOperationService.importFilesFromUris(checkedItems.map { it.dstPath!! }, uris){ failedItem ->
runOnUiThread { runOnUiThread {
callback(failedItem) callback(failedItem)
} }
@ -424,6 +425,20 @@ open class BaseExplorerActivity : BaseActivity() {
} }
} }
fun importDirectory(sourceUri: Uri, callback: (String?, List<Uri>) -> Unit) {
val tree = DocumentFile.fromTreeUri(this, sourceUri)!! //non-null after Lollipop
val operation = OperationFile.fromExplorerElement(ExplorerElement(tree.name!!, 0, parentPath = currentDirectoryPath))
checkPathOverwrite(arrayListOf(operation), currentDirectoryPath) { checkedOperation ->
checkedOperation?.let {
fileOperationService.importDirectory(checkedOperation[0].dstPath!!, tree) { failedItem, uris ->
runOnUiThread {
callback(failedItem, uris)
}
}
}
}
}
protected fun rename(old_name: String, new_name: String){ protected fun rename(old_name: String, new_name: String){
if (new_name.isEmpty()) { if (new_name.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()

View File

@ -2,6 +2,7 @@ package sushi.hardcore.droidfs.explorers
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.WindowManager import android.view.WindowManager
@ -44,7 +45,7 @@ class ExplorerActivity : BaseExplorerActivity() {
for (i in paths.indices) { for (i in paths.indices) {
operationFiles.add( operationFiles.add(
OperationFile.fromExplorerElement( OperationFile.fromExplorerElement(
ExplorerElement(File(paths[i]).name, types[i].toShort(), -1, -1, PathUtils.getParentPath(paths[i])) ExplorerElement(File(paths[i]).name, types[i].toShort(), parentPath = PathUtils.getParentPath(paths[i]))
) )
) )
if (types[i] == 0){ //directory if (types[i] == 0){ //directory
@ -57,7 +58,7 @@ class ExplorerActivity : BaseExplorerActivity() {
} else { } else {
operationFiles.add( operationFiles.add(
OperationFile.fromExplorerElement( OperationFile.fromExplorerElement(
ExplorerElement(File(path).name, 1, -1, -1, PathUtils.getParentPath(path)) ExplorerElement(File(path).name, 1, parentPath = PathUtils.getParentPath(path))
) )
) )
} }
@ -92,42 +93,11 @@ class ExplorerActivity : BaseExplorerActivity() {
private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
if (uris != null) { if (uris != null) {
importFilesFromUris(uris){ failedItem -> importFilesFromUris(uris){ failedItem ->
if (failedItem == null){ onImportComplete(failedItem, uris)
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) { _, _ ->
fileOperationService.wipeUris(uris) { errorMsg ->
runOnUiThread {
if (errorMsg == null){
Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).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()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
setCurrentPath(currentDirectoryPath)
} }
} }
} }
private val pickDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
if (uri != null) { if (uri != null) {
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem -> fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem ->
runOnUiThread { runOnUiThread {
@ -145,6 +115,46 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
unselectAll() unselectAll()
} }
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
rootUri?.let {
importDirectory(it, ::onImportComplete)
}
}
private fun onImportComplete(failedItem: String?, uris: List<Uri>) {
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) { _, _ ->
fileOperationService.wipeUris(uris) { errorMsg ->
runOnUiThread {
if (errorMsg == null){
Toast.makeText(this, R.string.wipe_successful, Toast.LENGTH_SHORT).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()
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
setCurrentPath(currentDirectoryPath)
}
override fun init() { override fun init() {
binding = ActivityExplorerBinding.inflate(layoutInflater) binding = ActivityExplorerBinding.inflate(layoutInflater)
@ -157,8 +167,9 @@ class ExplorerActivity : BaseExplorerActivity() {
adapter.items = listOf( adapter.items = listOf(
listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert), listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert),
listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt), listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt),
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown), listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
listOf("createFolder", R.string.mkdir, R.drawable.icon_folder), listOf("createFolder", R.string.mkdir, R.drawable.icon_create_new_folder),
listOf("takePhoto", R.string.take_photo, R.drawable.icon_camera) listOf("takePhoto", R.string.take_photo, R.drawable.icon_camera)
) )
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)
@ -175,6 +186,10 @@ class ExplorerActivity : BaseExplorerActivity() {
isStartingActivity = true isStartingActivity = true
pickFiles.launch(arrayOf("*/*")) pickFiles.launch(arrayOf("*/*"))
} }
"importFolder" -> {
isStartingActivity = true
pickImportDirectory.launch(null)
}
"createFile" -> { "createFile" -> {
val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null) val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null)
val dialogEditText = dialogEditTextView.findViewById<EditText>(R.id.dialog_edit_text) val dialogEditText = dialogEditTextView.findViewById<EditText>(R.id.dialog_edit_text)
@ -381,7 +396,7 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
R.id.decrypt -> { R.id.decrypt -> {
isStartingActivity = true isStartingActivity = true
pickDirectory.launch(null) pickExportDirectory.launch(null)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)

View File

@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.explorers
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import java.util.* import java.util.*
class ExplorerElement(val name: String, val elementType: Short, var size: Long, mTime: Long, val parentPath: String) { class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) {
val mTime = Date((mTime * 1000).toString().toLong()) val mTime = Date((mTime * 1000).toString().toLong())
val fullPath: String = PathUtils.pathJoin(parentPath, name) val fullPath: String = PathUtils.pathJoin(parentPath, name)

View File

@ -38,7 +38,7 @@ class FileOperationService : Service() {
return binder return binder
} }
private fun showNotification(message: Int, total: Int): FileOperationNotification { private fun showNotification(message: Int, total: Int?): FileOperationNotification {
++lastNotificationId ++lastNotificationId
if (!::notificationManager.isInitialized){ if (!::notificationManager.isInitialized){
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -73,11 +73,18 @@ class FileOperationService : Service() {
} }
notificationBuilder notificationBuilder
.setContentTitle(getString(message)) .setContentTitle(getString(message))
.setContentText("0/$total")
.setSmallIcon(R.mipmap.icon_launcher) .setSmallIcon(R.mipmap.icon_launcher)
.setOngoing(true) .setOngoing(true)
.setProgress(total, 0, false)
.addAction(notificationAction.build()) .addAction(notificationAction.build())
if (total != null) {
notificationBuilder
.setContentText("0/$total")
.setProgress(total, 0, false)
} else {
notificationBuilder
.setContentText(getString(R.string.discovering_files))
.setProgress(0, 0, true)
}
notifications[lastNotificationId] = false notifications[lastNotificationId] = false
notificationManager.notify(lastNotificationId, notificationBuilder.build()) notificationManager.notify(lastNotificationId, notificationBuilder.build())
return FileOperationNotification(notificationBuilder, lastNotificationId) return FileOperationNotification(notificationBuilder, lastNotificationId)
@ -198,33 +205,102 @@ class FileOperationService : Service() {
}.start() }.start()
} }
fun importFilesFromUris(items: ArrayList<OperationFile>, uris: List<Uri>, callback: (String?) -> Unit){ private fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, reuseNotification: FileOperationNotification? = null, callback: (String?) -> Unit){
val notification = reuseNotification ?: showNotification(R.string.file_op_import_msg, dstPaths.size)
var failedIndex = -1
for (i in dstPaths.indices) {
if (notifications[notification.notificationId]!!){
cancelNotification(notification)
return
}
try {
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) {
failedIndex = i
}
} catch (e: FileNotFoundException){
failedIndex = i
}
if (failedIndex == -1) {
updateNotificationProgress(notification, i, dstPaths.size)
} else {
cancelNotification(notification)
callback(uris[failedIndex].toString())
break
}
}
if (failedIndex == -1){
cancelNotification(notification)
callback(null)
}
}
fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, callback: (String?) -> Unit) {
Thread { Thread {
val notification = showNotification(R.string.file_op_import_msg, items.size) importFilesFromUris(dstPaths, uris, null, callback)
var failedIndex = -1 }.start()
for (i in 0 until items.size) { }
if (notifications[notification.notificationId]!!){
/**
* Map the content of an unencrypted directory to prepare its import
*
* Contents of dstFiles and srcUris, at the same index, will match each other
*
* @return false if cancelled early, true otherwise.
*/
private fun recursiveMapDirectoryForImport(
rootSrcDir: DocumentFile,
rootDstPath: String,
dstFiles: ArrayList<String>,
srcUris: ArrayList<Uri>,
dstDirs: ArrayList<String>,
notification: FileOperationNotification
): Boolean {
dstDirs.add(rootDstPath)
for (child in rootSrcDir.listFiles()) {
if (notifications[notification.notificationId]!!) {
cancelNotification(notification)
return false
}
child.name?.let { name ->
val subPath = PathUtils.pathJoin(rootDstPath, name)
if (child.isDirectory) {
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, notification)) {
return false
}
}
else if (child.isFile) {
srcUris.add(child.uri)
dstFiles.add(subPath)
}
}
}
return true
}
fun importDirectory(rootDstPath: String, rootSrcDir: DocumentFile, callback: (String?, List<Uri>) -> Unit) {
Thread {
val notification = showNotification(R.string.file_op_import_msg, null)
val dstFiles = arrayListOf<String>()
val srcUris = arrayListOf<Uri>()
val dstDirs = arrayListOf<String>()
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) {
return@Thread
}
updateNotificationProgress(notification, 0, dstDirs.size)
// create destination folders so the new files can use them
for (mkdir in dstDirs) {
if (notifications[notification.notificationId]!!) {
cancelNotification(notification) cancelNotification(notification)
return@Thread return@Thread
} }
try { gocryptfsVolume.mkdir(mkdir)
if (!gocryptfsVolume.importFile(this, uris[i], items[i].dstPath!!)){
failedIndex = i
}
} catch (e: FileNotFoundException){
failedIndex = i
}
if (failedIndex == -1) {
updateNotificationProgress(notification, i, items.size)
} else {
cancelNotification(notification)
callback(uris[failedIndex].toString())
break
}
} }
if (failedIndex == -1){
cancelNotification(notification) importFilesFromUris(dstFiles, srcUris, notification) { failedItem ->
callback(null) callback(failedItem, srcUris)
} }
}.start() }.start()
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10zM8,13.01l1.41,1.41L11,12.84L11,17h2v-4.16l1.59,1.59L16,13.01 12.01,9 8,13.01z"/>
</vector>

View File

@ -11,6 +11,8 @@
<string name="volume_path">Volume Path:</string> <string name="volume_path">Volume Path:</string>
<string name="volume_name">Volume Name:</string> <string name="volume_name">Volume Name:</string>
<string name="import_files">Import/Encrypt files</string> <string name="import_files">Import/Encrypt files</string>
<string name="import_folder">Import/Encrypt folder</string>
<string name="discovering_files">Discovering files…</string>
<string name="mkdir">Create folder</string> <string name="mkdir">Create folder</string>
<string name="dir_empty">Directory Empty</string> <string name="dir_empty">Directory Empty</string>
<string name="warning">Warning !</string> <string name="warning">Warning !</string>