Skip to content

Commit

Permalink
Fix folder creation bug when unzip
Browse files Browse the repository at this point in the history
  • Loading branch information
anggrayudi committed Jan 19, 2022
1 parent 421d75c commit 3a35d2f
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import android.view.View
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.callback.ZipCompressionCallback
import com.anggrayudi.storage.file.*
import com.anggrayudi.storage.file.MimeType
import com.anggrayudi.storage.file.compressToZip
import com.anggrayudi.storage.file.fullName
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.sample.R
import kotlinx.android.synthetic.main.activity_file_compression.*
import kotlinx.android.synthetic.main.view_file_picked.view.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.callback.ZipDecompressionCallback
import com.anggrayudi.storage.file.MimeType
import com.anggrayudi.storage.file.decompressZip
import com.anggrayudi.storage.file.fullName
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.sample.R
import kotlinx.android.synthetic.main.activity_file_decompression.*
import kotlinx.android.synthetic.main.view_file_picked.view.*
Expand All @@ -19,21 +21,28 @@ class FileDecompressionActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_file_compression)
setContentView(R.layout.activity_file_decompression)
setupSimpleStorage()
btnStartDecompress.setOnClickListener { startDecompress() }
}

private fun setupSimpleStorage() {
storageHelper.onFileSelected = { _, files ->
layoutDecompressFile_srcZip.tag = files.first()
val file = files.first()
layoutDecompressFile_srcZip.run {
tag = file
tvFilePath.text = file.fullName
}
}
layoutDecompressFile_srcZip.btnBrowse.setOnClickListener {
storageHelper.openFilePicker(filterMimeTypes = arrayOf(MimeType.ZIP))
}

storageHelper.onFolderSelected = { _, folder ->
layoutDecompressFile_destFolder.tag = folder
layoutDecompressFile_destFolder.run {
tag = folder
tvFilePath.text = folder.getAbsolutePath(context)
}
}
layoutDecompressFile_destFolder.btnBrowse.setOnClickListener {
storageHelper.openFolderPicker()
Expand All @@ -52,8 +61,20 @@ class FileDecompressionActivity : BaseActivity() {
return
}
ioScope.launch {
zipFile.decompressZip(applicationContext, targetFolder, object : ZipDecompressionCallback() {
zipFile.decompressZip(applicationContext, targetFolder, object : ZipDecompressionCallback(uiScope) {
override fun onCompleted(
zipFile: DocumentFile,
targetFolder: DocumentFile,
bytesDecompressed: Long,
totalFilesDecompressed: Int,
decompressionRate: Float
) {
Toast.makeText(applicationContext, "Decompressed $totalFilesDecompressed files from ${zipFile.name}", Toast.LENGTH_SHORT).show()
}

override fun onFailed(errorCode: ErrorCode) {
Toast.makeText(applicationContext, "$errorCode", Toast.LENGTH_SHORT).show()
}
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ abstract class ZipDecompressionCallback(var uiScope: CoroutineScope = GlobalScop
// default implementation
}

@UiThread
open fun onCalculateFinalFileSize() {
// default implementation
}

/**
* @param zipFile files to be decompressed
* @param workerThread Use [Thread.interrupt] to cancel the operation
Expand All @@ -36,13 +31,14 @@ abstract class ZipDecompressionCallback(var uiScope: CoroutineScope = GlobalScop
* Given `freeSpace` and `fileSize`, then you decide whether the process will be continued or not.
* You can give space tolerant here, e.g. 100MB
*
* @param fileSize actual size after decompressed
* @param freeSpace of target path
* @return `true` to continue process
*/
@WorkerThread
open fun onCheckFreeSpace(freeSpace: Long, fileSize: Long): Boolean {
return fileSize + 100 * FileSize.MB < freeSpace // Give tolerant 100MB
open fun onCheckFreeSpace(freeSpace: Long, zipFileSize: Long): Boolean {
// Give tolerant 100MB
// Estimate the final size of decompressed files is increased by 20%
return zipFileSize * 1.2 + 100 * FileSize.MB < freeSpace
}

@UiThread
Expand All @@ -54,7 +50,7 @@ abstract class ZipDecompressionCallback(var uiScope: CoroutineScope = GlobalScop
* @param decompressionRate size expansion in percent, e.g. 23.5
*/
@UiThread
open fun onCompleted(zipFile: DocumentFile, bytesCompressed: Long, totalFilesDecompressed: Int, decompressionRate: Float) {
open fun onCompleted(zipFile: DocumentFile, targetFolder: DocumentFile, bytesDecompressed: Long, totalFilesDecompressed: Int, decompressionRate: Float) {
// default implementation
}

Expand All @@ -63,7 +59,13 @@ abstract class ZipDecompressionCallback(var uiScope: CoroutineScope = GlobalScop
// default implementation
}

class Report(val progress: Float, val bytesDecompressed: Long, val writeSpeed: Int, val fileCount: Int)
/**
* Can't calculate write speed, progress and decompressed file size for the given period [onStart],
* because we can't get the final size of the decompressed files unless we unzip it first,
* so only `bytesDecompressed` and `fileCount` that can be provided.
* @param fileCount decompressed files in total
*/
class Report(val bytesDecompressed: Long, val writeSpeed: Int, val fileCount: Int)

enum class ErrorCode {
STORAGE_PERMISSION_DENIED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,41 +1189,29 @@ fun DocumentFile.decompressZip(
return
}

val zipSize = length()
if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetFolder.getStorageId(context)), zipSize)) {
callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}

val thread = Thread.currentThread()
val reportInterval = awaitUiResult(callback.uiScope) { callback.onStart(this, thread) }
if (reportInterval < 0) return

var success = false
var bytesDecompressed = 0L
var fileDecompressedCount = 0
var actualFilesSize = 0L
var timer: Job? = null
var zis: ZipInputStream? = null
var targetFile: DocumentFile? = null
try {
callback.uiScope.postToUi { callback.onCalculateFinalFileSize() }
ZipInputStream(openInputStream(context)).use {
var entry = it.nextEntry
while (entry != null) {
if (entry.size > 0) {
actualFilesSize += entry.size
}
entry = it.nextEntry
}

if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetFolder.getStorageId(context)), actualFilesSize)) {
callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
it.closeEntryQuietly()
return
}
}

val thread = Thread.currentThread()
val reportInterval = awaitUiResult(callback.uiScope) { callback.onStart(this, thread) }
if (reportInterval < 0) return

zis = ZipInputStream(openInputStream(context))
var writeSpeed = 0
// using timer on small file is useless. We set minimum 10MB.
if (reportInterval > 0 && actualFilesSize > 10 * FileSize.MB) {
if (reportInterval > 0 && zipSize > 10 * FileSize.MB) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
val report = ZipDecompressionCallback.Report(bytesDecompressed * 100f / actualFilesSize, bytesDecompressed, writeSpeed, fileDecompressedCount)
val report = ZipDecompressionCallback.Report(bytesDecompressed, writeSpeed, fileDecompressedCount)
callback.uiScope.postToUi { callback.onReport(report) }
writeSpeed = 0
}
Expand All @@ -1235,7 +1223,11 @@ fun DocumentFile.decompressZip(
if (entry.isDirectory) {
destFolder.makeFolder(context, entry.name, CreateMode.REUSE)
} else {
targetFile = destFolder.makeFile(context, entry.name)
val folder = entry.name.substringBeforeLast('/', "").let {
if (it.isEmpty()) destFolder else destFolder.makeFolder(context, it, CreateMode.REUSE)
} ?: throw IOException()
val fileName = entry.name.substringAfterLast('/')
targetFile = folder.makeFile(context, fileName)
if (targetFile == null) {
callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
canSuccess = false
Expand All @@ -1249,8 +1241,8 @@ fun DocumentFile.decompressZip(
writeSpeed += bytes
bytes = zis.read(buffer)
}
fileDecompressedCount++
} ?: throw IOException()
fileDecompressedCount++
}
entry = zis.nextEntry
}
Expand All @@ -1273,9 +1265,9 @@ fun DocumentFile.decompressZip(
zis.closeStreamQuietly()
}
if (success) {
val zipSize = length()
val sizeExpansion = (actualFilesSize - zipSize).toFloat() / zipSize * 100
callback.uiScope.postToUi { callback.onCompleted(this, actualFilesSize, fileDecompressedCount, sizeExpansion) }
// Sometimes, the decompressed size is smaller than the compressed size, and you may get negative values. You should worry about this.
val sizeExpansion = (bytesDecompressed - zipSize).toFloat() / zipSize * 100
callback.uiScope.postToUi { callback.onCompleted(this, destFolder, bytesDecompressed, fileDecompressedCount, sizeExpansion) }
} else {
targetFile?.delete()
}
Expand Down

0 comments on commit 3a35d2f

Please sign in to comment.