简体   繁体   English

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

[英]Universal way to write to external SD card on Android

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

Summary总结

You can grant read/write access to external SD card on the different api levels ( API23+ at run time ).您可以在不同的 api 级别运行时 API23+授予对外部 SD 卡的读/写访问权限

Since KitKat, permissions are not necessary if you use app-specific directories, required otherwise. 由于 KitKat,如果您使用特定于应用程序的目录,则不需要权限, 否则需要权限。

Universal way:通用方式:

The history says that there is no universal way to write to external SD card but continues... 历史说没有通用的方式来写入外部 SD 卡, 但继续......

This fact is demonstrated by these examples of external storage configurations for devices . 这些设备的外部存储配置示例证明了这一事实

API-based way:基于API的方式:

Prior to KitKat try 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.getExternalFilesDirs solves the access error when you don't need to share files. ContextCompat.getExternalFilesDirs解决不需要共享文件时的访问错误

The secure way of sharing it is to use a content provider or 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 view into external storage.默认情况下(或选择加入)以 Android Q 为目标的应用程序会获得外部存储的过滤视图


  1. Initial answer.初步答复。

Universal way to write to external SD card on Android在 Android 上写入外部 SD 卡的通用方法

There is no universal way to write to external SD card on Android due to continuous changes :由于不断变化, 在 Android 上没有通用的写入外部 SD 卡的方法

  • Pre-KitKat: official Android platform has not supported SD cards at all except for exceptions. Pre-KitKat:官方Android平台除例外情况外根本不支持SD卡。

  • KitKat: introduced APIs that let apps access files in app-specific directories on SD cards. KitKat:引入了 API,允许应用访问 SD 卡上应用特定目录中的文件。

  • Lollipop: added APIs to allow apps to request access to folders owned by other providers. Lollipop:添加 API 以允许应用请求访问其他提供商拥有的文件夹。

  • Nougat: provided a simplified API to access common external storage directories. Nougat:提供了一个简化的 API 来访问常见的外部存储目录。

  • ... Android Q privacy change: App-scoped and media-scoped storage ... 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 answer and mine , and Dave Smith and Mark Murphy blog posts: 1 , 2 , 3 :基于Doomsknight的回答我的,和戴夫·史密斯马克·墨菲博客文章:123


  1. Updated answer.更新了答案。

Update 1 .更新 1 I tried Method 1 from Doomknight's answer, with no avail:我尝试了 Doomknight 答案中的方法 1,但无济于事:

As you can see I'm checking for permissions at runtime before attempting to write on SD...如您所见,在尝试写入 SD 之前,我正在运行时检查权限...

I would use application-specific directories to avoid the issue of your updated question and ContextCompat.getExternalFilesDirs() using getExternalFilesDir documentation as reference.我将使用特定应用程序的目录来避免您更新的问题和ContextCompat.getExternalFilesDirs()使用getExternalFilesDir 文档作为参考的问题。

Improve the heuristics to 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 devices and third-party apps must go through the Storage Access Framework .请记住, Android 6.0 支持便携式存储设备,第三方应用程序必须通过存储访问框架 Your devices HTC10 and Shield K1 are probably API 23.您的设备HTC10Shield K1可能是 API 23。

Your log shows a permission denied exception accessing /mnt/media_rw , like this fix for 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 for trying 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 alternative to your getStorageDirectories() method it's a good starting point.也许Gizm0 替代getStorageDirectories()方法是一个很好的起点。

ContextCompat.getExternalFilesDirs solves the issue if you don't need access to other folders .如果您不需要访问其他文件夹, ContextCompat.getExternalFilesDirs可以解决此问题


  1. Android 1.0 .. Pre-KitKat. Android 1.0 .. Pre-KitKat。

Prior to KitKat try to use Doomsknight method 1 or read this response by 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.xml and read Getting access to external storage将下一个代码添加到您的AndroidManifest.xml并阅读获取对外部存储的访问权限

Access to external storage is protected by various Android permissions.对外部存储的访问受到各种 Android 权限的保护。

Starting in Android 1.0, write access is protected with the WRITE_EXTERNAL_STORAGE permission .从 Android 1.0 开始,写访问受WRITE_EXTERNAL_STORAGE权限保护。

