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

Vek*_*r88 71 java android android-sdcard android-external-storage

在我的应用程序中,我需要在设备存储中存储大量图像.这些文件往往满足设备存储,我想让用户能够选择外部SD卡作为目标文件夹.

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

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

更新1

我从Doomknight的答案中尝试了方法1,但没有结果:正如您所看到的那样,我在尝试在SD上写入之前在运行时检查权限:

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

但是我在两个不同的设备上尝试了访问错误: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
Run Code Online (Sandbox Code Playgroud)

alb*_*elu 110

Short answer.

ContextCompat.getExternalFilesDirs solves the access error when you don't need to share files.

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


Summary.

There is no universal way to write to external SD card on Android due to continuous changes:

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

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


1. Permissions.

You can 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 otherwise:

With Kitkat your chances for a "complete solution" without rooting are pretty much zero: the Android project has definitely screwed up here. No apps get full access to the external SD card:

  • 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. ContextCompat.getExternalFilesDirs()).

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)


2. About the universal solution.

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

这些事实通过这些设备的外部存储配置示例得以证实.

对外部存储的访问受各种Android权限的保护.从Android 1.0开始,写访问受android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT权限保护.从Android 4.1开始,读取权限受/mnt/media_rw权限保护.

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 for permission. For example, the app with package name getStorageDirectories() can now freely access ContextCompat.getExternalFilesDirs on external storage devices with no permissions. These synthesized permissions are accomplished by wrapping raw storage devices in a FUSE daemon.

Android 6.0 introduces a new runtime permissions model where apps request capabilities when needed at runtime. Because the new model includes the AndroidManifest.xml 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

3. About your update 1.

I would use application-specific directories to avoid the issue of your updated question and WRITE_EXTERNAL_STORAGE using getExternalFilesDir documentation as reference.

Improve the heuristics to determine what represents removable media based on the different api levels like READ_EXTERNAL_STORAGE

Remember that Android 6.0 supports portable storage devices and third-party apps must go through the Storage Access Framework. Your devices HTC10 and Shield K1 are probably API 23.

Your log shows a permission denied exception accessing WRITE_EXTERNAL_STORAGE, like this fix for 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>
Run Code Online (Sandbox Code Playgroud)

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

Perhaps Gizm0's alternative to your getExternalFilesDirs() method it's a good starting point.

Context.getExternalFilesDirs() solves the issue if you don't need access to other folders.


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

Add the next code to your Context.getExternalCacheDirs() and read Getting access to external storage

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

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;
}
Run Code Online (Sandbox Code Playgroud)

If you need to both..., you need to request only the Environment.getStorageState() permission.

Ignore the next note due to bugs, but try to use File.getFreeSpace():

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.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest> 
Run Code Online (Sandbox Code Playgroud)

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.

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
</manifest>
Run Code Online (Sandbox Code Playgroud)

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

Read Mark Murphy's explanation and recommended Dianne Hackborn and Dave Smith ones

  • 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.
  • 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 KiKat ContextCompat 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

Prior to KitKat try to use Doomsknight method 1 or read this response by Gnathonic or gist:

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

Also read Paolo Rovelli's explanation and try to use Jeff Sharkey's solution since KitKat:

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

The new WRITE_EXTERNAL_STORAGE and com.example.foo methods can return multiple paths, including both primary and secondary devices.

You can then iterate over them and check Android/data/com.example.foo/ and /sdcard/Android/data/<package_name_of_the_app> to determine the best place to store your files.

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


6. Lollipop also introduced changes and the DocumentFile helper class.

getExternalStorageState(File) Added in API 19, deprecated in API 21, use READ/WRITE_EXTERNAL_STORAGE

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


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

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.


8. Android O changes.

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, createAccessIntent() and createOpenDocumentTreeIntent()...


9. Related questions and recommended answers.

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


10. Related bugs and issues.

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

  • 一个非常深入的分析.+1.虽然我必须注意,但我没有写那种方法.它来自几年前的另一个来源:)希望它能帮到他. (2认同)

Doo*_*ght 9

我相信有两种方法可以达到这个目的:

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

我已经在许多设备版本上使用这种方法多年没有问题.信用是由于原始来源,因为不是我写了它.

它将所有已安装的媒体(包括Real SD卡)返回到字符串目录位置列表中.通过该列表,您可以询问用户保存位置等.

你可以用以下方法调用它:

 HashSet<String> extDirs = getStorageDirectories();
Run Code Online (Sandbox Code Playgroud)

方法:

/**
 * 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;
}
Run Code Online (Sandbox Code Playgroud)

方法2:

使用v4支持库

import android.support.v4.content.ContextCompat;
Run Code Online (Sandbox Code Playgroud)

只需调用以下内容即可获取File存储位置列表.

 File[] list = ContextCompat.getExternalFilesDirs(myContext, null);
Run Code Online (Sandbox Code Playgroud)

但是,这些地点的用途不同.

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

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

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

有关ContextCompat的更多信息

它们就像应用程序特定的文件.隐藏在其他应用中.

  • @EugenPechanec我完全同意.然而,几年前我从一个奇妙的来源获取此代码.我会来看看 (2认同)

归档时间:

查看次数:

40457 次

最近记录:

6 年,4 月 前