Everything about storage on Android

Pragnesh Ghoda
ProAndroidDev
Published in
5 min readAug 15, 2023

--

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

Photo by Pat Whelen on Unsplash

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.

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 are required to ask for WRITE_EXTERNAL_STORAGE permission up to Android 9. From Android 10 onwards, we don’t need 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.

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 to request permissions on the app side.

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

I’ve also published this article on my blogspot. Please read and share for support.

Thanks for reading this article. Hope you would have liked it!. Please clap, share, and subscribe to my blog to support.

--

--