Starting in Android 4.1, read access is protected with the READ_EXTERNAL_STORAGE permission.从 Android 4.1 开始,读取访问权限受READ_EXTERNAL_STORAGE权限保护。

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.如果两者都需要...,则只需要请求WRITE_EXTERNAL_STORAGE权限。

Read Mark Murphy's explanation and recommended Dianne Hackborn and Dave Smith posts阅读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.直到 Android 4.4,Android 还没有官方支持可移动媒体,从 KitKat 开始,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.以前的应用程序仅依赖于 MediaStore 索引、随硬件一起提供或检查挂载点并应用一些启发式方法来确定什么代表可移动媒体。

  1. Android 4.4 KitKat introduces the Storage Access Framework (SAF) . 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.自 Android 4.2 以来,Google 一直要求设备制造商锁定可移动媒体以确保安全(多用户支持),并且在 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).由于添加了 KitKat getExternalFilesDirs()和其他方法来返回所有可用存储卷上的可用路径(返回的第一项是主卷)。
  • The table below indicates what a developer might try to do and how KitKat will respond:下表说明了开发人员可能会尝试做什么以及 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.注意:从 Android 4.4 开始,如果您只读取或写入应用程序私有的文件,则不需要这些权限。 For more info..., see saving files that are app-private .有关更多信息...,请参阅保存应用程序私有的文件

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

Also read Paolo Rovelli's explanation and try to use Jeff Sharkey's solution since KitKat:另请阅读Paolo Rovelli 的解释并尝试使用自 KitKat 以来Jeff Sharkey 的解决方案

In KitKat there's now a public API for interacting with these secondary shared storage devices.在 KitKat 中,现在有一个公共 API 用于与这些辅助共享存储设备进行交互。

The new Context.getExternalFilesDirs() and Context.getExternalCacheDirs() methods can return multiple paths, including both primary and secondary devices.新的Context.getExternalFilesDirs()Context.getExternalCacheDirs()方法可以返回多个路径,包括主要和次要设备。

You can then iterate over them and check Environment.getStorageState() and File.getFreeSpace() to determine the best place to store your files.然后您可以迭代它们并检查Environment.getStorageState()File.getFreeSpace()以确定存储文件的最佳位置。

These methods are also available on ContextCompat in the support-v4 library.这些方法也可在 support-v4 库中的ContextCompat上使用。

Starting in Android 4.4, the owner, group and modes of files on external storage devices are now synthesized based on directory structure.从 Android 4.4 开始,外部存储设备上文件的所有者、组和模式现在基于目录结构进行合成。 This enables apps to manage their package-specific directories on external storage without requiring they hold the broad WRITE_EXTERNAL_STORAGE permission.这使应用程序能够在外部存储上管理其特定于包的目录,而无需持有广泛的WRITE_EXTERNAL_STORAGE权限。 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.例如,包名为com.example.foo的应用程序现在可以在没有权限的情况下自由访问外部存储设备上的Android/data/com.example.foo/ These synthesized permissions are accomplished by wrapping raw storage devices in a FUSE daemon.这些合成权限是通过将原始存储设备包装在 FUSE 守护程序中来实现的。

With KitKat your chances for a "complete solution" without rooting are pretty much zero:使用 KitKat,您无需生根即可获得“完整解决方案”的机会几乎为零:

The Android project has definitely screwed up here. Android 项目肯定在这里搞砸了。 No apps get full access to external SD cards:没有应用程序可以完全访问外部 SD 卡:

  • file managers: you cannot use them to manage your external SD card.文件管理器:您不能使用它们来管理您的外部 SD 卡。 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 3 rd party apps are allowed to write on your external card are "their own directories" (ie /sdcard/Android/data/<package_name_of_the_app> ).唯一允许 3 rd方应用程序在您的外部卡上写入的地方是“他们自己的目录”(即/sdcard/Android/data/<package_name_of_the_app> )。

