Java 在 Android 上写入外部 SD 卡的通用方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/40068984/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 21:41:47  来源:igfitidea点击:

Universal way to write to external SD card on Android

javaandroidandroid-sdcardandroid-external-storage

提问by Vektor88

In my application, I need to store lots of images in the device storage. Such files tend to fulfill the device storage, and I want to allow users to be able to choose external SD card as the destination folder.

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

I read everywhere that Android doesn't allow users to write to external SD card, by SD card I mean the external and mountable SD card and not the external storage, but file manager applications manage to write to External SD on all Android versions.

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

What is the better way to grant read/write access to external SD card on different API levels (Pre-KitKat, KitKat, Lollipop+)?

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

Update 1

更新 1

I tried Method 1 from Doomknight's answer, with no avail: As you can see I'm checking for permissions at runtime before attempting to write on SD:

我尝试了 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();
    }
}

But I get an access error, tried on two different devices: HTC10 and Shield K1.

但是我收到访问错误,在两种不同的设备上尝试过: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.access0(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

采纳答案by albodelu

Summary

概括

You can grant read/write accessto external SD card on the different api levels(API23+ at run time).

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

Since KitKat,permissions are not necessaryif you use app-specific directories, requiredotherwise.

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

Universal way:

通用方式:

The historysays that there is not universal way to write to external SD cardbut continues...

历史上说,没有写入外部SD卡普遍的方式,但继续...

This factis demostrated by these examples of external storage configurations for devices.

这一事实是通过实例阐述的设备外部存储配置的这些实施例

API-based way:

基于API的方式:

Prior to KitKattry to use Doomsknight method 1, method 2 otherwise.

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

Request permissions in manifest(Api < 23) and at run time(Api >= 23).

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

Recommended way:

推荐方式:

ContextCompat.getExternalFilesDirssolves the access errorwhen you don't need to share files.

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

The secure way of sharing it is to use a content provideror the new Storage Access Framework.

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

Privacy-aware way:

隐私意识方式:

As of Android Q Beta 4, apps that target Android 9 (API level 28) or lower see no change, by default.

从 Android Q Beta 4 开始,默认情况下,面向 Android 9(API 级别 28)或更低版本的应用没有任何变化。

Apps targeting Android Q by default (or opting into it) are given a filtered viewinto external storage.

默认情况下(或选择加入)以 Android Q 为目标的应用程序会获得外部存储的过滤视图



1. Initial answer.

1.初步回答。

Universal way to write to external SD card on Android

在 Android 上写入外部 SD 卡的通用方法

There is no universal wayto write to external SD card on Androiddue to continuous changes:

有没有普遍的方式写,在Android外置SD卡,由于连续的变化

  • Pre-KitKat: official Android platform has not supported SD cards at all except for exceptions.

  • KitKat: introduced APIs that let apps access files in app-specific directories on SD cards.

  • Lollipop: added APIs to allow apps to request access to folders owned by other providers.

  • Nougat: provided a simplified API to access common external storage directories.

  • ... Android Q privacy change: App-scoped and media-scoped storage

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

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

  • Lollipop:添加 API 以允许应用请求访问其他提供商拥有的文件夹。

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

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

What is the better way to grant read/write access to external SD card on different API levels

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

Based on Doomsknight's answerand mine, and Dave Smithand Mark Murphy blog posts: 1, 2, 3:

基于Doomsknight的回答我的,和戴夫·史密斯马克·墨菲博客文章:123



2. Updated answer.

2. 更新答案。

Update 1. I tried Method 1 from Doomknight's answer, with no avail:

As you can see I'm checking for permissions at runtime before attempting to write on SD...

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

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

I would use application-specific directories to avoid the issueof your updated question and ContextCompat.getExternalFilesDirs()using getExternalFilesDir documentationas reference.

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

Improve the heuristicsto determine what represents removable media based on the different api levels like android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT

改进启发式方法,根据不同的 api 级别确定什么代表可移动媒体,例如android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT

... But I get an access error, tried on two different devices: HTC10 and Shield K1.

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

Remember that Android 6.0 supports portable storage devicesand third-party apps must go through the Storage Access Framework. Your devices HTC10and Shield K1are probably API 23.

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

Your log shows a permission denied exceptionaccessing /mnt/media_rw, like this fixfor API 19+:

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

<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>

I never tried it so I can not share code but I would avoid the fortrying to write on all the returned directories and look for the best available storage directory to write into based on remaining space.

我从未尝试过,因此无法共享代码,但我会避免for尝试写入所有返回的目录,并根据剩余空间寻找要写入的最佳可用存储目录

Perhaps Gizm0's alternativeto your getStorageDirectories()method it's a good starting point.

也许Gizm0 可以替代您的getStorageDirectories()方法,这是一个很好的起点。

ContextCompat.getExternalFilesDirssolves the issueif you don't need access to other folders.

ContextCompat.getExternalFilesDirs如果您不需要访问其他文件夹,则可以解决此问题



3. Android 1.0 .. Pre-KitKat.

3. Android 1.0 .. Pre-KitKat。

Prior to KitKat try to use Doomsknight method 1or read this responseby Gnathonic.

在 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;
}

