Skip to content

Commit f3abaec

Browse files
add single file extract
1 parent 2346de1 commit f3abaec

3 files changed

Lines changed: 214 additions & 28 deletions

File tree

app/src/main/java/com/wirelessalien/zipxtract/constant/ServiceConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ object ServiceConstants {
3737
const val EXTRA_ITEMS_TO_ADD_JOB_ID = "com.wirelessalien.zipxtract.EXTRA_ITEMS_TO_ADD_JOB_ID"
3838
const val EXTRA_ITEMS_TO_REMOVE_JOB_ID = "com.wirelessalien.zipxtract.EXTRA_ITEMS_TO_REMOVE_JOB_ID"
3939
const val EXTRA_ITEMS_TO_ADD_NAMES = "itemsToAddNames"
40+
const val EXTRA_ITEMS_TO_EXTRACT = "items_to_extract"
4041
}

app/src/main/java/com/wirelessalien/zipxtract/fragment/SevenZipFragment.kt

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
4141
import com.google.android.material.chip.Chip
4242
import com.google.android.material.dialog.MaterialAlertDialogBuilder
4343
import com.wirelessalien.zipxtract.R
44+
import androidx.core.content.ContextCompat
45+
import androidx.core.view.WindowCompat
46+
import androidx.preference.PreferenceManager
47+
import com.google.android.material.bottomsheet.BottomSheetDialog
4448
import com.wirelessalien.zipxtract.adapter.ArchiveItemAdapter
49+
import com.wirelessalien.zipxtract.constant.BroadcastConstants
4550
import com.wirelessalien.zipxtract.constant.ServiceConstants
51+
import com.wirelessalien.zipxtract.databinding.BottomSheetOptionBinding
4652
import com.wirelessalien.zipxtract.databinding.FragmentSevenZipBinding
53+
import com.wirelessalien.zipxtract.databinding.PasswordInputDialogBinding
4754
import com.wirelessalien.zipxtract.helper.AppEvent
4855
import com.wirelessalien.zipxtract.helper.EventBus
4956
import com.wirelessalien.zipxtract.helper.FileOperationsDao
57+
import com.wirelessalien.zipxtract.helper.PathUtils
58+
import com.wirelessalien.zipxtract.service.ExtractArchiveService
5059
import com.wirelessalien.zipxtract.service.Update7zService
5160
import kotlinx.coroutines.launch
5261
import net.sf.sevenzipjbinding.IInArchive
@@ -65,7 +74,8 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
6574
val path: String,
6675
val isDirectory: Boolean,
6776
val size: Long,
68-
val lastModified: Date?
77+
val lastModified: Date?,
78+
val isEncrypted: Boolean
6979
)
7080

7181
private lateinit var binding: FragmentSevenZipBinding
@@ -241,15 +251,16 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
241251
val dirName = relativePath.substring(0, separatorIndex)
242252
val dirPath = if (currentPath.isEmpty()) dirName else "$currentPath/$dirName"
243253
if (!children.containsKey(dirPath)) {
244-
children[dirPath] = ArchiveItem(dirPath, true, 0, null)
254+
children[dirPath] = ArchiveItem(dirPath, true, 0, null, false)
245255
}
246256
} else {
247257
// It's a direct child file or an empty directory entry
248258
val isDirectory = it.getProperty(i, PropID.IS_FOLDER) as? Boolean ?: false
249259
val size = it.getProperty(i, PropID.SIZE) as? Long ?: 0L
250260
val lastModified = it.getProperty(i, PropID.LAST_MODIFICATION_TIME) as? Date
261+
val isEncrypted = it.getProperty(i, PropID.ENCRYPTED) as? Boolean ?: false
251262
if (!children.containsKey(itemPath)) {
252-
children[itemPath] = ArchiveItem(itemPath, isDirectory, size, lastModified)
263+
children[itemPath] = ArchiveItem(itemPath, isDirectory, size, lastModified, isEncrypted)
253264
}
254265
}
255266
}
@@ -270,7 +281,7 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
270281
if (item.isDirectory) {
271282
loadArchiveItems(item.path)
272283
} else {
273-
// will add a confirmation dialog to remove the file
284+
showBottomSheetOptions(item)
274285
}
275286
}
276287
}
@@ -354,6 +365,141 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
354365
requireContext().startService(intent)
355366
}
356367

