Skip to content

Commit

Permalink
add copy and move function to service
Browse files Browse the repository at this point in the history
WirelessAlien committed Jan 2, 2025
1 parent a9a4526 commit 36b6be8
Showing 7 changed files with 284 additions and 46 deletions.
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -128,6 +128,16 @@
android:foregroundServiceType="dataSync"
android:exported="false" />

<service
android:name=".service.DeleteFilesService"
android:foregroundServiceType="dataSync"
android:exported="false" />

<service
android:name=".service.CopyMoveService"
android:foregroundServiceType="dataSync"
android:exported="false" />

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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, _ ->
Original file line number Diff line number Diff line change
@@ -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,52 +732,30 @@ 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() {
MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog)
.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, _ ->
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2023 WirelessAlien <https://github.com/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 <https://www.gnu.org/licenses/>.
*/

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<File>, 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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2023 WirelessAlien <https://github.com/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 <https://www.gnu.org/licenses/>.
*/

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<File>) {
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<File>): 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)
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -171,4 +171,10 @@
<string name="copy_to">Copy to</string>
<string name="paste">Paste</string>
<string name="the_file_doesn_t_exist">The file doesn\'t exist: %1$s</string>
<string name="delete_files_notification_name">Delete File</string>
<string name="deleting_files">Deleting files: %1$d/%2$d</string>
<string name="delete_ongoing">Deleting...</string>
<string name="copy_move_files_notification_name">Copy/Move Files</string>
<string name="copying_moving_files">Progress...</string>
<string name="copying_moving_files_progress">Progress %1$d/%2$d</string>
</resources>

0 comments on commit 36b6be8

Please sign in to comment.