Skip to content

Commit

Permalink
add extraction support for - "tar.bz2", "tar.gz", "tar.lz4", "tar.lzm…
Browse files Browse the repository at this point in the history
…a", "tar.sz", "tar.xz", "tar.z", "tar.zstd"
WirelessAlien committed Dec 17, 2024
1 parent f0564d9 commit 6ffed44
Showing 9 changed files with 354 additions and 14 deletions.
9 changes: 6 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ android {

buildTypes {
release {
minifyEnabled false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable = false
applicationIdSuffix '.release'
@@ -74,8 +74,11 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.7.0'

//apache commons compress
implementation 'org.apache.commons:commons-compress:1.24.0'
implementation 'org.tukaani:xz:1.9'
implementation 'org.apache.commons:commons-compress:1.27.1'
implementation 'org.tukaani:xz:1.10'

//zstd-jni
implementation "com.github.luben:zstd-jni:1.5.6-3"

//zip4j
implementation 'net.lingala.zip4j:zip4j:2.11.5'
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -112,6 +112,11 @@
android:foregroundServiceType="dataSync"
android:exported="false" />

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



</application>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import android.os.FileObserver
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -62,6 +63,7 @@ import com.wirelessalien.zipxtract.service.Archive7zService
import com.wirelessalien.zipxtract.service.ArchiveSplitZipService
import com.wirelessalien.zipxtract.service.ArchiveZipService
import com.wirelessalien.zipxtract.service.ExtractArchiveService
import com.wirelessalien.zipxtract.service.ExtractCsArchiveService
import com.wirelessalien.zipxtract.service.ExtractMultipart7zService
import com.wirelessalien.zipxtract.service.ExtractMultipartZipService
import com.wirelessalien.zipxtract.service.ExtractRarService
@@ -686,12 +688,19 @@ class MainActivity : AppCompatActivity(), FileAdapter.OnItemClickListener, FileA
val btnCompress7z = bottomSheetView.findViewById<Button>(R.id.btnCompress7z)
val btnMultiZipExtract = bottomSheetView.findViewById<Button>(R.id.btnMultiZipExtract)

// Show extract option only for archive files
btnExtract.visibility = if (isArchiveFile(file)) View.VISIBLE else View.GONE

val filePath = file.absolutePath

btnExtract.setOnClickListener {
showPasswordInputDialog(filePaths)
val fileExtension = file.name.split('.').takeLast(2).joinToString(".").lowercase()
Log.d("FileExtension", fileExtension)
val supportedExtensions = listOf("tar.bz2", "tar.gz", "tar.lz4", "tar.lzma", "tar.sz", "tar.xz", "tar.z", "tar.zstd")

if (supportedExtensions.any { fileExtension.endsWith(it) }) {
startExtractionCsService(filePaths)
} else {
showPasswordInputDialog(filePaths)
}
bottomSheetDialog.dismiss()
}

@@ -723,10 +732,6 @@ class MainActivity : AppCompatActivity(), FileAdapter.OnItemClickListener, FileA
bottomSheetDialog.show()
}

private fun isArchiveFile(file: File): Boolean {
val archiveExtensions = listOf("zip", "7z", "rar")
return archiveExtensions.contains(file.extension.lowercase())
}

private fun showPasswordInputDialog(file: String) {
val dialogView = layoutInflater.inflate(R.layout.password_input_dialog, null)
@@ -806,6 +811,15 @@ class MainActivity : AppCompatActivity(), FileAdapter.OnItemClickListener, FileA
ContextCompat.startForegroundService(this, intent)
}

private fun startExtractionCsService(file: String) {
progressDialog.show()
val intent = Intent(this, ExtractCsArchiveService::class.java).apply {
putExtra(ExtractArchiveService.EXTRA_FILE_PATH, file)

}
ContextCompat.startForegroundService(this, intent)
}

private fun startRarExtractionService(file: String, password: String?) {
progressDialog.show()
val intent = Intent(this, ExtractRarService::class.java).apply {
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.wirelessalien.zipxtract.R
import java.io.File

class FilePathAdapter(private val filePaths: MutableList<String>, private val onDeleteClick: (String) -> Unit) :
RecyclerView.Adapter<FilePathAdapter.ViewHolder>() {

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val filePathText: TextView = itemView.findViewById(R.id.textFileName)
val deleteButton: Button = itemView.findViewById(R.id.deleteBtn)

init {
deleteButton.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val filePath = filePaths[position]
onDeleteClick(filePath)
}
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_file, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val filePath = filePaths[position]
holder.filePathText.text = filePath

val file = File(filePath)
if (file.isDirectory) {
holder.deleteButton.visibility = View.GONE
} else {
holder.deleteButton.visibility = View.VISIBLE
}
}

override fun getItemCount(): Int {
return filePaths.size
}

fun removeFilePath(filePath: String) {
val position = filePaths.indexOf(filePath)
if (position != -1) {
filePaths.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, filePaths.size)
}
}
}
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ object BroadcastConstants {
const val ACTION_ARCHIVE_ERROR = "ACTION_ARCHIVE_ERROR"
const val ACTION_EXTRACTION_COMPLETE = "ACTION_EXTRACTION_COMPLETE"
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 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
@@ -227,7 +227,6 @@ class ArchiveZipService : Service() {
}
}


private fun updateProgress(progress: Int) {
val notification = createNotification(progress)
val notificationManager = getSystemService(NotificationManager::class.java)
@@ -253,15 +252,15 @@ class ArchiveZipService : Service() {
private fun showCompletionNotification(error: String) {
stopForegroundService()

val notification = NotificationCompat.Builder(this, ExtractArchiveService.CHANNEL_ID)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Extraction Complete")
.setContentText(error)
.setSmallIcon(R.drawable.ic_notification_icon)
.setAutoCancel(true)
.build()

val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(ExtractArchiveService.NOTIFICATION_ID + 1, notification)
notificationManager.notify(NOTIFICATION_ID + 1, notification)
}

@Suppress("DEPRECATION")
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* 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.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.wirelessalien.zipxtract.R
import com.wirelessalien.zipxtract.constant.BroadcastConstants
import com.wirelessalien.zipxtract.constant.BroadcastConstants.ACTION_EXTRACTION_COMPLETE
import com.wirelessalien.zipxtract.constant.BroadcastConstants.ACTION_EXTRACTION_ERROR
import com.wirelessalien.zipxtract.constant.BroadcastConstants.ACTION_EXTRACT_CANCEL
import com.wirelessalien.zipxtract.constant.BroadcastConstants.EXTRACTION_NOTIFICATION_CHANNEL_ID
import com.wirelessalien.zipxtract.constant.BroadcastConstants.EXTRA_ERROR_MESSAGE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.apache.commons.compress.compressors.CompressorException
import org.apache.commons.compress.compressors.CompressorStreamFactory
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files

class ExtractCsArchiveService : Service() {

companion object {
const val NOTIFICATION_ID = 596
const val EXTRA_FILE_PATH = "file_path"
}

private var extractionJob: Job? = null

override fun onBind(intent: Intent): IBinder? = null

override fun onCreate() {
super.onCreate()
createNotificationChannel()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val filePath = intent?.getStringExtra(EXTRA_FILE_PATH)

if (filePath == null) {
stopSelf()
return START_NOT_STICKY
}

if (intent.action == ACTION_EXTRACT_CANCEL) {
extractionJob?.cancel()
stopForegroundService()
stopSelf()
return START_NOT_STICKY
}

startForeground(NOTIFICATION_ID, createNotification(0))

extractionJob = CoroutineScope(Dispatchers.IO).launch {
extractArchive(File(filePath))
}

return START_NOT_STICKY
}

override fun onDestroy() {
super.onDestroy()
extractionJob?.cancel()
}

private fun createCancelIntent(): PendingIntent {
val cancelIntent = Intent(this, ExtractCsArchiveService::class.java).apply {
action = ACTION_EXTRACT_CANCEL
}
return PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
EXTRACTION_NOTIFICATION_CHANNEL_ID,
"Extraction Service",
NotificationManager.IMPORTANCE_LOW
)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}

private fun createNotification(progress: Int): Notification {
val builder = NotificationCompat.Builder(this, EXTRACTION_NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.extraction_ongoing))
.setSmallIcon(R.drawable.ic_notification_icon)
.setProgress(100, progress, progress == 0)
.setOngoing(true)
.addAction(R.drawable.ic_round_cancel, getString(R.string.cancel), createCancelIntent())

return builder.build()
}

private fun sendLocalBroadcast(intent: Intent) {
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}

private fun extractArchive(file: File) {
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
val outputFile = File(file.parent, file.nameWithoutExtension)
val fin: InputStream
val outStream: OutputStream

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
fin = Files.newInputStream(file.toPath())
outStream = Files.newOutputStream(outputFile.toPath())
} else {
fin = FileInputStream(file)
outStream = FileOutputStream(outputFile)
}

val inStream = BufferedInputStream(fin)

val compressorInputStream = when (file.extension.lowercase()) {
"lzma" -> LZMACompressorInputStream(inStream)
else -> {
try {
CompressorStreamFactory().createCompressorInputStream(inStream)
} catch (e: CompressorException) {
showErrorNotification(getString(R.string.unsupported_compression_format))
sendLocalBroadcast(Intent(ACTION_EXTRACTION_ERROR).putExtra(EXTRA_ERROR_MESSAGE, getString(R.string.unsupported_compression_format)))
return
}
}
}

val totalBytes = file.length()
var bytesRead = 0L

var n: Int
while (compressorInputStream.read(buffer).also { n = it } != -1) {
if (extractionJob?.isCancelled == true) throw Exception("Extraction cancelled")
outStream.write(buffer, 0, n)
bytesRead += n
val progress = (bytesRead * 100 / totalBytes).toInt()
updateProgress(progress)
}

outStream.close()
compressorInputStream.close()

showCompletionNotification()
sendLocalBroadcast(Intent(ACTION_EXTRACTION_COMPLETE))

} catch (e: CompressorException) {
e.printStackTrace()
showErrorNotification(e.message ?: getString(R.string.extraction_failed))
sendLocalBroadcast(Intent(ACTION_EXTRACTION_ERROR).putExtra(EXTRA_ERROR_MESSAGE, e.message ?: getString(R.string.extraction_failed)))
} catch (e: Exception) {
e.printStackTrace()
showErrorNotification(e.message ?: getString(R.string.extraction_failed))
sendLocalBroadcast(Intent(ACTION_EXTRACTION_ERROR).putExtra(EXTRA_ERROR_MESSAGE, e.message ?: getString(R.string.extraction_failed)))
} catch (e: IOException) {
e.printStackTrace()
showErrorNotification(e.message ?: getString(R.string.extraction_failed))
sendLocalBroadcast(Intent(ACTION_EXTRACTION_ERROR).putExtra(EXTRA_ERROR_MESSAGE, e.message ?: getString(R.string.extraction_failed)))
}
}

private fun updateProgress(progress: Int) {
val notification = createNotification(progress)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(ExtractArchiveService.NOTIFICATION_ID, notification)

sendLocalBroadcast(Intent(BroadcastConstants.ACTION_EXTRACTION_PROGRESS).putExtra(
BroadcastConstants.EXTRA_PROGRESS, progress))
}

private fun showCompletionNotification() {
stopForegroundService()
val notification = NotificationCompat.Builder(this, EXTRACTION_NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.extraction_success))
.setSmallIcon(R.drawable.ic_notification_icon)
.setAutoCancel(true)
.build()

val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID + 1, notification)
}

private fun showErrorNotification(error: String) {
stopForegroundService()
val notification = NotificationCompat.Builder(this, EXTRACTION_NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.extraction_failed))
.setContentText(error)
.setSmallIcon(R.drawable.ic_notification_icon)
.setAutoCancel(true)
.build()

val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID + 2, notification)
}

@Suppress("DEPRECATION")
private fun stopForegroundService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.cancel(NOTIFICATION_ID)
}
}
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -101,4 +101,6 @@
<string name="size">Size</string>
<string name="time_of_creation">Time of creation</string>
<string name="extension">Extension</string>
<string name="unsupported_compression_format">Unsupported compression format</string>
<string name="extraction_ongoing">Extracting file</string>
</resources>

0 comments on commit 6ffed44

Please sign in to comment.