在安卓上写入外部SD卡的通用方式

在我的应用程序中,我需要在设备存储中存储大量图像。这样的文件往往满足设备存储,我希望允许用户能够选择外部SD卡作为目标文件夹。

我到处都读到Android不允许用户写入外部SD卡,SD卡是指外部和可安装的SD卡,而不是外部存储,但是文件管理器应用程序设法写入所有Android版本的外部SD。

在不同的API级别(Pre-KitKat,KitKat,Lollipop+)上授予对外部SD卡的读/写访问权限的更好方法是什么?

更新 1

我从Doomknight的答案中尝试了方法1,但无济于事:如您所见,我在尝试在SD上编写之前,正在运行时检查权限:

HashSet<String> extDirs = getStorageDirectories();
for(String dir: extDirs) {
    Log.e("SD",dir);
    File f = new File(new File(dir),"TEST.TXT");
    try {
        if(ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)==PackageManager.PERMISSION_GRANTED) {
            f.createNewFile();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

但是我收到一个访问错误,在两个不同的设备上尝试:HTC10和Shield K1。

10-22 14:52:57.329 30280-30280/? E/SD: /mnt/media_rw/F38E-14F8
10-22 14:52:57.329 30280-30280/? W/System.err: java.io.IOException: open failed: EACCES (Permission denied)
10-22 14:52:57.329 30280-30280/? W/System.err:     at java.io.File.createNewFile(File.java:939)
10-22 14:52:57.329 30280-30280/? W/System.err:     at com.myapp.activities.TestActivity.onResume(TestActivity.java:167)
10-22 14:52:57.329 30280-30280/? W/System.err:     at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1326)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.Activity.performResume(Activity.java:6338)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3336)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3384)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2574)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.access$900(ActivityThread.java:150)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1399)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:102)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.os.Looper.loop(Looper.java:168)
10-22 14:52:57.330 30280-30280/? W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:5885)
10-22 14:52:57.330 30280-30280/? W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
10-22 14:52:57.330 30280-30280/? W/System.err:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:819)
10-22 14:52:57.330 30280-30280/? W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:709)
10-22 14:52:57.330 30280-30280/? W/System.err: Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
10-22 14:52:57.330 30280-30280/? W/System.err:     at libcore.io.Posix.open(Native Method)
10-22 14:52:57.330 30280-30280/? W/System.err:     at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
10-22 14:52:57.330 30280-30280/? W/System.err:     at java.io.File.createNewFile(File.java:932)
10-22 14:52:57.330 30280-30280/? W/System.err:  ... 14 more

答案 1

总结

您可以在不同的 API 级别运行时为 API23+)上授予对外部 SD 卡的读/写访问权限

由于 KitKat,因此,如果您使用特定于应用程序的目录,则不需要权限,否则则需要权限。

通用方式:

历史说,没有通用的方法可以写入外部SD卡,但仍在继续...

这些设备的外部存储配置示例证明了这一事实

基于 API 的方式:

KitKat之前,请尝试使用Doomsknight方法1,否则使用方法2。

在清单(Api < 23)和运行时(Api >= 23)请求权限。

推荐方式:

ContextCompat.getExternalFilesDirs 解决了您不需要共享文件时的访问错误

共享它的安全方法是使用内容提供程序新的存储访问框架

隐私感知方式:

从 Android Q Beta 4 开始,默认情况下,定位到 Android 9(API 级别 28)或更低的应用不会发生任何变化。

默认情况下,系统会向面向 Android Q(或选择加入)的应用提供外部存储的过滤视图


  1. 初步答案。

在安卓上写入外部SD卡的通用方式

由于不断的变化,没有通用的方法可以写入Android上的外部SD卡

  • Pre-KitKat:官方Android平台除了例外之外,根本不支持SD卡。

  • KitKat:引入了允许应用程序访问SD卡上特定于应用程序的目录中的文件的API。

  • 棒棒糖:添加了API,允许应用程序请求访问其他提供商拥有的文件夹。

  • Nougat:提供了一个简化的 API 来访问常见的外部存储目录。

  • ...Android Q 隐私更改:应用范围和媒体范围的存储

