The TL;DR question summary: My Android app tries to write to the app's external storage directory on an SD card. It fails with a permissions error. But the same code (method), extracted into a minimal test app, succeeds!
Since our target API level includes KitKat and later (as well as JellyBean), and KitKat restricts apps from writing anywhere on the SD card except the app's designated external storage directory, the app tries to write to that designated directory, /path/to/sdcard/Android/data/com.example.myapp/files
. I verify this directory's path by getting a list of directories from Activity.getExternalFilesDirs(null);
and finding one that isRemovable()
. I.e. we're not hard-coding the path to the SD card, because it varies by manufacturer and device. Here is code that demonstrates the problem:
// Attempt to create a test file in dir.
private void testCreateFile(File dir) {
Log.d(TAG, ">> Testing dir " + dir.getAbsolutePath());
if (!checkDir(dir)) { return; }
// Now actually try to create a file in this dir.
File f = new File(dir, "foo.txt");
try {
boolean result = f.createNewFile();
Log.d(TAG, String.format("Attempted to create file. No errors. Result: %b. Now exists: %b",
result, f.exists()));
} catch (Exception e) {
Log.e(TAG, "Failed to create file " + f.getAbsolutePath(), e);
}
}
The checkDir() method is not as relevant, but I'll include it here for completeness. It just makes sure the directory is on removable storage that is mounted, and logs other properties of the directory (exists, writable).
private boolean checkDir(File dir) {
boolean isRemovable = false;
// Can't tell whether it's removable storage?
boolean cantTell = false;
String storageState = null;
// Is this the primary external storage directory?
boolean isPrimary = false;
try {
isPrimary = dir.getCanonicalPath()
.startsWith(Environment.getExternalStorageDirectory().getCanonicalPath());
} catch (IOException e) {
isPrimary = dir.getAbsolutePath()
.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
}
if (isPrimary) {
isRemovable = Environment.isExternalStorageRemovable();
storageState = Environment.getExternalStorageState();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// I actually use a try/catch for IllegalArgumentException here, but
// that doesn't affect this example.
isRemovable = Environment.isExternalStorageRemovable(dir);
storageState = Environment.getExternalStorageState(dir);
} else {
cantTell = true;
}
if (cantTell) {
Log.d(TAG, String.format(" exists: %b readable: %b writeable: %b primary: %b cantTell: %b",
dir.exists(), dir.canRead(), dir.canWrite(), isPrimary, cantTell));
} else {
Log.d(TAG, String.format(" exists: %b readable: %b writeable: %b primary: %b removable: %b state: %s cantTell: %b",
dir.exists(), dir.canRead(), dir.canWrite(), isPrimary, isRemovable, storageState, cantTell));
}
return (cantTell || (isRemovable && storageState.equalsIgnoreCase(MEDIA_MOUNTED)));
}
In the test app (running on Android 5.1.1), the following log output shows that the code is working fine:
10-25 19:56:40 D/MainActivity: >> Testing dir /storage/extSdCard/Android/data/com.example.testapp/files
10-25 19:56:40 D/MainActivity: exists: true readable: true writeable: true primary: false removable: true state: mounted cantTell: false
10-25 19:56:40 D/MainActivity: Attempted to create file. No errors. Result: false. Now exists: true
So the file was created successfully. But in my actual app (also running on Android 5.1.1), the call to createNewFile()
fails with a permissions error:
10-25 18:14:56... D/LessonsDB: >> Testing dir /storage/extSdCard/Android/data/com.example.myapp/files
10-25 18:14:56... D/LessonsDB: exists: true readable: true writeable: true primary: false removable: true state: mounted cantTell: false
10-25 18:14:56... E/LessonsDB: Failed to create file /storage/extSdCard/Android/data/com.example.myapp/files/foo.txt
java.io.IOException: open failed: EACCES (Permission denied)
at java.io.File.createNewFile(File.java:941)
at com.example.myapp.dmm.LessonsDB.testCreateFile(LessonsDB.java:169)
...
Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
at libcore.io.Posix.open(Native Method)
at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
at java.io.File.createNewFile(File.java:934)
...
Before you mark this as a duplicate: I have read through several other questions on SO describing permission failures when writing to an SD card under KitKat or later. But none of the causes or solutions given seem to apply to this situation:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
targetSdkVersion 22
(and buildToolsVersion '21.1.2'
, compileSdkVersion 23
).Summary of when it works and when it doesn't:
What difference is there between the test app and the real app? Well, obviously the real app has a lot more stuff in it. But I can't see anything that should matter. Both have identical compileSdkVersion
, targetSdkVersion
, buildToolsVersion
, etc. Both are also using compile 'com.android.support:appcompat-v7:23.4.0'
as a dependency.
Since one app worked and another didn't, the difference was between the apps, not with the device or card. The directory in question does not require any Android permissions (e.g., WRITE_EXTERNAL_STORAGE
). The only reason why you would not be able to write to it would be if the Android system had not set up the filesystem permissions properly.
I may have created that directory myself, and failed to set up permissions somewhere?
I am not certain that you can create that directory yourself from outside the app and have it work. Ideally that would be fine, but I have not tried it and I can see where that might pose problems.
Is there another reason not to trust it?
Given the filesystem shenanigans that are going on, I get very nervous when developers make assumptions regarding the nature of paths, that's all.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With