Unzipp file with Zip4j library

 Java library has a built-in class java.util.zip.ZipFile and java.util.zip.ZipInputSteam. However, those class only can unzip files without a password.  For those zip files with password protected, we use 3rd party library Zip4j

I have created a password protected zip file for testing. Structure like this. The name with ‘/’ is a directory. All files are photo in jpg format.

Photo2/

20190314_103435_HDR.jpg

20190314_112925_HDR.jpg

20190314_103702_HDR.jpg

primary/

20190316_121528_HDR.jpg

secondary1/

20190422_155924_HDR.jpg

seconday2/

20190314_103252_HDR.jpg 


Test zip file download link: https://drive.google.com/uc?export=download&id=1XDMUGuvOPkTR2TH_Ikb5-2SIR7nXQ6yk

Unzip password: aB3d


Add dependency in the module gradle.


implementation 'net.lingala.zip4j:zip4j:2.11.5'


I have created a new module for this practice and copied the code used in the previous practice download file with Okhttp. Three buttons inside LinearLayout are added below the download progress bar. I will show 3 methods to unzip with ZipFile and ZipInputStream from the library Zip4j. A  progress bar and TextView are added under the 3 buttons to show the unzip progress.



<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    
    <Button
        android:id="@+id/downloadButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:text="download" />
    
    <LinearLayout
        android:id="@+id/progressLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/downloadButton">

        <ProgressBar
            android:id="@+id/downloadProgressBar"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>

        <TextView
            android:id="@+id/downloadProgressText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0 %" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/buttonArrayLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/progressLayout">


        <Button
            android:id="@+id/unzip1Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="unzip1" />

        <Button
            android:id="@+id/unzip2Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="unzip2" />

        <Button
            android:id="@+id/unzip3Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="unzip3" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/unZipProgressLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/buttonArrayLayout">

        <ProgressBar
            android:id="@+id/unzipProgressBar"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>

        <TextView
            android:id="@+id/unzipProgressText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0 %" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

Then add the onClickListener to the 3 buttons in the onCreate() of the MainActivity.
val unzip1Button = findViewById(R.id.unzip1Button) as Button
unzip1Button.setOnClickListener{
    Log.d(TAG, "unzip1 " + savePath.absolutePath)
    unzip1(savePath)
}

val unzip2Button = findViewById(R.id.unzip2Button) as Button
unzip2Button.setOnClickListener{
    Log.d(TAG, "unzip2 " + savePath.absolutePath)
    unzip2(savePath)
}

val unzip3Button = findViewById(R.id.unzip3Button) as Button
unzip3Button.setOnClickListener {
    Log.d(TAG, "unzip3 " + savePath.absolutePath)
    unzip3(savePath)
}

Extract All files

The first method is the simplest way. The File object of the zipped file is used to create the ZipFIle instance. Then we set the password required for unzip. We just unzip all files(including the directories) into a folder by invoking ZipFile.extractAll(String) with the path to the destination folder. Since unzip may require heavy workload, execute the unzip workload inside a coroutine to avoid blocking the main thread. We cannot monitor the unzip progress by this method.

fun unzip1(file: File){
    val unzipedFolder = File(applicationContext.cacheDir, "folder1")
    if(!unzipedFolder.exists())
        unzipedFolder.mkdir()

    val zipFile = ZipFile(file) //ZipFile class from Zip4j library
    zipFile.setPassword(unzipPassword.toCharArray()) //unzip password
    networkRequestScope.launch {
        zipFile.extractAll(unzipedFolder.absolutePath)

    }

}

Looking at the Device Explorer of Android Studio. All files are extracted into the directory “folder1” with the same directory hierarchy of the zip file.
Get uncompressed size of files before extracting
If we want to ensure the device has enough free space for all files after unzip, we need to get the uncompressed size of all files before extracting. The 2nd method will sum up the uncompressed size of each entry iteratively. We want to show the progress during unzipping so we extract the files one by one by invoking ZipFile.extract(FileHeaders, String). The progress bar and text is updated after extracting 1 file. Noted that the 2nd parameter of ZipFile.extract(FileHeaders, String) is the path of the parent directory (in this case. /data/user/0/com.example.unzipfile/cache/folder2). The sub-directories storing the files will be created automatically by ZipFile.extract() method.

