diff --git a/app/src/main/java/sushi/hardcore/droidfs/FileOperationService.kt b/app/src/main/java/sushi/hardcore/droidfs/FileOperationService.kt index 34fb4d2..ae3d07c 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/FileOperationService.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/FileOperationService.kt @@ -1,6 +1,10 @@ package sushi.hardcore.droidfs +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.app.Service +import android.content.Context import android.content.Intent import android.net.Uri import android.os.* @@ -13,10 +17,16 @@ import sushi.hardcore.droidfs.util.Wiper import java.io.File import java.io.FileNotFoundException + class FileOperationService : Service() { + companion object { + const val NOTIFICATION_ID = 1 + const val NOTIFICATION_CHANNEL_ID = "FileOperations" + } private val binder = LocalBinder() private lateinit var gocryptfsVolume: GocryptfsVolume + private lateinit var notificationManager: NotificationManager inner class LocalBinder : Binder() { fun getService(): FileOperationService = this@FileOperationService @@ -29,6 +39,35 @@ class FileOperationService : Service() { return binder } + private fun showNotification(message: String, total: Int): Notification.Builder { + if (!::notificationManager.isInitialized){ + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + val notificationBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, getString(R.string.file_operations), NotificationManager.IMPORTANCE_LOW) + notificationManager.createNotificationChannel(channel) + Notification.Builder(this, NOTIFICATION_CHANNEL_ID) + } else { + Notification.Builder(this) + } + notificationBuilder.setOngoing(true) + .setContentTitle(getString(R.string.file_op_notification_title)) + .setContentText(message) + .setSmallIcon(R.mipmap.icon_launcher) + .setProgress(total, 0, false) + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + return notificationBuilder + } + + private fun updateNotificationProgress(notificationBuilder: Notification.Builder, progress: Int, total: Int){ + notificationBuilder.setProgress(total, progress, false) + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) + } + + private fun cancelNotification(){ + notificationManager.cancel(NOTIFICATION_ID) + } + private fun copyFile(srcPath: String, dstPath: String, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume): Boolean { var success = true val srcHandleId = remoteGocryptfsVolume.openReadMode(srcPath) @@ -60,55 +99,66 @@ class FileOperationService : Service() { fun copyElements(items: ArrayList, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){ Thread { + val notificationBuilder = showNotification(getString(R.string.file_op_copy_msg), items.size) var failedItem: String? = null - for (item in items){ - if (item.explorerElement.isDirectory){ - if (!gocryptfsVolume.pathExists(item.dstPath!!)) { - if (!gocryptfsVolume.mkdir(item.dstPath!!)) { - failedItem = item.explorerElement.fullPath + for (i in 0 until items.size){ + 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(item.explorerElement.fullPath, item.dstPath!!, remoteGocryptfsVolume)){ - failedItem = item.explorerElement.fullPath + if (!copyFile(items[i].explorerElement.fullPath, items[i].dstPath!!, remoteGocryptfsVolume)){ + failedItem = items[i].explorerElement.fullPath } } - if (failedItem != null){ + if (failedItem == null){ + updateNotificationProgress(notificationBuilder, i, items.size) + } else { break } } + cancelNotification() callback(failedItem) }.start() } fun moveElements(items: ArrayList, callback: (String?) -> Unit){ Thread { + val notificationBuilder = showNotification(getString(R.string.file_op_move_msg), items.size) val mergedFolders = ArrayList() var failedItem: String? = null - for (item in items){ - if (item.explorerElement.isDirectory && gocryptfsVolume.pathExists(item.dstPath!!)){ //folder will be merged - mergedFolders.add(item.explorerElement.fullPath) + 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 { - if (!gocryptfsVolume.rename(item.explorerElement.fullPath, item.dstPath!!)){ - failedItem = item.explorerElement.fullPath + if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)){ + failedItem = items[i].explorerElement.fullPath break + } else { + updateNotificationProgress(notificationBuilder, i, items.size) } } } if (failedItem == null){ - for (path in mergedFolders) { - if (!gocryptfsVolume.rmdir(path)){ - failedItem = path + for (i in 0 until mergedFolders.size) { + if (!gocryptfsVolume.rmdir(mergedFolders[i])){ + failedItem = mergedFolders[i] break + } else { + updateNotificationProgress(notificationBuilder, items.size-(mergedFolders.size-i), items.size) } } } + cancelNotification() callback(failedItem) }.start() } fun importFilesFromUris(items: ArrayList, uris: List, callback: (String?) -> Unit){ Thread { + val notificationBuilder = showNotification(getString(R.string.file_op_import_msg), items.size) var failedIndex = -1 for (i in 0 until items.size) { try { @@ -118,12 +168,16 @@ class FileOperationService : Service() { } catch (e: FileNotFoundException){ failedIndex = i } - if (failedIndex != -1){ + if (failedIndex == -1) { + updateNotificationProgress(notificationBuilder, i, items.size) + } else { + cancelNotification() callback(uris[failedIndex].toString()) break } } if (failedIndex == -1){ + cancelNotification() callback(null) } }.start() @@ -131,13 +185,17 @@ class FileOperationService : Service() { fun wipeUris(uris: List, callback: (String?) -> Unit){ Thread { + val notificationBuilder = showNotification(getString(R.string.file_op_wiping_msg), uris.size) var errorMsg: String? = null - for (uri in uris) { - errorMsg = Wiper.wipe(this, uri) - if (errorMsg != null) { + for (i in uris.indices) { + errorMsg = Wiper.wipe(this, uris[i]) + if (errorMsg == null) { + updateNotificationProgress(notificationBuilder, i, uris.size) + } else { break } } + cancelNotification() callback(errorMsg) }.start() } @@ -146,10 +204,10 @@ class FileOperationService : Service() { val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let { contentResolver.openOutputStream(it) } - return if (outputStream != null){ - gocryptfsVolume.exportFile(srcPath, outputStream) - } else { + return if (outputStream == null) { false + } else { + gocryptfsVolume.exportFile(srcPath, outputStream) } } @@ -176,17 +234,21 @@ class FileOperationService : Service() { Thread { contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile -> + val notificationBuilder = showNotification(getString(R.string.file_op_export_msg), items.size) var failedItem: String? = null - for (element in items) { - failedItem = if (element.isDirectory) { - recursiveExportDirectory(element.fullPath, treeDocumentFile) + for (i in items.indices) { + failedItem = if (items[i].isDirectory) { + recursiveExportDirectory(items[i].fullPath, treeDocumentFile) } else { - if (exportFileInto(element.fullPath, treeDocumentFile)) null else element.fullPath + if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath } - if (failedItem != null) { + if (failedItem == null) { + updateNotificationProgress(notificationBuilder, i, items.size) + } else { break } } + cancelNotification() callback(failedItem) } }.start() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a931ee3..b662c66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -186,4 +186,11 @@ Hidden volumes are stored in the app\'s internal storage. Other apps can\'t see these volumes without root access. However, if you uninstall DroidFS or clear data of the app, all your hidden volumes will be LOST. Be sure to make backups ! Camera permission is needed to take photo. Choose a resolution + File Operations + File Operations running… + Copying files… + Importing files… + Exporting files… + Moving files… + Wiping files…