Adding an 'Import/Encrypt Folder' button
This commit is contained in:
parent
60ba9531be
commit
5cc9abfd76
@ -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()
|
||||||
|
@ -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,6 +93,35 @@ 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 ->
|
||||||
|
onImportComplete(failedItem, uris)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val pickExportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem ->
|
||||||
|
runOnUiThread {
|
||||||
|
if (failedItem == null){
|
||||||
|
Toast.makeText(this, R.string.success_export, Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
ColoredAlertDialogBuilder(this)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(getString(R.string.export_failed, failedItem))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unselectAll()
|
||||||
|
}
|
||||||
|
private val pickImportDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { rootUri ->
|
||||||
|
rootUri?.let {
|
||||||
|
importDirectory(it, ::onImportComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onImportComplete(failedItem: String?, uris: List<Uri>) {
|
||||||
if (failedItem == null){
|
if (failedItem == null){
|
||||||
ColoredAlertDialogBuilder(this)
|
ColoredAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.success_import)
|
.setTitle(R.string.success_import)
|
||||||
@ -125,26 +155,6 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
}
|
}
|
||||||
setCurrentPath(currentDirectoryPath)
|
setCurrentPath(currentDirectoryPath)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
private val pickDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
fileOperationService.exportFiles(uri, explorerAdapter.selectedItems.map { i -> explorerElements[i] }){ failedItem ->
|
|
||||||
runOnUiThread {
|
|
||||||
if (failedItem == null){
|
|
||||||
Toast.makeText(this, R.string.success_export, Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
ColoredAlertDialogBuilder(this)
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(getString(R.string.export_failed, failedItem))
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unselectAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,24 +205,23 @@ 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){
|
||||||
Thread {
|
val notification = reuseNotification ?: showNotification(R.string.file_op_import_msg, dstPaths.size)
|
||||||
val notification = showNotification(R.string.file_op_import_msg, items.size)
|
|
||||||
var failedIndex = -1
|
var failedIndex = -1
|
||||||
for (i in 0 until items.size) {
|
for (i in dstPaths.indices) {
|
||||||
if (notifications[notification.notificationId]!!){
|
if (notifications[notification.notificationId]!!){
|
||||||
cancelNotification(notification)
|
cancelNotification(notification)
|
||||||
return@Thread
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!gocryptfsVolume.importFile(this, uris[i], items[i].dstPath!!)){
|
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) {
|
||||||
failedIndex = i
|
failedIndex = i
|
||||||
}
|
}
|
||||||
} catch (e: FileNotFoundException){
|
} catch (e: FileNotFoundException){
|
||||||
failedIndex = i
|
failedIndex = i
|
||||||
}
|
}
|
||||||
if (failedIndex == -1) {
|
if (failedIndex == -1) {
|
||||||
updateNotificationProgress(notification, i, items.size)
|
updateNotificationProgress(notification, i, dstPaths.size)
|
||||||
} else {
|
} else {
|
||||||
cancelNotification(notification)
|
cancelNotification(notification)
|
||||||
callback(uris[failedIndex].toString())
|
callback(uris[failedIndex].toString())
|
||||||
@ -226,6 +232,76 @@ class FileOperationService : Service() {
|
|||||||
cancelNotification(notification)
|
cancelNotification(notification)
|
||||||
callback(null)
|
callback(null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, callback: (String?) -> Unit) {
|
||||||
|
Thread {
|
||||||
|
importFilesFromUris(dstPaths, uris, null, callback)
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
gocryptfsVolume.mkdir(mkdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
importFilesFromUris(dstFiles, srcUris, notification) { failedItem ->
|
||||||
|
callback(failedItem, srcUris)
|
||||||
|
}
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
app/src/main/res/drawable/icon_create_new_folder.xml
Normal file
5
app/src/main/res/drawable/icon_create_new_folder.xml
Normal 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>
|
5
app/src/main/res/drawable/icon_import_folder.xml
Normal file
5
app/src/main/res/drawable/icon_import_folder.xml
Normal 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>
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user