Keywords: Android Storage | Scoped Storage | MediaStore | File Permissions | API Migration
Abstract: This article provides an in-depth analysis of the deprecation of Environment.getExternalStorageDirectory() in Android API Level 29, detailing alternative approaches using getExternalFilesDir(), MediaStore, and ACTION_CREATE_DOCUMENT. Through comprehensive code examples and step-by-step explanations, it helps developers understand scoped storage mechanisms and offers practical guidance for migrating from traditional file operations to modern Android storage APIs. The discussion also covers key issues such as permission management, media indexing, and compatibility handling to ensure smooth adaptation to Android's evolving storage system.
Introduction
As the Android ecosystem evolves, storage access mechanisms continue to improve. In API Level 29 (Android 10), the Environment.getExternalStorageDirectory() method was marked as deprecated, signaling a significant shift toward Scoped Storage in Android's storage system. This article provides a technical deep dive into the background, implications, and solutions related to this change.
Background and Rationale for Deprecation
The Environment.getExternalStorageDirectory() method has long been used by developers to obtain the root directory path of external storage. However, this approach presents significant security and privacy concerns:
- Applications could access the entire external storage space without restrictions
- User data was vulnerable to inspection and modification by malicious apps
- There was a lack of fine-grained permission control mechanisms
Android 10 introduced Scoped Storage to address these issues by providing isolated storage spaces for each application and strictly controlling access to shared storage.
Core Alternative Solutions
Using Context Methods
The most straightforward alternative involves using methods provided by the Context class:
// Get application-specific external files directory
File externalFilesDir = getExternalFilesDir(null);
String folderPath = externalFilesDir.getAbsolutePath();
// Get external cache directory
File externalCacheDir = getExternalCacheDir();
// Get array of external media directories
File[] externalMediaDirs = getExternalMediaDirs();These application-specific directories offer several advantages:
- Accessible without requiring
WRITE_EXTERNAL_STORAGEpermission - Files are automatically cleaned up when the application is uninstalled
- Other applications cannot directly access files in these directories
MediaStore Integration Approach
For media files that need to be saved to shared storage (such as the photo gallery), the MediaStore API is recommended:
private void saveImageWithMediaStore() {
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME,
System.currentTimeMillis() + ".png");
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/PhotoEditors");
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues);
if (imageUri != null) {
try (OutputStream outputStream = resolver.openOutputStream(imageUri)) {
// Write image data to output stream
saveImageToStream(outputStream);
// Automatically indexed by MediaStore, no manual media scan broadcast needed
showSnackbar("Image saved successfully");
} catch (IOException e) {
showSnackbar("Save failed: " + e.getMessage());
}
}
}Using ACTION_CREATE_DOCUMENT
For scenarios where users need to choose save locations, the ACTION_CREATE_DOCUMENT intent can be used:
private void createDocument() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/png");
intent.putExtra(Intent.EXTRA_TITLE, System.currentTimeMillis() + ".png");
startActivityForResult(intent, CREATE_DOCUMENT_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
saveImageToUri(uri);
}
}
}Code Migration Example
Below is a complete example demonstrating migration from legacy code to modern storage APIs:
private void saveImageModern() {
if (isStoragePermissionGranted()) {
showLoading("Saving...");
// Use MediaStore to save to shared storage
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME,
System.currentTimeMillis() + ".png");
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/PhotoEditors");
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues);
if (imageUri != null) {
SaveSettings saveSettings = new SaveSettings.Builder()
.setClearViewsEnabled(true)
.setTransparencyEnabled(true)
.build();
// Assuming mPhotoEditor supports Uri saving
mPhotoEditor.saveAsUri(imageUri, saveSettings, new PhotoEditor.OnSaveListener() {
@Override
public void onSuccess(@NonNull String imagePath) {
hideLoading();
showSnackbar("Image saved successfully");
mPhotoEditorView.getSource().setImageURI(Uri.parse(imagePath));
Intent intent = new Intent(EditImageActivity.this, StartActivity.class);
startActivity(intent);
finish();
}
@Override
public void onFailure(@NonNull Exception exception) {
hideLoading();
showSnackbar("Failed to save image");
}
});
}
}
}Compatibility Considerations
During migration, compatibility across different Android versions must be considered:
- Android 10 and above: Must use Scoped Storage APIs
- Android 9 and below: Can continue using legacy methods, but early migration is recommended
- Temporary solution: Adding
android:requestLegacyExternalStorage="true"to AndroidManifest.xml can temporarily disable Scoped Storage, but this is only a transitional measure
Permission Management Optimization
With storage API updates, permission management also requires adjustment:
private boolean isStoragePermissionGranted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ doesn't require WRITE_EXTERNAL_STORAGE permission for app-specific directories
return true;
} else {
return ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
}Conclusion
The evolution of Android's storage system represents significant progress toward enhanced security and privacy protection. While migrating to new storage APIs requires development effort, it is essential for ensuring long-term application compatibility and user data security. Developers should:
- Prioritize using application-specific directories like
getExternalFilesDir() - Use
MediaStoreAPI for shared media files - Employ
ACTION_CREATE_DOCUMENTwhen users need file selection capabilities - Update permission management logic promptly
- Thoroughly test compatibility across different Android versions
By adopting these modern storage practices, developers can not only meet the latest platform requirements but also provide users with safer, more consistent experiences.