package sushi.hardcore.droidfs.file_operations import android.app.* import android.content.Context import android.content.Intent import android.graphics.drawable.Icon import android.net.Uri import android.os.* import androidx.documentfile.provider.DocumentFile import sushi.hardcore.droidfs.GocryptfsVolume import sushi.hardcore.droidfs.R 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() { companion object { const val NOTIFICATION_CHANNEL_ID = "FileOperations" const val ACTION_CANCEL = "file_operation_cancel" } private val binder = LocalBinder() private lateinit var gocryptfsVolume: GocryptfsVolume private lateinit var notificationManager: NotificationManager private var notifications = HashMap() private var lastNotificationId = 0 inner class LocalBinder : Binder() { fun getService(): FileOperationService = this@FileOperationService fun setGocryptfsVolume(g: GocryptfsVolume) { gocryptfsVolume = g } } override fun onBind(p0: Intent?): IBinder { return binder } private fun showNotification(message: Int, total: Int): FileOperationNotification { ++lastNotificationId 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) } val cancelIntent = Intent(this, NotificationBroadcastReceiver::class.java).apply { val bundle = Bundle() bundle.putBinder("binder", LocalBinder()) bundle.putInt("notificationId", lastNotificationId) putExtra("bundle", bundle) action = ACTION_CANCEL } val cancelPendingIntent = PendingIntent.getBroadcast(this, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT) val notificationAction = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Notification.Action.Builder( Icon.createWithResource(this, R.drawable.icon_close), getString(R.string.cancel), cancelPendingIntent ) } else { Notification.Action.Builder( R.drawable.icon_close, getString(R.string.cancel), cancelPendingIntent ) } notificationBuilder .setContentTitle(getString(message)) .setContentText("0/$total") .setSmallIcon(R.mipmap.icon_launcher) .setOngoing(true) .setProgress(total, 0, false) .addAction(notificationAction.build()) notifications[lastNotificationId] = false notificationManager.notify(lastNotificationId, notificationBuilder.build()) return FileOperationNotification(notificationBuilder, lastNotificationId) } 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()) } private fun cancelNotification(notification: FileOperationNotification){ notificationManager.cancel(notification.notificationId) } fun cancelOperation(notificationId: Int){ notifications[notificationId] = true } 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 } fun copyElements(items: ArrayList, remoteGocryptfsVolume: GocryptfsVolume = gocryptfsVolume, callback: (String?) -> Unit){ Thread { val notification = showNotification(R.string.file_op_copy_msg, items.size) 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!!)) { 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) } else { break } } cancelNotification(notification) callback(failedItem) }.start() } fun moveElements(items: ArrayList, callback: (String?) -> Unit){ Thread { val notification = showNotification(R.string.file_op_move_msg, items.size) val mergedFolders = ArrayList() 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 } else { updateNotificationProgress(notification, i, 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) } } } cancelNotification(notification) callback(failedItem) }.start() } fun importFilesFromUris(items: ArrayList, uris: List, callback: (String?) -> Unit){ Thread { val notification = showNotification(R.string.file_op_import_msg, items.size) var failedIndex = -1 for (i in 0 until items.size) { if (notifications[notification.notificationId]!!){ cancelNotification(notification) return@Thread } try { if (!gocryptfsVolume.importFile(this, uris[i], items[i].dstPath!!)){ failedIndex = i } } catch (e: FileNotFoundException){ failedIndex = i } if (failedIndex == -1) { updateNotificationProgress(notification, i, items.size) } else { cancelNotification(notification) callback(uris[failedIndex].toString()) break } } if (failedIndex == -1){ cancelNotification(notification) callback(null) } }.start() } fun wipeUris(uris: List, callback: (String?) -> Unit){ Thread { val notification = showNotification(R.string.file_op_wiping_msg, uris.size) var errorMsg: String? = null for (i in uris.indices) { if (notifications[notification.notificationId]!!){ cancelNotification(notification) return@Thread } errorMsg = Wiper.wipe(this, uris[i]) if (errorMsg == null) { updateNotificationProgress(notification, i, uris.size) } else { break } } cancelNotification(notification) callback(errorMsg) }.start() } private fun exportFileInto(srcPath: String, treeDocumentFile: DocumentFile): Boolean { val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let { contentResolver.openOutputStream(it) } return if (outputStream == null) { false } else { gocryptfsVolume.exportFile(srcPath, outputStream) } } private fun recursiveExportDirectory(plain_directory_path: String, treeDocumentFile: DocumentFile): String? { treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree -> val explorerElements = gocryptfsVolume.listDir(plain_directory_path) for (e in explorerElements) { val fullPath = PathUtils.pathJoin(plain_directory_path, e.name) if (e.isDirectory) { val failedItem = recursiveExportDirectory(fullPath, childTree) failedItem?.let { return it } } else { if (!exportFileInto(fullPath, childTree)){ return fullPath } } } return null } return treeDocumentFile.name } fun exportFiles(uri: Uri, items: List, callback: (String?) -> Unit){ Thread { contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) DocumentFile.fromTreeUri(this, uri)?.let { treeDocumentFile -> val notification = showNotification(R.string.file_op_export_msg, items.size) var failedItem: String? = null for (i in items.indices) { if (notifications[notification.notificationId]!!){ cancelNotification(notification) return@Thread } failedItem = if (items[i].isDirectory) { recursiveExportDirectory(items[i].fullPath, treeDocumentFile) } else { if (exportFileInto(items[i].fullPath, treeDocumentFile)) null else items[i].fullPath } if (failedItem == null) { updateNotificationProgress(notification, i, items.size) } else { break } } cancelNotification(notification) callback(failedItem) } }.start() } }