|
|
|
@ -13,6 +13,7 @@ import android.os.IBinder
|
|
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
|
import androidx.core.app.NotificationManagerCompat
|
|
|
|
|
import androidx.documentfile.provider.DocumentFile
|
|
|
|
|
import kotlinx.coroutines.*
|
|
|
|
|
import sushi.hardcore.droidfs.GocryptfsVolume
|
|
|
|
|
import sushi.hardcore.droidfs.R
|
|
|
|
|
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
|
|
|
@ -30,7 +31,7 @@ class FileOperationService : Service() {
|
|
|
|
|
private val binder = LocalBinder()
|
|
|
|
|
private lateinit var gocryptfsVolume: GocryptfsVolume
|
|
|
|
|
private lateinit var notificationManager: NotificationManagerCompat
|
|
|
|
|
private var notifications = HashMap<Int, Boolean>()
|
|
|
|
|
private val tasks = HashMap<Int, Job>()
|
|
|
|
|
private var lastNotificationId = 0
|
|
|
|
|
|
|
|
|
|
inner class LocalBinder : Binder() {
|
|
|
|
@ -88,7 +89,6 @@ class FileOperationService : Service() {
|
|
|
|
|
.setContentText(getString(R.string.discovering_files))
|
|
|
|
|
.setProgress(0, 0, true)
|
|
|
|
|
}
|
|
|
|
|
notifications[lastNotificationId] = false
|
|
|
|
|
notificationManager.notify(lastNotificationId, notificationBuilder.build())
|
|
|
|
|
return FileOperationNotification(notificationBuilder, lastNotificationId)
|
|
|
|
|
}
|
|
|
|
@ -105,7 +105,20 @@ class FileOperationService : Service() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun cancelOperation(notificationId: Int){
|
|
|
|
|
notifications[notificationId] = true
|
|
|
|
|
tasks[notificationId]?.cancel()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open class TaskResult<T>(val cancelled: Boolean, val failedItem: T?)
|
|
|
|
|
|
|
|
|
|
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<T> {
|
|
|
|
|
tasks[notification.notificationId] = task
|
|
|
|
|
return try {
|
|
|
|
|
TaskResult(false, task.await())
|
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
|
TaskResult(true, null)
|
|
|
|
|
} finally {
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
|
|
|
|
@ -137,110 +150,105 @@ class FileOperationService : Service() {
|
|
|
|
|
return success
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun copyElements(items: ArrayList<OperationFile>, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){
|
|
|
|
|
Thread {
|
|
|
|
|
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
|
|
|
|
suspend fun copyElements(
|
|
|
|
|
items: ArrayList<OperationFile>,
|
|
|
|
|
remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume
|
|
|
|
|
): String? = coroutineScope {
|
|
|
|
|
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
|
|
|
|
val task = async {
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
for (i in 0 until items.size){
|
|
|
|
|
if (notifications[notification.notificationId]!!){
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
return@Thread
|
|
|
|
|
}
|
|
|
|
|
if (items[i].explorerElement.isDirectory){
|
|
|
|
|
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
|
|
|
|
|
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
|
|
|
|
|
for (i in 0 until items.size) {
|
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
|
if (items[i].explorerElement.isDirectory) {
|
|
|
|
|
if (!gocryptfsVolume.pathExists(items[i].dstPath!!)) {
|
|
|
|
|
if (!gocryptfsVolume.mkdir(items[i].dstPath!!)) {
|
|
|
|
|
failedItem = items[i].explorerElement.fullPath
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)) {
|
|
|
|
|
failedItem = items[i].explorerElement.fullPath
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)){
|
|
|
|
|
failedItem = items[i].explorerElement.fullPath
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null){
|
|
|
|
|
updateNotificationProgress(notification, i, items.size)
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
|
updateNotificationProgress(notification, i+1, items.size)
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
callback(failedItem)
|
|
|
|
|
}.start()
|
|
|
|
|
failedItem
|
|
|
|
|
}
|
|
|
|
|
// treat cancellation as success
|
|
|
|
|
waitForTask(notification, task).failedItem
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun moveElements(items: ArrayList<OperationFile>, callback: (String?) -> Unit){
|
|
|
|
|
Thread {
|
|
|
|
|
val notification = showNotification(R.string.file_op_move_msg, items.size)
|
|
|
|
|
val mergedFolders = ArrayList<String>()
|
|
|
|
|
suspend fun moveElements(items: ArrayList<OperationFile>): String? = coroutineScope {
|
|
|
|
|
val notification = showNotification(R.string.file_op_move_msg, items.size)
|
|
|
|
|
val task = async {
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
for (i in 0 until items.size){
|
|
|
|
|
if (notifications[notification.notificationId]!!){
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
return@Thread
|
|
|
|
|
}
|
|
|
|
|
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)){ //folder will be merged
|
|
|
|
|
mergedFolders.add(items[i].explorerElement.fullPath)
|
|
|
|
|
} else {
|
|
|
|
|
if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)){
|
|
|
|
|
failedItem = items[i].explorerElement.fullPath
|
|
|
|
|
break
|
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
|
val mergedFolders = ArrayList<String>()
|
|
|
|
|
for (i in 0 until items.size) {
|
|
|
|
|
if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)) { //folder will be merged
|
|
|
|
|
mergedFolders.add(items[i].explorerElement.fullPath)
|
|
|
|
|
} else {
|
|
|
|
|
updateNotificationProgress(notification, i, items.size)
|
|
|
|
|
if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)) {
|
|
|
|
|
failedItem = items[i].explorerElement.fullPath
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
updateNotificationProgress(notification, i+1, items.size)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null){
|
|
|
|
|
for (i in 0 until mergedFolders.size) {
|
|
|
|
|
if (notifications[notification.notificationId]!!){
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
return@Thread
|
|
|
|
|
}
|
|
|
|
|
if (!gocryptfsVolume.rmdir(mergedFolders[i])){
|
|
|
|
|
failedItem = mergedFolders[i]
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size)
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
|
for (i in 0 until mergedFolders.size) {
|
|
|
|
|
if (!gocryptfsVolume.rmdir(mergedFolders[i])) {
|
|
|
|
|
failedItem = mergedFolders[i]
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
callback(failedItem)
|
|
|
|
|
}.start()
|
|
|
|
|
failedItem
|
|
|
|
|
}
|
|
|
|
|
// treat cancellation as success
|
|
|
|
|
waitForTask(notification, task).failedItem
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
private suspend fun importFilesFromUris(
|
|
|
|
|
dstPaths: List<String>,
|
|
|
|
|
uris: List<Uri>,
|
|
|
|
|
notification: FileOperationNotification,
|
|
|
|
|
): String? {
|
|
|
|
|
var failedIndex = -1
|
|
|
|
|
for (i in dstPaths.indices) {
|
|
|
|
|
if (notifications[notification.notificationId]!!){
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if (!gocryptfsVolume.importFile(this, uris[i], dstPaths[i])) {
|
|
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
|
try {
|
|
|
|
|
if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
|
|
|
|
|
failedIndex = i
|
|
|
|
|
}
|
|
|
|
|
} catch (e: FileNotFoundException) {
|
|
|
|
|
failedIndex = i
|
|
|
|
|
}
|
|
|
|
|
} catch (e: FileNotFoundException){
|
|
|
|
|
failedIndex = i
|
|
|
|
|
}
|
|
|
|
|
if (failedIndex == -1) {
|
|
|
|
|
updateNotificationProgress(notification, i, dstPaths.size)
|
|
|
|
|
updateNotificationProgress(notification, i+1, dstPaths.size)
|
|
|
|
|
} else {
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
callback(uris[failedIndex].toString())
|
|
|
|
|
break
|
|
|
|
|
return uris[failedIndex].toString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failedIndex == -1){
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
callback(null)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>, callback: (String?) -> Unit) {
|
|
|
|
|
Thread {
|
|
|
|
|
importFilesFromUris(dstPaths, uris, null, callback)
|
|
|
|
|
}.start()
|
|
|
|
|
suspend fun importFilesFromUris(dstPaths: List<String>, uris: List<Uri>): TaskResult<String?> = coroutineScope {
|
|
|
|
|
val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
|
|
|
|
|
val task = async {
|
|
|
|
|
importFilesFromUris(dstPaths, uris, notification)
|
|
|
|
|
}
|
|
|
|
|
waitForTask(notification, task)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -256,18 +264,17 @@ class FileOperationService : Service() {
|
|
|
|
|
dstFiles: ArrayList<String>,
|
|
|
|
|
srcUris: ArrayList<Uri>,
|
|
|
|
|
dstDirs: ArrayList<String>,
|
|
|
|
|
notification: FileOperationNotification
|
|
|
|
|
scope: CoroutineScope,
|
|
|
|
|
): Boolean {
|
|
|
|
|
dstDirs.add(rootDstPath)
|
|
|
|
|
for (child in rootSrcDir.listFiles()) {
|
|
|
|
|
if (notifications[notification.notificationId]!!) {
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
if (!scope.isActive) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
child.name?.let { name ->
|
|
|
|
|
val subPath = PathUtils.pathJoin(rootDstPath, name)
|
|
|
|
|
if (child.isDirectory) {
|
|
|
|
|
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, notification)) {
|
|
|
|
|
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -280,48 +287,50 @@ class FileOperationService : Service() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun importDirectory(rootDstPath: String, rootSrcDir: DocumentFile, callback: (String?, List<Uri>) -> Unit) {
|
|
|
|
|
Thread {
|
|
|
|
|
val notification = showNotification(R.string.file_op_import_msg, null)
|
|
|
|
|
class ImportDirectoryResult(val taskResult: TaskResult<String?>, val uris: List<Uri>)
|
|
|
|
|
|
|
|
|
|
suspend fun importDirectory(
|
|
|
|
|
rootDstPath: String,
|
|
|
|
|
rootSrcDir: DocumentFile,
|
|
|
|
|
): ImportDirectoryResult = coroutineScope {
|
|
|
|
|
val notification = showNotification(R.string.file_op_import_msg, null)
|
|
|
|
|
val srcUris = arrayListOf<Uri>()
|
|
|
|
|
val task = async {
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
val dstFiles = arrayListOf<String>()
|
|
|
|
|
val srcUris = arrayListOf<Uri>()
|
|
|
|
|
val dstDirs = arrayListOf<String>()
|
|
|
|
|
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, notification)) {
|
|
|
|
|
return@Thread
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// create destination folders so the new files can use them
|
|
|
|
|
for (dir in dstDirs) {
|
|
|
|
|
if (notifications[notification.notificationId]!!) {
|
|
|
|
|
cancelNotification(notification)
|
|