The only ways to really fix that require either the manufacturer (some of them fixed it, eg Huawei with their Kitkat update for the P6) – or root... (Izzy's explanation continues here)真正解决这个问题的唯一方法需要制造商(其中一些人修复它,例如华为通过他们的 P6 Kitkat 更新) – 或 root ...... (Izzy 的解释在这里继续)


  1. Android 5.0 introduced changes and the DocumentFile helper class. Android 5.0 引入了更改和DocumentFile助手类。

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

Here's a great tutorial for interacting with the Storage Access Framework in KitKat. 这是与 KitKat 中的存储访问框架交互的很棒的教程

Interacting with the new APIs in Lollipop is very similar (Jeff Sharkey's explanation) .与 Lollipop 中的新 API 交互非常相似(Jeff Sharkey 的解释)


  1. Android 6.0 Marshmallow introduces a new runtime permissions model. Android 6.0 Marshmallow 引入了新的 运行时权限模型。

Request permissions at 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.从 Android 6.0(API 级别 23)开始,用户在应用程序运行时授予应用程序权限,而不是在安装应用程序...或更新应用程序时...用户可以撤销权限。

 // 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. Android 6.0 引入了一种新的 运行时权限模型,应用程序可以在运行时根据需要请求功能。 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.由于新模型包含READ/WRITE_EXTERNAL_STORAGE权限,平台需要动态授予存储访问权限,而无需终止或重新启动已运行的应用程序。 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/default 显示给没有特殊存储权限的应用程序...
  • /mnt/runtime/read is shown to apps with READ_EXTERNAL_STORAGE /mnt/runtime/read 显示给带有 READ_EXTERNAL_STORAGE 的应用程序
  • /mnt/runtime/write is shown to apps with WRITE_EXTERNAL_STORAGE /mnt/runtime/write 显示给具有 WRITE_EXTERNAL_STORAGE 的应用程序

  1. Android 7.0 provides a simplified API to access external storage dirs. 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... 范围目录访问在 Android 7.0 中,应用程序可以使用新的 API 来请求访问特定的外部存储目录,包括可移动媒体(如 SD 卡)上的目录...

For more information, see the Scoped Directory Access training .有关详细信息,请参阅范围目录访问培训

Read Mark Murphy posts: Be Careful with Scoped Directory Access .阅读 Mark Murphy 的帖子: 小心 Scoped Directory Access It was deprecated in Android Q : 它在 Android Q 中已被弃用

Note that the scoped directory access added in 7.0 is deprecated in Android Q.请注意,7.0 中添加的范围目录访问在 Android Q 中已弃用。

Specifically, the createAccessIntent() method on StorageVolume is deprecated.具体来说,不推荐使用StorageVolume上的createAccessIntent()方法。

They added a createOpenDocumentTreeIntent() that can be used as an alternative.他们添加了一个createOpenDocumentTreeIntent()可以用作替代。


  1. Android 8.0 Oreo .. Android Q Beta changes. 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... 从 Android O 开始,存储访问框架允许自定义文档提供程序为驻留在远程数据源中的文件创建可查找的文件描述符...

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

For apps targeting Android O , this behavior has been corrected.对于面向 Android O 的应用程序,此行为已得到纠正。 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 ...例如, READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE ...

Update: An Android Q earlier beta release temporarily replaced the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions with more fine-grained, media-specific permissions.更新: Android Q 早期测试版暂时用更细粒度的特定媒体权限替换了READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE权限。

Note: Google introduced roles on 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 .注意:在早期 Beta 版本中引入的特定于媒体收藏的权限 - READ_MEDIA_IMAGESREAD_MEDIA_AUDIOREAD_MEDIA_VIDEO -现在已过时 More info:更多信息:

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 —— 安德鲁·萨克斯


  1. Related questions and recommended answers.相关问题和推荐答案。

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:存储访问框架常见问题


  1. Related bugs and issues.相关的错误和问题。

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() 返回的目录失败

I believe there are two methods to achieve this:我相信有两种方法可以实现这一目标:

METHOD 1: (does NOT work 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.它将在字符串目录位置列表中返回所有已安装的媒体(包括真正的 SD 卡) With the list you can then ask the user where to save, etc.有了这个列表,你就可以询问用户在哪里保存,等等。

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 File locations 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.此处返回的外部存储设备被视为设备的永久部分,包括模拟外部存储和物理媒体插槽,例如电池仓中的 SD 卡。 The returned paths do not include transient devices, such as USB flash drives.返回的路径不包括临时设备,例如 USB 闪存驱动器。

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 有关 ContextCompat 的更多信息

They are like app specific files.它们就像应用程序特定的文件。 Hidden from other apps.对其他应用程序隐藏。

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 File and let the loop continue instead of calling break after the first one is found.注意:我只得到“第一个”外部 sd 卡,但是您可以修改它并返回ArrayList<File>而不是File并让循环继续而不是在找到第一个后调用break

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 .全尺寸图片太大而无法保存在 SQLite 数据库中,更不用说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.回想一下,您使用私有存储来保存 SQLite 数据库。 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)使用Context.getFileStreamPath(String)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_READABLE flag you can pass in to openFileOutput(String, int) , it is deprecated, and not completely reliable in its effects on newer devices.另一方面,如果您需要另一个应用程序来写入这些文件,那么您就不走运了:虽然您可以将Context.MODE_WORLD_READABLE标志传递给openFileOutput(String, int) ,但它已被弃用,并且不完全可靠它对较新设备的影响。 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.如果您要存储文件以与其他应用程序共享或从其他应用程序接收文件(如存储图片的文件),则需要将它们存储在外部存储上。

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() .所有 Android 设备至少有一个外部存储位置:主位置,位于Environment.getExternalStorageDirectory()返回的文件夹中。 This may be an SD card, but nowadays it is more commonly integrated into the device itself.这可能是一个 SD 卡,但现在它更常见地集成到设备本身中。 Some devices may have additional external storage.某些设备可能有额外的外部存储。 That would fall under “everything else.”那将属于“其他一切”。

Context provides quite a few methods for getting at external storage, too. Context 也提供了很多获取外部存储的方法。 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.所有这些方法也将文件存储在公开可用的位置,因此请小心使用它们。

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.从技术上讲,上面提供的外部文件夹可能不可用,因为某些设备使用可移动 SD 卡进行外部存储。 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.但是我们确实建议包含简单的代码来防止这种可能性,您稍后会这样做。

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.权限是您使用<uses-permission>标记放入清单中的众所周知的字符串值。 They tell Android that you want to do something that Android wants you to ask permission for.他们告诉 Android 你想做一些 Android 想让你征得许可的事情。

Here, Android expects you to ask permission because it wants to enforce some accountability.在这里,Android 希望您征得许可,因为它想要强制执行某些责任。 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.你告诉 Android 你需要访问外部存储,然后 Android 会告诉用户这是你的应用程序在尝试安装时所做的事情之一。 That way, nobody is surprised when you start saving things to their SD card.这样,当您开始将内容保存到他们的 SD 卡时,没有人会感到惊讶。

In Android 4.4, KitKat, they loosened this restriction.在 Android 4.4 KitKat 中,他们放宽了这一限制。 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.由于Context.getExternalFilesDir(String)返回一个特定于您的应用程序的文件夹,因此您希望能够读取和写入位于其中的文件是有道理的。 So on Android 4.4 (API 19) and up, you do not need this permission for this folder.因此,在 Android 4.4 (API 19) 及更高版本上,您不需要此文件夹的此权限。 (But you still need it for other kinds of external storage.) (但您仍然需要它用于其他类型的外部存储。)

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. maxSdkVersion 属性使您的应用程序仅在早于 API 19,Android KitKat 的 Android 版本上请求此权限。 Note that you are only asking to read external storage.请注意,您只是要求读取外部存储。 There is also a WRITE_EXTERNAL_STORAGE permission, but you do not need it.还有一个WRITE_EXTERNAL_STORAGE权限,但您不需要它。 You will not be writing anything to external storage: The camera app will do that for you您不会向外部存储写入任何内容:相机应用程序会为您做到这一点

The second resource is this link read all of it, but you can also jump to Using the External Storage section.第二个资源是这个链接阅读所有内容,但您也可以跳转到使用外部存储部分。

Reference:参考:

More reading stuff:更多阅读内容:

Disclaimer: This information was taken from Android Programming: The Big Nerd Ranch Guide with permission from the authors.免责声明:此信息取自 Android 编程:经作者许可的大书呆子牧场指南。 For more information on this book or to purchase a copy, please visit bignerdranch.com.有关本书的更多信息或购买副本,请访问 bignerdranch.com。

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

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).这是在外部存储中创建新文件的方法(如果设备中存在 SDCard,否则设备外部存储中存在)。 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 here or whatever in a file.当然,在这里您可以看到如何保存通用文件,现在您可以搜索如何在此处或文件中的任何内容保存图像。

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" /> (或者如果你喜欢 Marshmallow 以编程方式)

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 卡(已安装)希望这会有所帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM