Android Storage Permissions for API 33+
Problem Statement
Starting with Android 13 (API 33), the traditional WRITE_EXTERNAL_STORAGE
permission undergoes significant changes. Developers targeting API 33+ report that the runtime permission dialog for WRITE_EXTERNAL_STORAGE
doesn't appear, even when declared in the manifest. This issue arises because:
- Android 13 deprecates broad storage permissions
- Google introduced granular media permissions (
READ_MEDIA_IMAGES
,READ_MEDIA_VIDEO
,READ_MEDIA_AUDIO
) for media files only - There's confusion about how to handle non-media file operations
Many developers need to save app-specific files (like keystores) but can't find clear documentation for non-media storage scenarios.
Understanding Storage Changes
Key Android 13+ Behavior Changes
WRITE_EXTERNAL_STORAGE
is fully deprecated: Starting from Android 11 (API 30), requesting this permission provides no additional access- Granular media permissions: Only required when accessing photos, videos, or audio
- Reduced scope: Apps can no longer access arbitrary locations in external storage
Legacy Permissions Are Dead
Attempting to request WRITE_EXTERNAL_STORAGE
on Android 13+ is futile - the system will simply ignore the request. Even if granted, it provides no additional file access.
Recommended Solutions
1. Use App-Specific Storage (Recommended)
For private files that don't need user visibility (like your keystore file), use app-specific directories that require no permissions:
// Create directory in app-specific external storage
val dir = getExternalFilesDir("keystores")
if (!dir?.exists() == true) {
if (!dir?.mkdirs() == true) {
// Handle directory creation error
}
}
// Create file inside the directory
val keyFile = File(dir, "my_keystore.bin")
keyFile.writeBytes(keystoreData)
Key benefits:
- Zero permissions required
- Files are automatically deleted when app is uninstalled
- Works on all Android versions
Internal vs External Storage
getFilesDir()
: Internal storage (smaller space, always available)getExternalFilesDir()
: External storage (larger space, may be unavailable when mounted)
2. Storage Access Framework for Shared Files
For files that should be visible in system file managers (e.g., exported reports), use Android's document picker:
// Launch directory picker
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_CODE_DIR)
// Handle result
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_DIR && resultCode == RESULT_OK) {
data?.data?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
saveFileToPickedLocation(uri)
}
}
}
private fun saveFileToPickedLocation(baseUri: Uri) {
val dir = DocumentFile.fromTreeUri(context, baseUri)
val file = dir?.createFile("application/octet-stream", "keystore.bin")
file?.uri?.let { uri ->
contentResolver.openOutputStream(uri)?.use { stream ->
stream.write(keystoreData)
}
}
}
3. Media Store for Public Media (Special Cases)
For media files only, use the new granular permissions:
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
Permission Handling Compatibility
If supporting older Android versions, conditionally request permissions:
fun saveKeystore() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// Request legacy permissions on older devices
requestLegacyStoragePermission()
} else {
// Directly save to app-specific storage on API 33+
saveToAppStorage()
}
}
private fun requestLegacyStoragePermission() {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_LEGACY_STORAGE
)
}
Why These Solutions Work
App-specific storage:
- Requires no permissions
- Sandboxes files to your app's private area
- Follows Android's scoped storage guidelines
Storage Access Framework:
- User-driven file selection
- Provides explicit user consent
- Compatible with all Android versions
Granular permissions:
- Target specific media types
- Follow least-privilege principle
Performance Considerations
- Internal storage (
getFilesDir()
) is faster but has limited space - External storage (
getExternalFilesDir()
) has more capacity but may be slower
Migration Checklist
- Remove
WRITE_EXTERNAL_STORAGE
from manifest if unused - Replace all legacy storage paths with:
Context#getFilesDir()
Context#getExternalFilesDir()
MediaStore
for shared media
- Test file operations on Android 10+ devices
- For file manager functionality:
- Target Android 11+ (R)
- Request
MANAGE_EXTERNAL_STORAGE
- Through Play Store approval
Common Mistakes to Avoid
Requesting deprecated permissions:
xml<!-- DON'T DO THIS --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Using hardcoded paths:
kt// Avoid - won't work on Android 10+ File("/sdcard/MyApp/keystore.bin")
Ignoring compatibility:
kt// Always check SDK version if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Use scoped storage methods } else { // Use legacy methods with permissions }
Conclusion
Android 13 completes the decade-long storage permission overhaul that began with Android 10. Instead of fighting against WRITE_EXTERNAL_STORAGE
:
- Use app-specific storage with
getExternalFilesDir()
for private files - Use Storage Access Framework for user-visible documents
- Use granular media permissions for photos/videos/audio
These changes make Android storage safer while maintaining file operation capabilities.