Search code examples
androidandroid-intentzipunzipstorage-access-framework

How do you unzip a file with Storage Volume (/StorageAccessFramework)?


I'm using this code (Android 7.0/Nougat) to unpack a zip file in external storage (including multiple folder levels inside):

try {
    ZipFile zip = new ZipFile(zippath);
    Enumeration enu = zip.entries();

    while(enu.hasMoreElements()) {
        ZipEntry zipEntry = (ZipEntry) enu.nextElement();
        BufferedInputStream bis = null;
        String fileName = null;

        try {
            fileName = zipEntry.getName();
            fileName = fileName.replace("\\",File.separator).replace("/",File.separator);
            int p = fileName.lastIndexOf(File.separator);

            if(p>=0) {
                File fd=new File(folderpath+File.separator+fileName.substring(0,p));
                fd.mkdirs();
            }

            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(folderpath+File.separator+fileName));
            bis = new BufferedInputStream(zip.getInputStream(zipEntry));
            byte[] buffer = new byte[10000];
            int len = 0;

            while ((len = bis.read(buffer, 0, 10000)) > 0) {
                bos.write(buffer, 0, len);
            }

            bis.close();
            bos.close();
        } catch (IOException e1) {
            e1.printStackTrace();
            return;
        }
    }
} catch (IOException e2) {
    e2.printStackTrace();
}

To get write access to the SD card I'm using createAccessIntent (Storage Volume) which uses DocumentFiles instead of the normal File.

I already did this to get a ZipInputStream:

InputStream inputStream = this.getContentResolver().openInputStream(myDocumentFileZip.getUri());
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream);
ZipEntry zipEntry;

...and I'm guessing that you continue like this:

while ((zipEntry = zipInputStream.getNextEntry()) != null) {

But what do you do from there - how to you copy the files onto the SD card and still keep the folder structure like in the code above but using what Storage Volume (or Storage Access Framework) provides?


Solution

  • Unzip with Storage Volume:

    Careful: This way it creates copies if you unzip the same .zip file multiple times, while my original code in the first post (which you can't use for an SD card) doesn't but instead overwrites automatically!

    try {
        InputStream is = getContentResolver().openInputStream(myZip.getUri());
        BufferedInputStream bis = new BufferedInputStream(is);
        ZipInputStream zis = new ZipInputStream(bis);
        ZipEntry zipEntry;
    
        while ((zipEntry = zis.getNextEntry()) != null) {
            String fileName = null; 
    
            try {
                fileName = zipEntry.getName();        
                fileName = fileName.replace("\\",File.separator).replace("/",File.separator);
                int p=fileName.lastIndexOf(File.separator);        
                DocumentFile destFolder = myDestFolder; //DocumentFile of the destination folder
                String destName = fileName;
    
                if (p>=0) {
                    String[] split = fileName.split(File.separator);
    
                    //If the .zip file contains multiple folder levels, this is where you  
                    //have to check and then create them, e.g. for 3 levels:
                    if(split.length==1) {
                        destFolder = myFolder;
                        destName = filename;
                    } else if(split.length==2) {
                        if(mySubFolder==null) {
                            mySubFolder = myFolder.createDirectory(split[0]);
                        }
    
                        destFolder = mySubFolder;
                        destName = split[1];
                    } else if(split.length==3) {
                        if(mySubFolder==null) {
                            mySubFolder = myFolder.createDirectory(split[0]);
                        }
                        if(mySubSubFolder==null) {
                            mySubSubFolder = mySubFolder.createDirectory(split[1]);
                        }
    
                        destFolder = mySubSubFolder;
                        destName = split[2];
                    }
                }
    
                DocumentFile df = null;
    
                //Now you have to tell it what file extensions ("MIME" type) you want to use, e.g.:
                if(destName.endsWith(".txt")) {
                    df = destFolder.createFile("text/plain",destName.substring(0,destName.length()-4));
                } else if(destName.endsWith(".jpg")) {
                    df = destFolder.createFile("image/jpeg",destName.substring(0,destName.length()-4));
                }
    
                OutputStream out = getContentResolver().openOutputStream(df.getUri());
                BufferedOutputStream bos = new BufferedOutputStream(out);
                long zipfilesize = zipEntry.getSize();
    
                byte[] buffer = new byte[10000];
                int len = 0;
                int totlen = 0;
    
                while (((len = zis.read(buffer, 0, 10000)) > 0) && (totlen < zipfilesize)) {
                    bos.write(buffer, 0, len);
                    totlen += len;
                }
    
                bos.close();
            } catch (IOException e1) {
                e1.printStackTrace();
                return;
            }
        }
    
        is.close();
        bis.close();
        zis.close();
    } catch (IOException e2) {
        e2.printStackTrace();
    }
    

    Edit: Important: java.util.zip doesn't set the size or compressedSize (will return "-1"), that's why this code will only create files with a size of 0B with zip files that were created by the library - zip files that were created by hand (e.g. with WinRar) work fine. To fix it, replace

    while (((len = zis.read(buffer, 0, 10000)) > 0) && (totlen < zipfilesize)) {
    

    with

    while (((len = zis.read(buffer, 0, 10000)) > 0)) {
    

    It's possible to do this because:

    the call to ZipInputStream.getNextEntry() positions the InputStream at the start of the entry and therefore supplying the ZipInputStream is the equivalent of supplying a ZipEntry's InputStream.

    Source: https://stackoverflow.com/a/3233600/2016165

    The disadvantages with this (in comparison to my non-StorageVolume version) are that you a) can't get the total amount of files in the zip and b) also can't get the (total) size of the files, which means that you can't set the progress bar in an "Unzipping..." dialog unless you iterate through all the zip entries first to count them.