Add the next code to your AndroidManifest.xmland read Getting access to external storage

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

Access to external storage is protected by various Android permissions.

Starting in Android 1.0, write access is protected with the WRITE_EXTERNAL_STORAGEpermission.

Starting in Android 4.1, read access is protected with the READ_EXTERNAL_STORAGEpermission.

In order to ... write files on the external storage, your app must acquire ... system permissions:

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

If you need to both..., you need to request only the WRITE_EXTERNAL_STORAGEpermission.

对外部存储的访问受到各种 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许可。

Read Mark Murphy's explanationand recommendedDianne Hackbornand Dave Smithposts

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

  • Until Android 4.4, there was no official support for removable media in Android, Starting in KitKat, the concept of “primary” and “secondary” external storage emerges in the FMW API.
  • Prior apps are just relying on MediaStore indexing, ship with the hardware or examine mount points and apply some heuristics to determine what represents removable media.
  • 直到 Android 4.4,Android 还没有官方支持可移动媒体,从 KitKat 开始,FMW API 中出现了“主要”和“次要”外部存储的概念。
  • 以前的应用程序仅依赖于 MediaStore 索引、随硬件一起提供或检查挂载点并应用一些启发式方法来确定什么代表可移动媒体。


4. Android 4.4 KitKat introduces the Storage Access Framework (SAF).

4. Android 4.4 KitKat 引入了存储访问框架 (SAF)

Ignore the next note due to bugs, but try to use ContextCompat.getExternalFilesDirs():

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

  • Since Android 4.2, there has been a request from Google for device manufacturers to lock down removable media for security (multi-user support) and new tests were added in 4.4.
  • Since KitKat getExternalFilesDirs()and other methods were added to return a usable path on all available storage volumes (The first item returned is the primary volume).
  • The table below indicates what a developer might try to do and how KitKat will respond: enter image description here

Note:Beginning with Android 4.4, these permissions are not required if you're reading or writing only files that are private to your app. For more info..., see saving files that are app-private.

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
</manifest>
  • 自 Android 4.2 以来,Google 一直要求设备制造商锁定可移动媒体以确保安全(多用户支持),并且在 4.4 中添加了新的测试。
  • 由于getExternalFilesDirs()添加了KitKat和其他方法来返回所有可用存储卷上的可用路径(返回的第一项是主卷)。
  • 下表说明了开发人员可能会尝试做什么以及 KitKat 将如何响应: 在此处输入图片说明

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

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

Also read Paolo Rovelli's explanationand try to use Jeff Sharkey's solutionsince KitKat:

另请阅读Paolo Rovelli 的解释并尝试使用自 KitKat 以来Jeff Sharkey 的解决方案

In KitKat there's now a public API for interacting with these secondary shared storage devices.

The new Context.getExternalFilesDirs()and Context.getExternalCacheDirs()methods can return multiple paths, including both primary and secondary devices.

You can then iterate over them and check Environment.getStorageState()and File.getFreeSpace()to determine the best place to store your files.

These methods are also available on ContextCompatin the support-v4 library.

Starting in Android 4.4, the owner, group and modes of files on external storage devices are now synthesized based on directory structure. This enables apps to manage their package-specific directories on external storage without requiring they hold the broad WRITE_EXTERNAL_STORAGEpermission. For example, the app with package name com.example.foocan now freely access Android/data/com.example.foo/on external storage devices with no permissions. These synthesized permissions are accomplished by wrapping raw storage devices in a FUSE daemon.

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

