From 36b6be84506c082d4babbdf608408cad6241143f Mon Sep 17 00:00:00 2001 From: WirelessAlien <121420261+WirelessAlien@users.noreply.github.com> Date: Thu, 2 Jan 2025 23:46:00 +0530 Subject: [PATCH] add copy and move function to service --- app/src/main/AndroidManifest.xml | 10 ++ .../zipxtract/constant/BroadcastConstants.kt | 2 + .../zipxtract/fragment/ArchiveFragment.kt | 10 +- .../zipxtract/fragment/MainFragment.kt | 61 +++------ .../zipxtract/service/CopyMoveService.kt | 118 +++++++++++++++++ .../zipxtract/service/DeleteFilesService.kt | 123 ++++++++++++++++++ app/src/main/res/values/strings.xml | 6 + 7 files changed, 284 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/wirelessalien/zipxtract/service/CopyMoveService.kt create mode 100644 app/src/main/java/com/wirelessalien/zipxtract/service/DeleteFilesService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3fd92bd..1a6d1a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -128,6 +128,16 @@ android:foregroundServiceType="dataSync" android:exported="false" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/com/wirelessalien/zipxtract/constant/BroadcastConstants.kt b/app/src/main/java/com/wirelessalien/zipxtract/constant/BroadcastConstants.kt index bd1741f..17db20e 100644 --- a/app/src/main/java/com/wirelessalien/zipxtract/constant/BroadcastConstants.kt +++ b/app/src/main/java/com/wirelessalien/zipxtract/constant/BroadcastConstants.kt @@ -25,6 +25,8 @@ object BroadcastConstants { const val ACTION_EXTRACTION_ERROR = "ACTION_EXTRACTION_ERROR" const val ARCHIVE_NOTIFICATION_CHANNEL_ID = "archive_notification_channel" const val EXTRACTION_NOTIFICATION_CHANNEL_ID = "extraction_notification_channel" + const val DELETE_NOTIFICATION_CHANNEL_ID = "delete_notification_channel" + const val COPY_MOVE_NOTIFICATION_CHANNEL_ID = "copy_move_notification_channel" const val ACTION_EXTRACTION_PROGRESS = "ACTION_EXTRACTION_PROGRESS" const val ACTION_ARCHIVE_PROGRESS = "ACTION_ARCHIVE_PROGRESS" const val EXTRA_PROGRESS = "progress" diff --git a/app/src/main/java/com/wirelessalien/zipxtract/fragment/ArchiveFragment.kt b/app/src/main/java/com/wirelessalien/zipxtract/fragment/ArchiveFragment.kt index f9a1e23..a6d9460 100644 --- a/app/src/main/java/com/wirelessalien/zipxtract/fragment/ArchiveFragment.kt +++ b/app/src/main/java/com/wirelessalien/zipxtract/fragment/ArchiveFragment.kt @@ -63,6 +63,7 @@ import com.wirelessalien.zipxtract.activity.SettingsActivity import com.wirelessalien.zipxtract.adapter.FileAdapter import com.wirelessalien.zipxtract.constant.BroadcastConstants import com.wirelessalien.zipxtract.databinding.FragmentArchiveBinding +import com.wirelessalien.zipxtract.service.DeleteFilesService import com.wirelessalien.zipxtract.service.ExtractArchiveService import com.wirelessalien.zipxtract.service.ExtractCsArchiveService import com.wirelessalien.zipxtract.service.ExtractMultipart7zService @@ -450,12 +451,11 @@ class ArchiveFragment : Fragment(), FileAdapter.OnItemClickListener { .setTitle(getString(R.string.confirm_delete)) .setMessage(getString(R.string.confirm_delete_message)) .setPositiveButton(getString(R.string.delete)) { _, _ -> - if (file.delete()) { - Toast.makeText(requireContext(), getString(R.string.file_deleted), Toast.LENGTH_SHORT).show() - updateAdapterWithFullList() - } else { - Toast.makeText(requireContext(), getString(R.string.general_error_msg), Toast.LENGTH_SHORT).show() + val filesToDelete = arrayListOf(file.absolutePath) + val intent = Intent(requireContext(), DeleteFilesService::class.java).apply { + putStringArrayListExtra(DeleteFilesService.EXTRA_FILES_TO_DELETE, filesToDelete) } + ContextCompat.startForegroundService(requireContext(), intent) bottomSheetDialog.dismiss() } .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> diff --git a/app/src/main/java/com/wirelessalien/zipxtract/fragment/MainFragment.kt b/app/src/main/java/com/wirelessalien/zipxtract/fragment/MainFragment.kt index 86c2451..9038491 100644 --- a/app/src/main/java/com/wirelessalien/zipxtract/fragment/MainFragment.kt +++ b/app/src/main/java/com/wirelessalien/zipxtract/fragment/MainFragment.kt @@ -93,6 +93,8 @@ import com.wirelessalien.zipxtract.service.ArchiveSplitZipService import com.wirelessalien.zipxtract.service.ArchiveTarService import com.wirelessalien.zipxtract.service.ArchiveZipService import com.wirelessalien.zipxtract.service.CompressCsArchiveService +import com.wirelessalien.zipxtract.service.CopyMoveService +import com.wirelessalien.zipxtract.service.DeleteFilesService import com.wirelessalien.zipxtract.service.ExtractArchiveService import com.wirelessalien.zipxtract.service.ExtractCsArchiveService import com.wirelessalien.zipxtract.service.ExtractMultipart7zService @@ -730,36 +732,17 @@ class MainFragment : Fragment(), FileAdapter.OnItemClickListener, FileAdapter.On private fun pasteFiles() { val destinationPath = currentPath ?: return - CoroutineScope(Dispatchers.IO).launch { - for (file in fileOperationViewModel.filesToCopyMove) { - if (file.exists()) { - val destinationFile = File(destinationPath, file.name) - if (fileOperationViewModel.isCopyAction) { - file.copyRecursively(destinationFile, overwrite = true) - } else { - file.moveTo(destinationFile, overwrite = true) - } - } else { - withContext(Dispatchers.Main) { - Toast.makeText(requireContext(), - getString(R.string.the_file_doesn_t_exist, file.name), Toast.LENGTH_SHORT).show() - } - } - } - withContext(Dispatchers.Main) { - fileOperationViewModel.filesToCopyMove = emptyList() - binding.pasteFab.visibility = View.GONE - updateAdapterWithFullList() - } - } - } + val filesToCopyMove = fileOperationViewModel.filesToCopyMove.map { it.absolutePath } + val isCopyAction = fileOperationViewModel.isCopyAction - private fun File.moveTo(destination: File, overwrite: Boolean = false) { - if (overwrite && destination.exists()) { - destination.deleteRecursively() + val intent = Intent(requireContext(), CopyMoveService::class.java).apply { + putStringArrayListExtra(CopyMoveService.EXTRA_FILES_TO_COPY_MOVE, ArrayList(filesToCopyMove)) + putExtra(CopyMoveService.EXTRA_DESTINATION_PATH, destinationPath) + putExtra(CopyMoveService.EXTRA_IS_COPY_ACTION, isCopyAction) } - this.copyRecursively(destination, overwrite) - this.deleteRecursively() + ContextCompat.startForegroundService(requireContext(), intent) + fileOperationViewModel.filesToCopyMove = emptyList() + binding.pasteFab.visibility = View.GONE } private fun deleteSelectedFiles() { @@ -767,15 +750,12 @@ class MainFragment : Fragment(), FileAdapter.OnItemClickListener, FileAdapter.On .setTitle(getString(R.string.confirm_delete)) .setMessage(getString(R.string.confirm_delete_message)) .setPositiveButton(getString(R.string.delete)) { _, _ -> - CoroutineScope(Dispatchers.IO).launch { - for (file in selectedFiles) { - file.deleteRecursively() - } - withContext(Dispatchers.Main) { - unselectAllFiles() - updateAdapterWithFullList() - } + val filesToDelete = selectedFiles.map { it.absolutePath } + val intent = Intent(requireContext(), DeleteFilesService::class.java).apply { + putStringArrayListExtra(DeleteFilesService.EXTRA_FILES_TO_DELETE, ArrayList(filesToDelete)) } + ContextCompat.startForegroundService(requireContext(), intent) + unselectAllFiles() } .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.dismiss() @@ -1018,12 +998,11 @@ class MainFragment : Fragment(), FileAdapter.OnItemClickListener, FileAdapter.On .setTitle(getString(R.string.confirm_delete)) .setMessage(getString(R.string.confirm_delete_message)) .setPositiveButton(getString(R.string.delete)) { _, _ -> - if (file.delete()) { - Toast.makeText(requireContext(), getString(R.string.file_deleted), Toast.LENGTH_SHORT).show() - updateAdapterWithFullList() - } else { - Toast.makeText(requireContext(), getString(R.string.general_error_msg), Toast.LENGTH_SHORT).show() + val filesToDelete = arrayListOf(file.absolutePath) + val intent = Intent(requireContext(), DeleteFilesService::class.java).apply { + putStringArrayListExtra(DeleteFilesService.EXTRA_FILES_TO_DELETE, filesToDelete) } + ContextCompat.startForegroundService(requireContext(), intent) bottomSheetDialog.dismiss() } .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> diff --git a/app/src/main/java/com/wirelessalien/zipxtract/service/CopyMoveService.kt b/app/src/main/java/com/wirelessalien/zipxtract/service/CopyMoveService.kt new file mode 100644 index 0000000..2e3792e --- /dev/null +++ b/app/src/main/java/com/wirelessalien/zipxtract/service/CopyMoveService.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 WirelessAlien + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.wirelessalien.zipxtract.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.wirelessalien.zipxtract.R +import com.wirelessalien.zipxtract.constant.BroadcastConstants.COPY_MOVE_NOTIFICATION_CHANNEL_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class CopyMoveService : Service() { + + companion object { + const val EXTRA_FILES_TO_COPY_MOVE = "extra_files_to_copy_move" + const val EXTRA_DESTINATION_PATH = "extra_destination_path" + const val EXTRA_IS_COPY_ACTION = "extra_is_copy_action" + const val NOTIFICATION_ID = 2 + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val filesToCopyMove = intent?.getStringArrayListExtra(EXTRA_FILES_TO_COPY_MOVE)?.map { File(it) } ?: return START_NOT_STICKY + val destinationPath = intent.getStringExtra(EXTRA_DESTINATION_PATH) ?: return START_NOT_STICKY + val isCopyAction = intent.getBooleanExtra(EXTRA_IS_COPY_ACTION, true) + + createNotificationChannel() + startForeground(NOTIFICATION_ID, createNotification(0, filesToCopyMove.size)) + + CoroutineScope(Dispatchers.IO).launch { + copyMoveFiles(filesToCopyMove, destinationPath, isCopyAction) + } + + return START_STICKY + } + + private fun copyMoveFiles(files: List, destinationPath: String, isCopyAction: Boolean) { + var processedFilesCount = 0 + for (file in files) { + val destinationFile = File(destinationPath, file.name) + if (isCopyAction) { + file.copyRecursively(destinationFile, overwrite = true) + } else { + file.moveTo(destinationFile, overwrite = true) + } + processedFilesCount++ + updateNotification(processedFilesCount, files.size) + } + stopForegroundService() + stopSelf() + } + + private fun File.moveTo(destination: File, overwrite: Boolean = false) { + if (overwrite && destination.exists()) { + destination.deleteRecursively() + } + this.copyRecursively(destination, overwrite) + this.deleteRecursively() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + COPY_MOVE_NOTIFICATION_CHANNEL_ID, + getString(R.string.copy_move_files_notification_name), + NotificationManager.IMPORTANCE_LOW + ) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + private fun createNotification(progress: Int, total: Int): Notification { + return NotificationCompat.Builder(this, COPY_MOVE_NOTIFICATION_CHANNEL_ID) + .setContentTitle(getString(R.string.copying_moving_files)) + .setContentText(getString(R.string.copying_moving_files_progress, progress, total)) + .setSmallIcon(R.drawable.ic_notification_icon) + .setProgress(total, progress, false) + .setOngoing(true) + .build() + } + + private fun updateNotification(progress: Int, total: Int) { + val notification = createNotification(progress, total) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.notify(NOTIFICATION_ID, notification) + } + + private fun stopForegroundService() { + stopForeground(STOP_FOREGROUND_REMOVE) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.cancel(Archive7zService.NOTIFICATION_ID) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wirelessalien/zipxtract/service/DeleteFilesService.kt b/app/src/main/java/com/wirelessalien/zipxtract/service/DeleteFilesService.kt new file mode 100644 index 0000000..0ee923f --- /dev/null +++ b/app/src/main/java/com/wirelessalien/zipxtract/service/DeleteFilesService.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2023 WirelessAlien + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.wirelessalien.zipxtract.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.wirelessalien.zipxtract.R +import com.wirelessalien.zipxtract.constant.BroadcastConstants.DELETE_NOTIFICATION_CHANNEL_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +class DeleteFilesService : Service() { + + companion object { + const val EXTRA_FILES_TO_DELETE = "extra_files_to_delete" + const val NOTIFICATION_ID = 23 + } + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val filesToDelete = intent?.getStringArrayListExtra(EXTRA_FILES_TO_DELETE)?.map { File(it) } ?: return START_NOT_STICKY + + createNotificationChannel() + startForeground(NOTIFICATION_ID, createNotification(0, filesToDelete.size)) + + CoroutineScope(Dispatchers.IO).launch { + deleteFiles(filesToDelete) + } + + return START_STICKY + } + + private fun deleteFiles(files: List) { + val totalFilesCount = countTotalFiles(files) + var deletedFilesCount = 0 + + fun deleteFile(file: File) { + if (file.isDirectory) { + file.listFiles()?.forEach { deleteFile(it) } + } + file.deleteRecursively() + deletedFilesCount++ + updateNotification(deletedFilesCount, totalFilesCount) + } + + for (file in files) { + deleteFile(file) + } + + stopForegroundService() + stopSelf() + } + + private fun countTotalFiles(files: List): Int { + var count = 0 + for (file in files) { + if (file.isDirectory) { + count += countTotalFiles(file.listFiles()?.toList() ?: emptyList()) + } else { + count++ + } + } + return count + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + DELETE_NOTIFICATION_CHANNEL_ID, + getString(R.string.delete_files_notification_name), + NotificationManager.IMPORTANCE_LOW + ) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + private fun createNotification(progress: Int, total: Int): Notification { + return NotificationCompat.Builder(this, DELETE_NOTIFICATION_CHANNEL_ID) + .setContentTitle(getString(R.string.delete_ongoing)) + .setContentText(getString(R.string.deleting_files, progress, total)) + .setSmallIcon(R.drawable.ic_notification_icon) + .setProgress(total, progress, false) + .setOngoing(true) + .build() + } + + private fun updateNotification(progress: Int, total: Int) { + val notification = createNotification(progress, total) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.notify(NOTIFICATION_ID, notification) + } + + private fun stopForegroundService() { + stopForeground(STOP_FOREGROUND_REMOVE) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.cancel(Archive7zService.NOTIFICATION_ID) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6da6c83..1be06a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -171,4 +171,10 @@ Copy to Paste The file doesn\'t exist: %1$s + Delete File + Deleting files: %1$d/%2$d + Deleting... + Copy/Move Files + Progress... + Progress %1$d/%2$d \ No newline at end of file