Android storage permission adaptation and reading/writing media files

Wan Xiao
7 min readApr 9, 2023

--

Over the years of my work, I have come across many storage-related codes, such as reading and writing media files. Most of them are wrong, such as applying for permissions when not necessary, reading files by querying file paths and then read. These codes can only work by chance. Even the code samples provided by the official documentation may not work sometimes. In addition, Android has been making modifications to the storage permission in its recent versions and has only recently provided official guidelines. This means that to write correct code, one still needs to pay attention to certain things.

Access app-specific private internal storage

Related APIs: Context.getFilesDir, Context.getCacheDir.

Reading and writing to the app’s private storage does not require any permission. You can use the File API directly.

The path returned by getFilesDir on Android 13 is /data/user/0/{packageName}/files.

Access app-specific external storage

Related APIs: Context.getExternalFilesDir, Context.getExternalCacheDir.

You can directly use the File API to access the app-specific external storage.

On Android 13, getExternalFilesDir returns the path /storage/emulated/0/Android/data/{packageName}/files.

Permission requirement

API level < 19 (Android 4.4): WRITE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE required.

API level ≥ 19 (Android 4.4): No permission required.

Access external shared storage

Related APIs: Environment.getExternalStorageDirectory, Environment.getExternalStoragePublicDirectory.

Just use the File API directly.

Note that starting from Android 11 (API level 30), due to the enforced scoped storage, these APIs can no longer be used, and I strongly recommend avoiding using these APIs. We will discuss this in detail later.

Permission requirement

WRITE_EXTERNAL_STORAGE or READ_EXTERNAL_STORAGE required.

To use these APIs on Android 10 (API level 29), you need to set requestLegacyExternalStorage to true to opt-out of scoped storage.

getExternalStorageDirectory returns /storage/emulated/0 on Android 9.

Next, let’s talk about reading and writing media files, which refer to files such as images, audio, and video.

Reading media files

Using Intents

Use Intent.ACTION_GET_CONTENT or Intent.ACTION_PICK action to call the system or third-party app to obtain a user-selected media file path.

Permission requirement

API level < 29 (Android 10): READ_EXTERNAL_STORAGE required.

This is because accessing early versions of MediaStore Uri requires the app to have permission to read external storage. Since we don’t know where the Uri we obtained came from, we need to request permission to read external storage.

API level ≥ 29 (Android 10): No permission required

This is because modern apps grant read permission to the returned Uri, so we don’t need to request permission.

The correct way to read files

It should be noted that for Uris obtained through this method, the correct way to read the file is to use methods like ContentResolver.openInputStream to read the file, rather than querying the file path through ContentResolver and then accessing it with File API.

If you insist on querying the file path and accessing it with File API, you must set requestLegacyExternalStorage to true on Android 10 to opt-out of scoped storage, and do not assume that you will always get the file path, as the latest version of FileProvider no longer discloses the file path to queriers.

Traversing external shared storage file paths manually

Note that this method cannot be used at all on Android 11 and above. I also strongly against using this method.

Your app may not want to use a third-party media file picker and decides to create its own picker. You can directly traverse the user’s files through Environment.getExternalStoragePublicDirectory.

Permission requirement

API level < 29 (Android 10): READ_EXTERNAL_STORAGE required

API level = 29 (Android 10): READ_EXTERNAL_STORAGE required and declare android:requestLegacyExternalStorage="true" in the Manifest.

API level > 29 (Android 10): this method is completely unusable.

Using MediaStore API

Use MediaStore API to read files. For usage, please refer to the official documentation.

Permission requirement

API level < 29 (Android 10): READ_EXTERNAL_STORAGE required.

MediaStore requires READ_EXTERNAL_STORAGE permission to read files before Android 10.

29 <= API level < 33 (Android 13):

If you only need to read and write files saved by your app in the media store, no permission required.

If you want to read other apps’ files in the media store, you need to request READ_EXTERNAL_STORAGE permission.

API level ≥ 33 (Android 13):

If you only need to read and write files saved by your app in the media store, no permission required.

If you want to read other apps’ files in the media store, you need to request the corresponding permission based on the file type:

  • READ_MEDIA_IMAGES: Access other apps’ images .
  • READ_MEDIA_VIDEO: Access other apps’ video.
  • READ_MEDIA_AUDIO: Access other apps’ audio.

Do not try to check or request READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE permissions on Android 13 or later. The system will reject the request directly.

Writing media files

Please note that in most cases, we only need to save our own media files to the media store and do not need to edit or delete files belonging to other apps.

