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

Universal way to write to external SD card on Android

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

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

在不同 API 级别(Pre-KitKat、KitKat、Lollipop+)授予 read/write 访问外部 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.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

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

但对于 Marshmallow 及更高版本的设备,您需要在 运行 时间授予权限。

通过使用

Environment.getExternalStorageDirectory();

可以直接访问外置SD卡(挂载一个) 希望这有帮助。

我相信有两种方法可以实现:

方法 1:NOT 是否适用于 6.0 及更高版本,由于权限更改)

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

它会 return 所有安装的媒体(包括真正的 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;
}

方法二:

使用 v4 支持库

import android.support.v4.content.ContextCompat;

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

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

不过,这些位置在用法上有所不同。

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

More Info on ContextCompat

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

总结

你可以grant read/write access to external SD card on the different api levels (API23+ at run time).

Since KitKat, permissions are not necessary if you use app-specific directories, required否则

通用方式:

history says that there is no universal way to write to external SD card but continues...

This fact is demonstrated by these examples of external storage configurations for devices.

API-based方式:

KitKat try to use Doomsknight method 1, method 2 otherwise.

之前

请求权限 in manifest (Api < 23) and at run time(Api >= 23)。

推荐方式:

ContextCompat.getExternalFilesDirs solves the access error 当您不需要共享文件时。

安全的共享方式是使用content provider or the new Storage Access Framework

Privacy-aware方式:

As of Android Q Beta 4,默认情况下,针对 Android 9(API 级别 28)或更低级别的应用没有变化。

默认情况下(或选择加入)以 Android Q 为目标的应用程序被给予 filtered view 到外部存储。


  1. 初步回答。

Universal way to write to external SD card on Android

没有universal way to write to external SD card on Android due to continuous changes:

  • Pre-KitKat: 官方Android平台除特例外,完全不支持SD卡

  • KitKat:引入了 APIs,让应用程序可以访问 SD 卡上 app-specific 目录中的文件。

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

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

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

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

基于Doomsknight's answer and mine, and Dave Smith and Mark Murphy blog posts: 1, 2, 3


  1. 更新答案。

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

我会使用 application-specific 个目录 to avoid the issue of your updated question and ContextCompat.getExternalFilesDirs() using getExternalFilesDir documentation 作为参考。

改进 heuristics 以根据 android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT

等不同 api 级别来确定代表可移动媒体的内容

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

记住 Android 6.0 supports portable storage devices and third-party apps must go through the Storage Access Framework. Your devices HTC10 and Shield K1 大概是 API 23.

您的日志显示 a permission denied exception accessing /mnt/media_rw, like this fix 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>

我从未尝试过,所以我无法共享代码,但我会避免 for 尝试写入所有返回的目录并查找 the best available storage directory to write into based on remaining space.

也许 Gizm0's alternative 你的 getStorageDirectories() 方法是一个很好的起点。

ContextCompat.getExternalFilesDirs 解决 the issue if you don't need access to other folders.


  1. Android 1.0 .. Pre-KitKat.

在 KitKat 之前尝试使用 Gnathonic 的 Doomsknight method 1 or read this response

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 并阅读 Getting access to external storage

Access to external storage is protected by various Android permissions.

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

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

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_STORAGE permission.

阅读 Mark Murphy's explanation and recommended Dianne Hackborn and Dave 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.

  1. Android 4.4 KitKat 介绍 the Storage Access Framework (SAF).

由于bug请忽略下注,但尝试使用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).
  • 下面的 table 表示开发人员可能会尝试做什么以及 KitKat 将如何响应:

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>

另请参阅 Paolo Rovelli's explanation and try to use Jeff Sharkey's solution 自 KitKat 以来:

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 ContextCompat in 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_STORAGE permission. For example, the app with package name com.example.foo can 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,您有机会获得无需 root 的“完整解决方案” 几乎为零:

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 3rd party 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)


  1. Android 5.0 引入了变化和 DocumentFile 助手 class.

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

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

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


  1. Android 6.0 Marshmallow 引入了一个新的 runtime permissions 模型。

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

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 permissions model where apps request capabilities when needed at runtime. Because the new model includes the READ/WRITE_EXTERNAL_STORAGE permissions, 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

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

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

For more information, see the Scoped Directory Access training.

阅读 Mark Murphy 的帖子:Be Careful with Scoped Directory Access. It was deprecated in Android Q:

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

Specifically, the createAccessIntent() method on StorageVolume is deprecated.

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


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

Starting in Android O, the Storage Access Framework allows custom documents providers to 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_STORAGE and WRITE_EXTERNAL_STORAGE...

更新: Android Q 早期测试版暂时用更多 fine-grained 替换了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限, media-specific 权限。

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

注意: 特定于媒体 collections 的权限已在早期测试版中引入——READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEOare now obsolete更多信息:

Q Beta 4(最终 APIs)Mark Murphy 的评论:The Death of External Storage: The End of the Saga(?)

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


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

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

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

Diff between getExternalFilesDir and getExternalStorageDirectory()

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

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

Writing to external SD card in Android 5.0 and above

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

SAFFAQ: The Storage Access Framework FAQ


  1. 相关错误和问题。

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

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

这是一种在外部存储中创建新文件的方法(如果设备中存在 SDCard,如果没有,则为设备外部存储)。只需将 "foldername" 替换为您想要的目标文件夹的名称,并将 "filename" 替换为您要保存的文件的名称。当然在这里您可以看到如何保存通用文件,现在您可以搜索如何保存图像 maybe here 或文件中的任何内容。

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

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

希望对您有所帮助!

编辑:抱歉,我忘了您必须在您的清单中请求 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> (或者如果您喜欢 Marshmallow,则以编程方式)

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

这最初是我在这里 () 发布的,目的是在 Android 5.0+

上获取外部 SD 卡的大小

获取外部 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;
}

注意:我只有 "first" 外部 SD 卡,但是您可以修改它并 return ArrayList<File> 而不是 File 让循环继续而不是调用break找到第一个后

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

第一个资源来自 Android 编程,The Big Nerd Ranch Guide 第二版,第 16 章,第 294 页。

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

以下部分摘自本书:

外部存储

您的照片需要的不仅仅是屏幕上的一个位置。全尺寸图片太大,无法贴在 SQLite 数据库,更不用说 Intent。他们将需要在您设备的文件系统上有一个地方。 通常,您会将它们放在您的私人存储中。回想一下,您使用了您的私人存储空间 保存您的 SQLite 数据库。使用 Context.getFileStreamPath(String)Context.getFilesDir(),您也可以对常规文件执行相同的操作(它们将位于 SQLite 数据库所在的数据库子文件夹旁边的子文件夹)

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|

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

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

有两种外部存储:主要存储和其他存储。所有 Android 台设备都位于 至少一个外部存储位置:主要位置,位于返回的文件夹中 Environment.getExternalStorageDirectory()。这可能是一张 SD 卡,但现在更多 通常集成到设备本身。一些设备可能有额外的外部存储。那 将属于“其他所有内容”。

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

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

外部存储权限

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

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

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

在您的清单中添加一行请求读取外部存储的权限,但仅限于 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" />

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

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

参考:

更多阅读资料:

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.

这个主题有点老,但我一直在寻找解决方案,经过一些研究后,我使用下面的代码来检索可用 "external" 挂载点的列表,据我所知,这些挂载点可以用于许多不同的设备。

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

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

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