From 3a35d2f6edc45f31091738f729da634c7c5337c3 Mon Sep 17 00:00:00 2001 From: Anggrayudi H Date: Thu, 20 Jan 2022 01:34:20 +0700 Subject: [PATCH] Fix folder creation bug when unzip --- .../activity/FileCompressionActivity.kt | 5 +- .../activity/FileDecompressionActivity.kt | 29 +++++++++-- .../callback/ZipDecompressionCallback.kt | 22 ++++---- .../storage/file/DocumentFileExt.kt | 50 ++++++++----------- 4 files changed, 62 insertions(+), 44 deletions(-) diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt b/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt index e8a6f6d..8cbe0ec 100644 --- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt +++ b/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt @@ -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.* diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt b/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt index 6e83c71..46caf8b 100644 --- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt +++ b/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt @@ -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.* @@ -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() @@ -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() + } }) } } diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt b/storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt index dd14ee3..aab313b 100644 --- a/storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt +++ b/storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt @@ -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 @@ -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 @@ -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 } @@ -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, diff --git a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt b/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt index 9912f32..3188fa4 100644 --- a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt +++ b/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt @@ -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 } @@ -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 @@ -1249,8 +1241,8 @@ fun DocumentFile.decompressZip( writeSpeed += bytes bytes = zis.read(buffer) } - fileDecompressedCount++ } ?: throw IOException() + fileDecompressedCount++ } entry = zis.nextEntry } @@ -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() }