为什么我没有权限写入外部存储上的应用程序目录?
Why don't I have permission to write to app dir on external storage?
TL;DR 问题摘要:我的 Android 应用程序尝试 将 写入应用程序的 外部存储目录 SD卡。它因 权限错误 而失败。但是提取到最小测试应用程序中的相同代码(方法)成功了!
由于我们的目标 API 级别包括 KitKat 及更高版本(以及 JellyBean),并且 KitKat 限制应用程序写入 SD 卡上除应用程序指定的外部存储目录以外的任何位置,应用程序尝试写入到该指定目录,/path/to/sdcard/Android/data/com.example.myapp/files
。我通过从 Activity.getExternalFilesDirs(null);
获取目录列表并找到一个 isRemovable()
来验证此目录的路径。 IE。我们没有对 SD 卡的路径进行硬编码,因为它因制造商和设备而异。这是演示问题的代码:
// 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);
}
}
checkDir() 方法不那么相关,但为了完整起见,我将其包含在此处。它只是确保目录位于已安装的可移动存储上,并记录目录的其他属性(存在、可写)。
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)));
}
在测试应用程序(运行ning on Android 5.1.1)中,以下日志输出表明代码工作正常:
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
文件创建成功。但是在我的实际应用程序中(也在 Android 5.1.1 上 运行ning),对 createNewFile()
的调用失败并出现权限错误:
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)
...
在您将此标记为重复之前:我已经阅读了其他几个关于 SO 的问题,这些问题描述了在 KitKat 或更高版本下写入 SD 卡时的权限失败。但是 none 的原因或解决方案似乎适用于这种情况:
- 设备未连接为大容量存储器。我仔细检查过。但是,MTP 已打开。 (如果不拔下用于查看日志的 USB 数据线,我无法将其关闭。)
- 我的清单包括
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- 我的目标是 API 22 级,所以我不应该在 运行 时 请求 许可(la 棉花糖)。 build.gradle 有
targetSdkVersion 22
(和 buildToolsVersion '21.1.2'
、compileSdkVersion 23
)。
- 我运行喜欢 KitKat 和 Lollipop;我什至没有 Marshmallow 设备。再说一次,即使我的目标是 API 级别 23,我也不应该在 运行 时请求许可。
- 如上所述,我正在写入 指定的外部存储目录,我的应用程序应该可以写入该目录,即使在 KitKat 上也是如此。
- 外部存储已挂载;代码验证了这一点。
何时有效和何时无效的总结:
- 在我的 pre-KitKat 设备上,测试应用和真实应用都运行良好。他们成功地在 SD 卡上的应用程序目录中创建了一个文件。
- 在我的 KitKat 和 Lollipop 设备上,测试应用程序运行良好,但实际应用程序运行不正常。相反,它在上面的日志中显示错误。
测试应用和真实应用有什么区别?好吧,显然真正的应用程序里面有更多的东西。但我看不到任何重要的事情。两者具有相同的 compileSdkVersion
、targetSdkVersion
、buildToolsVersion
等。两者也都使用 compile 'com.android.support:appcompat-v7:23.4.0'
作为依赖项。
由于一个应用可以运行而另一个不能运行,所以区别在于应用之间,而不在于设备或卡。有问题的目录不需要任何 Android 权限(例如 WRITE_EXTERNAL_STORAGE
)。您无法写入的唯一原因是 Android 系统没有正确设置文件系统权限。
I may have created that directory myself, and failed to set up permissions somewhere?
我不确定您能否从应用程序外部自行创建该目录并让它运行。理想情况下会很好,但我没有尝试过,我可以看到可能会带来问题的地方。
Is there another reason not to trust it?
鉴于正在发生的文件系统恶作剧,当开发人员对路径的性质做出假设时,我感到非常紧张,仅此而已。
关于这个问题我了解了更多,它与 CommonsWare 的回答有很大不同,我认为它值得一个新的回答。
- 让我原来的场景有所不同的是 SD 卡:如果一张卡 已经有 一个
/Android/data/com.example.myapp
文件夹,但 没有在此 phone 上创建,则该应用可能无法获得写入该文件夹的权限。而如果该文件夹不存在,应用程序可以创建它并写入它。至少在 KitKat 及以后的版本中是这样。
- 这得到了 this article 中解释的支持:
... starting with API Level 19 [KitKat], READ_EXTERNAL_STORAGE was no longer required to access files located on external storage – provided the data folder created by the FUSE daemon matches the app’s package name. FUSE would handle synthesizing the owner, group, and modes of files on external storage when an application is installed [emphasis added].
- 所以这证实了我的猜测,即问题的出现是因为我手动创建了应用程序的数据文件夹,而不是让 Android 根据需要进行设置。未来的研究:不要像 WRITE_EXTERNAL_STORAGE 这样只查看应用程序权限,还要检查文件系统上应用程序数据文件夹的用户、组和模式设置:手动创建和通过应用程序安装创建时。比较与对比!也许这将提供足够的信息以允许手动创建应用程序的数据文件夹并且仍然有效。请记住,SD 卡的 FAT32 文件系统上有一个包装器/仿真层,因此我们需要考虑这两个文件系统层。
- 在后来的场景中,我发现应用程序必须调用
context.getExternalFilesDirs(null)
才能在 SD 卡上创建 /Android/data/com.example.myapp
文件夹。 (至少,在 Android 5.1 Lollipop 及更高版本上。需要在 KitKat 上进行测试。)
TL;DR 问题摘要:我的 Android 应用程序尝试 将 写入应用程序的 外部存储目录 SD卡。它因 权限错误 而失败。但是提取到最小测试应用程序中的相同代码(方法)成功了!
由于我们的目标 API 级别包括 KitKat 及更高版本(以及 JellyBean),并且 KitKat 限制应用程序写入 SD 卡上除应用程序指定的外部存储目录以外的任何位置,应用程序尝试写入到该指定目录,/path/to/sdcard/Android/data/com.example.myapp/files
。我通过从 Activity.getExternalFilesDirs(null);
获取目录列表并找到一个 isRemovable()
来验证此目录的路径。 IE。我们没有对 SD 卡的路径进行硬编码,因为它因制造商和设备而异。这是演示问题的代码:
// 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);
}
}
checkDir() 方法不那么相关,但为了完整起见,我将其包含在此处。它只是确保目录位于已安装的可移动存储上,并记录目录的其他属性(存在、可写)。
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)));
}
在测试应用程序(运行ning on Android 5.1.1)中,以下日志输出表明代码工作正常:
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
文件创建成功。但是在我的实际应用程序中(也在 Android 5.1.1 上 运行ning),对 createNewFile()
的调用失败并出现权限错误:
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)
...
在您将此标记为重复之前:我已经阅读了其他几个关于 SO 的问题,这些问题描述了在 KitKat 或更高版本下写入 SD 卡时的权限失败。但是 none 的原因或解决方案似乎适用于这种情况:
- 设备未连接为大容量存储器。我仔细检查过。但是,MTP 已打开。 (如果不拔下用于查看日志的 USB 数据线,我无法将其关闭。)
- 我的清单包括
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- 我的目标是 API 22 级,所以我不应该在 运行 时 请求 许可(la 棉花糖)。 build.gradle 有
targetSdkVersion 22
(和buildToolsVersion '21.1.2'
、compileSdkVersion 23
)。 - 我运行喜欢 KitKat 和 Lollipop;我什至没有 Marshmallow 设备。再说一次,即使我的目标是 API 级别 23,我也不应该在 运行 时请求许可。
- 如上所述,我正在写入 指定的外部存储目录,我的应用程序应该可以写入该目录,即使在 KitKat 上也是如此。
- 外部存储已挂载;代码验证了这一点。
何时有效和何时无效的总结:
- 在我的 pre-KitKat 设备上,测试应用和真实应用都运行良好。他们成功地在 SD 卡上的应用程序目录中创建了一个文件。
- 在我的 KitKat 和 Lollipop 设备上,测试应用程序运行良好,但实际应用程序运行不正常。相反,它在上面的日志中显示错误。
测试应用和真实应用有什么区别?好吧,显然真正的应用程序里面有更多的东西。但我看不到任何重要的事情。两者具有相同的 compileSdkVersion
、targetSdkVersion
、buildToolsVersion
等。两者也都使用 compile 'com.android.support:appcompat-v7:23.4.0'
作为依赖项。
由于一个应用可以运行而另一个不能运行,所以区别在于应用之间,而不在于设备或卡。有问题的目录不需要任何 Android 权限(例如 WRITE_EXTERNAL_STORAGE
)。您无法写入的唯一原因是 Android 系统没有正确设置文件系统权限。
I may have created that directory myself, and failed to set up permissions somewhere?
我不确定您能否从应用程序外部自行创建该目录并让它运行。理想情况下会很好,但我没有尝试过,我可以看到可能会带来问题的地方。
Is there another reason not to trust it?
鉴于正在发生的文件系统恶作剧,当开发人员对路径的性质做出假设时,我感到非常紧张,仅此而已。
关于这个问题我了解了更多,它与 CommonsWare 的回答有很大不同,我认为它值得一个新的回答。
- 让我原来的场景有所不同的是 SD 卡:如果一张卡 已经有 一个
/Android/data/com.example.myapp
文件夹,但 没有在此 phone 上创建,则该应用可能无法获得写入该文件夹的权限。而如果该文件夹不存在,应用程序可以创建它并写入它。至少在 KitKat 及以后的版本中是这样。- 这得到了 this article 中解释的支持:
... starting with API Level 19 [KitKat], READ_EXTERNAL_STORAGE was no longer required to access files located on external storage – provided the data folder created by the FUSE daemon matches the app’s package name. FUSE would handle synthesizing the owner, group, and modes of files on external storage when an application is installed [emphasis added].
- 所以这证实了我的猜测,即问题的出现是因为我手动创建了应用程序的数据文件夹,而不是让 Android 根据需要进行设置。未来的研究:不要像 WRITE_EXTERNAL_STORAGE 这样只查看应用程序权限,还要检查文件系统上应用程序数据文件夹的用户、组和模式设置:手动创建和通过应用程序安装创建时。比较与对比!也许这将提供足够的信息以允许手动创建应用程序的数据文件夹并且仍然有效。请记住,SD 卡的 FAT32 文件系统上有一个包装器/仿真层,因此我们需要考虑这两个文件系统层。
- 这得到了 this article 中解释的支持:
- 在后来的场景中,我发现应用程序必须调用
context.getExternalFilesDirs(null)
才能在 SD 卡上创建/Android/data/com.example.myapp
文件夹。 (至少,在 Android 5.1 Lollipop 及更高版本上。需要在 KitKat 上进行测试。)