在不同 API 级别上授予对外部 SD 卡的读/写访问权限的更好方法是什么?

基于Doomsknight的答案和我的答案,以及Dave SmithMark Murphy的博客文章:123


  1. 更新了答案。

更新 1.我从Doomknight的答案中尝试了方法1,但无济于事:

如您所见,在尝试在SD上写入之前,我正在运行时检查权限...

我会使用特定于应用程序的目录来避免更新问题的问题,并使用getExternalFilesDir文档作为参考。ContextCompat.getExternalFilesDirs()

改进试探法,根据不同的 API 级别(如android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT

...但是我收到一个访问错误,在两个不同的设备上尝试:HTC10和Shield K1。

请记住,Android 6.0 支持便携式存储设备,第三方应用必须通过存储访问框架。您的设备 HTC10Shield K1 可能是 API 23。

您的日志显示访问的权限被拒绝异常,例如 API 19+ 的修复程序:/mnt/media_rw

<permission name="android.permission.WRITE_EXTERNAL_STORAGE" >
<group gid="sdcard_r" />
<group gid="sdcard_rw" />
<group gid="media_rw" /> // this line is added via root in the link to fix it.
</permission>

我从来没有尝试过,所以我不能共享代码,但我会避免尝试写入所有返回的目录,并根据剩余空间寻找最佳的可用存储目录。for

也许Gizm0是你的方法的替代品,它是一个很好的起点。getStorageDirectories()

ContextCompat.getExternalFilesDirs解决了您不需要访问其他文件夹的问题


  1. 安卓 1.0 ..Pre-KitKat.

在KitKat之前,请尝试使用Doomsknight方法1或阅读Gnathonic的响应

public static HashSet<String> getExternalMounts() {
    final HashSet<String> out = new HashSet<String>();
    String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
    String s = "";
    try {
        final Process process = new ProcessBuilder().command("mount")
                .redirectErrorStream(true).start();
        process.waitFor();
        final InputStream is = process.getInputStream();
        final byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            s = s + new String(buffer);
        }
        is.close();
    } catch (final Exception e) {
        e.printStackTrace();
    }

    // parse output
    final String[] lines = s.split("\n");
    for (String line : lines) {
        if (!line.toLowerCase(Locale.US).contains("asec")) {
            if (line.matches(reg)) {
                String[] parts = line.split(" ");
                for (String part : parts) {
                    if (part.startsWith("/"))
                        if (!part.toLowerCase(Locale.US).contains("vold"))
                            out.add(part);
                }
            }
        }
    }
    return out;
}

将下一个代码添加到您的代码,并阅读获取对外部存储的访问权限AndroidManifest.xml

对外部存储的访问受各种Android权限的保护。

从 Android 1.0 开始,写访问权限受WRITE_EXTERNAL_STORAGE权限的保护。

从 Android 4.1 开始,读取访问权限受该权限的保护。READ_EXTERNAL_STORAGE

为了...在外部存储上写入文件,您的应用程序必须获取...系统权限:

<manifest ...>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest> 

如果你需要两者...,你只需要请求权限。WRITE_EXTERNAL_STORAGE

阅读Mark Murphy的解释推荐Dianne HackbornDave Smith的帖子

  • 在Android 4.4之前,Android中没有对可移动媒体的官方支持,从KitKat开始,FMW API中出现了“主”和“辅助”外部存储的概念。
  • 以前的应用仅依赖于 MediaStore 索引,随硬件一起提供或检查装入点,并应用一些试探法来确定什么表示可移动媒体。

  1. Android 4.4 KitKat 引入了 Storage Access Framework (SAF)。

由于错误,请忽略下一个注释,但请尝试使用:ContextCompat.getExternalFilesDirs()

  • 自Android 4.2以来,Google一直要求设备制造商锁定可移动媒体以确保安全性(多用户支持),并在4.4中添加了新的测试。
  • 由于添加了 KitKat 和其他方法以返回所有可用存储卷上的可用路径(返回的第一项是主卷)。getExternalFilesDirs()
  • 下表显示了开发人员可能会尝试执行的操作以及 KitKat 将如何响应:在此输入图像描述

