Smartly handling EXTERNAL_STORAGE permission in Android
While working on one of the applications where I needed to access the external storage, everything was going great until Android 11(API level 30) was born. With no surprise, Google as always came up with a myriad of deprecations and sets of new APIs.
One of the issues our app encountered was, we were not able to access all the files in external storage on devices running Android 11 (or above). This is due to the security reason that with the old school permissions like READ_EXTERNAL_STORAGE
or WRITE_EXTERNAL_STORAGE
, applications could exploit user storage. Therefore Google Play has now restricted the use of high-risk or sensitive permissions, including special app access called All files access. But this is only applicable to apps that target Android 11 and declare MANAGE_EXTERNAL_STORAGE
permission, which is added in Android 11. For previous versions, everything remains the same.
If your app meets the policy requirements for acceptable use or is eligible for an exception, you will be required to declare this and any other high-risk permissions using the Declaration Form in Play Console. Failing to do so will restrict your application to be published.
Implementation 🚀
Let’s jump into the code!
AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Or if you want to manage the files as well rather than just reading them -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- This is mandatory to work on API >= 30 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /><!-- Make sure to add this line to the application tag to support API level 29. -->
<application
...
android:requestLegacyExternalStorage="true"
...>
</application>
Kotlin
After completing the implementation, our caller will have a representation like this
if (hasPermissions()) {
// Nothing to do. Proceed with your work
} else {
requestPermission()
}
First, we create a simple function to indicate if the device’s Android version is Android 11 (or above) because we will need this check at multiple places.
private fun isAndroid11OrAbove() = SDK_INT >= VERSION_CODES.R
Check if permissions have already been granted 🔓
Before requesting the permissions, check if these are already granted.
If you request the
WRITE_EXTERNAL_STORAGE
then there is no need to request theREAD_EXTERNAL_STORAGE
permission as that automatically comes in with the Write access.
private fun hasPermissions(): Boolean {
// Check if the API level is >= 30.
return if (isAndroid11OrAbove()) {
Environment.isExternalStorageManager()
}
// If the API level is < 30, check the permissions in the traditional way
else {
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED
}
}
Requesting the permission(s)
If the hasPermissions()
returns false, we need to trigger the permission request. Considering the riskiness of this sensitive permission i.e. WRITE_EXTERNAL_STORAGE
Google wanted to come up with a more user-conscious or action-conscious approach and hence rather than relying on the traditional Marshmallow permission dialog, they explicitly want to navigate the user to the Settings application where the user can press the toggle button to grant access permission.
Considering the principle of Single Source of Truth, we want the callers to just request permission irrespective of the API level and let the helper function handle the rest. This way we can achieve the abstraction so that the outside world aka the callers don’t know the logic of deciding which method to call to ask for the permissions, therefore we create a generic method that asks for the permissions based on the API level.
private fun requestPermission() {
if (isAndroid11OrAbove()) {
try {
// We first try to open our application's detail screen in the Settings application via package name.
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = Uri.parse(String.format("package:%s", applicationContext.packageName))
resultLauncher.launch(intent)
} catch (e: Exception) {
// If for some reason, Settings screen can't locate our app in the list, just open the Files Access
// screen and let user find our app
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
resultLauncher.launch(intent)
}
} else {
// For API level < 30, handle the permission in the normal way
ActivityCompat.requestPermissions(
this,
arrayOf(WRITE_EXTERNAL_STORAGE),
STORAGE_PERMISSION_REQUEST_CODE
)
}
}
Responding to Permission Results (Pre Android 11) 🥍
Although I’m sure most of you guys have already used this code, I believe it's worthwhile to mention it while doing the comparison.
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
STORAGE_PERMISSION_REQUEST_CODE -> if (grantResults.isNotEmpty()) {
val writeStoragePermission = grantResults[0] == PackageManager.PERMISSION_GRANTED
// Make sure both the permissions are given
if (writeStoragePermission) {
// ... Proceed with your work
} else {
Toast.makeText(this, "Please allow permission to proceed", Toast.LENGTH_SHORT).show()
}
}
}
}
Responding to Permission Results (Android 11 and above) 🥍
Since All Files Access is an activity inside the Settings app. Therefore after performing the action i.e. granting/revoking permission and then pressing the back button resumes our application. This is where onActivityResult()
is called. Since onActivityResult()
method of the Activity’s lifecycle is deprecated and is replaced by ActivityResultCallBack
(For more information, the first solution at StackOverflow is really helpful), we will create its instance. When the activity is resumed, resultLauncher
will be called which will contain the result.
@RequiresApi(VERSION_CODES.R)
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if(it.resultCode == RESULT_OK) {
// ... Proceed with your work
} else {
Toast.makeText(this, "Please allow permission to proceed", Toast.LENGTH_SHORT).show()
}
}
So that’s complete everything you need to do to access files.
Thanks for reading. To access the complete source code, please check out this Github repository.