fun unzip2(file: File) {
        val unzipedFolder = File(applicationContext.cacheDir, "folder2")
        if (!unzipedFolder.exists())
            unzipedFolder.mkdir()

        val zipFile = ZipFile(file) //this ZipFile from Zip4j library
        zipFile.setPassword(unzipPassword.toCharArray())
        val fileHeaders = zipFile.fileHeaders
        var uncompressedSize = 0L


        networkRequestScope.launch {
            fileHeaders.forEach {
                uncompressedSize += it.uncompressedSize // can use to check if the device has enough free space
            }

            val totalSize = uncompressedSize
            uncompressedSize = 0L

            fileHeaders.forEach {
                Log.d(TAG, "Entry name: ${it.fileName} size: ${it.uncompressedSize}")

                if(!it.isDirectory){
                    zipFile.extractFile(it, unzipedFolder.absolutePath)
                    uncompressedSize += it.uncompressedSize
                    val percentage = (((uncompressedSize * 100) / totalSize))
                    runOnUiThread{
                        unzipProgressBar.progress = percentage.toInt()
                        unzipProgressText.text = "${percentage.toInt()}%"
                }
            }
        }
    }
}

The Logcat output. You can find the size of directories are 0.
All files are extracted into the directory “folder2” with the same directory hierarchy of the zip file.

Monitor unzip progress of each file

The 3nd method is to monitor the extracting progress of a single file. If a big file is zipped, we may also want to know the  progress to extracting it. 

One way is to use ZipFile.getInputStream(FileHeader) to extract a file, instead of ZipFile.extract(FileHeaders, String) . Then we count the number of bytes read by the method InputStream.read(byte[]).  The file size can be obtained by FileHeader.uncompressedSize in the 2nd method. I put the code into the block comment /**/


Here I use another way: ZipInputStream (from Zip4j library) to extract files, instead of ZipFile.

First, I have a while loop to calculate the total uncompressed size of all files. Then actually extraction is done in the 2nd while loop. Each entry is checked if it is a directory. If it is a directory, then a directory of the same name is created in the destination folder. If not, we use InputStream and OutputStream to transfer the data. After each read/write transaction, we could update the extraction progress. The file uncompressed size is obtained from the Entry(LocalFileHeader).
fun unzip3(file: File){
    val buffer = ByteArray(READ_BUFFER_SIZE)
    var byteRead = 0
    var fileByteRead = 0L

    val unzipedFolder = File(applicationContext.cacheDir, "folder3")
    if(!unzipedFolder.exists())
        unzipedFolder.mkdir()

    networkRequestScope.launch {
        //first calculate the total uncompressed size of all files
        var uncompressedSize = 0L
        val zipIS = ZipInputStream(FileInputStream(file), unzipPassword.toCharArray())
        while(true){
            val entry = zipIS.nextEntry
            if (entry == null)
                break
            uncompressedSize += entry.uncompressedSize
        }
        val totalSize = uncompressedSize
        uncompressedSize = 0L
        Log.d(TAG, "total size: ${totalSize}")


        val zipInputStream = ZipInputStream(FileInputStream(file), unzipPassword.toCharArray())
        while (true) {
            val entry = zipInputStream.nextEntry
            if (entry == null)
                break
            val temp = File(unzipedFolder, entry.fileName)
            Log.d(TAG, "unzip3 entry: ${entry.fileName}")
            if (entry.isDirectory) {
                temp.mkdir()
            } else {
                val fileSize = entry.uncompressedSize
                fileByteRead = 0L
                val bos = BufferedOutputStream(FileOutputStream(temp))
                while (true) {
                    byteRead = zipInputStream.read(buffer)
                    if (byteRead == -1)
                        break
                    bos.write(buffer, 0, byteRead)
                    fileByteRead += byteRead
                    Log.d(TAG, "${entry.fileName} ${fileByteRead} of ${fileSize} has been read")
                }
                bos.flush()
                bos.close()

                uncompressedSize += fileSize
                val percentage = (((uncompressedSize * 100) / totalSize))
                runOnUiThread{
                    unzipProgressBar.progress = percentage.toInt()
                    unzipProgressText.text = "${percentage.toInt()}%"
                }


            }
        }
    }
}

All files are unzipped into folder3


The completed code can be found in Github



Reference

Zip4j library https://github.com/srikanth-lingala/zip4j

Comments