注意:从 Android 4.4 开始,如果您仅读取或写入应用私有文件,则不需要这些权限。有关详细信息...,请参阅保存应用专用文件

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
</manifest>

另请阅读Paolo Rovelli的解释,并尝试使用Jeff Sharkey的解决方案,因为KitKat:

在KitKat中,现在有一个公共API,用于与这些辅助共享存储设备进行交互。

新的 和 方法可以返回多个路径,包括主设备和辅助设备。Context.getExternalFilesDirs()Context.getExternalCacheDirs()

然后,您可以循环访问它们并检查并确定存储文件的最佳位置。Environment.getStorageState()File.getFreeSpace()

这些方法在 support-v4 库中的 ContextCompat 上也可用。

从 Android 4.4 开始,外部存储设备上文件的所有者、组和模式现在根据目录结构进行合成。这使应用能够在外部存储上管理其特定于程序包的目录,而无需它们拥有广泛的权限。例如,具有程序包名称的应用现在可以在没有权限的外部存储设备上自由访问。这些合成权限是通过将原始存储设备包装在 FUSE 守护程序中来完成的。WRITE_EXTERNAL_STORAGEcom.example.fooAndroid/data/com.example.foo/

使用KitKat,您获得“完整解决方案”而无需生根的机会几乎为零:

Android项目在这里肯定搞砸了。没有应用可以完全访问外部 SD 卡:

  • 文件管理器:您不能使用它们来管理外部SD卡。在大多数地区,他们只能读,不能写。
  • 媒体应用:您无法再重新标记/重新组织媒体收藏,因为这些应用无法写入其中。
  • 办公应用:几乎相同

唯一允许3rd方应用程序在外部卡上写入的地方是“他们自己的目录”(即)。/sdcard/Android/data/<package_name_of_the_app>

真正修复需要制造商(其中一些修复它,例如华为为P6更新Kitkat)的唯一方法 - 或者root...(Izzy的解释在这里继续)


  1. Android 5.0 引入了更改和 DocumentFile 帮助程序类。

getStorageState在 API 19 中添加,在 API 21 中已弃用,使用 getExternalStorageState(File)

这是一个很棒的教程,用于与 KitKat 中的存储访问框架进行交互。

与棒棒糖中的新API进行交互非常相似(Jeff Sharkey的解释)。


  1. Android 6.0 Marshmallow 引入了新的运行时权限模型。

如果 API 级别为 23+,则在运行时请求权限,并在运行时读取请求权限

从Android 6.0(API级别23)开始,用户在应用程序运行时向应用程序授予权限,而不是在安装应用程序时授予权限。或更新应用程序...用户可以撤消权限。

// Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
        Manifest.permission.WRITE_EXTERNAL_STORAGE);

Android 6.0 引入了新的运行时权限模型,应用可在运行时在需要时请求功能。由于新模型包含权限,因此平台需要动态授予存储访问权限,而不会终止或重新启动已运行的应用。它通过维护所有已装载存储设备的三个不同视图来实现此目的:READ/WRITE_EXTERNAL_STORAGE

  • /mnt/runtime/default 向没有特殊存储权限的应用显示...
  • /mnt/runtime/read 向具有READ_EXTERNAL_STORAGE
  • /mnt/runtime/write 向具有WRITE_EXTERNAL_STORAGE

  1. Android 7.0 提供了一个简化的 API 来访问外部存储 dirs。

作用域目录访问在 Android 7.0 中,应用可以使用新的 API 来请求访问特定的外部存储目录,包括可移动介质(如 SD 卡)上的目录。

有关详细信息,请参阅作用域目录访问培训

阅读 Mark Murphy 的文章:小心作用域目录访问它在Android Q中被弃用

请注意,在 7.0 中添加的作用域目录访问在 Android Q 中已弃用。

具体来说,已弃用 StorageVolume 上的方法。createAccessIntent()

他们添加了一个可以用作替代方案的。createOpenDocumentTreeIntent()


  1. 安卓 8.0 奥利奥 ..Android Q Beta 更改。

从 Android O 开始,存储访问框架允许自定义文档提供程序为驻留在远程数据源中的文件创建可搜索的文件描述符...

