Android SharedPreferences Tutorial and Example

Compressing and decompressing files with ZIP on Android

Android apps regularly produce files that need to move as one unit: exported projects, offline help packs, diagnostic logs, or user-selected documents. The platform’s standard library can create and read ZIP archives without adding a large dependency, which is useful when an app must support modest hardware and several Android versions.

A ZIP file is a container, not an encryption mechanism. Each entry has a name, optional metadata, and compressed bytes, so the main design choices are which Java ZIP API to use, where an archive can legally live on current Android, and how malformed or oversized content is handled. These Kotlin patterns cover internal files, document-provider URIs, and safe restoration.

Creating a ZIP archive in Kotlin

Use ZipOutputStream when the app assembles an archive from files or directories. A buffered output stream reduces small writes, while walkTopDown() makes it easy to include an entire tree. Give every entry a relative path because an absolute source path can expose device-specific folders and break when the archive is opened elsewhere.

private fun createZip(sourceDir: File, destination: File) {
    val sources = sourceDir.walkTopDown().toList()
    destination.parentFile?.mkdirs()

    ZipOutputStream(
        BufferedOutputStream(FileOutputStream(destination))
    ).use { zip ->
        sources.forEach { source ->
            val name = source.relativeTo(sourceDir)
                .invariantSeparatorsPath

            if (source.isDirectory) {
                if (name.isNotEmpty()) {
                    zip.putNextEntry(ZipEntry("$name/"))
                    zip.closeEntry()
                }
            } else {
                zip.putNextEntry(ZipEntry(name))
                source.inputStream().use { input ->
                    input.copyTo(zip)
                }
                zip.closeEntry()
            }
        }
    }
}

putNextEntry() starts one member and closeEntry() finishes it. Directory entries are optional for many readers, but including them preserves empty folders in tools such as Windows Explorer. Kotlin’s use closes the stream when an exception occurs. Keep the destination outside the source tree and validate any untrusted entry names before writing them.

Extracting files without path traversal

Reading an archive requires more than calling copyTo(). A malicious entry can contain ../ segments and direct extraction outside the chosen folder. Resolve the final file with canonicalFile, compare it with the canonical extraction root, and reject anything that is not a child of that root.

val root = outputDir.canonicalFile
val target = File(root, entry.name).canonicalFile

if (!target.path.startsWith(root.path + File.separator)) {
    throw SecurityException("Unsafe archive entry")
}

The destination should also be written separately so a large member is not loaded into memory at once. Add counters for entry count, individual file size, and total expanded size; otherwise a tiny archive can expand into gigabytes and exhaust storage. Delete partial output when a limit is exceeded or an I/O error interrupts the operation.

ZipInputStream processes members sequentially, which suits one-pass extraction. ZipFile provides indexed access when the archive is already a local file and several members must be read repeatedly. Neither standard API creates or opens password-protected ZIP archives, so encrypted material needs a separate, reviewed encryption design.

Using scoped storage and document pickers

Android 10 and later apply scoped storage, and an app should not assume that a shared external folder is writable. context.filesDir is private and persistent, while context.cacheDir is suitable for temporary archives. Both work without broad storage permission. Place a final export under filesDir only when it is app-owned; otherwise let the user choose a destination.

The Storage Access Framework provides OpenDocument for reading an existing archive and CreateDocument("application/zip") for writing one. Both return a content:// URI, not necessarily a File. Open it through ContentResolver.openInputStream() or openOutputStream(), retain read access when appropriate, and account for the permission’s lifetime.

File managers and some enterprise document systems may label ZIPs as application/octet-stream, so accept a reasonable MIME fallback where the product allows it. Many Australian users first move a sizeable export over Telstra or Optus mobile data; showing compressed and estimated expanded sizes helps them avoid a costly or failed transfer.

Comparing the main ZIP workflows

Choose the workflow according to how data enters and leaves the app rather than from one preferred class. The standard library covers conventional DEFLATE compression, while ZIP packaging adds independent entries and metadata.

Workflow Input Output Best suited to Main limitation
ZipOutputStream Files or directories One archive stream Generated exports and offline bundles Entry names must be controlled
ZipInputStream Archive stream Extracted files Sequential, memory-conscious restoration No random entry access
ZipFile Local archive file Indexed entries Searches and repeated reads The archive must be locally accessible
SAF contracts Content-provider URI User-selected file Importing or exporting documents Permission and provider behaviour vary

Compression gains depend on the payload. Text, XML, and uncompressed bitmaps usually shrink well; JPEG, PNG, MP4, and many already-compressed packages gain little. DEFLATE offers no password protection, and choosing a smaller archive can increase battery use because the receiver must spend CPU expanding it.

Practical routines for Australian Android developers

Archive creation follows a compact checklist:

  • Put temporary inputs in cacheDir and completed app-owned archives in filesDir.
  • Convert each member name to a relative path with forward slashes.
  • Limit member count and total source bytes before writing.
  • Test empty folders, nested paths, Unicode names, and binary files.

Restoration needs a different checklist:

  • Resolve every canonical destination beneath the extraction root.
  • Write each member to temporary storage until its size checks pass.
  • Track expanded bytes and abort cleanly when a configured limit is reached.

Australian teams should test on the lower-memory Samsung and Motorola hardware common in the local Google Play market rather than restricting checks to a flagship handset. For a user in Sydney or Melbourne moving between work and home, an export should survive an interruption cleanly. For someone near Alice Springs using a mobile hotspot, progress and byte estimates matter. Friendly wording such as “no worries” can accompany an error message, but it should not hide the failure or the recovery action.

Testing and protecting archive data

Automated tests should create a small fixture, read it back, compare entry names and bytes, and attempt extraction with a traversal name such as ../../private.txt. Device tests should cover a cancelled document picker, insufficient space, a revoked URI grant, and an interrupted download. Keeping sample archives in test resources makes these cases repeatable in continuous integration.

When an archive contains personal information, combine narrow access controls with current device security guidance and Android’s own platform advice. Never treat ZIP compression as secrecy, and avoid placing credentials, access tokens, or raw Keystore material in an archive. HTTPS protects data in transit, but it does not remove the need for safe storage on the receiving device.

For confidential payloads, an organisation can encrypt the archive or its contents with an Android Keystore-backed key and a vetted cryptographic library, then define key rotation and recovery before release. Standard java.util.zip does not implement the traditional ZIP password scheme. Predictable exports and verified restores are the behaviours users trust when an archive crosses app boundaries.