diff --git a/app/build.gradle b/app/build.gradle
index 557056c..9b1bffe 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -36,7 +36,7 @@ android {
defaultConfig {
applicationId "sushi.hardcore.droidfs"
minSdkVersion 21
- targetSdkVersion 32
+ targetSdkVersion 33
versionCode 36
versionName "2.1.3"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f045d33..502260f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,9 @@
android:installLocation="auto">
+
+
+
@@ -56,7 +59,7 @@
-
+
diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
index ec681b7..be3fb76 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
@@ -132,13 +132,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
}
startService(Intent(this, WiperService::class.java))
- Intent(this, FileOperationService::class.java).also {
- bindService(it, object : ServiceConnection {
- override fun onServiceConnected(className: ComponentName, service: IBinder) {
- fileOperationService = (service as FileOperationService.LocalBinder).getService()
- }
- override fun onServiceDisconnected(arg0: ComponentName) {}
- }, Context.BIND_AUTO_CREATE)
+ FileOperationService.bind(this) {
+ fileOperationService = it
}
}
@@ -434,9 +429,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
lifecycleScope.launch {
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
when (result.taskResult.state) {
- TaskResult.State.CANCELLED -> {
- result.dstRootDirectory?.delete()
- }
TaskResult.State.SUCCESS -> {
result.dstRootDirectory?.let {
getResultVolume(it)?.let { volume ->
@@ -458,6 +450,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
.show()
}
TaskResult.State.ERROR -> result.taskResult.showErrorAlertDialog(this@MainActivity, theme)
+ TaskResult.State.CANCELLED -> {}
}
}
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
index 1955915..2dd734c 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
@@ -190,15 +190,9 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
setContentView(R.layout.activity_explorer)
}
- protected open fun bindFileOperationService(){
- Intent(this, FileOperationService::class.java).also {
- bindService(it, object : ServiceConnection {
- override fun onServiceConnected(className: ComponentName, service: IBinder) {
- val binder = service as FileOperationService.LocalBinder
- fileOperationService = binder.getService()
- }
- override fun onServiceDisconnected(arg0: ComponentName) {}
- }, Context.BIND_AUTO_CREATE)
+ protected open fun bindFileOperationService() {
+ FileOperationService.bind(this) {
+ fileOperationService = it
}
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationNotification.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationNotification.kt
deleted file mode 100644
index 7f8f8bf..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationNotification.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package sushi.hardcore.droidfs.file_operations
-
-import androidx.core.app.NotificationCompat
-
-class FileOperationNotification(val notificationBuilder: NotificationCompat.Builder, val notificationId: Int)
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
index 0ce022f..d953dc3 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
@@ -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()
- 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(
+ val title: Int,
+ val total: Int?,
+ private val getTask: (Int) -> Deferred,
+ private val onStart: (taskId: Int, job: Deferred) -> Unit,
+ ) {
+ fun start(taskId: Int): Deferred = 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
+ private var askForNotificationPermission = true
+ private lateinit var notificationManager: NotificationManagerCompat
+ private val notifications = HashMap()
+ private var foregroundNotificationId = -1
+ private val tasks = HashMap()
+ 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,87 +198,173 @@ class FileOperationService : Service() {
)
}
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
- notificationBuilder
- .setContentTitle(getString(message))
- .setSmallIcon(R.drawable.ic_notification)
- .setOngoing(true)
- .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 or PendingIntent.FLAG_IMMUTABLE
- )
- ))
- if (total != null) {
+ .setContentTitle(getString(task.title))
+ .setSmallIcon(R.drawable.ic_notification)
+ .setOngoing(true)
+ .addAction(NotificationCompat.Action(
+ R.drawable.icon_close,
+ getString(R.string.cancel),
+ PendingIntent.getBroadcast(
+ this,
+ newTaskId,
+ Intent(this, NotificationBroadcastReceiver::class.java).apply {
+ putExtra("bundle", Bundle().apply {
+ putBinder("binder", LocalBinder())
+ putInt("taskId", newTaskId)
+ })
+ action = ACTION_CANCEL
+ },
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ ))
+ 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 waitForTask(notification: FileOperationNotification, task: Deferred): TaskResult {
- 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 waitForTask(
+ taskId: Int,
+ task: Deferred,
+ onCancelled: (suspend () -> Unit)?,
+ ): TaskResult {
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 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 newTask(
+ title: Int,
+ total: Int?,
+ getTask: (taskId: Int) -> Deferred,
+ onCancelled: (suspend () -> Unit)?,
): TaskResult {
- 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 volumeTask(
+ title: Int,
+ total: Int?,
+ volumeId: Int,
+ task: suspend (taskId: Int, encryptedVolume: EncryptedVolume) -> T
+ ): TaskResult {
+ return newTask(title, total, { taskId ->
+ volumeManger.getCoroutineScope(volumeId).async {
+ task(taskId, getEncryptedVolume(volumeId))
+ }
+ }, null)
+ }
+
+ private suspend fun globalTask(
+ title: Int,
+ total: Int?,
+ task: suspend (taskId: Int) -> T,
+ onCancelled: (suspend () -> Unit)? = null,
+ ): TaskResult {
+ 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,
srcVolumeId: Int = volumeId,
): TaskResult {
- 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, toClean: List): TaskResult {
- 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,
uris: List,
- 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, uris: List): TaskResult {
- 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()
- 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()
val dstDirs = arrayListOf()
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, rootFile: DocumentFile? = null): TaskResult {
- 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, uri: Uri): TaskResult {
- 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): 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?,
- notification: FileOperationNotification,
+ taskId: Int,
total: Int,
- scope: CoroutineScope,
progress: ObjRef = 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, val dstRootDirectory: DocumentFile?)
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult {
- val notification = showNotification(R.string.copy_volume_notification, null)
val dstRootDirectory = ObjRef(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)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt
index e10840c..ae0ebca 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt
@@ -6,13 +6,11 @@ import android.content.Intent
class NotificationBroadcastReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
- if (intent.action == FileOperationService.ACTION_CANCEL){
+ if (intent.action == FileOperationService.ACTION_CANCEL) {
intent.getBundleExtra("bundle")?.let { bundle ->
- (bundle.getBinder("binder") as FileOperationService.LocalBinder?)?.let { binder ->
- val notificationId = bundle.getInt("notificationId")
- val service = binder.getService()
- service.cancelOperation(notificationId)
- }
+ // TODO: use peekService instead?
+ val binder = (bundle.getBinder("binder") as FileOperationService.LocalBinder?)
+ binder?.getService()?.cancelOperation(bundle.getInt("taskId"))
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fd68b14..11a2e58 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -279,4 +279,6 @@
Debug
DroidFS Logcat
Logcat saved
+ Later
+ Notification permission has been denied. Background file operations won\'t be visible. You can change this in the app\'s permission settings.