diff --git a/app/build.gradle b/app/build.gradle
index 97184da..9b7be21 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 25d23ca..a219b4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -112,6 +112,11 @@
android:foregroundServiceType="dataSync"
android:exported="false" />
+
+
diff --git a/app/src/main/java/com/wirelessalien/zipxtract/CreateZipFragment.kt b/app/src/main/java/com/wirelessalien/zipxtract/CreateZipFragment.kt
index d0acd62..8494a9b 100644
--- a/app/src/main/java/com/wirelessalien/zipxtract/CreateZipFragment.kt
+++ b/app/src/main/java/com/wirelessalien/zipxtract/CreateZipFragment.kt
@@ -1 +1 @@
-/*
* 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
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.system.ErrnoException
import android.system.OsConstants
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.ImageButton
import android.widget.Spinner
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.wirelessalien.zipxtract.databinding.FragmentCreateZipBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.exception.ZipException
import net.lingala.zip4j.model.ZipParameters
import net.lingala.zip4j.model.enums.AesKeyStrength
import net.lingala.zip4j.model.enums.CompressionLevel
import net.lingala.zip4j.model.enums.CompressionMethod
import net.lingala.zip4j.model.enums.EncryptionMethod
import net.lingala.zip4j.progress.ProgressMonitor
import net.sf.sevenzipjbinding.ICryptoGetTextPassword
import net.sf.sevenzipjbinding.IOutCreateCallback
import net.sf.sevenzipjbinding.IOutFeatureSetEncryptHeader
import net.sf.sevenzipjbinding.IOutItem7z
import net.sf.sevenzipjbinding.ISequentialInStream
import net.sf.sevenzipjbinding.SevenZip
import net.sf.sevenzipjbinding.SevenZipException
import net.sf.sevenzipjbinding.impl.OutItemFactory
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream
import net.sf.sevenzipjbinding.impl.RandomAccessFileOutStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
class CreateZipFragment : Fragment(), FileAdapter.OnDeleteClickListener, FileAdapter.OnFileClickListener {
private lateinit var binding: FragmentCreateZipBinding
private var outputDirectory: DocumentFile? = null
private var pickedDirectory: DocumentFile? = null
private lateinit var sharedPreferences: SharedPreferences
private lateinit var prefs: SharedPreferences
private val tempFiles = mutableListOf()
private var selectedFileUri: Uri? = null
private val cachedFiles = mutableListOf()
private var cachedDirectoryName: String? = null
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: FileAdapter
private lateinit var fileList: MutableList
private var cacheFile: File? = null
private val pickFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
if (result.data != null) {
val clipData = result.data!!.clipData
if (clipData != null) {
tempFiles.clear()
binding.circularProgressBar.visibility = View.VISIBLE
CoroutineScope(Dispatchers.IO).launch {
for (i in 0 until clipData.itemCount) {
val filesUri = clipData.getItemAt(i).uri
val cursor = requireActivity().contentResolver.query(filesUri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val displayName = cursor.getString(displayNameIndex)
val tempFile = File(requireContext().cacheDir, displayName)
// Copy the content from the selected file URI to a temporary file
requireActivity().contentResolver.openInputStream(filesUri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
tempFiles.add(tempFile)
// show picked files name
val selectedFilesText = getString(R.string.selected_files_text, tempFiles.size)
withContext(Dispatchers.Main) {
binding.fileNameTextView.text = selectedFilesText
binding.fileNameTextView.isSelected = true
}
cursor.close()
}
}
// Hide the progress bar on the main thread
withContext(Dispatchers.Main) {
binding.circularProgressBar.visibility = View.GONE
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
}
}
}
}
}
}
private val pickFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
selectedFileUri = result.data?.data
if (selectedFileUri != null) {
showToast(getString(R.string.file_picked_success))
binding.createZipMBtn.isEnabled = true
binding.circularProgressBar.visibility = View.VISIBLE
// Display the file name from the intent
val fileName = getZipFileName(selectedFileUri!!)
val selectedFileText = getString(R.string.selected_file_text, fileName)
binding.fileNameTextView.text = selectedFileText
binding.fileNameTextView.isSelected = true
// Copy the file to the cache directory in background
CoroutineScope(Dispatchers.IO).launch {
cacheFile = File(requireContext().cacheDir, fileName.toString())
cacheFile!!.outputStream().use { cache ->
requireContext().contentResolver.openInputStream(selectedFileUri!!)?.use { it.copyTo(cache) }
}
withContext(Dispatchers.Main) {
binding.circularProgressBar.visibility = View.GONE
}
}
} else {
showToast(getString(R.string.file_picked_fail))
}
}
}
private val directoryFilesPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
requireActivity().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
pickedDirectory = DocumentFile.fromTreeUri(requireContext(), uri)
copyFilesToCache(uri)
val directoryName = pickedDirectory?.name
binding.fileNameTextView.text = directoryName
binding.fileNameTextView.isSelected = true
}
}
private val directoryPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
requireActivity().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
outputDirectory = DocumentFile.fromTreeUri(requireContext(), uri)
val fullPath = outputDirectory?.uri?.path
val displayedPath = fullPath?.replace("/tree/primary", "")
if (displayedPath != null) {
val directoryText = getString(R.string.directory_path, displayedPath)
binding.directoryTextView.text = directoryText
}
// Save the output directory URI in SharedPreferences
val editor = sharedPreferences.edit()
editor.putString("outputDirectoryUriZip", uri.toString())
editor.apply()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentCreateZipBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.slide_right)
sharedPreferences = requireActivity().getSharedPreferences("prefs", AppCompatActivity.MODE_PRIVATE)
prefs = requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fileList = getFilesInCacheDirectory(requireContext().cacheDir)
binding.progressBar.visibility = View.GONE
binding.circularProgressBar.visibility = View.GONE
binding.progressBarNI.visibility = View.GONE
binding.pickFileButton.setOnClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
pickFileLauncher.launch(intent)
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
}
binding.pickFilesButton.setOnClickListener {
openFile()
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
}
binding.changeDirectoryButton.setOnClickListener {
chooseOutputDirectory()
}
binding.pickFolderButton.setOnClickListener {
changeDirectoryFilesPicker()
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
}
binding.settingsInfo.setOnClickListener {
//show alert dialog with info
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.settings_info_text))
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
dialog.dismiss()
}
.show()
}
binding.clearCacheBtnDP.setOnClickListener {
val editor = sharedPreferences.edit()
editor.putString("outputDirectoryUriZip", null)
editor.apply()
// clear output directory
outputDirectory = null
binding.directoryTextView.text = getString(R.string.no_directory_selected)
binding.directoryTextView.isSelected = false
showToast(getString(R.string.output_directory_cleared))
}
binding.clearCacheBtnPF.setOnClickListener {
//delete the complete cache directory folder, files and subdirectories
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
selectedFileUri = null
binding.fileNameTextView.text = getString(R.string.no_file_selected)
binding.fileNameTextView.isSelected = false
showToast(getString(R.string.selected_file_cleared))
}
binding.createZipMBtn.setOnClickListener {
when {
cacheFile != null -> {
showZipOptionsDialog()
}
tempFiles.isNotEmpty() -> {
showZipOptionsDialog()
}
cachedFiles.isNotEmpty() -> {
showZipOptionsDialog()
}
else -> {
showToast(getString(R.string.file_picked_fail))
}
}
}
binding.create7zBtn.setOnClickListener {
when {
cacheFile != null -> {
create7zSingleFile()
}
tempFiles.isNotEmpty() -> {
create7zFile()
}
cachedFiles.isNotEmpty() -> {
create7zFile()
}
else -> {
showToast(getString(R.string.file_picked_fail))
}
}
}
if (requireActivity().intent?.action == Intent.ACTION_VIEW) {
val uri = requireActivity().intent.data
if (uri != null) {
selectedFileUri = uri
binding.createZipMBtn.isEnabled = true
// Display the file name from the intent
val fileName = getZipFileName(selectedFileUri)
val selectedFileText = getString(R.string.selected_file_text, fileName)
binding.fileNameTextView.text = selectedFileText
binding.fileNameTextView.isSelected = true
} else {
showToast(getString(R.string.file_picked_fail))
}
}
val intent = requireActivity().intent
if (intent.action == Intent.ACTION_SEND) {
val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM)
selectedFileUri = fileUri
if (fileUri != null) {
cacheFile = File(requireActivity().cacheDir, getZipFileName(fileUri).toString())
cacheFile!!.outputStream().use { outputStream ->
requireActivity().contentResolver.openInputStream(fileUri)?.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
val fileUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
fileUris?.forEach { fileUri ->
val file = File(requireActivity().cacheDir, getZipFileName(fileUri).toString())
file.outputStream().use { outputStream ->
requireActivity().contentResolver.openInputStream(fileUri)?.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
tempFiles.add(file)
}
}
val savedDirectoryUri = sharedPreferences.getString("outputDirectoryUriZip", null)
if (savedDirectoryUri != null) {
outputDirectory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(savedDirectoryUri))
val fullPath = outputDirectory?.uri?.path
val displayedPath = fullPath?.replace("/tree/primary:", "")
if (displayedPath != null) {
val directoryText = getString(R.string.directory_path, displayedPath)
binding.directoryTextView.text = directoryText
}
} else {
//do nothing
}
recyclerView = binding.recyclerViewFiles
recyclerView.layoutManager = LinearLayoutManager(requireContext())
// Replace getFilesInCacheDirectory with a function to get the initial list of files
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter = FileAdapter(fileList, this, this)
recyclerView.adapter = adapter
}
private fun getFilesInCacheDirectory(directory: File): MutableList {
val filesList = mutableListOf()
// Recursive function to traverse the directory
fun traverseDirectory(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach {
traverseDirectory(it)
}
} else {
filesList.add(file)
}
}
traverseDirectory(directory)
return filesList
}
override fun onResume() {
super.onResume()
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
}
override fun onFileClick(file: File) {
//open activity
val intent = Intent(requireContext(), PickedFilesActivity::class.java)
startActivity(intent)
}
@SuppressLint("NotifyDataSetChanged")
override fun onDeleteClick(file: File) {
val cacheFile = File(requireContext().cacheDir, file.name)
cacheFile.delete()
// use relative path to delete the file from cache directory
val relativePath = file.path.replace("${requireContext().cacheDir}/", "")
val fileToDelete = File(requireContext().cacheDir, relativePath)
fileToDelete.delete()
val parentFolder = fileToDelete.parentFile
if (parentFolder != null) {
if (parentFolder.listFiles()?.isEmpty() == true) {
parentFolder.delete()
}
}
adapter.fileList.remove(file)
adapter.notifyDataSetChanged()
}
private fun changeDirectoryFilesPicker() {
directoryFilesPicker.launch(null)
}
private fun openFile() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
pickFilesLauncher.launch(intent)
}
private fun chooseOutputDirectory() {
directoryPicker.launch(null)
}
private fun copyFilesToCache(directoryUri: Uri) {
binding.circularProgressBar.visibility = View.VISIBLE
val contentResolver = requireContext().contentResolver
val directory = DocumentFile.fromTreeUri(requireContext(), directoryUri)
cachedDirectoryName = directory?.name
if (directory != null && cachedDirectoryName != null) {
val cachedDirectory = File(requireContext().cacheDir, cachedDirectoryName!!)
cachedDirectory.mkdirs()
val allFiles = mutableListOf()
getAllFilesInDirectory(directory, allFiles)
lifecycleScope.launch(Dispatchers.IO) {
for (file in allFiles) {
val relativePath = getRelativePath(directory, file)
val outputFile = File(cachedDirectory, relativePath)
outputFile.parentFile?.mkdirs()
val inputStream = contentResolver.openInputStream(file.uri)
val outputStream = FileOutputStream(outputFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
cachedFiles.add(outputFile)
}
// Switch to the Main dispatcher to update the UI
withContext(Dispatchers.Main) {
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
binding.circularProgressBar.visibility = View.GONE
}
}
}
}
private fun getRelativePath(baseDirectory: DocumentFile, file: DocumentFile): String {
// Get the relative path of the file with respect to the base directory
val basePath = baseDirectory.uri.path ?: ""
val filePath = file.uri.path ?: ""
return if (filePath.startsWith(basePath)) {
filePath.substring(basePath.length)
} else {
// Handle the case where the file is not within the base directory
file.name ?: ""
}
}
private val cachedDirectory: File? get() {
return if (cachedDirectoryName != null) {
File(requireContext().cacheDir, cachedDirectoryName!!)
} else {
null
}
}
private fun getAllFilesInDirectory(directory: DocumentFile?, fileList: MutableList) {
if (directory != null && directory.isDirectory) {
val files = directory.listFiles()
for (file in files) {
if (file.isFile) {
fileList.add(file)
} else if (file.isDirectory) {
// Recursively get files in child directories
getAllFilesInDirectory(file, fileList)
}
}
}
}
private fun getRelativePathForFile(baseDirectory: File, file: File): String {
val basePath = baseDirectory.path
val filePath = file.path
return if (filePath.startsWith(basePath)) {
filePath.substring(basePath.length)
} else {
file.name
}
}
//Not so happy with this function
//Sometimes it fails to create the 7z file
//I will try to improve it in the future
//But for now it works
private fun create7zSingleFile() {
showPasswordInputDialog7z { password, _, level, solid, thread ->
CoroutineScope(Dispatchers.IO).launch {
try {
showProgressBar(true)
showProgressBarText(true)
val tempZipFile = File(requireContext().cacheDir, "temp_archive.7z")
val outputFileName = getZipFileName(selectedFileUri)
withContext(Dispatchers.IO) {
RandomAccessFile(tempZipFile, "rw").use { raf ->
val outArchive = SevenZip.openOutArchive7z()
outArchive.setLevel(level)
outArchive.setSolid(solid)
outArchive.setThreadCount(thread)
outArchive.setHeaderEncryption(true)
val fileToArchive = cacheFile!!
outArchive.createArchive(RandomAccessFileOutStream(raf), 1,
object : IOutCreateCallback, ICryptoGetTextPassword,
IOutFeatureSetEncryptHeader {
override fun cryptoGetTextPassword(): String? {
return password
}
override fun setOperationResult(operationResultOk: Boolean) {
CoroutineScope(Dispatchers.Main).launch {
showToast(getString(R.string.please_wait_end_of_job))
}
}
override fun setTotal(total: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.max = total.toInt()
binding.progressText.text = formatFileSize(total)
}
}
override fun setCompleted(complete: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.progress = complete.toInt()
binding.progressText.text = formatFileSize(complete)
}
}
override fun getItemInformation(index: Int, outItemFactory: OutItemFactory): IOutItem7z {
val item = outItemFactory.createOutItem()
if (fileToArchive.isDirectory) {
// Directory
item.propertyIsDir = true
} else {
// File
item.dataSize = fileToArchive.length()
}
item.propertyPath = fileToArchive.name
return item
}
override fun getStream(i: Int): RandomAccessFileInStream {
return RandomAccessFileInStream(RandomAccessFile(fileToArchive, "r"))
}
override fun setHeaderEncryption(enabled: Boolean) {
outArchive.setHeaderEncryption(enabled)
}
})
outArchive.close()
withContext(Dispatchers.Main) {
showProgressBar(true)
showProgressBarText(true)
}
}
}
if (outputDirectory != null && tempZipFile.exists()) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(
outputDirectory!!.uri,
DocumentsContract.getTreeDocumentId(outputDirectory!!.uri))
val outputZipUri = DocumentsContract.createDocument(
requireActivity().contentResolver, outputUri, "application/x-7z-compressed", outputFileName.toString())
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w").use { outputStream ->
FileInputStream(tempZipFile).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
showToast(getString(R.string.sevenz_creation_success))
showProgressBar(false)
showProgressBarText(false)
showExtractionCompletedSnackbar(outputDirectory)
}
} catch (e: SevenZipException) {
e.printStackTraceExtended()
showToast(getString(R.string.sevenz_creation_failed))
showProgressBar(false)
showProgressBarText(false)
} catch (e: IOException) {
if (isNoStorageSpaceException(e)) {
showProgressBar(false)
showProgressBarText(false)
showToast("No storage available")
} else {
showToast("${getString(R.string.extraction_failed)} ${e.message}")
}
} catch (e: OutOfMemoryError) {
e.printStackTrace()
showToast("Out of memory")
showProgressBar(false)
showProgressBarText(false)
}
}
}
}
private fun create7zFile() {
showPasswordInputDialog7z { password, archiveName, level, solid, thread ->
CoroutineScope(Dispatchers.IO).launch {
try {
showProgressBar(true)
showProgressBarText(true)
val filesToArchive = tempFiles.ifEmpty { cachedFiles }
val tempZipFile = File(requireContext().cacheDir, "temp_archive.7z")
val outputFileName = "$archiveName.7z"
withContext(Dispatchers.IO) {
RandomAccessFile(tempZipFile, "rw").use { raf ->
val outArchive = SevenZip.openOutArchive7z()
outArchive.setLevel(level)
outArchive.setSolid(solid)
outArchive.setThreadCount(thread)
outArchive.setHeaderEncryption(true)
outArchive.createArchive(RandomAccessFileOutStream(raf), filesToArchive.size,
object : IOutCreateCallback, ICryptoGetTextPassword,
IOutFeatureSetEncryptHeader {
override fun cryptoGetTextPassword(): String? {
return password
}
override fun setOperationResult(operationResultOk: Boolean) {
CoroutineScope(Dispatchers.Main).launch {
showToast(getString(R.string.please_wait_end_of_job))
}
}
override fun setTotal(total: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.max = total.toInt()
binding.progressText.text = formatFileSize(total)
}
}
override fun setCompleted(complete: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.progress = complete.toInt()
binding.progressText.text = formatFileSize(complete)
}
}
override fun getItemInformation(index: Int, outItemFactory: OutItemFactory): IOutItem7z {
val item = outItemFactory.createOutItem()
val file = filesToArchive[index]
if (file.isDirectory) {
// Directory
item.propertyIsDir = true
} else {
// File
item.dataSize = file.length()
}
// Set the file structure inside the archive
if (cachedFiles.contains(file)) {
val relativePath = getRelativePathForFile(cachedDirectory!!, file)
item.propertyPath = relativePath + (if (file.isDirectory) File.separator else "")
} else {
// Use the relative path for tempFiles
val relativePath = getRelativePathForFile(cachedDirectory!!, file)
item.propertyPath = relativePath
}
return item
}
override fun getStream(i: Int): ISequentialInStream {
return RandomAccessFileInStream(RandomAccessFile(filesToArchive[i], "r"))
}
override fun setHeaderEncryption(enabled: Boolean) {
outArchive.setHeaderEncryption(enabled)
}
})
outArchive.close()
withContext(Dispatchers.Main) {
showProgressBar(true)
showProgressBarText(true)
}
}
}
if (outputDirectory != null && tempZipFile.exists()) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(
outputDirectory!!.uri,
DocumentsContract.getTreeDocumentId(outputDirectory!!.uri))
val outputZipUri = DocumentsContract.createDocument(
requireActivity().contentResolver, outputUri, "application/x-7z-compressed", outputFileName)
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w").use { outputStream ->
FileInputStream(tempZipFile).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
showToast(getString(R.string.sevenz_creation_success))
showProgressBar(false)
showProgressBarText(false)
showExtractionCompletedSnackbar(outputDirectory)
}
} catch (e: SevenZipException) {
e.printStackTraceExtended()
showToast(getString(R.string.sevenz_creation_failed))
showProgressBar(false)
showProgressBarText(false)
} catch (e: IOException) {
if (isNoStorageSpaceException(e)) {
showProgressBar(false)
showProgressBarText(false)
showToast("No storage available")
} else {
showToast("${getString(R.string.extraction_failed)} ${e.message}")
}
} catch (e: OutOfMemoryError) {
e.printStackTrace()
showToast("Out of memory")
showProgressBar(false)
showProgressBarText(false)
}
}
}
}
private fun isNoStorageSpaceException(e: IOException): Boolean {
return e.message?.contains("ENOSPC") == true || e.cause is ErrnoException && (e.cause as ErrnoException).errno == OsConstants.ENOSPC
}
private fun showZipOptionsDialog() {
showCompressionSettingsDialog { _, _, _, _, _, _, _, _ ->
}
}
private fun createZipFile(
archiveName: String,
password: String?,
compressionMethod: CompressionMethod,
compressionLevel: CompressionLevel,
isEncrypted: Boolean,
encryptionMethod: EncryptionMethod?,
aesStrength: AesKeyStrength?
){
CoroutineScope(Dispatchers.IO).launch {
try {
showProgressBar(true)
showProgressBarText(true)
var outputFileName =
archiveName.ifEmpty { getZipFileName(selectedFileUri) ?: "outputZip" }
if (outputFileName.isEmpty()) {
// Use default name if empty
outputFileName = getString(R.string.output_file_name)
} else if (!outputFileName.endsWith(".zip", ignoreCase = true)) {
// Automatically add .zip extension
outputFileName += ".zip"
}
val zipParameters = ZipParameters()
zipParameters.compressionMethod = compressionMethod
zipParameters.compressionLevel = compressionLevel
zipParameters.encryptionMethod = if (isEncrypted) encryptionMethod else null
zipParameters.isEncryptFiles = isEncrypted
zipParameters.aesKeyStrength = if (isEncrypted) aesStrength else null
val tempZipFile = File(requireContext().cacheDir, "temp_archive.zip")
val zipFile = ZipFile(tempZipFile)
if (isEncrypted) {
zipFile.setPassword(password?.toCharArray())
}
val cachedDirectory = cachedDirectory
zipFile.isRunInThread = true
val progressMonitor = zipFile.progressMonitor
try {
when {
cachedDirectory != null -> {
zipFile.addFolder(cachedDirectory, zipParameters)
}
tempFiles.isNotEmpty() -> {
zipFile.addFiles(tempFiles, zipParameters)
}
cacheFile != null -> {
zipFile.addFile(cacheFile, zipParameters)
}
else -> {
showToast(getString(R.string.please_select_files))
}
}
while (!progressMonitor.state.equals(ProgressMonitor.State.READY)) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text =
getString(R.string.percent_complete, progress)
}
}
if (progressMonitor.result == ProgressMonitor.Result.SUCCESS) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text =
getString(R.string.percent_complete, progress)
showToast(getString(R.string.please_wait_end_of_job))
}
} else {
showToast(getString(R.string.zip_creation_failed))
}
} catch (e: ZipException) {
showToast("${getString(R.string.zip_creation_failed)} ${e.message}")
showProgressBar(false)
showProgressBarText(false)
} finally {
when {
cachedFiles.isNotEmpty() -> {
for (cachedFile in cachedFiles) {
cachedFile.delete()
}
cachedFiles.clear()
}
tempFiles.isNotEmpty() -> {
for (tempFile in tempFiles) {
tempFile.delete()
}
tempFiles.clear()
}
cacheFile != null -> {
cacheFile!!.delete()
}
}
showProgressBar(true)
showProgressBarText(true)
}
if (outputDirectory != null && tempZipFile.exists()) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(
outputDirectory!!.uri,
DocumentsContract.getTreeDocumentId(outputDirectory!!.uri)
)
val outputZipUri = DocumentsContract.createDocument(
requireActivity().contentResolver,
outputUri,
"application/zip",
outputFileName
)
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w")
.use { outputStream ->
FileInputStream(tempZipFile).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer)
.also { bytesRead = it } != -1
) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
showExtractionCompletedSnackbar(outputDirectory)
showToast(getString(R.string.zip_creation_success))
} else {
showToast(getString(R.string.select_output_directory))
}
if (tempZipFile.exists())
tempZipFile.delete()
} catch (e: Exception) {
e.printStackTrace()
showToast(getString(R.string.zip_creation_failed))
}
}
}
private fun createSplitZipFile(
archiveName: String,
password: String?,
compressionMethod: CompressionMethod,
compressionLevel: CompressionLevel,
isEncrypted: Boolean,
encryptionMethod: EncryptionMethod?,
aesStrength: AesKeyStrength?,
splitZipSize: Long
) {
CoroutineScope(Dispatchers.IO).launch {
val baseFileName = archiveName.ifEmpty { getZipFileName(selectedFileUri) ?: "splitArchive" }.removeSuffix(".zip")
try {
showProgressBar(true)
showProgressBarText(true)
val zipParameters = ZipParameters()
zipParameters.compressionMethod = compressionMethod
zipParameters.compressionLevel = compressionLevel
zipParameters.encryptionMethod = if (isEncrypted) encryptionMethod else null
zipParameters.isEncryptFiles = isEncrypted
zipParameters.aesKeyStrength = if (isEncrypted) aesStrength else null
val tempZipFile = File(requireContext().cacheDir, "$baseFileName.zip")
val zipFile = ZipFile(tempZipFile)
if (isEncrypted) {
zipFile.setPassword(password?.toCharArray())
}
val cachedDirectory = cachedDirectory
zipFile.isRunInThread = true
val progressMonitor = zipFile.progressMonitor
try {
when {
cachedDirectory != null -> {
zipFile.createSplitZipFileFromFolder(cachedDirectory, zipParameters, true, splitZipSize)
}
tempFiles.isNotEmpty() -> {
zipFile.createSplitZipFile(tempFiles, zipParameters, true, splitZipSize)
}
cacheFile != null -> {
cacheFile?.let { singleFile ->
val fileList = listOf(singleFile)
zipFile.createSplitZipFile(fileList, zipParameters, true, splitZipSize)
}
}
else -> {
showToast(getString(R.string.please_select_files))
}
}
while (!progressMonitor.state.equals(ProgressMonitor.State.READY)) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text = getString(R.string.percent_complete, progress)
}
}
if (progressMonitor.result == ProgressMonitor.Result.SUCCESS) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text = getString(R.string.percent_complete, progress)
showToast(getString(R.string.please_wait_end_of_job))
}
} else {
showToast(getString(R.string.zip_creation_failed))
}
val splitFiles = File(tempZipFile.parent!!).listFiles { _, name -> name.startsWith("$baseFileName.z") }
val lastPartIndex = splitFiles?.mapNotNull { it.name.substringAfterLast(".z").toIntOrNull() }?.maxOrNull() ?: 0
val nextIndex = if (lastPartIndex < 9) {
lastPartIndex + 1 // Increment by 1 for parts less than 9
} else {
lastPartIndex + 1 // Increment by 1 for parts 9 or greater
}
val lastPartIndexString = if (nextIndex < 10) {
String.format("%02d", nextIndex)
} else {
nextIndex.toString()
}
val lastPart = File(tempZipFile.parent, "$baseFileName.z$lastPartIndexString")
tempZipFile.renameTo(lastPart)
} catch (e: ZipException) {
showToast("${getString(R.string.zip_creation_failed)} ${e.message}")
showProgressBar(false)
showProgressBarText(false)
} finally {
when {
cachedFiles.isNotEmpty() -> {
for (cachedFile in cachedFiles) {
cachedFile.delete()
}
cachedFiles.clear()
}
tempFiles.isNotEmpty() -> {
for (tempFile in tempFiles) {
tempFile.delete()
}
tempFiles.clear()
}
cacheFile != null -> {
cacheFile!!.delete()
}
}
showProgressBar(true)
showProgressBarText(true)
}
val cacheFiles = requireContext().cacheDir.listFiles()
cacheFiles?.forEach { file ->
if (file.extension.matches(Regex("z\\d{2}"))) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(outputDirectory!!.uri, DocumentsContract.getTreeDocumentId(
outputDirectory!!.uri))
val outputZipUri = DocumentsContract.createDocument(requireActivity().contentResolver, outputUri, "application/octet-stream", file.name)
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w").use { outputStream ->
FileInputStream(file).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
}
}
showExtractionCompletedSnackbar(outputDirectory)
showToast(getString(R.string.zip_creation_success))
} catch (e: Exception) {
e.printStackTrace()
showToast(getString(R.string.zip_creation_failed))
}
}
}
private suspend fun showProgressBar(show: Boolean) {
withContext(Dispatchers.Main) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
}
}
private suspend fun showProgressBarText(show: Boolean) {
withContext(Dispatchers.Main) {
binding.progressText.visibility = if (show) View.VISIBLE else View.GONE
}
}
private suspend fun showExtractionCompletedSnackbar(outputDirectory: DocumentFile?) {
withContext(Dispatchers.Main) {
binding.progressBar.visibility = View.GONE
// Show a snackbar with a button to open the ZIP file
val snackbar = Snackbar.make(binding.root, getString(R.string.zip_creation_success), Snackbar.LENGTH_LONG)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
snackbar.setAction(getString(R.string.open_folder)) {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(outputDirectory?.uri, DocumentsContract.Document.MIME_TYPE_DIR)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
startActivity(intent)
}
}
snackbar.show()
}
}
private fun getZipFileName(selectedFileUri: Uri?): String? {
if (selectedFileUri != null) {
val cursor = requireActivity().contentResolver.query(selectedFileUri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val displayNameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (displayNameIndex != -1) {
return it.getString(displayNameIndex)
}
}
}
}
return null
}
private fun showCompressionSettingsDialog(
onCompressionSettingsEntered: (
String,
String?,
CompressionMethod,
CompressionLevel,
Boolean,
EncryptionMethod?,
AesKeyStrength?,
Long?) -> Unit) {
val layoutInflater = LayoutInflater.from(requireContext())
val customView = layoutInflater.inflate(R.layout.zip_settings_dialog, null)
val compressionMethodSpinner = customView.findViewById(R.id.compression_method_input)
val compressionLevelSpinner = customView.findViewById(R.id.compression_level_input)
val encryptionMethodSpinner = customView.findViewById(R.id.encryption_method_input)
val encryptionStrengthSpinner = customView.findViewById(R.id.encryption_strength_input)
val passwordInput = customView.findViewById(R.id.passwordEditText)
val showPasswordButton = customView.findViewById(R.id.showPasswordButton)
val zipNameEditText = customView.findViewById(R.id.zipNameEditText)
val splitZipCheckbox = customView.findViewById(R.id.splitZipCheckbox)
val splitSizeInput = customView.findViewById(R.id.splitSizeEditText)
val splitSizeUnitSpinner = customView.findViewById(R.id.splitSizeUnitSpinner)
val splitSizeUnits = arrayOf("KB", "MB", "GB")
val splitSizeUnitAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, splitSizeUnits)
splitSizeUnitAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
splitSizeUnitSpinner.adapter = splitSizeUnitAdapter
splitZipCheckbox.setOnCheckedChangeListener { _, isChecked ->
splitSizeInput.isEnabled = isChecked
if (!isChecked) {
splitSizeInput.text.clear()
}
}
val compressionMethods = CompressionMethod.values().filter { it != CompressionMethod.AES_INTERNAL_ONLY }.map { it.name }.toTypedArray()
val compressionMethodAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, compressionMethods)
compressionMethodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
compressionMethodSpinner.adapter = compressionMethodAdapter
val compressionLevels = CompressionLevel.values().map { it.name }.toTypedArray()
val compressionLevelAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, compressionLevels)
compressionLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
compressionLevelSpinner.adapter = compressionLevelAdapter
val encryptionMethods = EncryptionMethod.values().map { it.name }.toTypedArray()
val encryptionMethodAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, encryptionMethods)
encryptionMethodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
encryptionMethodSpinner.adapter = encryptionMethodAdapter
val encryptionStrengths = AesKeyStrength.values().filter { it != AesKeyStrength.KEY_STRENGTH_192 }.map { it.name }.toTypedArray()
val encryptionStrengthAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, encryptionStrengths)
encryptionStrengthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
encryptionStrengthSpinner.adapter = encryptionStrengthAdapter
val filesToArchive = tempFiles.ifEmpty { cachedFiles }
var isPasswordVisible = false
val builder = MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog)
builder.setView(customView)
builder.setPositiveButton(getString(R.string.ok)) { _, _ ->
val defaultName = if (filesToArchive.isNotEmpty()) {
filesToArchive[0].nameWithoutExtension
} else {
""
}
val archiveName = zipNameEditText.text.toString().ifBlank { defaultName }
val password = passwordInput.text.toString()
val isEncryptionEnabled = password.isNotEmpty()
val selectedCompressionMethod = CompressionMethod.valueOf(compressionMethods[compressionMethodSpinner.selectedItemPosition])
val selectedCompressionLevel = CompressionLevel.valueOf(compressionLevels[compressionLevelSpinner.selectedItemPosition])
val selectedEncryptionMethod = if (encryptionMethodSpinner.selectedItemPosition != 0) {
EncryptionMethod.valueOf(encryptionMethods[encryptionMethodSpinner.selectedItemPosition])
} else {
null
}
val selectedEncryptionStrength = if (selectedEncryptionMethod != null && selectedEncryptionMethod != EncryptionMethod.NONE) {
AesKeyStrength.valueOf(encryptionStrengths[encryptionStrengthSpinner.selectedItemPosition])
} else {
null
}
val splitSizeText = splitSizeInput.text.toString()
val splitSizeUnit = splitSizeUnits[splitSizeUnitSpinner.selectedItemPosition]
val splitZipSize = if (splitZipCheckbox.isChecked) {
convertToBytes(splitSizeText.toLongOrNull() ?: 64, splitSizeUnit)
} else {
null
}
onCompressionSettingsEntered.invoke(
archiveName,
password.takeIf { isEncryptionEnabled },
selectedCompressionMethod,
selectedCompressionLevel,
isEncryptionEnabled,
selectedEncryptionMethod,
selectedEncryptionStrength,
splitZipSize
)
// Call createSplitZipFile function only if splitZipCheckbox is checked
if (splitZipCheckbox.isChecked) {
createSplitZipFile(
archiveName,
password,
selectedCompressionMethod,
selectedCompressionLevel,
isEncryptionEnabled,
selectedEncryptionMethod,
selectedEncryptionStrength,
splitZipSize!!
)
} else {
createZipFile(
archiveName,
password,
selectedCompressionMethod,
selectedCompressionLevel,
isEncryptionEnabled,
selectedEncryptionMethod,
selectedEncryptionStrength
)
}
}
builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
showPasswordButton.setOnClickListener {
isPasswordVisible = !isPasswordVisible
if (isPasswordVisible) {
// Show password
passwordInput.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
showPasswordButton.setImageResource(R.drawable.ic_visibility_on)
} else {
// Hide password
passwordInput.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
showPasswordButton.setImageResource(R.drawable.ic_visibility_off)
}
}
encryptionMethodSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedEncryptionMethod = EncryptionMethod.valueOf(encryptionMethods[position])
passwordInput.isEnabled = selectedEncryptionMethod != EncryptionMethod.NONE
encryptionStrengthSpinner.isEnabled = selectedEncryptionMethod == EncryptionMethod.AES
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
}
}
builder.show()
}
private fun convertToBytes(size: Long?, unit: String): Long? {
return size?.times(when (unit) {
"KB" -> 1024L
"MB" -> 1024L * 1024
"GB" -> 1024L * 1024 * 1024
else -> 1024L
})
}
private fun showPasswordInputDialog7z(onPasswordEntered: (String?, String, Int, Boolean, Int) -> Unit) {
val layoutInflater = LayoutInflater.from(requireContext())
val customView = layoutInflater.inflate(R.layout.seven_z_option_dialog, null)
val passwordEditText = customView.findViewById(R.id.passwordEditText)
val compressionSpinner = customView.findViewById(R.id.compressionSpinner)
val solidCheckBox = customView.findViewById(R.id.solidCheckBox)
val threadCountEditText = customView.findViewById(R.id.threadCountEditText)
val showPasswordButton = customView.findViewById(R.id.showPasswordButton)
val archiveNameEditText = customView.findViewById(R.id.archiveNameEditText)
val filesToArchive = tempFiles.ifEmpty { cachedFiles }
var isPasswordVisible = false
showPasswordButton.setOnClickListener {
isPasswordVisible = !isPasswordVisible
if (isPasswordVisible) {
// Show password
passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
showPasswordButton.setImageResource(R.drawable.ic_visibility_on)
} else {
// Hide password
passwordEditText.inputType =
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
showPasswordButton.setImageResource(R.drawable.ic_visibility_off)
}
}
MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog)
.setView(customView)
.setPositiveButton(getString(R.string.ok)) { _, _ ->
val defaultName = if (filesToArchive.isNotEmpty()) {
filesToArchive[0].nameWithoutExtension
} else {
"archive"
}
val archiveName = archiveNameEditText.text.toString().ifBlank { defaultName }
val password = passwordEditText.text.toString()
val compressionLevel = when (compressionSpinner.selectedItemPosition) {
0 -> 0
1 -> 1
2 -> 3
3 -> 5
4 -> 7
5 -> 9
else -> -1
}
val solid = solidCheckBox.isChecked
val threadCount = threadCountEditText.text.toString().toIntOrNull() ?: -1
onPasswordEntered.invoke(
password.ifBlank { null },
archiveName,
compressionLevel,
solid,
threadCount
)
}
.setNegativeButton(getString(R.string.no_password)) { _, _ ->
val defaultName = if (filesToArchive.isNotEmpty()) {
filesToArchive[0].nameWithoutExtension
} else {
"archive"
}
val archiveName = archiveNameEditText.text.toString().ifBlank { defaultName }
val compressionLevel = when (compressionSpinner.selectedItemPosition) {
0 -> 0
1 -> 1
2 -> 3
3 -> 5
4 -> 7
5 -> 9
else -> -1
}
val solid = solidCheckBox.isChecked
val threadCount = threadCountEditText.text.toString().toIntOrNull() ?: -1
onPasswordEntered.invoke(
null,
archiveName,
compressionLevel,
solid,
threadCount
)
}
.show()
}
private fun showToast(message: String) {
CoroutineScope(Dispatchers.Main).launch {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
fun formatFileSize(bytes: Long): String {
val kilobyte = 1024
val megabyte = kilobyte * 1024
val gigabyte = megabyte * 1024
return when {
bytes < kilobyte -> "$bytes B"
bytes < megabyte -> String.format("%.2f KB", bytes.toFloat() / kilobyte)
bytes < gigabyte -> String.format("%.2f MB", bytes.toFloat() / megabyte)
else -> String.format("%.2f GB", bytes.toFloat() / gigabyte)
}
}
companion object {
private const val PREFS_NAME = "ZipPrefs"
}
}
\ No newline at end of file
+/*
* 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
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.system.ErrnoException
import android.system.OsConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.Spinner
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.wirelessalien.zipxtract.databinding.FragmentCreateZipBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.exception.ZipException
import net.lingala.zip4j.model.ZipParameters
import net.lingala.zip4j.model.enums.AesKeyStrength
import net.lingala.zip4j.model.enums.CompressionLevel
import net.lingala.zip4j.model.enums.CompressionMethod
import net.lingala.zip4j.model.enums.EncryptionMethod
import net.lingala.zip4j.progress.ProgressMonitor
import net.sf.sevenzipjbinding.ICryptoGetTextPassword
import net.sf.sevenzipjbinding.IOutCreateCallback
import net.sf.sevenzipjbinding.IOutFeatureSetEncryptHeader
import net.sf.sevenzipjbinding.IOutItem7z
import net.sf.sevenzipjbinding.ISequentialInStream
import net.sf.sevenzipjbinding.SevenZip
import net.sf.sevenzipjbinding.SevenZipException
import net.sf.sevenzipjbinding.impl.OutItemFactory
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream
import net.sf.sevenzipjbinding.impl.RandomAccessFileOutStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.RandomAccessFile
class CreateZipFragment : Fragment(), FileAdapter.OnDeleteClickListener, FileAdapter.OnFileClickListener {
private lateinit var binding: FragmentCreateZipBinding
private var outputDirectory: DocumentFile? = null
private var pickedDirectory: DocumentFile? = null
private lateinit var sharedPreferences: SharedPreferences
private lateinit var prefs: SharedPreferences
private val tempFiles = mutableListOf()
private var selectedFileUri: Uri? = null
private val cachedFiles = mutableListOf()
private var cachedDirectoryName: String? = null
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: FileAdapter
private lateinit var fileList: MutableList
private var cacheFile: File? = null
private val pickFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
if (result.data != null) {
val clipData = result.data!!.clipData
if (clipData != null) {
tempFiles.clear()
binding.circularProgressBar.visibility = View.VISIBLE
CoroutineScope(Dispatchers.IO).launch {
for (i in 0 until clipData.itemCount) {
val filesUri = clipData.getItemAt(i).uri
val cursor = requireActivity().contentResolver.query(filesUri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val displayName = cursor.getString(displayNameIndex)
val tempFile = File(requireContext().cacheDir, displayName)
// Copy the content from the selected file URI to a temporary file
requireActivity().contentResolver.openInputStream(filesUri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
tempFiles.add(tempFile)
// show picked files name
val selectedFilesText = getString(R.string.selected_files_text, tempFiles.size)
withContext(Dispatchers.Main) {
binding.fileNameTextView.text = selectedFilesText
binding.fileNameTextView.isSelected = true
}
cursor.close()
}
}
// Hide the progress bar on the main thread
withContext(Dispatchers.Main) {
binding.circularProgressBar.visibility = View.GONE
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
}
}
}
}
}
}
private val pickFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
selectedFileUri = result.data?.data
if (selectedFileUri != null) {
showToast(getString(R.string.file_picked_success))
binding.createZipMBtn.isEnabled = true
binding.circularProgressBar.visibility = View.VISIBLE
// Display the file name from the intent
val fileName = getZipFileName(selectedFileUri!!)
val selectedFileText = getString(R.string.selected_file_text, fileName)
binding.fileNameTextView.text = selectedFileText
binding.fileNameTextView.isSelected = true
// Copy the file to the cache directory in background
CoroutineScope(Dispatchers.IO).launch {
cacheFile = File(requireContext().cacheDir, fileName.toString())
cacheFile!!.outputStream().use { cache ->
requireContext().contentResolver.openInputStream(selectedFileUri!!)?.use { it.copyTo(cache) }
}
withContext(Dispatchers.Main) {
binding.circularProgressBar.visibility = View.GONE
}
}
} else {
showToast(getString(R.string.file_picked_fail))
}
}
}
private val directoryFilesPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
requireActivity().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
pickedDirectory = DocumentFile.fromTreeUri(requireContext(), uri)
copyFilesToCache(uri)
val directoryName = pickedDirectory?.name
binding.fileNameTextView.text = directoryName
binding.fileNameTextView.isSelected = true
}
}
private val directoryPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
uri?.let {
requireActivity().contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
outputDirectory = DocumentFile.fromTreeUri(requireContext(), uri)
val fullPath = outputDirectory?.uri?.path
val displayedPath = fullPath?.replace("/tree/primary", "")
if (displayedPath != null) {
val directoryText = getString(R.string.directory_path, displayedPath)
binding.directoryTextView.text = directoryText
}
// Save the output directory URI in SharedPreferences
val editor = sharedPreferences.edit()
editor.putString("outputDirectoryUriZip", uri.toString())
editor.apply()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentCreateZipBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.slide_right)
sharedPreferences = requireActivity().getSharedPreferences("prefs", AppCompatActivity.MODE_PRIVATE)
prefs = requireActivity().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
fileList = getFilesInCacheDirectory(requireContext().cacheDir)
binding.progressBar.visibility = View.GONE
binding.circularProgressBar.visibility = View.GONE
binding.progressBarNI.visibility = View.GONE
binding.pickFileButton.setOnClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
pickFileLauncher.launch(intent)
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
}
binding.pickFilesButton.setOnClickListener {
openFile()
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
}
binding.changeDirectoryButton.setOnClickListener {
chooseOutputDirectory()
}
binding.pickFolderButton.setOnClickListener {
changeDirectoryFilesPicker()
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
}
binding.settingsInfo.setOnClickListener {
//show alert dialog with info
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.settings_info_text))
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
dialog.dismiss()
}
.show()
}
binding.clearCacheBtnDP.setOnClickListener {
val editor = sharedPreferences.edit()
editor.putString("outputDirectoryUriZip", null)
editor.apply()
// clear output directory
outputDirectory = null
binding.directoryTextView.text = getString(R.string.no_directory_selected)
binding.directoryTextView.isSelected = false
showToast(getString(R.string.output_directory_cleared))
}
binding.clearCacheBtnPF.setOnClickListener {
//delete the complete cache directory folder, files and subdirectories
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
selectedFileUri = null
binding.fileNameTextView.text = getString(R.string.no_file_selected)
binding.fileNameTextView.isSelected = false
showToast(getString(R.string.selected_file_cleared))
}
binding.createZipMBtn.setOnClickListener {
when {
cacheFile != null -> {
showZipOptionsDialog()
}
tempFiles.isNotEmpty() -> {
showZipOptionsDialog()
}
cachedFiles.isNotEmpty() -> {
showZipOptionsDialog()
}
else -> {
showToast(getString(R.string.file_picked_fail))
}
}
}
binding.create7zBtn.setOnClickListener {
when {
cacheFile != null -> {
create7zSingleFile()
}
tempFiles.isNotEmpty() -> {
create7zFile()
}
cachedFiles.isNotEmpty() -> {
create7zFile()
}
else -> {
showToast(getString(R.string.file_picked_fail))
}
}
}
if (requireActivity().intent?.action == Intent.ACTION_VIEW) {
val uri = requireActivity().intent.data
if (uri != null) {
selectedFileUri = uri
binding.createZipMBtn.isEnabled = true
// Display the file name from the intent
val fileName = getZipFileName(selectedFileUri)
val selectedFileText = getString(R.string.selected_file_text, fileName)
binding.fileNameTextView.text = selectedFileText
binding.fileNameTextView.isSelected = true
} else {
showToast(getString(R.string.file_picked_fail))
}
}
val intent = requireActivity().intent
if (intent.action == Intent.ACTION_SEND) {
val fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM)
selectedFileUri = fileUri
if (fileUri != null) {
cacheFile = File(requireActivity().cacheDir, getZipFileName(fileUri).toString())
cacheFile!!.outputStream().use { outputStream ->
requireActivity().contentResolver.openInputStream(fileUri)?.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
val fileUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
fileUris?.forEach { fileUri ->
val file = File(requireActivity().cacheDir, getZipFileName(fileUri).toString())
file.outputStream().use { outputStream ->
requireActivity().contentResolver.openInputStream(fileUri)?.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
tempFiles.add(file)
}
}
val savedDirectoryUri = sharedPreferences.getString("outputDirectoryUriZip", null)
if (savedDirectoryUri != null) {
outputDirectory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(savedDirectoryUri))
val fullPath = outputDirectory?.uri?.path
val displayedPath = fullPath?.replace("/tree/primary:", "")
if (displayedPath != null) {
val directoryText = getString(R.string.directory_path, displayedPath)
binding.directoryTextView.text = directoryText
}
} else {
//do nothing
}
recyclerView = binding.recyclerViewFiles
recyclerView.layoutManager = LinearLayoutManager(requireContext())
// Replace getFilesInCacheDirectory with a function to get the initial list of files
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter = FileAdapter(fileList, this, this)
recyclerView.adapter = adapter
}
private fun getFilesInCacheDirectory(directory: File): MutableList {
val filesList = mutableListOf()
// Recursive function to traverse the directory
fun traverseDirectory(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach {
traverseDirectory(it)
}
} else {
filesList.add(file)
}
}
traverseDirectory(directory)
return filesList
}
override fun onResume() {
super.onResume()
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
}
override fun onFileClick(file: File) {
//open activity
val intent = Intent(requireContext(), PickedFilesActivity::class.java)
startActivity(intent)
}
@SuppressLint("NotifyDataSetChanged")
override fun onDeleteClick(file: File) {
val cacheFile = File(requireContext().cacheDir, file.name)
cacheFile.delete()
// use relative path to delete the file from cache directory
val relativePath = file.path.replace("${requireContext().cacheDir}/", "")
val fileToDelete = File(requireContext().cacheDir, relativePath)
fileToDelete.delete()
val parentFolder = fileToDelete.parentFile
if (parentFolder != null) {
if (parentFolder.listFiles()?.isEmpty() == true) {
parentFolder.delete()
}
}
adapter.fileList.remove(file)
adapter.notifyDataSetChanged()
}
private fun changeDirectoryFilesPicker() {
directoryFilesPicker.launch(null)
}
private fun openFile() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
pickFilesLauncher.launch(intent)
}
private fun chooseOutputDirectory() {
directoryPicker.launch(null)
}
private fun copyFilesToCache(directoryUri: Uri) {
binding.circularProgressBar.visibility = View.VISIBLE
val contentResolver = requireContext().contentResolver
val directory = DocumentFile.fromTreeUri(requireContext(), directoryUri)
cachedDirectoryName = directory?.name
if (directory != null && cachedDirectoryName != null) {
val cachedDirectory = File(requireContext().cacheDir, cachedDirectoryName!!)
cachedDirectory.mkdirs()
val allFiles = mutableListOf()
getAllFilesInDirectory(directory, allFiles)
lifecycleScope.launch(Dispatchers.IO) {
for (file in allFiles) {
val relativePath = getRelativePath(directory, file)
val outputFile = File(cachedDirectory, relativePath)
outputFile.parentFile?.mkdirs()
val inputStream = contentResolver.openInputStream(file.uri)
val outputStream = FileOutputStream(outputFile)
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
cachedFiles.add(outputFile)
}
// Switch to the Main dispatcher to update the UI
withContext(Dispatchers.Main) {
val fileList = getFilesInCacheDirectory(requireContext().cacheDir)
adapter.updateFileList(fileList)
binding.circularProgressBar.visibility = View.GONE
}
}
}
}
private fun getRelativePath(baseDirectory: DocumentFile, file: DocumentFile): String {
// Get the relative path of the file with respect to the base directory
val basePath = baseDirectory.uri.path ?: ""
val filePath = file.uri.path ?: ""
return if (filePath.startsWith(basePath)) {
filePath.substring(basePath.length)
} else {
// Handle the case where the file is not within the base directory
file.name ?: ""
}
}
private val cachedDirectory: File? get() {
return if (cachedDirectoryName != null) {
File(requireContext().cacheDir, cachedDirectoryName!!)
} else {
null
}
}
private fun getAllFilesInDirectory(directory: DocumentFile?, fileList: MutableList) {
if (directory != null && directory.isDirectory) {
val files = directory.listFiles()
for (file in files) {
if (file.isFile) {
fileList.add(file)
} else if (file.isDirectory) {
// Recursively get files in child directories
getAllFilesInDirectory(file, fileList)
}
}
}
}
private fun getRelativePathForFile(baseDirectory: File, file: File): String {
val basePath = baseDirectory.path
val filePath = file.path
return if (filePath.startsWith(basePath)) {
filePath.substring(basePath.length)
} else {
file.name
}
}
//Not so happy with this function
//Sometimes it fails to create the 7z file
//I will try to improve it in the future
//But for now it works
private fun create7zSingleFile() {
showPasswordInputDialog7z { password, _, level, solid, thread ->
CoroutineScope(Dispatchers.IO).launch {
try {
showProgressBar(true)
showProgressBarText(true)
val tempZipFile = File(requireContext().cacheDir, "temp_archive.7z")
val outputFileName = getZipFileName(selectedFileUri)
withContext(Dispatchers.IO) {
RandomAccessFile(tempZipFile, "rw").use { raf ->
val outArchive = SevenZip.openOutArchive7z()
outArchive.setLevel(level)
outArchive.setSolid(solid)
outArchive.setThreadCount(thread)
outArchive.setHeaderEncryption(true)
val fileToArchive = cacheFile!!
outArchive.createArchive(RandomAccessFileOutStream(raf), 1,
object : IOutCreateCallback, ICryptoGetTextPassword,
IOutFeatureSetEncryptHeader {
override fun cryptoGetTextPassword(): String? {
return password
}
override fun setOperationResult(operationResultOk: Boolean) {
CoroutineScope(Dispatchers.Main).launch {
showToast(getString(R.string.please_wait_end_of_job))
}
}
override fun setTotal(total: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.max = total.toInt()
binding.progressText.text = formatFileSize(total)
}
}
override fun setCompleted(complete: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.progress = complete.toInt()
binding.progressText.text = formatFileSize(complete)
}
}
override fun getItemInformation(index: Int, outItemFactory: OutItemFactory): IOutItem7z {
val item = outItemFactory.createOutItem()
if (fileToArchive.isDirectory) {
// Directory
item.propertyIsDir = true
} else {
// File
item.dataSize = fileToArchive.length()
}
item.propertyPath = fileToArchive.name
return item
}
override fun getStream(i: Int): RandomAccessFileInStream {
return RandomAccessFileInStream(RandomAccessFile(fileToArchive, "r"))
}
override fun setHeaderEncryption(enabled: Boolean) {
outArchive.setHeaderEncryption(enabled)
}
})
outArchive.close()
withContext(Dispatchers.Main) {
showProgressBar(true)
showProgressBarText(true)
}
}
}
if (outputDirectory != null && tempZipFile.exists()) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(
outputDirectory!!.uri,
DocumentsContract.getTreeDocumentId(outputDirectory!!.uri))
val outputZipUri = DocumentsContract.createDocument(
requireActivity().contentResolver, outputUri, "application/x-7z-compressed", outputFileName.toString())
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w").use { outputStream ->
FileInputStream(tempZipFile).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
showToast(getString(R.string.sevenz_creation_success))
showProgressBar(false)
showProgressBarText(false)
showExtractionCompletedSnackbar(outputDirectory)
}
} catch (e: SevenZipException) {
e.printStackTraceExtended()
showToast(getString(R.string.sevenz_creation_failed))
showProgressBar(false)
showProgressBarText(false)
} catch (e: IOException) {
if (isNoStorageSpaceException(e)) {
showProgressBar(false)
showProgressBarText(false)
showToast("No storage available")
} else {
showToast("${getString(R.string.extraction_failed)} ${e.message}")
}
} catch (e: OutOfMemoryError) {
e.printStackTrace()
showToast("Out of memory")
showProgressBar(false)
showProgressBarText(false)
}
}
}
}
private fun create7zFile() {
showPasswordInputDialog7z { password, archiveName, level, solid, thread ->
CoroutineScope(Dispatchers.IO).launch {
try {
showProgressBar(true)
showProgressBarText(true)
val filesToArchive = tempFiles.ifEmpty { cachedFiles }
val tempZipFile = File(requireContext().cacheDir, "temp_archive.7z")
val outputFileName = "$archiveName.7z"
withContext(Dispatchers.IO) {
RandomAccessFile(tempZipFile, "rw").use { raf ->
val outArchive = SevenZip.openOutArchive7z()
outArchive.setLevel(level)
outArchive.setSolid(solid)
outArchive.setThreadCount(thread)
outArchive.setHeaderEncryption(true)
outArchive.createArchive(RandomAccessFileOutStream(raf), filesToArchive.size,
object : IOutCreateCallback, ICryptoGetTextPassword,
IOutFeatureSetEncryptHeader {
override fun cryptoGetTextPassword(): String? {
return password
}
override fun setOperationResult(operationResultOk: Boolean) {
CoroutineScope(Dispatchers.Main).launch {
showToast(getString(R.string.please_wait_end_of_job))
}
}
override fun setTotal(total: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.max = total.toInt()
binding.progressText.text = formatFileSize(total)
}
}
override fun setCompleted(complete: Long) {
CoroutineScope(Dispatchers.Main).launch {
binding.progressBar.progress = complete.toInt()
binding.progressText.text = formatFileSize(complete)
}
}
override fun getItemInformation(index: Int, outItemFactory: OutItemFactory): IOutItem7z {
val item = outItemFactory.createOutItem()
val file = filesToArchive[index]
if (file.isDirectory) {
// Directory
item.propertyIsDir = true
} else {
// File
item.dataSize = file.length()
}
// Set the file structure inside the archive
if (cachedFiles.contains(file)) {
val relativePath = getRelativePathForFile(cachedDirectory!!, file)
item.propertyPath = relativePath + (if (file.isDirectory) File.separator else "")
} else {
// Use the relative path for tempFiles
val relativePath = getRelativePathForFile(cachedDirectory!!, file)
item.propertyPath = relativePath
}
return item
}
override fun getStream(i: Int): ISequentialInStream {
return RandomAccessFileInStream(RandomAccessFile(filesToArchive[i], "r"))
}
override fun setHeaderEncryption(enabled: Boolean) {
outArchive.setHeaderEncryption(enabled)
}
})
outArchive.close()
withContext(Dispatchers.Main) {
showProgressBar(true)
showProgressBarText(true)
}
}
}
if (outputDirectory != null && tempZipFile.exists()) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(
outputDirectory!!.uri,
DocumentsContract.getTreeDocumentId(outputDirectory!!.uri))
val outputZipUri = DocumentsContract.createDocument(
requireActivity().contentResolver, outputUri, "application/x-7z-compressed", outputFileName)
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w").use { outputStream ->
FileInputStream(tempZipFile).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
showToast(getString(R.string.sevenz_creation_success))
showProgressBar(false)
showProgressBarText(false)
showExtractionCompletedSnackbar(outputDirectory)
}
} catch (e: SevenZipException) {
e.printStackTraceExtended()
showToast(getString(R.string.sevenz_creation_failed))
showProgressBar(false)
showProgressBarText(false)
} catch (e: IOException) {
if (isNoStorageSpaceException(e)) {
showProgressBar(false)
showProgressBarText(false)
showToast("No storage available")
} else {
showToast("${getString(R.string.extraction_failed)} ${e.message}")
}
} catch (e: OutOfMemoryError) {
e.printStackTrace()
showToast("Out of memory")
showProgressBar(false)
showProgressBarText(false)
}
}
}
}
private fun isNoStorageSpaceException(e: IOException): Boolean {
return e.message?.contains("ENOSPC") == true || e.cause is ErrnoException && (e.cause as ErrnoException).errno == OsConstants.ENOSPC
}
private fun showZipOptionsDialog() {
showCompressionSettingsDialog { _, _, _, _, _, _, _, _ ->
}
}
private fun createZipFile(
archiveName: String,
password: String?,
compressionMethod: CompressionMethod,
compressionLevel: CompressionLevel,
isEncrypted: Boolean,
encryptionMethod: EncryptionMethod?,
aesStrength: AesKeyStrength?
){
CoroutineScope(Dispatchers.IO).launch {
try {
showProgressBar(true)
showProgressBarText(true)
var outputFileName =
archiveName.ifEmpty { getZipFileName(selectedFileUri) ?: "outputZip" }
if (outputFileName.isEmpty()) {
// Use default name if empty
outputFileName = getString(R.string.output_file_name)
} else if (!outputFileName.endsWith(".zip", ignoreCase = true)) {
// Automatically add .zip extension
outputFileName += ".zip"
}
val zipParameters = ZipParameters()
zipParameters.compressionMethod = compressionMethod
zipParameters.compressionLevel = compressionLevel
zipParameters.encryptionMethod = if (isEncrypted) encryptionMethod else null
zipParameters.isEncryptFiles = isEncrypted
zipParameters.aesKeyStrength = if (isEncrypted) aesStrength else null
val tempZipFile = File(requireContext().cacheDir, "temp_archive.zip")
val zipFile = ZipFile(tempZipFile)
if (isEncrypted) {
zipFile.setPassword(password?.toCharArray())
}
val cachedDirectory = cachedDirectory
zipFile.isRunInThread = true
val progressMonitor = zipFile.progressMonitor
try {
when {
cachedDirectory != null -> {
zipFile.addFolder(cachedDirectory, zipParameters)
}
tempFiles.isNotEmpty() -> {
zipFile.addFiles(tempFiles, zipParameters)
}
cacheFile != null -> {
zipFile.addFile(cacheFile, zipParameters)
}
else -> {
showToast(getString(R.string.please_select_files))
}
}
while (!progressMonitor.state.equals(ProgressMonitor.State.READY)) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text =
getString(R.string.percent_complete, progress)
}
}
if (progressMonitor.result == ProgressMonitor.Result.SUCCESS) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text =
getString(R.string.percent_complete, progress)
showToast(getString(R.string.please_wait_end_of_job))
}
} else {
showToast(getString(R.string.zip_creation_failed))
}
} catch (e: ZipException) {
showToast("${getString(R.string.zip_creation_failed)} ${e.message}")
showProgressBar(false)
showProgressBarText(false)
} finally {
when {
cachedFiles.isNotEmpty() -> {
for (cachedFile in cachedFiles) {
cachedFile.delete()
}
cachedFiles.clear()
}
tempFiles.isNotEmpty() -> {
for (tempFile in tempFiles) {
tempFile.delete()
}
tempFiles.clear()
}
cacheFile != null -> {
cacheFile!!.delete()
}
}
showProgressBar(true)
showProgressBarText(true)
}
if (outputDirectory != null && tempZipFile.exists()) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(
outputDirectory!!.uri,
DocumentsContract.getTreeDocumentId(outputDirectory!!.uri)
)
val outputZipUri = DocumentsContract.createDocument(
requireActivity().contentResolver,
outputUri,
"application/zip",
outputFileName
)
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w")
.use { outputStream ->
FileInputStream(tempZipFile).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer)
.also { bytesRead = it } != -1
) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
val cacheDir = requireContext().cacheDir
if (cacheDir.isDirectory) {
val children: Array = cacheDir.list()!!
for (i in children.indices) {
File(cacheDir, children[i]).deleteRecursively()
}
}
showExtractionCompletedSnackbar(outputDirectory)
showToast(getString(R.string.zip_creation_success))
} else {
showToast(getString(R.string.select_output_directory))
}
if (tempZipFile.exists())
tempZipFile.delete()
} catch (e: Exception) {
e.printStackTrace()
showToast(getString(R.string.zip_creation_failed))
}
}
}
private fun createSplitZipFile(
archiveName: String,
password: String?,
compressionMethod: CompressionMethod,
compressionLevel: CompressionLevel,
isEncrypted: Boolean,
encryptionMethod: EncryptionMethod?,
aesStrength: AesKeyStrength?,
splitZipSize: Long
) {
CoroutineScope(Dispatchers.IO).launch {
val baseFileName = archiveName.ifEmpty { getZipFileName(selectedFileUri) ?: "splitArchive" }.removeSuffix(".zip")
try {
showProgressBar(true)
showProgressBarText(true)
val zipParameters = ZipParameters()
zipParameters.compressionMethod = compressionMethod
zipParameters.compressionLevel = compressionLevel
zipParameters.encryptionMethod = if (isEncrypted) encryptionMethod else null
zipParameters.isEncryptFiles = isEncrypted
zipParameters.aesKeyStrength = if (isEncrypted) aesStrength else null
val tempZipFile = File(requireContext().cacheDir, "$baseFileName.zip")
val zipFile = ZipFile(tempZipFile)
if (isEncrypted) {
zipFile.setPassword(password?.toCharArray())
}
val cachedDirectory = cachedDirectory
zipFile.isRunInThread = true
val progressMonitor = zipFile.progressMonitor
try {
when {
cachedDirectory != null -> {
zipFile.createSplitZipFileFromFolder(cachedDirectory, zipParameters, true, splitZipSize)
}
tempFiles.isNotEmpty() -> {
zipFile.createSplitZipFile(tempFiles, zipParameters, true, splitZipSize)
}
cacheFile != null -> {
cacheFile?.let { singleFile ->
val fileList = listOf(singleFile)
zipFile.createSplitZipFile(fileList, zipParameters, true, splitZipSize)
}
}
else -> {
showToast(getString(R.string.please_select_files))
}
}
while (!progressMonitor.state.equals(ProgressMonitor.State.READY)) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text = getString(R.string.percent_complete, progress)
}
}
if (progressMonitor.result == ProgressMonitor.Result.SUCCESS) {
val progress = progressMonitor.percentDone
withContext(Dispatchers.Main) {
binding.progressBar.progress = progress
binding.progressText.text = getString(R.string.percent_complete, progress)
showToast(getString(R.string.please_wait_end_of_job))
}
} else {
showToast(getString(R.string.zip_creation_failed))
}
val splitFiles = File(tempZipFile.parent!!).listFiles { _, name -> name.startsWith("$baseFileName.z") }
val lastPartIndex = splitFiles?.mapNotNull { it.name.substringAfterLast(".z").toIntOrNull() }?.maxOrNull() ?: 0
val nextIndex = if (lastPartIndex < 9) {
lastPartIndex + 1 // Increment by 1 for parts less than 9
} else {
lastPartIndex + 1 // Increment by 1 for parts 9 or greater
}
val lastPartIndexString = if (nextIndex < 10) {
String.format("%02d", nextIndex)
} else {
nextIndex.toString()
}
val lastPart = File(tempZipFile.parent, "$baseFileName.z$lastPartIndexString")
tempZipFile.renameTo(lastPart)
} catch (e: ZipException) {
showToast("${getString(R.string.zip_creation_failed)} ${e.message}")
showProgressBar(false)
showProgressBarText(false)
} finally {
when {
cachedFiles.isNotEmpty() -> {
for (cachedFile in cachedFiles) {
cachedFile.delete()
}
cachedFiles.clear()
}
tempFiles.isNotEmpty() -> {
for (tempFile in tempFiles) {
tempFile.delete()
}
tempFiles.clear()
}
cacheFile != null -> {
cacheFile!!.delete()
}
}
showProgressBar(true)
showProgressBarText(true)
}
val cacheFiles = requireContext().cacheDir.listFiles()
cacheFiles?.forEach { file ->
if (file.extension.matches(Regex("z\\d{2}"))) {
val outputUri = DocumentsContract.buildDocumentUriUsingTree(outputDirectory!!.uri, DocumentsContract.getTreeDocumentId(
outputDirectory!!.uri))
val outputZipUri = DocumentsContract.createDocument(requireActivity().contentResolver, outputUri, "application/octet-stream", file.name)
requireActivity().contentResolver.openOutputStream(outputZipUri!!, "w").use { outputStream ->
FileInputStream(file).use { tempInputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
while (tempInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream!!.write(buffer, 0, bytesRead)
}
}
}
}
}
showExtractionCompletedSnackbar(outputDirectory)
showToast(getString(R.string.zip_creation_success))
} catch (e: Exception) {
e.printStackTrace()
showToast(getString(R.string.zip_creation_failed))
}
}
}
private suspend fun showProgressBar(show: Boolean) {
withContext(Dispatchers.Main) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
}
}
private suspend fun showProgressBarText(show: Boolean) {
withContext(Dispatchers.Main) {
binding.progressText.visibility = if (show) View.VISIBLE else View.GONE
}
}
private suspend fun showExtractionCompletedSnackbar(outputDirectory: DocumentFile?) {
withContext(Dispatchers.Main) {
binding.progressBar.visibility = View.GONE
// Show a snackbar with a button to open the ZIP file
val snackbar = Snackbar.make(binding.root, getString(R.string.zip_creation_success), Snackbar.LENGTH_LONG)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
snackbar.setAction(getString(R.string.open_folder)) {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(outputDirectory?.uri, DocumentsContract.Document.MIME_TYPE_DIR)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
startActivity(intent)
}
}
snackbar.show()
}
}
private fun getZipFileName(selectedFileUri: Uri?): String? {
if (selectedFileUri != null) {
val cursor = requireActivity().contentResolver.query(selectedFileUri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val displayNameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (displayNameIndex != -1) {
return it.getString(displayNameIndex)
}
}
}
}
return null
}
private fun showCompressionSettingsDialog(
onCompressionSettingsEntered: (
String,
String?,
CompressionMethod,
CompressionLevel,
Boolean,
EncryptionMethod?,
AesKeyStrength?,
Long?) -> Unit) {
val layoutInflater = LayoutInflater.from(requireContext())
val customView = layoutInflater.inflate(R.layout.zip_option_dialog, null)
val compressionMethodSpinner = customView.findViewById(R.id.compression_method_input)
val compressionLevelSpinner = customView.findViewById(R.id.compression_level_input)
val encryptionMethodSpinner = customView.findViewById(R.id.encryption_method_input)
val encryptionStrengthSpinner = customView.findViewById(R.id.encryption_strength_input)
val passwordInput = customView.findViewById(R.id.passwordEditText)
val zipNameEditText = customView.findViewById(R.id.zipNameEditText)
val splitZipCheckbox = customView.findViewById(R.id.splitZipCheckbox)
val splitSizeInput = customView.findViewById(R.id.splitSizeEditText)
val splitSizeUnitSpinner = customView.findViewById(R.id.splitSizeUnitSpinner)
val splitSizeUnits = arrayOf("KB", "MB", "GB")
val splitSizeUnitAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, splitSizeUnits)
splitSizeUnitAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
splitSizeUnitSpinner.adapter = splitSizeUnitAdapter
splitZipCheckbox.setOnCheckedChangeListener { _, isChecked ->
splitSizeInput.isEnabled = isChecked
if (!isChecked) {
splitSizeInput.text.clear()
}
}
val compressionMethods = CompressionMethod.values().filter { it != CompressionMethod.AES_INTERNAL_ONLY }.map { it.name }.toTypedArray()
val compressionMethodAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, compressionMethods)
compressionMethodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
compressionMethodSpinner.adapter = compressionMethodAdapter
val compressionLevels = CompressionLevel.values().map { it.name }.toTypedArray()
val compressionLevelAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, compressionLevels)
compressionLevelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
compressionLevelSpinner.adapter = compressionLevelAdapter
val encryptionMethods = EncryptionMethod.values().map { it.name }.toTypedArray()
val encryptionMethodAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, encryptionMethods)
encryptionMethodAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
encryptionMethodSpinner.adapter = encryptionMethodAdapter
val encryptionStrengths = AesKeyStrength.values().filter { it != AesKeyStrength.KEY_STRENGTH_192 }.map { it.name }.toTypedArray()
val encryptionStrengthAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, encryptionStrengths)
encryptionStrengthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
encryptionStrengthSpinner.adapter = encryptionStrengthAdapter
val filesToArchive = tempFiles.ifEmpty { cachedFiles }
var isPasswordVisible = false
val builder = MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog)
builder.setView(customView)
builder.setPositiveButton(getString(R.string.ok)) { _, _ ->
val defaultName = if (filesToArchive.isNotEmpty()) {
filesToArchive[0].nameWithoutExtension
} else {
""
}
val archiveName = zipNameEditText.text.toString().ifBlank { defaultName }
val password = passwordInput.text.toString()
val isEncryptionEnabled = password.isNotEmpty()
val selectedCompressionMethod = CompressionMethod.valueOf(compressionMethods[compressionMethodSpinner.selectedItemPosition])
val selectedCompressionLevel = CompressionLevel.valueOf(compressionLevels[compressionLevelSpinner.selectedItemPosition])
val selectedEncryptionMethod = if (encryptionMethodSpinner.selectedItemPosition != 0) {
EncryptionMethod.valueOf(encryptionMethods[encryptionMethodSpinner.selectedItemPosition])
} else {
null
}
val selectedEncryptionStrength = if (selectedEncryptionMethod != null && selectedEncryptionMethod != EncryptionMethod.NONE) {
AesKeyStrength.valueOf(encryptionStrengths[encryptionStrengthSpinner.selectedItemPosition])
} else {
null
}
val splitSizeText = splitSizeInput.text.toString()
val splitSizeUnit = splitSizeUnits[splitSizeUnitSpinner.selectedItemPosition]
val splitZipSize = if (splitZipCheckbox.isChecked) {
convertToBytes(splitSizeText.toLongOrNull() ?: 64, splitSizeUnit)
} else {
null
}
onCompressionSettingsEntered.invoke(
archiveName,
password.takeIf { isEncryptionEnabled },
selectedCompressionMethod,
selectedCompressionLevel,
isEncryptionEnabled,
selectedEncryptionMethod,
selectedEncryptionStrength,
splitZipSize
)
// Call createSplitZipFile function only if splitZipCheckbox is checked
if (splitZipCheckbox.isChecked) {
createSplitZipFile(
archiveName,
password,
selectedCompressionMethod,
selectedCompressionLevel,
isEncryptionEnabled,
selectedEncryptionMethod,
selectedEncryptionStrength,
splitZipSize!!
)
} else {
createZipFile(
archiveName,
password,
selectedCompressionMethod,
selectedCompressionLevel,
isEncryptionEnabled,
selectedEncryptionMethod,
selectedEncryptionStrength
)
}
}
builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}
encryptionMethodSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedEncryptionMethod = EncryptionMethod.valueOf(encryptionMethods[position])
passwordInput.isEnabled = selectedEncryptionMethod != EncryptionMethod.NONE
encryptionStrengthSpinner.isEnabled = selectedEncryptionMethod == EncryptionMethod.AES
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Do nothing
}
}
builder.show()
}
private fun convertToBytes(size: Long?, unit: String): Long? {
return size?.times(when (unit) {
"KB" -> 1024L
"MB" -> 1024L * 1024
"GB" -> 1024L * 1024 * 1024
else -> 1024L
})
}
private fun showPasswordInputDialog7z(onPasswordEntered: (String?, String, Int, Boolean, Int) -> Unit) {
val layoutInflater = LayoutInflater.from(requireContext())
val customView = layoutInflater.inflate(R.layout.seven_z_option_dialog, null)
val passwordEditText = customView.findViewById(R.id.passwordEditText)
val compressionSpinner = customView.findViewById(R.id.compressionSpinner)
val solidCheckBox = customView.findViewById(R.id.solidCheckBox)
val threadCountEditText = customView.findViewById(R.id.threadCountEditText)
val archiveNameEditText = customView.findViewById(R.id.archiveNameEditText)
val filesToArchive = tempFiles.ifEmpty { cachedFiles }
MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog)
.setView(customView)
.setPositiveButton(getString(R.string.ok)) { _, _ ->
val defaultName = if (filesToArchive.isNotEmpty()) {
filesToArchive[0].nameWithoutExtension
} else {
"archive"
}
val archiveName = archiveNameEditText.text.toString().ifBlank { defaultName }
val password = passwordEditText.text.toString()
val compressionLevel = when (compressionSpinner.selectedItemPosition) {
0 -> 0
1 -> 1
2 -> 3
3 -> 5
4 -> 7
5 -> 9
else -> -1
}
val solid = solidCheckBox.isChecked
val threadCount = threadCountEditText.text.toString().toIntOrNull() ?: -1
onPasswordEntered.invoke(
password.ifBlank { null },
archiveName,
compressionLevel,
solid,
threadCount
)
}
.setNegativeButton(getString(R.string.no_password)) { _, _ ->
val defaultName = if (filesToArchive.isNotEmpty()) {
filesToArchive[0].nameWithoutExtension
} else {
"archive"
}
val archiveName = archiveNameEditText.text.toString().ifBlank { defaultName }
val compressionLevel = when (compressionSpinner.selectedItemPosition) {
0 -> 0
1 -> 1
2 -> 3
3 -> 5
4 -> 7
5 -> 9
else -> -1
}
val solid = solidCheckBox.isChecked
val threadCount = threadCountEditText.text.toString().toIntOrNull() ?: -1
onPasswordEntered.invoke(
null,
archiveName,
compressionLevel,
solid,
threadCount
)
}
.show()
}
private fun showToast(message: String) {
CoroutineScope(Dispatchers.Main).launch {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
fun formatFileSize(bytes: Long): String {
val kilobyte = 1024
val megabyte = kilobyte * 1024
val gigabyte = megabyte * 1024
return when {
bytes < kilobyte -> "$bytes B"
bytes < megabyte -> String.format("%.2f KB", bytes.toFloat() / kilobyte)
bytes < gigabyte -> String.format("%.2f MB", bytes.toFloat() / megabyte)
else -> String.format("%.2f GB", bytes.toFloat() / gigabyte)
}
}
companion object {
private const val PREFS_NAME = "ZipPrefs"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/wirelessalien/zipxtract/activity/MainActivity.kt b/app/src/main/java/com/wirelessalien/zipxtract/activity/MainActivity.kt
index f9ae996..1e9ca55 100644
--- a/app/src/main/java/com/wirelessalien/zipxtract/activity/MainActivity.kt
+++ b/app/src/main/java/com/wirelessalien/zipxtract/activity/MainActivity.kt
@@ -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