为啥我无权写入外部存储上的应用程序目录?

Posted

技术标签:

【中文标题】为啥我无权写入外部存储上的应用程序目录?【英文标题】:Why don't I have permission to write to app dir on external storage?为什么我无权写入外部存储上的应用程序目录? 【发布时间】:2017-03-08 05:52:47 【问题描述】:

TL;DR 问题摘要:我的 android 应用尝试写入到应用在 SD 卡上的外部存储目录。它因权限错误而失败。但是同样的代码(方法),提取到一个最小的测试应用程序中,成功了!

由于我们的目标 API 级别包括 KitKat 及更高版本(以及 JellyBean),并且 KitKat 限制应用程序写入 SD 卡上除应用程序指定的外部存储目录之外的任何位置,因此应用程序尝试写入该指定目录,@987654321 @。我通过从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)));

在测试应用(运行于 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 上运行),对 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 卡时权限失败。但给出的原因或解决方案似乎都不适用于这种情况:

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

什么时候有效,什么时候无效的总结:

在我之前的 KitKat 设备上,测试应用程序和真实应用程序都可以正常工作。他们成功地在 SD 卡上的应用程序目录中创建了一个文件。 在我的 KitKat 和 Lollipop 设备上,测试应用程序运行良好,但真正的应用程序不能。相反,它会在上述日志中显示错误。

测试应用和真实应用有什么区别?好吧,显然真正的应用程序中有更多的东西。但我看不到任何重要的东西。两者都有相同的compileSdkVersiontargetSdkVersionbuildToolsVersion 等。两者都使用compile 'com.android.support:appcompat-v7:23.4.0' 作为依赖项。

【问题讨论】:

就您的问题而言,请完全卸载失败的应用程序(例如,adb uninstall,或从“设置”中删除)。确保此目录已从可移动存储中删除。再次安装并运行您的应用,看看它现在是否表现得更好。 @CommonsWare:看起来卸载应用程序并确保删除了目录就可以了!我可能自己创建了该目录,但未能在某处设置权限??谢谢!如果您创建答案,我将有地方奖励赏金。但是弄清楚将来如何避免这个问题会很有帮助......能够在安装应用程序之前创建目录会很有帮助,而且我不想在这样做时弄乱权限。 @CommonsWare:“getExternalFilesDirs() 不会在 Android 4.4 之前返回单独的可移动存储卷” - 从我正在查看的文档中,getExternalFilesDirs() 在 Android 4.4 之前不可用(API 19 级)。所以我不确定你的意思。 “在 Android 4.4 之前,大多数应用程序应该直接忽略可移动存储。”我不怀疑你是对的。不幸的是,这对我们来说不是一个选择……我们的应用程序都是关于可移动存储的,而且可能超过一半的用户是 4.4 之前的版本。附言感谢您调查此问题。 @LarsH:“所以我不确定你的意思”——对不起,我在想ActivityCompat,它有一个向后兼容的getExternalFilesDirs(),它返回一个元素列表一直在 4.3 及以上版本。 @LarsH:正确。这种情况几乎就是他们添加android:maxSdkVersion的原因。 【参考方案1】:

由于一个应用程序有效而另一个无效,因此差异在于应用程序之间,而不是设备或卡。相关目录不需要任何 Android 权限(例如,WRITE_EXTERNAL_STORAGE)。您无法写入它的唯一原因是Android系统没有正确设置文件系统权限。

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

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

还有其他不信任它的理由吗?

鉴于正在发生的文件系统恶作剧,当开发人员对路径的性质做出假设时,我会非常紧张,仅此而已。

【讨论】:

我将奖励这个答案,除非有人很快提出更好的答案。这个答案已经给了我一个突破。我获得更好答案的标准是告诉我如何提前在 SD 卡上创建和填充应用程序目录(即,在任何手机上安装应用程序之前),或者提供可靠的参考,证明这样的事情不是可能。 @LarsH:我怀疑你能否找到可靠的参考资料来说明这是否可行。 为了清楚起见,供未来的读者参考:确实,一个app 工作而另一个没有(在同一设备和卡上);但同样真实的是,一个 device 工作而另一个没有(对于同一个应用程序),一个 card 工作而另一个没有工作(对于同一个应用程序) .事实证明,不同之处在于卡片:在一张卡片上,实际应用程序(但不是测试应用程序)的应用程序目录是事先手动创建的;而在另一张卡上(在另一台设备上),这两个应用程序目录都是由 Android 自动创建的。无论如何,这是我能说的最好的了。【参考方案2】:

我已经了解了更多关于这个问题的信息,它与 CommonsWare 的回答有很大不同,我认为值得一个新的回答。

让我原来的场景与众不同的是 SD 卡:如果卡上已经有一个/Android/data/com.example.myapp 文件夹不是在此手机上创建的,那么应用程序可能无法获得写入该文件夹的权限。而如果该文件夹不存在,应用程序可以创建它并写入它。至少在 KitKat 及以后。 this article 中解释的内容支持这一点:

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

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

【讨论】:

以上是关于为啥我无权写入外部存储上的应用程序目录?的主要内容,如果未能解决你的问题,请参考以下文章

除非在授予权限后重新启动应用程序,否则无法写入外部存储

为啥我的外部存储在 Android 7 中不可读写 [重复]

MCS-51单片机扩展系统中,片外程序存储器和片外数据存储器共处同一个地址空间,为啥不会发生总线冲突?

为啥 PHPMyAdmin 不能写入我的 AWS Ubuntu 12.04 LTS 实例上的配置目录?

为啥公共可写目录容易受到攻击?

如何从另一个人写入控制台应用程序