2020-12-31 13:15:13 +01:00
|
|
|
package sushi.hardcore.droidfs.file_operations
|
2020-12-29 17:05:02 +01:00
|
|
|
|
2022-03-06 14:45:52 +01:00
|
|
|
import android.app.NotificationChannel
|
|
|
|
import android.app.NotificationManager
|
|
|
|
import android.app.PendingIntent
|
|
|
|
import android.app.Service
|
2020-12-29 17:05:02 +01:00
|
|
|
import android.content.Intent
|
|
|
|
import android.net.Uri
|
2022-03-06 14:45:52 +01:00
|
|
|
import android.os.Binder
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.IBinder
|
2021-11-09 16:27:59 +01:00
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
import androidx.core.app.NotificationManagerCompat
|
2020-12-29 17:05:02 +01:00
|
|
|
import androidx.documentfile.provider.DocumentFile
|
2022-04-20 15:17:33 +02:00
|
|
|
import kotlinx.coroutines.*
|
2021-06-11 16:27:08 +02:00
|
|
|
import sushi.hardcore.droidfs.GocryptfsVolume
|
2020-12-31 13:15:13 +01:00
|
|
|
import sushi.hardcore.droidfs.R
|
2020-12-29 17:05:02 +01:00
|
|
|
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
|
|
|
import sushi.hardcore.droidfs.util.PathUtils
|
|
|
|
import sushi.hardcore.droidfs.util.Wiper
|
|
|
|
import java.io.File
|
|
|
|
import java.io.FileNotFoundException
|
|
|
|
|
|
|
|
class FileOperationService : Service() {
|
2020-12-29 18:39:43 +01:00
|
|
|
companion object {
|
|
|
|
const val NOTIFICATION_CHANNEL_ID = "FileOperations"
|
2020-12-31 15:57:52 +01:00
|
|
|
const val ACTION_CANCEL = "file_operation_cancel"
|
2020-12-29 18:39:43 +01:00
|
|
|
}
|
2020-12-29 17:05:02 +01:00
|
|
|
|
|
|
|
private val binder = LocalBinder()
|
|
|
|
private lateinit var gocryptfsVolume: GocryptfsVolume
|
2021-11-09 16:27:59 +01:00
|
|
|
private lateinit var notificationManager: NotificationManagerCompat
|
2022-04-20 15:17:33 +02:00
|
|
|
private val tasks = HashMap<Int, Job>()
|
2020-12-31 13:15:13 +01:00
|
|
|
private var lastNotificationId = 0
|
2020-12-29 17:05:02 +01:00
|
|
|
|
|
|
|
inner class LocalBinder : Binder() {
|
|
|
|
fun getService(): FileOperationService = this@FileOperationService
|
|
|
|
fun setGocryptfsVolume(g: GocryptfsVolume) {
|
|
|
|
gocryptfsVolume = g
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onBind(p0: Intent?): IBinder {
|
|
|
|
return binder
|
|
|
|
}
|
|
|
|
|
2021-08-19 23:13:32 +02:00
|
|
|
private fun showNotification(message: Int, total: Int?): FileOperationNotification {
|
2020-12-31 15:57:52 +01:00
|
|
|
++lastNotificationId
|
2020-12-29 18:39:43 +01:00
|
|
|
if (!::notificationManager.isInitialized){
|
2021-11-09 16:27:59 +01:00
|
|
|
notificationManager = NotificationManagerCompat.from(this)
|
2020-12-29 18:39:43 +01:00
|
|
|
}
|
2021-11-09 16:27:59 +01:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
notificationManager.createNotificationChannel(
|
|
|
|
NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID,
|
|
|
|
getString(R.string.file_operations),
|
|
|
|
NotificationManager.IMPORTANCE_LOW
|
|
|
|
)
|
2020-12-31 15:57:52 +01:00
|
|
|
)
|
|
|
|
}
|
2021-11-09 16:27:59 +01:00
|
|
|
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
2020-12-31 13:15:13 +01:00
|
|
|
notificationBuilder
|
|
|
|
.setContentTitle(getString(message))
|
2020-12-29 18:39:43 +01:00
|
|
|
.setSmallIcon(R.mipmap.icon_launcher)
|
2020-12-31 15:57:52 +01:00
|
|
|
.setOngoing(true)
|
2021-11-09 16:27:59 +01:00
|
|
|
.addAction(NotificationCompat.Action(
|
|
|
|
R.drawable.icon_close,
|
|
|
|
getString(R.string.cancel),
|
|
|
|
PendingIntent.getBroadcast(
|
|
|
|
this,
|
|
|
|
0,
|
|
|
|
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
|
|
|
val bundle = Bundle()
|
|
|
|
bundle.putBinder("binder", LocalBinder())
|
|
|
|
bundle.putInt("notificationId", lastNotificationId)
|
|
|
|
putExtra("bundle", bundle)
|
|
|
|
action = ACTION_CANCEL
|
|
|
|
},
|
|
|
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
|
|
)
|
|
|
|
))
|
2021-08-19 23:13:32 +02:00
|
|
|
if (total != null) {
|
|
|
|
notificationBuilder
|
|
|
|
.setContentText("0/$total")
|
|
|
|
.setProgress(total, 0, false)
|
|
|
|
} else {
|
|
|
|
notificationBuilder
|
|
|
|
.setContentText(getString(R.string.discovering_files))
|
|
|
|
.setProgress(0, 0, true)
|
|
|
|
}
|
2020-12-31 13:15:13 +01:00
|
|
|
notificationManager.notify(lastNotificationId, notificationBuilder.build())
|
|
|
|
return FileOperationNotification(notificationBuilder, lastNotificationId)
|
2020-12-29 18:39:43 +01:00
|
|
|
}
|
|
|
|
|
2020-12-31 13:15:13 +01:00
|
|
|
private fun updateNotificationProgress(notification: FileOperationNotification, progress: Int, total: Int){
|
|
|
|
notification.notificationBuilder
|
|
|
|
.setProgress(total, progress, false)
|
|
|
|
.setContentText("$progress/$total")
|
|
|
|
notificationManager.notify(notification.notificationId, notification.notificationBuilder.build())
|
2020-12-29 18:39:43 +01:00
|
|
|
}
|
|
|
|
|
2020-12-31 13:15:13 +01:00
|
|
|
private fun cancelNotification(notification: FileOperationNotification){
|
|
|
|
notificationManager.cancel(notification.notificationId)
|
2020-12-29 18:39:43 +01:00
|
|
|
}
|
|
|
|
|
2020-12-31 15:57:52 +01:00
|
|
|
fun cancelOperation(notificationId: Int){
|
2022-04-20 15:17:33 +02:00
|
|
|
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)
|
|
|
|
}
|
2020-12-31 15:57:52 +01:00
|
|
|
}
|
|
|
|
|
2020-12-29 17:05:02 +01:00
|
|
|
private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean {
|
|
|
|
var success = true
|
|
|
|
val srcHandleId = remoteGocryptfsVolume.openReadMode(srcPath)
|
|
|
|
if (srcHandleId != -1){
|
|
|
|
val dstHandleId = gocryptfsVolume.openWriteMode(dstPath)
|
|
|
|
if (dstHandleId != -1){
|
|
|
|
var offset: Long = 0
|
|
|
|
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
|
|
|
|
var length: Int
|
|
|
|
while (remoteGocryptfsVolume.readFile(srcHandleId, offset, ioBuffer).also { length = it } > 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
|
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
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 {
|
2020-12-29 17:05:02 +01:00
|
|
|
var failedItem: String? = null
|
2022-04-20 15:17:33 +02:00
|
|
|
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)) {
|
2020-12-29 18:39:43 +01:00
|
|
|
failedItem = items[i].explorerElement.fullPath
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
if (failedItem == null) {
|
|
|
|
updateNotificationProgress(notification, i+1, items.size)
|
2020-12-29 18:39:43 +01:00
|
|
|
} else {
|
2020-12-29 17:05:02 +01:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
failedItem
|
|
|
|
}
|
|
|
|
// treat cancellation as success
|
|
|
|
waitForTask(notification, task).failedItem
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
|
2022-04-21 14:06:36 +02:00
|
|
|
suspend fun moveElements(toMove: List<OperationFile>, toClean: List<ExplorerElement>): String? = coroutineScope {
|
|
|
|
val notification = showNotification(R.string.file_op_move_msg, toMove.size)
|
|
|
|
val task = async(Dispatchers.IO) {
|
|
|
|
val total = toMove.size+toClean.size
|
2020-12-29 17:05:02 +01:00
|
|
|
var failedItem: String? = null
|
2022-04-21 14:06:36 +02:00
|
|
|
for ((i, item) in toMove.withIndex()) {
|
|
|
|
if (!gocryptfsVolume.rename(item.explorerElement.fullPath, item.dstPath!!)) {
|
|
|
|
failedItem = item.explorerElement.fullPath
|
|
|
|
break
|
|
|
|
} else {
|
|
|
|
updateNotificationProgress(notification, i+1, total)
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
2022-04-21 14:06:36 +02:00
|
|
|
}
|
|
|
|
if (failedItem == null) {
|
|
|
|
for ((i, folder) in toClean.asReversed().withIndex()) {
|
|
|
|
if (!gocryptfsVolume.rmdir(folder.fullPath)) {
|
|
|
|
failedItem = folder.fullPath
|
|
|
|
break
|
|
|
|
} else {
|
|
|
|
updateNotificationProgress(notification, toMove.size+i+1, total)
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
failedItem
|
|
|
|
}
|
|
|
|
// treat cancellation as success
|
|
|
|
waitForTask(notification, task).failedItem
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
private suspend fun importFilesFromUris(
|
|
|
|
dstPaths: List<String>,
|
|
|
|
uris: List<Uri>,
|
|
|
|
notification: FileOperationNotification,
|
|
|
|
): String? {
|
2021-08-19 23:13:32 +02:00
|
|
|
var failedIndex = -1
|
|
|
|
for (i in dstPaths.indices) {
|
2022-04-20 15:17:33 +02:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
try {
|
|
|
|
if (!gocryptfsVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
|
|
|
|
failedIndex = i
|
|
|
|
}
|
|
|
|
} catch (e: FileNotFoundException) {
|
2021-08-19 23:13:32 +02:00
|
|
|
failedIndex = i
|
2020-12-31 15:57:52 +01:00
|
|
|
}
|
2021-08-19 23:13:32 +02:00
|
|
|
}
|
|
|
|
if (failedIndex == -1) {
|
2022-04-20 15:17:33 +02:00
|
|
|
updateNotificationProgress(notification, i+1, dstPaths.size)
|
2021-08-19 23:13:32 +02:00
|
|
|
} else {
|
2022-04-20 15:17:33 +02:00
|
|
|
return uris[failedIndex].toString()
|
2021-08-19 23:13:32 +02:00
|
|
|
}
|
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
return null
|
2021-08-19 23:13:32 +02:00
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
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)
|
2021-08-19 23:13:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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>,
|
2022-04-20 15:17:33 +02:00
|
|
|
scope: CoroutineScope,
|
2021-08-19 23:13:32 +02:00
|
|
|
): Boolean {
|
|
|
|
dstDirs.add(rootDstPath)
|
|
|
|
for (child in rootSrcDir.listFiles()) {
|
2022-04-20 15:17:33 +02:00
|
|
|
if (!scope.isActive) {
|
2021-08-19 23:13:32 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
child.name?.let { name ->
|
|
|
|
val subPath = PathUtils.pathJoin(rootDstPath, name)
|
|
|
|
if (child.isDirectory) {
|
2022-04-20 15:17:33 +02:00
|
|
|
if (!recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs, scope)) {
|
2021-08-19 23:13:32 +02:00
|
|
|
return false
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
}
|
2021-08-19 23:13:32 +02:00
|
|
|
else if (child.isFile) {
|
|
|
|
srcUris.add(child.uri)
|
|
|
|
dstFiles.add(subPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
class ImportDirectoryResult(val taskResult: TaskResult<String?>, val uris: List<Uri>)
|
2021-08-19 23:13:32 +02:00
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
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
|
2021-08-19 23:13:32 +02:00
|
|
|
val dstFiles = arrayListOf<String>()
|
|
|
|
val dstDirs = arrayListOf<String>()
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
if (!recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs, this)) {
|
|
|
|
return@withContext
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
|
|
|
|
// create destination folders so the new files can use them
|
|
|
|
for (dir in dstDirs) {
|
|
|
|
if (!gocryptfsVolume.mkdir(dir)) {
|
|
|
|
failedItem = dir
|
|
|
|
break
|
|
|
|
}
|
2021-08-27 19:47:35 +02:00
|
|
|
}
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
if (failedItem == null) {
|
|
|
|
failedItem = importFilesFromUris(dstFiles, srcUris, notification)
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
failedItem
|
|
|
|
}
|
|
|
|
ImportDirectoryResult(waitForTask(notification, task), srcUris)
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): String? = coroutineScope {
|
|
|
|
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
|
|
|
|
val task = async {
|
2020-12-29 17:05:02 +01:00
|
|
|
var errorMsg: String? = null
|
2020-12-29 18:39:43 +01:00
|
|
|
for (i in uris.indices) {
|
2022-04-20 15:17:33 +02:00
|
|
|
withContext(Dispatchers.IO) {
|
|
|
|
errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
|
2020-12-31 15:57:52 +01:00
|
|
|
}
|
2020-12-29 18:39:43 +01:00
|
|
|
if (errorMsg == null) {
|
2022-04-20 15:17:33 +02:00
|
|
|
updateNotificationProgress(notification, i+1, uris.size)
|
2020-12-29 18:39:43 +01:00
|
|
|
} else {
|
2020-12-29 17:05:02 +01:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2021-08-28 11:18:51 +02:00
|
|
|
if (errorMsg == null) {
|
|
|
|
rootFile?.delete()
|
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
errorMsg
|
|
|
|
}
|
|
|
|
// treat cancellation as success
|
|
|
|
waitForTask(notification, task).failedItem
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean {
|
|
|
|
val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let {
|
|
|
|
contentResolver.openOutputStream(it)
|
|
|
|
}
|
2020-12-29 18:39:43 +01:00
|
|
|
return if (outputStream == null) {
|
2020-12-29 17:05:02 +01:00
|
|
|
false
|
2020-12-29 18:39:43 +01:00
|
|
|
} else {
|
|
|
|
gocryptfsVolume.exportFile(srcPath, outputStream)
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
private fun recursiveExportDirectory(
|
|
|
|
plain_directory_path: String,
|
|
|
|
treeDocumentFile: DocumentFile,
|
|
|
|
scope: CoroutineScope
|
|
|
|
): String? {
|
2020-12-29 17:05:02 +01:00
|
|
|
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
|
|
|
|
val explorerElements = gocryptfsVolume.listDir(plain_directory_path)
|
|
|
|
for (e in explorerElements) {
|
2022-04-20 15:17:33 +02:00
|
|
|
if (!scope.isActive) {
|
|
|
|
return null
|
|
|
|
}
|
2020-12-29 17:05:02 +01:00
|
|
|
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
|
|
|
|
if (e.isDirectory) {
|
2022-04-20 15:17:33 +02:00
|
|
|
val failedItem = recursiveExportDirectory(fullPath, childTree, scope)
|
2020-12-29 17:05:02 +01:00
|
|
|
failedItem?.let { return it }
|
|
|
|
} else {
|
|
|
|
if (!exportFileInto(fullPath, childTree)){
|
|
|
|
return fullPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
return treeDocumentFile.name
|
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
suspend fun exportFiles(uri: Uri, items: List<ExplorerElement>): TaskResult<String?> = coroutineScope {
|
|
|
|
val notification = showNotification(R.string.file_op_export_msg, items.size)
|
|
|
|
val task = async {
|
2020-12-29 17:05:02 +01:00
|
|
|
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
2022-04-20 15:17:33 +02:00
|
|
|
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
|
|
|
|
var failedItem: String? = null
|
|
|
|
for (i in items.indices) {
|
|
|
|
withContext(Dispatchers.IO) {
|
2020-12-29 18:39:43 +01:00
|
|
|
failedItem = if (items[i].isDirectory) {
|
2022-04-20 15:17:33 +02:00
|
|
|
recursiveExportDirectory(items[i].fullPath, treeDocumentFile, this)
|
2020-12-29 17:05:02 +01:00
|
|
|
} else {
|
2020-12-29 18:39:43 +01:00
|
|
|
if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
if (failedItem == null) {
|
|
|
|
updateNotificationProgress(notification, i+1, items.size)
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
2022-04-20 15:17:33 +02:00
|
|
|
failedItem
|
|
|
|
}
|
|
|
|
waitForTask(notification, task)
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|
2022-03-06 14:45:52 +01:00
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
private fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int {
|
|
|
|
if (!scope.isActive) {
|
|
|
|
return 0
|
|
|
|
}
|
2022-03-06 14:45:52 +01:00
|
|
|
val children = rootDirectory.listFiles()
|
|
|
|
var count = children.size
|
|
|
|
for (child in children) {
|
|
|
|
if (child.isDirectory) {
|
2022-04-20 15:17:33 +02:00
|
|
|
count += recursiveCountChildElements(child, scope)
|
2022-03-06 14:45:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return count
|
|
|
|
}
|
|
|
|
|
|
|
|
internal class ObjRef<T>(var value: T)
|
|
|
|
|
|
|
|
private fun recursiveCopyVolume(
|
|
|
|
src: DocumentFile,
|
|
|
|
dst: DocumentFile,
|
|
|
|
dstRootDirectory: ObjRef<DocumentFile?>?,
|
|
|
|
notification: FileOperationNotification,
|
|
|
|
total: Int,
|
2022-04-20 15:17:33 +02:00
|
|
|
scope: CoroutineScope,
|
2022-03-06 14:45:52 +01:00
|
|
|
progress: ObjRef<Int> = ObjRef(0)
|
|
|
|
): DocumentFile? {
|
|
|
|
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
|
2022-04-20 15:17:33 +02:00
|
|
|
dstRootDirectory?.let { it.value = dstDir }
|
2022-03-06 14:45:52 +01:00
|
|
|
for (child in src.listFiles()) {
|
2022-04-20 15:17:33 +02:00
|
|
|
if (!scope.isActive) {
|
2022-03-06 14:45:52 +01:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (child.isFile) {
|
|
|
|
val dstFile = dstDir.createFile("", child.name ?: return child) ?: return child
|
|
|
|
val outputStream = contentResolver.openOutputStream(dstFile.uri)
|
|
|
|
val inputStream = contentResolver.openInputStream(child.uri)
|
|
|
|
if (outputStream == null || inputStream == null) return child
|
|
|
|
val written = inputStream.copyTo(outputStream)
|
|
|
|
outputStream.close()
|
|
|
|
inputStream.close()
|
|
|
|
if (written != child.length()) return child
|
|
|
|
} else {
|
2022-04-20 15:17:33 +02:00
|
|
|
recursiveCopyVolume(child, dstDir, null, notification, total, scope, progress)?.let { return it }
|
2022-03-06 14:45:52 +01:00
|
|
|
}
|
|
|
|
progress.value++
|
|
|
|
updateNotificationProgress(notification, progress.value, total)
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2022-04-20 15:17:33 +02:00
|
|
|
class CopyVolumeResult(val taskResult: TaskResult<DocumentFile?>, val dstRootDirectory: DocumentFile?)
|
|
|
|
|
|
|
|
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult = coroutineScope {
|
|
|
|
val notification = showNotification(R.string.copy_volume_notification, null)
|
|
|
|
val dstRootDirectory = ObjRef<DocumentFile?>(null)
|
|
|
|
val task = async(Dispatchers.IO) {
|
|
|
|
val total = recursiveCountChildElements(src, this)
|
|
|
|
if (isActive) {
|
|
|
|
updateNotificationProgress(notification, 0, total)
|
|
|
|
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// treat cancellation as success
|
|
|
|
CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
|
2022-03-06 14:45:52 +01:00
|
|
|
}
|
2020-12-29 17:05:02 +01:00
|
|
|
}
|