newContext.getExternalFilesDirs()Context.getExternalCacheDirs()方法可以返回多个路径,包括主要和次要设备。

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

ContextCompatsupport-v4 库中也提供了这些方法。

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

With KitKat your chances for a "complete solution" without rooting are pretty much zero:

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

The Android project has definitely screwed up here. No apps get full access to external SD cards:

  • file managers:you cannot use them to manage your external SD card. In most areas, they can only read but not write.
  • media apps:you cannot retag/re-organize your media collection any longer, as those apps cannot write to it.
  • office apps:pretty much the same

The only place 3rdparty apps are allowed to write on your external card are "their own directories" (i.e. /sdcard/Android/data/<package_name_of_the_app>).

The only ways to really fix that require either the manufacturer (some of them fixed it, e.g. Huawei with their Kitkat update for the P6) – or root... (Izzy's explanation continues here)

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

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

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

真正解决这个问题的唯一方法需要制造商(其中一些人修复它,例如华为通过他们的 P6 Kitkat 更新) – 或 root ...... (Izzy 的解释在这里继续)



5. Android 5.0 introduced changes and the DocumentFilehelper class.

5. Android 5.0 引入了更改和DocumentFile帮助器类。

getStorageStateAdded in API 19, deprecated in API 21, use getExternalStorageState(File)

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

Here's a great tutorialfor interacting with the Storage Access Framework in KitKat.

Interacting with the new APIs in Lollipop is very similar (Jeff Sharkey's explanation).

这是与 KitKat 中的存储访问框架交互的很棒的教程

与 Lollipop 中的新 API 交互非常相似(Jeff Sharkey 的解释)



6. Android 6.0 Marshmallow introduces a new runtime permissionsmodel.

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

Request permissionsat runtime if API level 23+ and read Requesting Permissions at Run Time

请求权限在运行时,如果API级别23+和阅读在运行时请求权限

Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app ... or update the app ... user can revoke the permissions.

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

Android 6.0 introduces a new runtime permissionsmodel where apps request capabilities when needed at runtime. Because the new model includes the READ/WRITE_EXTERNAL_STORAGEpermissions, the platform needs to dynamically grant storage access without killing or restarting already-running apps. It does this by maintaining three distinct views of all mounted storage devices:

  • /mnt/runtime/default is shown to apps with no special storage permissions...
  • /mnt/runtime/read is shown to apps with READ_EXTERNAL_STORAGE
  • /mnt/runtime/write is shown to apps with WRITE_EXTERNAL_STORAGE

从 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 的应用程序


7. Android 7.0 provides a simplified API to access external storage dirs.

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

Scoped Directory AccessIn Android 7.0, apps can use new APIs to request access to specific external storagedirectories, including directories on removable media such as SD cards...

For more information, see the Scoped Directory Access training.

范围目录访问在 Android 7.0 中,应用程序可以使用新的 API 来请求访问特定的 外部存储目录,包括可移动媒体(如 SD 卡)上的目录...

有关详细信息,请参阅范围目录访问培训

Read Mark Murphy posts: Be Careful with Scoped Directory Access. It was deprecated in Android Q:

阅读 Mark Murphy 的帖子:小心 Scoped Directory Access它在 Android Q 中已被弃用

Notethat the scoped directory access added in 7.0 is deprecated in Android Q.

Specifically, the createAccessIntent()method on StorageVolumeis deprecated.

They added a createOpenDocumentTreeIntent()that can be used as an alternative.

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

具体来说,不推荐使用StorageVolumecreateAccessIntent()上的方法。

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



8. Android 8.0 Oreo .. Android Q Beta changes.

8. Android 8.0 Oreo .. Android Q Beta 更改。

Starting in Android O, the Storage Access Framework allows custom documents providersto create seekable file descriptors for files residing in a remote data source...

Permissions, prior to Android O, if an app requested a permission at runtime and the permission was granted, the system also incorrectly granted the app the rest of the permissions that belonged to the same permission group, and that were registered in the manifest.

For apps targeting Android O, this behavior has been corrected. The app is granted only the permissions it has explicitly requested. However, once the user grants a permission to the app, all subsequent requests for permissions in that permission group are automatically granted.

For example, READ_EXTERNAL_STORAGEand WRITE_EXTERNAL_STORAGE...

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

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

对于面向 Android O 的应用程序,此行为已得到纠正。该应用程序仅被授予它明确请求的权限。但是,一旦用户向应用授予权限,该权限组中的所有后续权限请求都会自动授予。

例如,READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE...

Update:An Android Q earlier beta release temporarily replaced the READ_EXTERNAL_STORAGEand WRITE_EXTERNAL_STORAGEpermissions with more fine-grained, media-specific permissions.

更新:Android Q 早期测试版暂时用更细粒度的特定媒体权限替换了READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE权限。

Note:Google introduced roleson Beta 1 and removed them from the documentation before Beta 2...

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

Note:The permissions specific to media collections that were introduced in earlier beta releases—READ_MEDIA_IMAGES, READ_MEDIA_AUDIO, and READ_MEDIA_VIDEOare now obsolete. More info:

注:该权限具体到媒体集合在早期测试releases-中引入READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEO-现已过时更多信息:

Q Beta 4 (final APIs) review by Mark Murphy: The Death of External Storage: The End of the Saga(?)

Mark Murphy 的 Q Beta 4(最终 API)评论:外部存储的死亡:传奇的终结(?)

"Death is more universal than life. Everyone dies, but not everyone lives." ― Andrew Sachs

“死亡比生命更普遍。每个人都会死,但不是每个人都活着。” ——安德鲁·萨克斯



9. Related questions and recommended answers.

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

How can I get external SD card path for Android 4.0+?

如何获取适用于 Android 4.0+ 的外部 SD 卡路径?

mkdir() works while inside internal flash storage, but not SD card?

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

Diff between getExternalFilesDir and getExternalStorageDirectory()

getExternalFilesDir 和 getExternalStorageDirectory() 之间的差异

Why getExternalFilesDirs() doesn't work on some devices?

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

How to use the new SD card access API presented for Android 5.0 (Lollipop)

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

Writing to external SD card in Android 5.0 and above

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

Android SD Card Write Permission using SAF (Storage Access Framework)

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

SAFFAQ: The Storage Access Framework FAQ

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



10. Related bugs and issues.

10.相关错误和问题。

Bug: On Android 6, when using getExternalFilesDirs, it won't let you create new files in its results

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

Writing to directory returned by getExternalCacheDir() on Lollipop fails without write permission

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

回答by Geet Choubey

For versions below Marshmallow you can directly give the permissions in the manifest.

对于低于 Marshmallow 的版本,您可以直接在清单中授予权限。

But for devices with Marshmallow and above you need to grant the permissions on run time.

但是对于带有 Marshmallow 及以上版本的设备,您需要在运行时授予权限。

By using

通过使用

Environment.getExternalStorageDirectory();

you can directly access the External SD card (Mounted one) Hope this helps.

您可以直接访问外部 SD 卡(已安装)希望这会有所帮助。

回答by Doomsknight

I believe there are two methods to achieve this:

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

METHOD 1:(does NOTwork on 6.0 and above, due to permission changes)

方法1: (不上工作6.0及以上,由于权限更改)

I have been using this method for years on many device version with no issue. Credit is due to the original source, as it was not me who wrote it.

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

It will return all mounted media (including Real SD Cards)in a list of strings directory locations. With the list you can then ask the user where to save, etc.

它将在字符串目录位置列表中返回所有已安装的媒体(包括真正的 SD 卡)。有了这个列表,你就可以询问用户在哪里保存,等等。

You can call it with the following:

您可以使用以下命令调用它:

 HashSet<String> extDirs = getStorageDirectories();

Method:

方法:

/**
 * 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;
}

METHOD 2:

方法二:

Use the v4 support library

使用 v4 支持库

import android.support.v4.content.ContextCompat;

Just call the following to get a list of Filelocations of storage.

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

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

The locations differ in usage however though.

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

Returns absolute paths to application-specific directories on all external storage devices where the application can place persistent files it owns. These files are internal to the application, and not typically visible to the user as media.

External storage devices returned here are considered a permanent part of the device, including both emulated external storage and physical media slots, such as SD cards in a battery compartment. The returned paths do not include transient devices, such as USB flash drives.

An application may store data on any or all of the returned devices. For example, an app may choose to store large files on the device with the most available space

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

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

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

More Info on ContextCompat

有关 ContextCompat 的更多信息

They are like app specific files. Hidden from other apps.

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

回答by Hugo

Here is a way of creating a new file in the External Storage (SDCard if present in the device or device External Storage if not). Just replace "foldername" with the name of your desired destination folder and "filename" with the name of the file you are saving. Of course here you can see how to save a generic File, now you can search for how to save images maybe hereor whatever in a file.

这是在外部存储中创建新文件的方法(如果设备中存在 SDCard,否则设备外部存储中存在)。只需将“文件夹名”替换为所需目标文件夹的名称,将“文件名”替换为要保存的文件名。当然,在这里您可以看到如何保存通用文件,现在您可以搜索如何在此处或文件中的任何内容保存图像。

try {
            File dir =  new File(Environment.getExternalStorageDirectory() + "/foldername/");
            if (!dir.exists()){
                dir.mkdirs();
            }
            File sdCardFile = new File(Environment.getExternalStorageDirectory() + "/foldername/" + fileName );
            int num = 1;
            String fileNameAux = fileName;
            while (sdCardFile.exists()){
                fileNameAux = fileName+"_"+num;
                sdCardFile = new File(Environment.getExternalStorageDirectory() + "/foldername/" + fileNameAux);
                num++;
            }

This also controls that file exists and adds a number in the end of the name of the new file to save it.

这也控制该文件是否存在并在新文件的名称末尾添加一个数字以保存它。

Hope it helps!

希望能帮助到你!

EDIT: Sorry, i forgot you have to ask for <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />in your manifest (or programatically if you prefer from Marshmallow)

编辑:对不起,我忘了你必须<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />在你的清单中要求(或者如果你喜欢棉花糖的话,可以通过编程方式)

回答by ????????

Just another answer. This answer only shows 5.0+ because I believe Doomknight's answer posted here is the best way to do for Android 4.4 and below.

只是另一个答案。此答案仅显示 5.0+,因为我相信此处发布的 Doomknight 的答案是适用于 Android 4.4 及以下版本的最佳方法。

This is originally posted here (Is there a way to get SD Card size in Android?) by me to get the external SD Card's size on Android 5.0+

这最初发布在这里(有没有办法在 Android 中获取 SD 卡大小?)由我在 Android 5.0+ 上获取外部 SD 卡的大小

To get the External SD card as a File:

要将外部 SD 卡作为File

public File getExternalSdCard() {
    File externalStorage = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        File storage = new File("/storage");

        if(storage.exists()) {
            File[] files = storage.listFiles();

            for (File file : files) {
                if (file.exists()) {
                    try {
                        if (Environment.isExternalStorageRemovable(file)) {
                            externalStorage = file;
                            break;
                        }
                    } catch (Exception e) {
                        Log.e("TAG", e.toString());
                    }
                }
            }
        }
    } else {
        // do one of many old methods
        // I believe Doomsknight's method is the best option here
    }

    return externalStorage;
}

Note: I only get the "first" external sd card however you can modify it and return ArrayList<File>instead of Fileand let the loop continue instead of calling breakafter the first one is found.

注意:我只得到“第一个”外部 SD 卡,但是您可以修改它并返回ArrayList<File>而不是File让循环继续而不是break在找到第一个后调用。

回答by maytham-???????

In addition to all other nice answers, I could add a bit more to this question so it can give wider coverage for readers. In my answer here, I would use 2 countable resources to present External Storage.

除了所有其他不错的答案之外,我还可以为这个问题添加更多内容,以便为读者提供更广泛的覆盖范围。在我的回答中,我将使用 2 个可数的资源来呈现外部存储。

The first resource is from Android Programming, The Big Nerd Ranch Guide 2nd edition, chapter 16, page 294.

第一个资源来自Android Programming,The Big Nerd Ranch Guide 2nd edition,第 16 章,第 294 页。

The book describes the basic and external file and directory methods. I will try to make a resume of what could be relevant to your question.

本书介绍了基本的和外部的文件和目录方法。我将尝试制作一份可能与您的问题相关的简历。

The following part from the book:

书中的以下部分:

External Storage

外置储存

Your photo needs more than a place on the screen. Full-size pictures are too large to stick inside a SQLite database, much less an Intent. They will need a place to live on your device's filesystem. Normally, you would put them in your private storage. Recall that you used your private storage to save your SQLite database. With methods like Context.getFileStreamPath(String)and Context.getFilesDir(), you can do the same thing with regular files, too (which will live in a subfolder adjacent to the databases subfolder your SQLite database lives in)

您的照片不仅需要在屏幕上占据一席之地。全尺寸图片太大而无法放入 SQLite 数据库中,更不用说Intent. 他们需要在你设备的文件系统上有一个地方。通常,您会将它们放在您的私人存储中。回想一下,您使用私有存储来保存 SQLite 数据库。使用Context.getFileStreamPath(String)and 之类的 方法Context.getFilesDir(),您也可以对常规文件执行相同的操作(这些文件将位于与 SQLite 数据库所在的数据库子文件夹相邻的子文件夹中)

Basic file and directory methods in Context

Context 中的基本文件和目录方法

| Method                                                                                |
|---------------------------------------------------------------------------------------|
|File getFilesDir()                                                                      |
| - Returns a handle to the directory for private application files.                    |
|                                                                                       |
|FileInputStream openFileInput(String name)                                             |
| - Opens an existing file for input (relative to the files directory).                 |
|                                                                                       |
|FileOutputStream openFileOutput(String name, int mode)                                 |
| - Opens a file for output, possibly creating it (relative to the files directory).    |
|                                                                                       |
|File getDir(String name, int mode)                                                     |
| - Gets (and possibly creates) a subdirectory within the files directory.              |
|                                                                                       |
|String[] fileList()                                                                    |
| - Gets a list of file names in the main files directory, such as for use with         |
|   openFileInput(String).                                                              |
|                                                                                       |
|File getCacheDir()                                                                     |
| - Returns a handle to a directory you can use specifically for storing cache files.   |
|   You should take care to keep this directory tidy and use as little space as possible|

If you are storing files that only your current application needs to use, these methods are exactly what you need.

如果您正在存储只有当前应用程序需要使用的文件,那么这些方法正是您所需要的。

On the other hand, if you need another application to write to those files, you are out of luck: while there is a Context.MODE_WORLD_READABLEflag you can pass in to openFileOutput(String, int), it is deprecated, and not completely reliable in its effects on newer devices. If you are storing files to share with other apps or receiving files from other apps (files like stored pictures), you need to store them on external storage instead.

另一方面,如果您需要另一个应用程序来写入这些文件,那么您就不走运了:虽然有一个Context.MODE_WORLD_READABLE标志可以传递给openFileOutput(String, int),但它已被弃用,并且其对较新设备的影响并不完全可靠。如果您要存储文件以与其他应用程序共享或从其他应用程序接收文件(如存储图片的文件),则需要将它们存储在外部存储上。

There are two kinds of external storage: primary, and everything else. All Android devices have at least one location for external storage: the primary location, which is located in the folder returned by Environment.getExternalStorageDirectory(). This may be an SD card, but nowadays it is more commonly integrated into the device itself. Some devices may have additional external storage. That would fall under “everything else.”

有两种外部存储:主存储和其他所有存储。所有 Android 设备都至少有一个外部存储位置:主位置,位于 Environment.getExternalStorageDirectory(). 这可能是一个 SD 卡,但现在它更常见地集成到设备本身中。某些设备可能有额外的外部存储。那将属于“其他一切”。

Context provides quite a few methods for getting at external storage, too. These methods provide easy ways to get at your primary external storage, and kinda-sorta-easy ways to get at everything else. All of these methods store files in publicly available places, too, so be careful with them.

Context 也提供了很多获取外部存储的方法。这些方法提供了获取主要外部存储的简单方法,以及获取其他所有内容的有点简单的方法。所有这些方法也将文件存储在公开可用的位置,因此请小心使用它们。

External file and directory methods in Context

Context 中的外部文件和目录方法

| Method                                                                                |
| --------------------------------------------------------------------------------------|
|File getExternalCacheDir()                                                             |
| - Returns a handle to a cache folder in primary external storage. Treat it like you do|
|   getCacheDir(), except a little more carefully. Android is even less likely to clean |
|   up this folder than the private storage one.                                        |
|                                                                                       |
|File[] getExternalCacheDirs()                                                          |
| - Returns cache folders for multiple external storage locations.                      |
|                                                                                       |
|File getExternalFilesDir(String)                                                       |
| - Returns a handle to a folder on primary external storage in which to store regular  |
|   files. If you pass in a type String, you can access a specific subfolder dedicated  |
|   to a particular type of content. Type constants are defined in Environment, where   |
|   they are prefixed with DIRECTORY_.                                                  |
|   For example, pictures go in Environment.DIRECTORY_PICTURES.                         |
|                                                                                       |
|File[] getExternalFilesDirs(String)                                                    |
| - Same as getExternalFilesDir(String), but returns all possible file folders for the  |
|   given type.                                                                         |
|                                                                                       |
|File[] getExternalMediaDirs()                                                          |
| - Returns handles to all the external folders Android makes available for storing     |
|   media – pictures, movies, and music. What makes this different from calling         |
|   getExternalFilesDir(Environment.DIRECTORY_PICTURES) is that the media scanner       |
|   automatically scans this folder. The media scanner makes files available to         |
|   applications that play music, or browse movies and photos, so anything that you     |
|   put in a folder returned by getExternalMediaDirs() will automatically appear in     |
|   those apps.                                                                         |

Technically, the external folders provided above may not be available, since some devices use a removable SD card for external storage. In practice this is rarely an issue, because almost all modern devices have nonremovable internal storage for their “external” storage. So it is not worth going to extreme lengths to account for it. But we do recommended including simple code to guard against the possibility, which you will do in a moment.

从技术上讲,上面提供的外部文件夹可能不可用,因为某些设备使用可移动 SD 卡进行外部存储。在实践中,这很少成为问题,因为几乎所有现代设备都具有不可移动的内部存储作为其“外部”存储。因此,不值得花大力气来解释它。但是我们确实建议包含简单的代码来防止这种可能性,您稍后会这样做。

External storage permission

外部存储权限

In general, you need a permission to write or read from external storage. Permissions are well-known string values you put in your manifest using the <uses-permission>tag. They tell Android that you want to do something that Android wants you to ask permission for.

通常,您需要获得从外部存储写入或读取的权限。权限是您使用<uses-permission>标签放入清单中的众所周知的字符串值。他们告诉 Android 你想做一些 Android 想让你征得许可的事情。

Here, Android expects you to ask permission because it wants to enforce some accountability. You tell Android that you need to access external storage, and Android will then tell the user that this is one of the things your application does when they try to install it. That way, nobody is surprised when you start saving things to their SD card.

在这里,Android 希望您征得许可,因为它想要强制执行某些责任。你告诉 Android 你需要访问外部存储,然后 Android 会告诉用户这是你的应用程序在尝试安装时所做的事情之一。这样,当您开始将内容保存到他们的 SD 卡时,没有人会感到惊讶。

In Android 4.4, KitKat, they loosened this restriction. Since Context.getExternalFilesDir(String)returns a folder that is specific to your app, it makes sense that you would want to be able to read and write files that live there. So on Android 4.4 (API 19) and up, you do not need this permission for this folder. (But you still need it for other kinds of external storage.)

在 Android 4.4 KitKat 中,他们放宽了这一限制。由于Context.getExternalFilesDir(String)返回一个特定于您的应用程序的文件夹,因此您希望能够读取和写入位于其中的文件是有道理的。因此,在 Android 4.4 (API 19) 及更高版本上,您不需要此文件夹的此权限。(但您仍然需要它用于其他类型的外部存储。)

Add a line to your manifest that requests the permission to read external storage, but only up to API Listing 16.5 Requesting external storage permission (AndroidManifest.xml)

在清单中添加一行请求读取外部存储的权限,但仅限于 API 清单 16.5 请求外部存储权限 ( AndroidManifest.xml)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.bignerdranch.android.criminalintent" >
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
         android:maxSdkVersion="18" />

The maxSdkVersion attribute makes it so that your app only asks for this permission on versions of Android that are older than API 19, Android KitKat. Note that you are only asking to read external storage. There is also a WRITE_EXTERNAL_STORAGEpermission, but you do not need it. You will not be writing anything to external storage: The camera app will do that for you

maxSdkVersion 属性使您的应用程序仅在早于 API 19,Android KitKat 的 Android 版本上请求此权限。请注意,您只是要求读取外部存储。还有一个WRITE_EXTERNAL_STORAGE权限,但你不需要它。您不会向外部存储写入任何内容:相机应用程序会为您做到这一点

The second resource is this linkread all of it, but you can also jump to Using the External Storagesection.

第二个资源是这个链接阅读所有内容,但您也可以跳转到使用外部存储部分。

Reference:

参考:

More reading stuff:

更多阅读内容:

Disclaimer:This information was taken from Android Programming: The Big Nerd Ranch Guide with permission from the authors. For more information on this book or to purchase a copy, please visit bignerdranch.com.

免责声明:此信息取自 Android 编程:经作者许可的大书呆子牧场指南。有关本书的更多信息或购买副本,请访问 bignerdranch.com。

回答by Václav Hodek

This topic is a bit old but I was looking for a solution and after some research I came with the code below to retrieve a list of available "external" mount points that, according to my knowledge, works on many different devices.

这个话题有点老了,但我一直在寻找解决方案,经过一些研究,我使用下面的代码来检索可用的“外部”挂载点列表,据我所知,这些挂载点可在许多不同的设备上运行。

Basically, it reads available mount points, filters out invalid ones, tests the rest if they are accessible and adds them if all the conditions are satisfied.

基本上,它读取可用的挂载点,过滤掉无效的挂载点,测试其余的挂载点是否可访问,并在满足所有条件时添加它们。

Of course, required permissions must be granted before the code is invoked.

当然,必须在调用代码之前授予所需的权限。

// Notice: FileSystemDevice is just my own wrapper class. Feel free to replace it with your own. 

private List<FileSystemDevice> getDevices() {

    List<FileSystemDevice> devices = new ArrayList<>();

    // Add default external storage if available.
    File sdCardFromSystem = null;
    switch(Environment.getExternalStorageState()) {
        case Environment.MEDIA_MOUNTED:
        case Environment.MEDIA_MOUNTED_READ_ONLY:
        case Environment.MEDIA_SHARED:
            sdCardFromSystem = Environment.getExternalStorageDirectory();
            break;
    }

    if (sdCardFromSystem != null) {
        devices.add(new FileSystemDevice(sdCardFromSystem));
    }

    // Read /proc/mounts and add all mount points that are available
    // and are not "special". Also, check if the default external storage
    // is not contained inside the mount point. 
    try {
        FileInputStream fs = new FileInputStream("/proc/mounts");
        String mounts = IOUtils.toString(fs, "UTF-8");
        for(String line : mounts.split("\n")) {
            String[] parts = line.split(" ");

            // parts[0] - mount type
            // parts[1] - mount point
            if (parts.length > 1) {
                try {

                    // Skip "special" mount points and mount points that can be accessed
                    // directly by Android's functions. 
                    if (parts[0].equals("proc")) { continue; }
                    if (parts[0].equals("rootfs")) { continue; }
                    if (parts[0].equals("devpts")) { continue; }
                    if (parts[0].equals("none")) { continue; }
                    if (parts[0].equals("sysfs")) { continue; }
                    if (parts[0].equals("selinuxfs")) { continue; }
                    if (parts[0].equals("debugfs")) { continue; }
                    if (parts[0].equals("tmpfs")) { continue; }
                    if (parts[1].equals(Environment.getRootDirectory().getAbsolutePath())) { continue; }
                    if (parts[1].equals(Environment.getDataDirectory().getAbsolutePath())) { continue; }
                    if (parts[1].equals(Environment.getExternalStorageDirectory().getAbsolutePath())) { continue; }

                    // Verify that the mount point is accessible by listing its content. 
                    File file = new File(parts[1]);
                    if (file.listFiles() != null) {
                        try {

                            // Get canonical path for case it's just symlink to another mount point.
                            String devPath = file.getCanonicalPath();

                            for(FileSystemDevice device : devices) {

                                if (!devices.contains(devPath)) {                        
                                    devices.add(new FileSystemDevice(new File(devPath)));
                                }

                            }
                        } catch (Exception e) {
                            // Silently skip the exception as it can only occur if the mount point is not valid. 
                            e.printStackTrace();
                        }
                    }
                } catch (Exception e) {
                    // Silently skip the exception as it can only occur if the mount point is not valid. 
                    e.printStackTrace();
                }
            }
        }

        fs.close();
    } catch (FileNotFoundException e) {
        // Silently skip the exception as it can only occur if the /proc/mounts file is unavailable. 
        // Possibly, another detection method can be called here.
        e.printStackTrace();
    } catch (IOException e) {
        // Silently skip the exception as it can only occur if the /proc/mounts file is unavailable.
        // Possibly, another detection method can be called here.
        e.printStackTrace();            
    }

    return devices;
}