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 files 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")
Job Offers
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.
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 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.
This article was previously published on proandroiddev.com