Writing external shared storage through File API

Traverse the user’s files directly through Environment.getExternalStoragePublicDirectory and perform write operations on them.

Here’s an example of saving a Bitmap as a PNG image to the user’s device using the File API:

fun saveImageToFile(displayName: String, bitmap: Bitmap): Uri? {
val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val imageFile = File(directory, "$displayName.png")
if (!directory.isDirectory) {
directory.mkdir()
}
return if (directory.isDirectory) {
try {
imageFile.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
imageFile.toUri()
} catch (e: IOException) {
null
}
} else {
null
}
}

Note that this method is completely unusable on Android 11 and above. However, I recommend you using this method to save media files on systems below Android 10 because the MediaStore API may not automatically create folders on systems below Android 10, leading to failed file saving.

Permission requirement

API level < 29(Android 10): WRITE_EXTERNAL_STORAGE required. Only recommended for saving media files to users’ device in this case.

API level = 29(Android 10): WRITE_EXTERNAL_STORAGE required and declare android:requestLegacyExternalStorage="true" in the Manifest.

API level > 29(Android 10): This method is completely unusable.

Trigger media scan manually

The above code only saves the file, but it does not trigger the media scan. Since this photo is not recorded in the media store, users cannot see the file in the album app, which displays data from the MediaStore. They can only see them in the file manager.

You can use the following code to trigger the media scanner to scan the file to add it to MediaStore:

context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))

Using MediaStore API

Here’s an example of how to save a Bitmap as a PNG image to the user’s media store using the MediaStore API:

fun saveImageToMediaStore(context: Context, displayName: String, bitmap: Bitmap): Uri? {
val imageCollections = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}

val imageDetails = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}

val resolver = context.applicationContext.contentResolver
val imageContentUri = resolver.insert(imageCollections, imageDetails) ?: return null

return try {
resolver.openOutputStream(imageContentUri, "w").use { os ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
imageDetails.clear()
imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(imageContentUri, imageDetails, null, null)
}

imageContentUri
} catch (e: FileNotFoundException) {
// Some legacy devices won't create directory for the Uri if dir not exist, resulting in
// a FileNotFoundException. To resolve this issue, we should use the File API to save the
// image, which allows us to create the directory ourselves.
null
}
}

Please refer to the official documentation for usage instructions.

Permission requirement

API level < 29 (Android 10): WRITE_EXTERNAL_STORAGE required.

API level = 29 (Android 10):

If you only need to save media files to the media store, no permission required.

If you need to write other apps’ files, it is recommended that you use one of the following methods:

  • Recommend using requestLegacyExternalStorage to opt-out of scoped storage, and then use the same code as in API level < 29.
  • Catch and handle RecoverableSecurityException, refer to:

API level > 29 (Android 10):

If you only need to save media files to the media store, no permission required.

If you need to write other apps’ files, you need to use the create*Request methods from the MediaStore API, such as createWriteRequest, createTrashRequest, to write to those files. For more information, please refer to:

Do not try to check or request READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE permissions on Android 13 or later. The system will reject the request directly.

Why I recommend writing external shared storage through File API on Android versions below 10

Although saving media files to the media store using the MediaStore API allows for the same core code to be used across all Android versions, You may still encounter some issues.

During my investigation, I found that on some devices running Android versions prior to Android 10, saving media files using the MediaStore API does not trigger a media scan. In such cases, you need to obtain the file path and trigger a scan for that file, as shown in the code below:

val filePath = getFilePathOfUri(context, imageUri)
if (filePath != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
context.sendBroadcast(
Intent(
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(
File(filePath)
)
)
)
}


fun getFilePathOfUri(context: Context, uri: Uri): String? {
var result: String? = null;
context.contentResolver.query(uri, arrayOf(MediaStore.Images.Media.DATA), null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val dataColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
if (dataColumnIndex >= 0) {
result = cursor.getString(dataColumnIndex)
}
}
}
return result
}

I have also found that on some devices running Android versions before 10, if the folder for saving the media file in the media store does not exist when saving through the MediaStore API, it will not be created automatically, leading to a failed file write. As the MediaStore API does not have a method to create a directory, this problem cannot be solved solely by using the MediaStore API.

Therefore, I recommend using the File API directly to write to external shared storage for API levels < 29 (Android 10) and manually triggering a media scan after the file is written. For API level ≥ 29 (Android 10), using the MediaStore API as it doesn’t require requesting permissions or triggering media scan.

--

--