|
|
|
@ -1,65 +1,191 @@
|
|
|
|
|
package sushi.hardcore.droidfs.file_operations
|
|
|
|
|
|
|
|
|
|
import android.Manifest
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
|
import android.app.Notification
|
|
|
|
|
import android.app.NotificationChannel
|
|
|
|
|
import android.app.NotificationManager
|
|
|
|
|
import android.app.PendingIntent
|
|
|
|
|
import android.app.Service
|
|
|
|
|
import android.content.ComponentName
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.content.Intent
|
|
|
|
|
import android.content.ServiceConnection
|
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
|
import android.content.pm.ServiceInfo
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.os.Binder
|
|
|
|
|
import android.os.Build
|
|
|
|
|
import android.os.Bundle
|
|
|
|
|
import android.os.IBinder
|
|
|
|
|
import android.provider.Settings
|
|
|
|
|
import android.util.Log
|
|
|
|
|
import androidx.activity.result.ActivityResultLauncher
|
|
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
|
import androidx.core.app.NotificationManagerCompat
|
|
|
|
|
import androidx.core.app.ServiceCompat
|
|
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
|
import androidx.documentfile.provider.DocumentFile
|
|
|
|
|
import kotlinx.coroutines.CancellationException
|
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.Deferred
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
import kotlinx.coroutines.MainScope
|
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
import kotlinx.coroutines.coroutineScope
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
|
import kotlinx.coroutines.yield
|
|
|
|
|
import sushi.hardcore.droidfs.BaseActivity
|
|
|
|
|
import sushi.hardcore.droidfs.Constants
|
|
|
|
|
import sushi.hardcore.droidfs.R
|
|
|
|
|
import sushi.hardcore.droidfs.VolumeManager
|
|
|
|
|
import sushi.hardcore.droidfs.VolumeManagerApp
|
|
|
|
|
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
|
|
|
|
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
|
|
|
|
import sushi.hardcore.droidfs.filesystems.Stat
|
|
|
|
|
import sushi.hardcore.droidfs.util.ObjRef
|
|
|
|
|
import sushi.hardcore.droidfs.util.PathUtils
|
|
|
|
|
import sushi.hardcore.droidfs.util.Wiper
|
|
|
|
|
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
|
|
|
|
import java.io.File
|
|
|
|
|
import java.io.FileNotFoundException
|
|
|
|
|
import kotlin.coroutines.resume
|
|
|
|
|
import kotlin.coroutines.suspendCoroutine
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Foreground service for file operations.
|
|
|
|
|
*
|
|
|
|
|
* Clients **must** bind to it using the [bind] method.
|
|
|
|
|
*
|
|
|
|
|
* This implementation is not thread-safe. It must only be called from the main UI thread.
|
|
|
|
|
*/
|
|
|
|
|
class FileOperationService : Service() {
|
|
|
|
|
companion object {
|
|
|
|
|
const val NOTIFICATION_CHANNEL_ID = "FileOperations"
|
|
|
|
|
const val ACTION_CANCEL = "file_operation_cancel"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val binder = LocalBinder()
|
|
|
|
|
private lateinit var volumeManger: VolumeManager
|
|
|
|
|
private var serviceScope = MainScope()
|
|
|
|
|
private lateinit var notificationManager: NotificationManagerCompat
|
|
|
|
|
private val tasks = HashMap<Int, Job>()
|
|
|
|
|
private var lastNotificationId = 0
|
|
|
|
|
|
|
|
|
|
inner class LocalBinder : Binder() {
|
|
|
|
|
fun getService(): FileOperationService = this@FileOperationService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onBind(p0: Intent?): IBinder {
|
|
|
|
|
volumeManger = (application as VolumeManagerApp).volumeManager
|
|
|
|
|
return binder
|
|
|
|
|
inner class PendingTask<T>(
|
|
|
|
|
val title: Int,
|
|
|
|
|
val total: Int?,
|
|
|
|
|
private val getTask: (Int) -> Deferred<T>,
|
|
|
|
|
private val onStart: (taskId: Int, job: Deferred<T>) -> Unit,
|
|
|
|
|
) {
|
|
|
|
|
fun start(taskId: Int): Deferred<T> = getTask(taskId).also { onStart(taskId, it) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun showNotification(message: Int, total: Int?): FileOperationNotification {
|
|
|
|
|
++lastNotificationId
|
|
|
|
|
if (!::notificationManager.isInitialized){
|
|
|
|
|
companion object {
|
|
|
|
|
const val TAG = "FileOperationService"
|
|
|
|
|
const val NOTIFICATION_CHANNEL_ID = "FileOperations"
|
|
|
|
|
const val ACTION_CANCEL = "file_operation_cancel"
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bind to the service.
|
|
|
|
|
*
|
|
|
|
|
* Registers an [ActivityResultLauncher] in the provided activity to request notification permission. Consequently, the activity must not yet be started.
|
|
|
|
|
*
|
|
|
|
|
* The activity must stay running while calling the service's methods.
|
|
|
|
|
*
|
|
|
|
|
* If multiple activities bind simultaneously, only the latest one will be used by the service.
|
|
|
|
|
*/
|
|
|
|
|
fun bind(activity: BaseActivity, onBound: (FileOperationService) -> Unit) {
|
|
|
|
|
var service: FileOperationService? = null
|
|
|
|
|
val launcher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
|
|
|
|
if (granted) {
|
|
|
|
|
service!!.processPendingTask()
|
|
|
|
|
} else {
|
|
|
|
|
CustomAlertDialogBuilder(activity, activity.theme)
|
|
|
|
|
.setTitle(R.string.warning)
|
|
|
|
|
.setMessage(R.string.notification_denied_msg)
|
|
|
|
|
.setPositiveButton(R.string.settings) { _, _ ->
|
|
|
|
|
activity.startActivity(
|
|
|
|
|
Intent(
|
|
|
|
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
|
|
|
Uri.fromParts("package", activity.packageName, null)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.setNegativeButton(R.string.later, null)
|
|
|
|
|
.setOnDismissListener { service!!.processPendingTask() }
|
|
|
|
|
.show()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
activity.bindService(Intent(activity, FileOperationService::class.java), object : ServiceConnection {
|
|
|
|
|
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
|
|
|
|
onBound((binder as FileOperationService.LocalBinder).getService().also {
|
|
|
|
|
it.notificationPermissionLauncher = launcher
|
|
|
|
|
service = it
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
override fun onServiceDisconnected(arg0: ComponentName) {}
|
|
|
|
|
}, Context.BIND_AUTO_CREATE)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var isStarted = false
|
|
|
|
|
private val binder = LocalBinder()
|
|
|
|
|
private lateinit var volumeManger: VolumeManager
|
|
|
|
|
private var serviceScope = MainScope()
|
|
|
|
|
private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String>
|
|
|
|
|
private var askForNotificationPermission = true
|
|
|
|
|
private lateinit var notificationManager: NotificationManagerCompat
|
|
|
|
|
private val notifications = HashMap<Int, NotificationCompat.Builder>()
|
|
|
|
|
private var foregroundNotificationId = -1
|
|
|
|
|
private val tasks = HashMap<Int, Job>()
|
|
|
|
|
private var newTaskId = 1
|
|
|
|
|
private var pendingTask: PendingTask<*>? = null
|
|
|
|
|
|
|
|
|
|
override fun onCreate() {
|
|
|
|
|
volumeManger = (application as VolumeManagerApp).volumeManager
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onBind(p0: Intent?): IBinder = binder
|
|
|
|
|
|
|
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
|
|
|
startPendingTask { id, notification ->
|
|
|
|
|
// on service start, the pending task is the foreground task
|
|
|
|
|
setForeground(id, notification)
|
|
|
|
|
}
|
|
|
|
|
isStarted = true
|
|
|
|
|
return START_NOT_STICKY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDestroy() {
|
|
|
|
|
isStarted = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun processPendingTask() {
|
|
|
|
|
if (isStarted) {
|
|
|
|
|
startPendingTask { id, notification ->
|
|
|
|
|
if (foregroundNotificationId == -1) {
|
|
|
|
|
// service started but not in foreground yet
|
|
|
|
|
setForeground(id, notification)
|
|
|
|
|
} else {
|
|
|
|
|
// already running in foreground, just add a new notification
|
|
|
|
|
notificationManager.notify(id, notification)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ContextCompat.startForegroundService(
|
|
|
|
|
this,
|
|
|
|
|
Intent(this, FileOperationService::class.java)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start the pending task and create an associated notification.
|
|
|
|
|
*/
|
|
|
|
|
private fun startPendingTask(showNotification: (id: Int, Notification) -> Unit) {
|
|
|
|
|
val task = pendingTask
|
|
|
|
|
pendingTask = null
|
|
|
|
|
if (task == null) {
|
|
|
|
|
Log.w(TAG, "Started without pending task")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!::notificationManager.isInitialized) {
|
|
|
|
|
notificationManager = NotificationManagerCompat.from(this)
|
|
|
|
|
}
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
@ -72,8 +198,7 @@ class FileOperationService : Service() {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
|
|
|
|
notificationBuilder
|
|
|
|
|
.setContentTitle(getString(message))
|
|
|
|
|
.setContentTitle(getString(task.title))
|
|
|
|
|
.setSmallIcon(R.drawable.ic_notification)
|
|
|
|
|
.setOngoing(true)
|
|
|
|
|
.addAction(NotificationCompat.Action(
|
|
|
|
@ -81,78 +206,165 @@ class FileOperationService : Service() {
|
|
|
|
|
getString(R.string.cancel),
|
|
|
|
|
PendingIntent.getBroadcast(
|
|
|
|
|
this,
|
|
|
|
|
0,
|
|
|
|
|
newTaskId,
|
|
|
|
|
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
|
|
|
|
val bundle = Bundle()
|
|
|
|
|
bundle.putBinder("binder", LocalBinder())
|
|
|
|
|
bundle.putInt("notificationId", lastNotificationId)
|
|
|
|
|
putExtra("bundle", bundle)
|
|
|
|
|
putExtra("bundle", Bundle().apply {
|
|
|
|
|
putBinder("binder", LocalBinder())
|
|
|
|
|
putInt("taskId", newTaskId)
|
|
|
|
|
})
|
|
|
|
|
action = ACTION_CANCEL
|
|
|
|
|
},
|
|
|
|
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
|
|
|
PendingIntent.FLAG_IMMUTABLE
|
|
|
|
|
)
|
|
|
|
|
))
|
|
|
|
|
if (total != null) {
|
|
|
|
|
if (task.total != null) {
|
|
|
|
|
notificationBuilder
|
|
|
|
|
.setContentText("0/$total")
|
|
|
|
|
.setProgress(total, 0, false)
|
|
|
|
|
.setContentText("0/${task.total}")
|
|
|
|
|
.setProgress(task.total, 0, false)
|
|
|
|
|
} else {
|
|
|
|
|
notificationBuilder
|
|
|
|
|
.setContentText(getString(R.string.discovering_files))
|
|
|
|
|
.setProgress(0, 0, true)
|
|
|
|
|
}
|
|
|
|
|
notificationManager.notify(lastNotificationId, notificationBuilder.build())
|
|
|
|
|
return FileOperationNotification(notificationBuilder, lastNotificationId)
|
|
|
|
|
showNotification(newTaskId, notificationBuilder.build())
|
|
|
|
|
notifications[newTaskId] = notificationBuilder
|
|
|
|
|
tasks[newTaskId] = task.start(newTaskId)
|
|
|
|
|
newTaskId++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun updateNotificationProgress(notification: FileOperationNotification, progress: Int, total: Int){
|
|
|
|
|
notification.notificationBuilder
|
|
|
|
|
private fun setForeground(id: Int, notification: Notification) {
|
|
|
|
|
ServiceCompat.startForeground(this, id, notification,
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
|
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
foregroundNotificationId = id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun updateNotificationProgress(taskId: Int, progress: Int, total: Int) {
|
|
|
|
|
val notificationBuilder = notifications[taskId] ?: return
|
|
|
|
|
notificationBuilder
|
|
|
|
|
.setProgress(total, progress, false)
|
|
|
|
|
.setContentText("$progress/$total")
|
|
|
|
|
notificationManager.notify(notification.notificationId, notification.notificationBuilder.build())
|
|
|
|
|
notificationManager.notify(taskId, notificationBuilder.build())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun cancelNotification(notification: FileOperationNotification){
|
|
|
|
|
notificationManager.cancel(notification.notificationId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun cancelOperation(notificationId: Int){
|
|
|
|
|
tasks[notificationId]?.cancel()
|
|
|
|
|
fun cancelOperation(taskId: Int) {
|
|
|
|
|
tasks[taskId]?.cancel()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getEncryptedVolume(volumeId: Int): EncryptedVolume {
|
|
|
|
|
return volumeManger.getVolume(volumeId) ?: throw IllegalArgumentException("Invalid volumeId: $volumeId")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<out T> {
|
|
|
|
|
tasks[notification.notificationId] = task
|
|
|
|
|
/**
|
|
|
|
|
* Wait on a task, returning the appropriate [TaskResult].
|
|
|
|
|
*
|
|
|
|
|
* This method also performs cleanup and foreground state management so it must be always used.
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun <T> waitForTask(
|
|
|
|
|
taskId: Int,
|
|
|
|
|
task: Deferred<T>,
|
|
|
|
|
onCancelled: (suspend () -> Unit)?,
|
|
|
|
|
): TaskResult<out T> {
|
|
|
|
|
return coroutineScope {
|
|
|
|
|
withContext(serviceScope.coroutineContext) {
|
|
|
|
|
try {
|
|
|
|
|
TaskResult.completed(task.await())
|
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
|
onCancelled?.invoke()
|
|
|
|
|
TaskResult.cancelled()
|
|
|
|
|
} catch (e: Throwable) {
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
TaskResult.error(e.localizedMessage)
|
|
|
|
|
} finally {
|
|
|
|
|
cancelNotification(notification)
|
|
|
|
|
notificationManager.cancel(taskId)
|
|
|
|
|
notifications.remove(taskId)
|
|
|
|
|
tasks.remove(taskId)
|
|
|
|
|
if (tasks.size == 0) {
|
|
|
|
|
// last task finished, remove from foreground state but don't stop the service
|
|
|
|
|
ServiceCompat.stopForeground(this@FileOperationService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
|
|
|
foregroundNotificationId = -1
|
|
|
|
|
} else if (taskId == foregroundNotificationId) {
|
|
|
|
|
// foreground task finished, falling back to the next one
|
|
|
|
|
val entry = notifications.entries.first()
|
|
|
|
|
setForeground(entry.key, entry.value.build())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun <T> volumeTask(
|
|
|
|
|
volumeId: Int,
|
|
|
|
|
notification: FileOperationNotification,
|
|
|
|
|
task: suspend (encryptedVolume: EncryptedVolume) -> T
|
|
|
|
|
/**
|
|
|
|
|
* Create and run a new task until completion.
|
|
|
|
|
*
|
|
|
|
|
* Handles notification permission request, service startup and notification management.
|
|
|
|
|
*
|
|
|
|
|
* Overrides [pendingTask] without checking! (safe if user is not insanely fast)
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun <T> newTask(
|
|
|
|
|
title: Int,
|
|
|
|
|
total: Int?,
|
|
|
|
|
getTask: (taskId: Int) -> Deferred<T>,
|
|
|
|
|
onCancelled: (suspend () -> Unit)?,
|
|
|
|
|
): TaskResult<out T> {
|
|
|
|
|
return waitForTask(
|
|
|
|
|
notification,
|
|
|
|
|
volumeManger.getCoroutineScope(volumeId).async {
|
|
|
|
|
task(getEncryptedVolume(volumeId))
|
|
|
|
|
val startedTask = suspendCoroutine { continuation ->
|
|
|
|
|
val task = PendingTask(title, total, getTask) { taskId, job ->
|
|
|
|
|
continuation.resume(Pair(taskId, job))
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
pendingTask = task
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
|
|
|
if (
|
|
|
|
|
ContextCompat.checkSelfPermission(
|
|
|
|
|
this,
|
|
|
|
|
Manifest.permission.POST_NOTIFICATIONS
|
|
|
|
|
) != PackageManager.PERMISSION_GRANTED
|
|
|
|
|
&& askForNotificationPermission
|
|
|
|
|
) {
|
|
|
|
|
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
|
|
|
askForNotificationPermission = false // only ask once per service instance
|
|
|
|
|
return@suspendCoroutine
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
processPendingTask()
|
|
|
|
|
}
|
|
|
|
|
return waitForTask(startedTask.first, startedTask.second, onCancelled)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun <T> volumeTask(
|
|
|
|
|
title: Int,
|
|
|
|
|
total: Int?,
|
|
|
|
|
volumeId: Int,
|
|
|
|
|
task: suspend (taskId: Int, encryptedVolume: EncryptedVolume) -> T
|
|
|
|
|
): TaskResult<out T> {
|
|
|
|
|
return newTask(title, total, { taskId ->
|
|
|
|
|
volumeManger.getCoroutineScope(volumeId).async {
|
|
|
|
|
task(taskId, getEncryptedVolume(volumeId))
|
|
|
|
|
}
|
|
|
|
|
}, null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun <T> globalTask(
|
|
|
|
|
title: Int,
|
|
|
|
|
total: Int?,
|
|
|
|
|
task: suspend (taskId: Int) -> T,
|
|
|
|
|
onCancelled: (suspend () -> Unit)? = null,
|
|
|
|
|
): TaskResult<out T> {
|
|
|
|
|
return newTask(title, total, { taskId ->
|
|
|
|
|
serviceScope.async(Dispatchers.IO) {
|
|
|
|
|
task(taskId)
|
|
|
|
|
}
|
|
|
|
|
}, if (onCancelled == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
{
|
|
|
|
|
serviceScope.launch(Dispatchers.IO) {
|
|
|
|
|
onCancelled()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun copyFile(
|
|
|
|
@ -196,9 +408,8 @@ class FileOperationService : Service() {
|
|
|
|
|
items: List<OperationFile>,
|
|
|
|
|
srcVolumeId: Int = volumeId,
|
|
|
|
|
): TaskResult<out String?> {
|
|
|
|
|
val notification = showNotification(R.string.file_op_copy_msg, items.size)
|
|
|
|
|
val srcEncryptedVolume = getEncryptedVolume(srcVolumeId)
|
|
|
|
|
return volumeTask(volumeId, notification) { encryptedVolume ->
|
|
|
|
|
return volumeTask(R.string.file_op_copy_msg, items.size, volumeId) { taskId, encryptedVolume ->
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
for (i in items.indices) {
|
|
|
|
|
yield()
|
|
|
|
@ -212,7 +423,7 @@ class FileOperationService : Service() {
|
|
|
|
|
failedItem = items[i].srcPath
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
|
updateNotificationProgress(notification, i+1, items.size)
|
|
|
|
|
updateNotificationProgress(taskId, i+1, items.size)
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
@ -222,8 +433,7 @@ class FileOperationService : Service() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun moveElements(volumeId: Int, toMove: List<OperationFile>, toClean: List<String>): TaskResult<out String?> {
|
|
|
|
|
val notification = showNotification(R.string.file_op_move_msg, toMove.size)
|
|
|
|
|
return volumeTask(volumeId, notification) { encryptedVolume ->
|
|
|
|
|
return volumeTask(R.string.file_op_move_msg, toMove.size, volumeId) { taskId, encryptedVolume ->
|
|
|
|
|
val total = toMove.size+toClean.size
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
for ((i, item) in toMove.withIndex()) {
|
|
|
|
@ -231,7 +441,7 @@ class FileOperationService : Service() {
|
|
|
|
|
failedItem = item.srcPath
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
updateNotificationProgress(notification, i+1, total)
|
|
|
|
|
updateNotificationProgress(taskId, i+1, total)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
@ -240,7 +450,7 @@ class FileOperationService : Service() {
|
|
|
|
|
failedItem = folderPath
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
updateNotificationProgress(notification, toMove.size+i+1, total)
|
|
|
|
|
updateNotificationProgress(taskId, toMove.size+i+1, total)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -252,7 +462,7 @@ class FileOperationService : Service() {
|
|
|
|
|
encryptedVolume: EncryptedVolume,
|
|
|
|
|
dstPaths: List<String>,
|
|
|
|
|
uris: List<Uri>,
|
|
|
|
|
notification: FileOperationNotification,
|
|
|
|
|
taskId: Int,
|
|
|
|
|
): String? {
|
|
|
|
|
var failedIndex = -1
|
|
|
|
|
for (i in dstPaths.indices) {
|
|
|
|
@ -265,7 +475,7 @@ class FileOperationService : Service() {
|
|
|
|
|
failedIndex = i
|
|
|
|
|
}
|
|
|
|
|
if (failedIndex == -1) {
|
|
|
|
|
updateNotificationProgress(notification, i+1, dstPaths.size)
|
|
|
|
|
updateNotificationProgress(taskId, i+1, dstPaths.size)
|
|
|
|
|
} else {
|
|
|
|
|
return uris[failedIndex].toString()
|
|
|
|
|
}
|
|
|
|
@ -274,9 +484,8 @@ class FileOperationService : Service() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun importFilesFromUris(volumeId: Int, dstPaths: List<String>, uris: List<Uri>): TaskResult<out String?> {
|
|
|
|
|
val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
|
|
|
|
|
return volumeTask(volumeId, notification) { encryptedVolume ->
|
|
|
|
|
importFilesFromUris(encryptedVolume, dstPaths, uris, notification)
|
|
|
|
|
return volumeTask(R.string.file_op_import_msg, dstPaths.size, volumeId) { taskId, encryptedVolume ->
|
|
|
|
|
importFilesFromUris(encryptedVolume, dstPaths, uris, taskId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -284,8 +493,6 @@ class FileOperationService : Service() {
|
|
|
|
|
* 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 suspend fun recursiveMapDirectoryForImport(
|
|
|
|
|
rootSrcDir: DocumentFile,
|
|
|
|
@ -316,36 +523,35 @@ class FileOperationService : Service() {
|
|
|
|
|
rootDstPath: String,
|
|
|
|
|
rootSrcDir: DocumentFile,
|
|
|
|
|
): ImportDirectoryResult {
|
|
|
|
|
val notification = showNotification(R.string.file_op_import_msg, null)
|
|
|
|
|
val srcUris = arrayListOf<Uri>()
|
|
|
|
|
return ImportDirectoryResult(volumeTask(volumeId, notification) { encryptedVolume ->
|
|
|
|
|
return ImportDirectoryResult(volumeTask(R.string.file_op_import_msg, null, volumeId) { taskId, encryptedVolume ->
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
val dstFiles = arrayListOf<String>()
|
|
|
|
|
val dstDirs = arrayListOf<String>()
|
|
|
|
|
recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs)
|
|
|
|
|
// create destination folders so the new files can use them
|
|
|
|
|
for (dir in dstDirs) {
|
|
|
|
|
if (!encryptedVolume.mkdir(dir)) {
|
|
|
|
|
// if directory creation fails, check if it was already present
|
|
|
|
|
if (!encryptedVolume.mkdir(dir) && encryptedVolume.getAttr(dir)?.type != Stat.S_IFDIR) {
|
|
|
|
|
failedItem = dir
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
|
failedItem = importFilesFromUris(encryptedVolume, dstFiles, srcUris, notification)
|
|
|
|
|
failedItem = importFilesFromUris(encryptedVolume, dstFiles, srcUris, taskId)
|
|
|
|
|
}
|
|
|
|
|
failedItem
|
|
|
|
|
}, srcUris)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): TaskResult<out String?> {
|
|
|
|
|
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
|
|
|
|
|
val task = serviceScope.async(Dispatchers.IO) {
|
|
|
|
|
return globalTask(R.string.file_op_wiping_msg, uris.size, { taskId ->
|
|
|
|
|
var errorMsg: String? = null
|
|
|
|
|
for (i in uris.indices) {
|
|
|
|
|
yield()
|
|
|
|
|
errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
|
|
|
|
|
if (errorMsg == null) {
|
|
|
|
|
updateNotificationProgress(notification, i+1, uris.size)
|
|
|
|
|
updateNotificationProgress(taskId, i+1, uris.size)
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
@ -354,8 +560,7 @@ class FileOperationService : Service() {
|
|
|
|
|
rootFile?.delete()
|
|
|
|
|
}
|
|
|
|
|
errorMsg
|
|
|
|
|
}
|
|
|
|
|
return waitForTask(notification, task)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun exportFileInto(encryptedVolume: EncryptedVolume, srcPath: String, treeDocumentFile: DocumentFile): Boolean {
|
|
|
|
@ -391,8 +596,7 @@ class FileOperationService : Service() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun exportFiles(volumeId: Int, items: List<ExplorerElement>, uri: Uri): TaskResult<out String?> {
|
|
|
|
|
val notification = showNotification(R.string.file_op_export_msg, items.size)
|
|
|
|
|
return volumeTask(volumeId, notification) { encryptedVolume ->
|
|
|
|
|
return volumeTask(R.string.file_op_export_msg, items.size, volumeId) { taskId, encryptedVolume ->
|
|
|
|
|
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
for (i in items.indices) {
|
|
|
|
@ -407,7 +611,7 @@ class FileOperationService : Service() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
|
updateNotificationProgress(notification, i+1, items.size)
|
|
|
|
|
updateNotificationProgress(taskId, i+1, items.size)
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
@ -436,8 +640,7 @@ class FileOperationService : Service() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun removeElements(volumeId: Int, items: List<ExplorerElement>): String? {
|
|
|
|
|
val notification = showNotification(R.string.file_op_delete_msg, items.size)
|
|
|
|
|
return volumeTask(volumeId, notification) { encryptedVolume ->
|
|
|
|
|
return volumeTask(R.string.file_op_delete_msg, items.size, volumeId) { taskId, encryptedVolume ->
|
|
|
|
|
var failedItem: String? = null
|
|
|
|
|
for ((i, element) in items.withIndex()) {
|
|
|
|
|
yield()
|
|
|
|
@ -447,7 +650,7 @@ class FileOperationService : Service() {
|
|
|
|
|
failedItem = element.fullPath
|
|
|
|
|
}
|
|
|
|
|
if (failedItem == null) {
|
|
|
|
|
updateNotificationProgress(notification, i + 1, items.size)
|
|
|
|
|
updateNotificationProgress(taskId, i + 1, items.size)
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
@ -456,13 +659,13 @@ class FileOperationService : Service() {
|
|
|
|
|
}.failedItem // treat cancellation as success
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int {
|
|
|
|
|
private suspend fun recursiveCountChildElements(rootDirectory: DocumentFile): Int {
|
|
|
|
|
yield()
|
|
|
|
|
val children = rootDirectory.listFiles()
|
|
|
|
|
var count = children.size
|
|
|
|
|
for (child in children) {
|
|
|
|
|
if (child.isDirectory) {
|
|
|
|
|
count += recursiveCountChildElements(child, scope)
|
|
|
|
|
count += recursiveCountChildElements(child)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count
|
|
|
|
@ -472,9 +675,8 @@ class FileOperationService : Service() {
|
|
|
|
|
src: DocumentFile,
|
|
|
|
|
dst: DocumentFile,
|
|
|
|
|
dstRootDirectory: ObjRef<DocumentFile?>?,
|
|
|
|
|
notification: FileOperationNotification,
|
|
|
|
|
taskId: Int,
|
|
|
|
|
total: Int,
|
|
|
|
|
scope: CoroutineScope,
|
|
|
|
|
progress: ObjRef<Int> = ObjRef(0)
|
|
|
|
|
): DocumentFile? {
|
|
|
|
|
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
|
|
|
|
@ -491,10 +693,10 @@ class FileOperationService : Service() {
|
|
|
|
|
inputStream.close()
|
|
|
|
|
if (written != child.length()) return child
|
|
|
|
|
} else {
|
|
|
|
|
recursiveCopyVolume(child, dstDir, null, notification, total, scope, progress)?.let { return it }
|
|
|
|
|
recursiveCopyVolume(child, dstDir, null, taskId, total, progress)?.let { return it }
|
|
|
|
|
}
|
|
|
|
|
progress.value++
|
|
|
|
|
updateNotificationProgress(notification, progress.value, total)
|
|
|
|
|
updateNotificationProgress(taskId, progress.value, total)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
@ -502,13 +704,14 @@ class FileOperationService : Service() {
|
|
|
|
|
class CopyVolumeResult(val taskResult: TaskResult<out DocumentFile?>, val dstRootDirectory: DocumentFile?)
|
|
|
|
|
|
|
|
|
|
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult {
|
|
|
|
|
val notification = showNotification(R.string.copy_volume_notification, null)
|
|
|
|
|
val dstRootDirectory = ObjRef<DocumentFile?>(null)
|
|
|
|
|
val task = serviceScope.async(Dispatchers.IO) {
|
|
|
|
|
val total = recursiveCountChildElements(src, this)
|
|
|
|
|
updateNotificationProgress(notification, 0, total)
|
|
|
|
|
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
|
|
|
|
|
}
|
|
|
|
|
return CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
|
|
|
|
|
val result = globalTask(R.string.copy_volume_notification, null, { taskId ->
|
|
|
|
|
val total = recursiveCountChildElements(src)
|
|
|
|
|
updateNotificationProgress(taskId, 0, total)
|
|
|
|
|
recursiveCopyVolume(src, dst, dstRootDirectory, taskId, total)
|
|
|
|
|
}, {
|
|
|
|
|
dstRootDirectory.value?.delete()
|
|
|
|
|
})
|
|
|
|
|
return CopyVolumeResult(result, dstRootDirectory.value)
|
|
|
|
|
}
|
|
|
|
|
}
|