权限在 Android O 之前,如果应用在运行时请求权限并授予了该权限,则系统还会错误地向该应用授予属于同一权限组并在清单中注册的其余权限。

对于定位到 Android O 的应用,此行为已得到纠正。仅向应用授予它已显式请求的权限。但是,一旦用户向应用授予权限,将自动授予对该权限组中的所有后续权限请求。

例如,并且...READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE

更新:Android Q 早期的 Beta 版暂时将 和 权限替换为更细粒度、特定于媒体的权限。READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE

注意:Google在Beta 1上引入了角色,并在Beta 2之前将其从文档中删除...

注意:在早期 beta 版本中引入的特定于媒体集合的权限—, , 和 — 现已过时更多信息:READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEO

Q Beta 4(最终 API)由 Mark Murphy 评测:The Death of External Storage: The End of the Saga(?)

“死亡比生命更普遍。每个人都会死,但不是每个人都活着。


  1. 相关问题和推荐答案。

如何获取安卓4.0+的外部SD卡路径?

mkdir() 在内部闪存存储中工作,但不是SD卡?

getExternalFilesDir 和 getExternalStorageDirectory() 之间的差异

为什么 getExternalFilesDirs() 在某些设备上不起作用?

如何使用为 Android 5.0(棒棒糖)提供的新 SD 卡访问 API

写入 Android 5.0 及更高版本中的外部 SD 卡

Android SD 卡写入权限使用 SAF(存储访问框架)

SAFFAQ:存储访问框架常见问题解答


  1. 相关的错误和问题。

Bug:在Android 6上,当使用getExternalFilesDirs时,它不会让你在其结果中创建新文件

在没有写入权限的情况下,写入 getExternalCacheDir() 在 Lollipop 上返回的目录失败


答案 2

我相信有两种方法可以实现这一目标:

方法 1:(由于权限更改,不适用于 6.0 及更高版本)

多年来,我一直在许多设备版本上使用此方法,没有任何问题。功劳归于原始来源,因为不是我写的。

它将在字符串目录位置列表中返回所有已装载的媒体(包括真正的SD卡)。使用列表,您可以询问用户保存位置等。

您可以通过以下方式调用它:

 HashSet<String> extDirs = getStorageDirectories();

方法:

/**
 * Returns all the possible SDCard directories
 */
public static HashSet<String> getStorageDirectories() {
    final HashSet<String> out = new HashSet<String>();
    String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
    String s = "";
    try {
        final Process process = new ProcessBuilder().command("mount")
                .redirectErrorStream(true).start();
        process.waitFor();
        final InputStream is = process.getInputStream();
        final byte[] buffer = new byte[1024];
        while (is.read(buffer) != -1) {
            s = s + new String(buffer);
        }
        is.close();
    } catch (final Exception e) {
        e.printStackTrace();
    }

    // parse output
    final String[] lines = s.split("\n");
    for (String line : lines) {
        if (!line.toLowerCase().contains("asec")) {
            if (line.matches(reg)) {
                String[] parts = line.split(" ");
                for (String part : parts) {
                    if (part.startsWith("/"))
                        if (!part.toLowerCase().contains("vold"))
                            out.add(part);
                }
            }
        }
    }
    return out;
}

方法 2:

使用 v4 支持库

import android.support.v4.content.ContextCompat;

只需调用以下命令即可获取存储位置的列表。File

 File[] list = ContextCompat.getExternalFilesDirs(myContext, null);

但是,这些位置的用法有所不同。

返回应用程序可以放置其拥有的永久文件的所有外部存储设备上特定于应用程序的目录的绝对路径。这些文件位于应用程序内部,通常不作为媒体对用户可见。

此处返回的外部存储设备被视为设备的永久部分,包括模拟的外部存储和物理媒体插槽,例如电池仓中的 SD 卡。返回的路径不包括瞬态设备,如 USB 闪存驱动器。

应用程序可以将数据存储在任何或所有返回的设备上。例如,应用可能会选择在具有最多可用空间的设备上存储大文件。

有关上下文比较的更多信息

它们就像应用程序特定的文件。对其他应用隐藏。


推荐