368+
private fun showBottomSheetOptions(item: ArchiveItem) {
369+
val binding = BottomSheetOptionBinding.inflate(layoutInflater)
370+
val bottomSheetDialog = BottomSheetDialog(requireContext())
371+
bottomSheetDialog.window?.let { window ->
372+
WindowCompat.setDecorFitsSystemWindows(window, false)
373+
window.statusBarColor = android.graphics.Color.TRANSPARENT
374+
window.navigationBarColor = android.graphics.Color.TRANSPARENT
375+
}
376+
bottomSheetDialog.setContentView(binding.root)
377+
378+
binding.fileName.text = item.path.substringAfterLast('/')
379+
val fileSizeText = bytesToString(item.size)
380+
binding.fileSize.text = fileSizeText
381+
382+
val dateFormat = java.text.DateFormat.getDateTimeInstance(
383+
java.text.DateFormat.DEFAULT,
384+
java.text.DateFormat.SHORT,
385+
java.util.Locale.getDefault()
386+
)
387+
binding.fileDate.text = item.lastModified?.let { dateFormat.format(it) } ?: ""
388+
389+
val extension = item.path.substringAfterLast('.', "")
390+
binding.fileExtension.text = if (extension.isNotEmpty()) {
391+
if (extension.length > 4) {
392+
"FILE"
393+
} else {
394+
if (extension.length == 4) {
395+
binding.fileExtension.textSize = 16f
396+
} else {
397+
binding.fileExtension.textSize = 18f
398+
}
399+
extension.uppercase(java.util.Locale.getDefault())
400+
}
401+
} else {
402+
"..."
403+
}
404+
405+
binding.btnPreviewArchive.visibility = View.GONE
406+
binding.btnShare.visibility = View.GONE
407+
binding.btnOpenWith.visibility = View.GONE
408+
binding.btnFileInfo.visibility = View.GONE
409+
binding.btnDelete.visibility = View.GONE
410+
binding.lowStorageWarning.visibility = View.GONE
411+
412+
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
413+
val extractPath = sharedPreferences.getString(BroadcastConstants.PREFERENCE_EXTRACT_DIR_PATH, null)
414+
val defaultPath = if (!extractPath.isNullOrEmpty()) {
415+
if (File(extractPath).isAbsolute) {
416+
extractPath
417+
} else {
418+
File(android.os.Environment.getExternalStorageDirectory(), extractPath).absolutePath
419+
}
420+
} else {
421+
File(archivePath ?: android.os.Environment.getExternalStorageDirectory().absolutePath).parent ?: android.os.Environment.getExternalStorageDirectory().absolutePath
422+
}
423+
424+
binding.outputPathInput.setText(defaultPath)
425+
binding.outputPathDisplay.text = PathUtils.formatPath(defaultPath, requireContext())
426+
427+
binding.outputPathLayout.setEndIconOnClickListener {
428+
val pathPicker = PathPickerFragment.newInstance()
429+
pathPicker.setPathPickerListener(object : PathPickerFragment.PathPickerListener {
430+
override fun onPathSelected(path: String) {
431+
binding.outputPathInput.setText(path)
432+
binding.outputPathDisplay.text = PathUtils.formatPath(path, requireContext())
433+
}
434+
})
435+
pathPicker.show(parentFragmentManager, "path_picker")
436+
}
437+
438+
binding.outputPathInput.addTextChangedListener(object : android.text.TextWatcher {
439+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
440+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
441+
override fun afterTextChanged(s: android.text.Editable?) {
442+
val path = s.toString()
443+
binding.outputPathDisplay.text = PathUtils.formatPath(path, requireContext())
444+
}
445+
})
446+
447+
binding.btnExtract.setOnClickListener {
448+
val destinationPath = binding.outputPathInput.text.toString()
449+
if (item.isEncrypted) {
450+
showPasswordInputDialog(item, destinationPath)
451+
} else {
452+
startExtractionService(item, null, destinationPath)
453+
}
454+
bottomSheetDialog.dismiss()
455+
}
456+
457+
bottomSheetDialog.show()
458+
}
459+
460+
private fun showPasswordInputDialog(item: ArchiveItem, destinationPath: String) {
461+
val binding = PasswordInputDialogBinding.inflate(layoutInflater)
462+
463+
MaterialAlertDialogBuilder(requireContext(), R.style.MaterialDialog)
464+
.setTitle(getString(R.string.enter_password))
465+
.setView(binding.root)
466+
.setPositiveButton(getString(R.string.ok)) { _, _ ->
467+
val password = binding.passwordInput.text.toString()
468+
startExtractionService(item, password.ifBlank { null }, destinationPath)
469+
}
470+
.setNegativeButton(getString(R.string.no_password)) { _, _ ->
471+
startExtractionService(item, null, destinationPath)
472+
}
473+
.show()
474+
}
475+
476+
private fun startExtractionService(item: ArchiveItem, password: String?, destinationPath: String) {
477+
val jobId = fileOperationsDao.addFilesForJob(listOf(archivePath!!))
478+
val itemsToExtract = ArrayList<String>()
479+
itemsToExtract.add(item.path)
480+
481+
val intent = Intent(requireContext(), ExtractArchiveService::class.java).apply {
482+
putExtra(ServiceConstants.EXTRA_JOB_ID, jobId)
483+
putExtra(ServiceConstants.EXTRA_PASSWORD, password)
484+
putExtra(ServiceConstants.EXTRA_DESTINATION_PATH, destinationPath)
485+
putStringArrayListExtra(ServiceConstants.EXTRA_ITEMS_TO_EXTRACT, itemsToExtract)
486+
}
487+
ContextCompat.startForegroundService(requireContext(), intent)
488+
}
489+
490+
private fun bytesToString(bytes: Long): String {
491+
val kilobyte = 1024
492+
val megabyte = kilobyte * 1024
493+
val gigabyte = megabyte * 1024
494+
495+
return when {
496+
bytes < kilobyte -> "$bytes B"
497+
bytes < megabyte -> String.format(java.util.Locale.US, "%.2f KB", bytes.toFloat() / kilobyte)
498+
bytes < megabyte -> String.format(java.util.Locale.US, "%.2f MB", bytes.toFloat() / megabyte)
499+
else -> String.format(java.util.Locale.US, "%.2f GB", bytes.toFloat() / gigabyte)
500+
}
501+
}
502+
357503
private fun handleBackNavigation() {
358504
if (currentPath.isNotEmpty()) {
359505
currentPath = currentPath.substringBeforeLast('/', "")

app/src/main/java/com/wirelessalien/zipxtract/service/ExtractArchiveService.kt

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class ExtractArchiveService : Service() {
122122
val password = intent?.getStringExtra(ServiceConstants.EXTRA_PASSWORD)
123123
val useAppNameDir = intent?.getBooleanExtra(ServiceConstants.EXTRA_USE_APP_NAME_DIR, false) ?: false
124124
val destinationPath = intent?.getStringExtra(ServiceConstants.EXTRA_DESTINATION_PATH)
125+
val itemsToExtract = intent?.getStringArrayListExtra(ServiceConstants.EXTRA_ITEMS_TO_EXTRACT)
125126

126127
if (jobId == null) {
127128
stopSelf()
@@ -142,7 +143,7 @@ class ExtractArchiveService : Service() {
142143
}
143144
val filePath = filesToExtract[0]
144145

145-
extractArchive(filePath, password, useAppNameDir, destinationPath)
146+
extractArchive(filePath, password, useAppNameDir, destinationPath, itemsToExtract)
146147
fileOperationsDao.deleteFilesForJob(jobId)
147148
stopSelf()
148149
}
@@ -187,7 +188,7 @@ class ExtractArchiveService : Service() {
187188
return builder.build()
188189
}
189190

190-
private fun extractArchive(filePath: String, password: String?, useAppNameDir: Boolean, destinationPath: String?) {
191+
private fun extractArchive(filePath: String, password: String?, useAppNameDir: Boolean, destinationPath: String?, itemsToExtract: ArrayList<String>?) {
191192
if (filePath.isEmpty()) {
192193
val errorMessage = getString(R.string.no_files_to_archive)
193194
showErrorNotification(errorMessage)
@@ -199,7 +200,7 @@ class ExtractArchiveService : Service() {
199200
val file = File(filePath)
200201
when {
201202
file.extension.equals("zip", ignoreCase = true) -> {
202-
extractZipArchive(file, password, useAppNameDir, destinationPath)
203+
extractZipArchive(file, password, useAppNameDir, destinationPath, itemsToExtract)
203204
return
204205
}
205206
file.extension.equals("tar", ignoreCase = true) -> {
@@ -250,7 +251,7 @@ class ExtractArchiveService : Service() {
250251
counter++
251252
}
252253

253-
var success = trySevenZip(file, destinationDir, password)
254+
var success = trySevenZip(file, destinationDir, password, itemsToExtract)
254255
if (success) {
255256
if (useAppNameDir) {
256257
filesDir.deleteRecursively()
@@ -290,7 +291,7 @@ class ExtractArchiveService : Service() {
290291
}
291292
}
292293

293-
private fun trySevenZip(file: File, destinationDir: File, password: String?): Boolean {
294+
private fun trySevenZip(file: File, destinationDir: File, password: String?, itemsToExtract: ArrayList<String>?): Boolean {
294295
var inStream: RandomAccessFileInStream? = null
295296
try {
296297
inStream = RandomAccessFileInStream(RandomAccessFile(file, "r"))
@@ -299,7 +300,20 @@ class ExtractArchiveService : Service() {
299300

300301
try {
301302
val extractCallback = ExtractCallback(inArchive, destinationDir, password)
302-
inArchive.extract(null, false, extractCallback)
303+
304+
if (itemsToExtract != null && itemsToExtract.isNotEmpty()) {
305+
val indices = mutableListOf<Int>()
306+
val count = inArchive.numberOfItems
307+
for (i in 0 until count) {
308+
val path = inArchive.getStringProperty(i, PropID.PATH).replace("\\", "/")
309+
if (itemsToExtract.contains(path)) {
310+
indices.add(i)
311+
}
312+
}
313+
inArchive.extract(indices.toIntArray(), false, extractCallback)
314+
} else {
315+
inArchive.extract(null, false, extractCallback)
316+
}
303317

304318
if (extractCallback.hasError) {
305319
return false
@@ -668,7 +682,7 @@ class ExtractArchiveService : Service() {
668682
}
669683
}
670684

671-
private fun extractZipArchive(file: File, password: String?, useAppNameDir: Boolean, destinationPath: String?) {
685+
private fun extractZipArchive(file: File, password: String?, useAppNameDir: Boolean, destinationPath: String?, itemsToExtract: ArrayList<String>?) {
672686
var destinationDir: File? = null
673687
try {
674688
val zipFile = ZipFile(file)
@@ -723,26 +737,51 @@ class ExtractArchiveService : Service() {
723737

724738
zipFile.isRunInThread = true
725739
val directories = mutableListOf<DirectoryInfo>()
726-
for (fileHeader in zipFile.fileHeaders) {
727-
if (fileHeader.isDirectory) {
728-
val directoryPath = File(finalDestinationDir, fileHeader.fileName).path
729-
val lastModified = if (fileHeader.lastModifiedTime > 0) fileHeader.lastModifiedTimeEpoch else System.currentTimeMillis()
730-
directories.add(DirectoryInfo(directoryPath, lastModified))
731-
}
732-
}
733-
zipFile.extractAll(finalDestinationDir.absolutePath)
734740

735-
progressMonitor = zipFile.progressMonitor
736-
var lastProgress = -1
737-
while (!progressMonitor!!.state.equals(ProgressMonitor.State.READY)) {
738-
if (progressMonitor!!.state.equals(ProgressMonitor.State.BUSY)) {
739-
val percentDone = (progressMonitor!!.percentDone)
740-
if (percentDone > lastProgress) {
741-
lastProgress = percentDone
742-
updateProgress(percentDone)
741+
if (itemsToExtract != null && itemsToExtract.isNotEmpty()) {
742+
for (itemPath in itemsToExtract) {
743+
zipFile.extractFile(itemPath, finalDestinationDir.absolutePath)
744+
745+
progressMonitor = zipFile.progressMonitor
746+
var lastProgress = -1
747+
while (!progressMonitor!!.state.equals(ProgressMonitor.State.READY)) {
748+
if (progressMonitor!!.state.equals(ProgressMonitor.State.BUSY)) {
749+
val percentDone = (progressMonitor!!.percentDone)
750+
if (percentDone > lastProgress) {
751+
lastProgress = percentDone
752+
updateProgress(percentDone)
753+
}
754+
}
755+
Thread.sleep(100)
756+
}
757+
if (progressMonitor!!.result == ProgressMonitor.Result.CANCELLED) {
758+
break
759+
} else if (progressMonitor!!.result == ProgressMonitor.Result.ERROR) {
760+
throw progressMonitor!!.exception
761+
}
762+
}
763+
} else {
764+
for (fileHeader in zipFile.fileHeaders) {
765+
if (fileHeader.isDirectory) {
766+
val directoryPath = File(finalDestinationDir, fileHeader.fileName).path
767+
val lastModified = if (fileHeader.lastModifiedTime > 0) fileHeader.lastModifiedTimeEpoch else System.currentTimeMillis()
768+
directories.add(DirectoryInfo(directoryPath, lastModified))
769+
}
770+
}
771+
zipFile.extractAll(finalDestinationDir.absolutePath)
772+
773+
progressMonitor = zipFile.progressMonitor
774+
var lastProgress = -1
775+
while (!progressMonitor!!.state.equals(ProgressMonitor.State.READY)) {
776+
if (progressMonitor!!.state.equals(ProgressMonitor.State.BUSY)) {
777+
val percentDone = (progressMonitor!!.percentDone)
778+
if (percentDone > lastProgress) {
779+
lastProgress = percentDone
780+
updateProgress(percentDone)
781+
}
743782
}
783+
Thread.sleep(100)
744784
}
745-
Thread.sleep(100)
746785
}
747786

748787
if (progressMonitor!!.result == ProgressMonitor.Result.CANCELLED) {

0 commit comments

Comments
 (0)