Understanding the key concepts of Android Shared Storage with Kotlin

Understand the key concepts of storage and take advantage of recent APIs to improve both your developer productivity and users’ privacy.

Understanding the key concepts of Android Shared Storage with Kotlin
Photo Credits: Susan Q Yin


Discover the benefits of Android shared storage and simplify your file management. Learn about the Android file system, how to easily share files on Android, and optimize your data management. Unlock the potential of seamless file sharing and efficient data organization on your Android device.

Storage Architecture

Android provides different APIs to access or expose files with different tradeoffs. You can use app data to store user personal info only accessible to the app or can use shared storage for user data that can or should be accessible to other apps and saved even if the user uninstalls your app.

Credits: Android Dev Summit

History of Storage Permissions

Up to Android 9, files in the shared storage were readable and writable by apps that requested the proper permissions which are WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGE.

On Android 10, Google released a privacy upgrade regarding shared file access named Scoped Storage.

So from Android 10 onwards, the app will give limited access (scoped access) only to media files like photos, videos, and audio by requesting READ_EXTERNAL_STORAGE.

WRITE_EXTERNAL_STORAGE is now deprecated and no longer required to add files to shared storage anymore.

PDF, ZIP, and DOCX files are accessible through the Storage Access Framework (SAF) via document picker. Document picker allows users to retain complete control over which document files they give access to the app.

As per privacy concerns, Android has removed location metadata from the media files i.e. photos, videos, and audio unless the app has asked for ACCESS_MEDIA_LOCATION permission.

Common Storage Use Cases

Let’s explore some of the common use cases for Android Storage and which API to use.


Downloading a file to internal storage

Let’s say you want to download a file from API response and store it in internal storage only accessible to your app. We will use filesDir which allows us to store files in an internal directory for the application.

// create client and request
val client = OkHttpClient()
val request = Request.Builder().url(CONFIG_URL).build()


/** 
/* By using .use() method, it will close any underlying network socket 
/* automatically at the end of lambdas to avoid memory leaks 
 */
client.newCall(request).execute().use { response ->
    response.body?.byteStream()?.use { input ->
      // using context.filesDir data will be stored into app's internal storage
      val target = File(context.filesDir, "user-config.json")
      
      target.outputStream().use { output ->
        input.copyTo(output)
      }
    }
}
  


Store Files based on available location

Let’s imagine you want to download a big file/asset in our app that is not confidential but meaningful only to our app.

val fileSize = 500_000_000L // 500MB

// check if filesDir has usable space bigger than our file size,
// if not we can check into app's external storage directory. 
val target= if(context.filesDir.usableSpace > fileSize) {
  context.filesDir
} else {
  context.getExternalFilesDir(null).find { externalStorage ->
    externalStorage.usableSpace > fileSize
  }
} ?: throw IOException("Not Enough Space")

// create and save the file based on the target
val file = File(target, "big-file.asset")
  


Add image to shared storage

Now, let’s look into how we can add a media file to shared storage. Please note that if we save files to shared storage, users can access them through other apps.

To save the image, we require to ask for WRITE_EXTERNAL_STORAGE permission up to Android 9. From Android 10 onwards, we don’t require to ask this permission anymore.

fun saveMediaToStorage(context: Context, bitmap: Bitmap) {
    // Generating a file name
    val filename = BILL_FILE_NAME + "_${System.currentTimeMillis()}.jpg"

    // Output stream
    var fos: OutputStream? = null

    // For devices running android >= Q
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // getting the contentResolver
        context.contentResolver?.also { resolver ->

            // Content resolver will process the content values
            val contentValues = ContentValues().apply {
                // putting file information in content values
                put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
                put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
                put(
                    MediaStore.MediaColumns.RELATIVE_PATH,
                    Environment.DIRECTORY_DCIM + BILL_FILE_DIR
                )
            }

            // Inserting the contentValues to contentResolver 
            // and getting the Uri
            val imageUri: Uri? =
                resolver.insert(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    contentValues
                )

            // Opening an output stream with the Uri that we got
            fos = imageUri?.let { resolver.openOutputStream(it) }
        }
    } else {
        // These for devices running on android < Q
        val imagesDir = Environment
            .getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DCIM + BILL_FILE_DIR
            )

        // check if the imagesDir exist, if not make a one
        if (!imagesDir.exists()) {
            imagesDir.mkdir()
        }

        val image = File(imagesDir, filename)
        fos = FileOutputStream(image)

        // request the media scanner to scan the files 
        // at the specified path with a callback
        MediaScannerConnection.scanFile(
            context,
            arrayOf(image.toString()),
            arrayOf("image/jpeg")
        ) { path, uri ->
            Log.d("Media Scanner", "New Image - $path || $uri")
        }
    }

    fos?.use {
        // Finally writing the bitmap to the output stream that we opened
        bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, it)
    }
}
  


Select a file with the document picker

Now, let’s say we need to access document files, so we will rely on the document picker via the action OpenDocument Intent.

For that, I’m using Jetpack Activity dependency in the project.

  // add the Jetpack Activity dependency first

// create documentPicker object by registering for OpenDocument activity result
// which will handle the intent-resolve logic
val documentPicker = rememberLauncherForActivityResult(OpenDocument()) { uri ->
    if(uri == null) return

    context.contentResolver.openInputStream(uri)?.use {
        // we can copy the file content, you can refer to above code 
        // to save that content to file or use it other way.
    }
}

// usage: launch our intent-handler with MIME type of PDF
documentPicker.launch(arrayOf("application/pdf"))
  

The action OpenDocument Intent is available on devices running 4.4 and higher.


Conclusion

Android is working on improving privacy and transparency for Android users along with the latest releases with event UX enhancements like the photo picker.

Android is also working on adding more Permission-less APIs that keep the user in control of giving access without the need of requesting permissions on the app side.


For more detailed information, please read the documentation for Scoped Storage.


I've also published this article on Medium. Thanks for reading this article. Hope you would have liked it!. Please share and subscribe to my blog to support.


Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

1 Comments

Please let us know about any concerns or query.

Previous Post Next Post

Contact Form