为什么我没有权限在外部存储上写入app dir?

Lar*_*rsH 19 android file-permissions android-sdcard android-external-storage

TL; DR问题摘要:我的Android应用程序尝试写入 SD卡上应用程序的外部存储目录.它失败并出现权限错误.但是,提取到最小测试应用程序中的相同代码(方法)成功了!

由于我们的目标API级别包括KitKat及更高版本(以及JellyBean),并且KitKat限制应用程序在SD卡上的任何位置写入(应用程序指定的外部存储目录除外),应用程序会尝试写入该指定目录/path/to/sdcard/Android/data/com.example.myapp/files.我通过获取目录列表Activity.getExternalFilesDirs(null);并找到一个目录来验证此目录的路径isRemovable().即我们不是硬编码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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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)));
}
Run Code Online (Sandbox Code Playgroud)

在测试应用程序(在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
Run Code Online (Sandbox Code Playgroud)

因此文件已成功创建.但在我的实际应用程序中(也在Android 5.1.1上运行),调用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)
    ...
Run Code Online (Sandbox Code Playgroud)

在将此标记为重复之前:我已经阅读了几个关于SO的其他问题,这些问题描述了在KitKat或更高版本下写入SD卡时的权限失败.但是,给出的原因或解决方案似乎都不适用于这种情况:

  • 设备连接为大容量存储设备.我仔细检查了一下.但是,MTP已开启.(如果不拔掉用于查看日志的USB线,我无法将其关闭.)
  • 我的清单包括<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • 我的目标是API级别22,所以我不应该在运行时请求权限(la Marshmallow).build.gradle有targetSdkVersion 22(和buildToolsVersion '21.1.2',compileSdkVersion 23).
  • 我正在使用KitKat和Lollipop; 我甚至没有棉花糖装置.所以,我不应该在运行时请求权限,即使我的目标是API级别23.
  • 如上所述,我正在写入指定的外部存储目录,该目录应该由我的应用程序编写,即使在KitKat上也是如此.
  • 外部存储器已安装 ; 代码验证了这一点.

它何时起作用以及何时起作用的摘要:

  • 在我的pre-KitKat设备上,测试应用程序和真实应用程序都可以正常工作.他们在SD卡的app dir中成功创建了一个文件.
  • 在我的KitKat和Lollipop设备上,测试应用程序工作正常,但真正的应用程序没有.相反,它显示上述日志中的错误.

测试应用程序和真实应用程序之间有什么区别?嗯,显然真正的应用程序中有更多的东西.但我看不出任何重要的事情.两者具有相同的compileSdkVersion,targetSdkVersion,buildToolsVersion等两者也使用compile 'com.android.support:appcompat-v7:23.4.0'作为依赖.

Com*_*are 8

由于一个应用程序正常工作而另一个应用程序没有,因此不同之处在于应用程序之间,而不是设备或卡.相关目录不需要任何Android权限(例如WRITE_EXTERNAL_STORAGE).如果Android系统没有正确设置文件系统权限,那么你无法写入它的唯一原因就是它.

我自己可能创建了该目录,但未能在某处设置权限?

我不确定您是否可以从应用程序外部创建该目录并使其正常工作.理想情况下这样会很好,但我没有尝试过,我可以看到可能造成问题的地方.

还有另一个不信任它的理由吗?

鉴于正在进行的文件系统恶作剧,当开发人员对路径的性质做出假设时,我会非常紧张,这就是全部.


Lar*_*rsH 2

我对这个问题有了更多的了解,它与 CommonsWare 的答案有很大不同,我认为值得一个新的答案。

\n\n
    \n
  • 与我最初的场景不同的是 SD 卡:如果卡上已经有一个不是在此手机上创建的文件夹/Android/data/com.example.myapp文件夹,那么应用程序可能无法获得写入该文件夹的权限。然而,如果该文件夹不存在,应用程序可以创建它并写入它。至少在 KitKat 及更高版本中是这样。\n\n
      \n
    • 本文中解释的内容支持了这一点:\n\n
      \n

      ...从 API 级别 19 [KitKat] 开始,不再需要 READ_EXTERNAL_STORAGE 来访问位于外部存储 \xe2\x80\x93 上的文件,前提是FUSE 守护程序创建的数据文件夹与 app\xe2\x80\x99s 包名称匹配。当安装应用程序时, FUSE 将处理综合外部存储上文件的所有者、组和模式[强调]。

      \n
    • \n
    • 因此,这证实了问题出现的猜测,因为我手动创建了应用程序的数据文件夹,而不是让 Android 根据需要进行设置。未来的研究:不要只查看像 WRITE_EXTERNAL_STORAGE 这样的应用程序权限,还要检查文件系统上应用程序数据文件夹的用户、组和模式设置:手动创建和通过应用程序安装创建时。比较和对比!也许这将提供足够的信息来允许手动创建应用程序的数据文件夹并且仍然可以工作。请记住,SD 卡的 FAT32 文件系统上有一个包装器/模拟层,因此我们需要考虑两个文件系统层。
    • \n
  • \n
  • 在后来的场景中,我发现应用程序有必要调用context.getExternalFilesDirs(null)才能/Android/data/com.example.myapp在 SD 卡上创建文件夹。(至少,在 Android 5.1 Lollipop 及更高版本上。需要在 KitKat 上进行测试。)
  • \n
\n