【问题标题】:Force access to external removable microSD card强制访问外部可移动 microSD 卡
【发布时间】:2018-05-23 21:56:27
【问题描述】:

我使用的是三星 A3,Android 5.0.2。我正在使用 this setup 编译应用程序,即 Android 4.1 Jelly Bean (API 16) 目标。

我确切地知道外部可移动 microSD 卡的路径,它是/mnt/extSdCard/(另请参阅下面的注释 #7)。

问题:我注意到了

File myDir = new File("/mnt/extSdCard/test");
myDir.mkdirs();

不起作用:没有创建目录。

还有:

File file = new File("/mnt/extSdCard/books/test.txt");   // the folder "books" already exists on the external microSD card, has been created from computer with USB connection
FileOutputStream fos = new FileOutputStream(file);

产生这个错误:

java.io.FileNotFoundException: /mnt/extSdCard/books/test.txt: open failed: EACCES (Permission denied) at libcore.io.IoBridge.open(...

如何强制对外部可移动 microSD 卡进行读写访问?

注意事项:

  1. Environment.getExternalStorageDirectory().toString() 提供 /storage/emulated/0 这是我的手机内部存储,即不是我想要的。

  2. getExternalFilesDir(null) 给出/storage/emulated/0/Android/data/com.blahblah.appname/files/ 即不是我想要的。请注意,我不能将getExternalFilesDirs 与最终的s 一起使用,因为这在API16 中不可用。 API16 中也没有运行时权限。

  3. 我已经有<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />,还有READ_EXTERNAL_STORAGE

  4. 我读了很多诸如this onethis one之类的主题,实际上可能有二十个类似的问题,但最后看起来很复杂,一切都说反了。这就是我正在寻找针对这种情况的解决方案。

  5. 我不想要ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT,实际上我不想要任何GUI解决方案。

  6. 我拥有的一些应用程序(同步 Resilio)可以成功修改 /mnt/extSdCard/music/,在那里创建新文件等。

  7. 顺便ls -la /mnt/extSdCard/给了

    drwxrwx--x root     sdcard_r          2017-10-15 01:21 Android
    drwxrwx--- root     sdcard_r          2017-10-14 00:59 LOST.DIR
    drwxrwx--- root     sdcard_r          2017-12-05 16:44 books
    drwxrwx--- root     sdcard_r          2017-11-21 22:55 music
    

【问题讨论】:

  • 你忘了提到运行时权限。
  • 看看 getExternalFilesDirs()。如果幸运的话,它会返回两条路径。然后第二个将是 micro SD 卡上的可写。
  • 你可以用谷歌搜索运行时权限。但是对于访问 micro SD 卡,它们是没有用的。
  • @greenapps 在 API16 中,带有复数 (Dirs) 的 getExternalFilesDirs 不可用。运行时权限也不可用,请参见注释 2。还有其他想法吗?
  • 您需要使用DocumentFile。没有公共 API 可以获取可移动 SD 卡的路径(据我所知)。这应该会有所帮助:stackoverflow.com/a/35175460/1048340 为什么不能使用内部存储来代替可移动 SD 卡?

标签: java android android-sdcard android-external-storage


【解决方案1】:

请记住,有些 Android 设备的 SD 卡路径不同,而有些设备没有可移动的 SD 卡。

您不必直接设置路径!

File myDir = new File("/mnt/extSdCard/test");
myDir.mkdirs();

您可以先检查您的设备是否安装了可移动 SD 卡:

public static boolean isSDCardAvailable(Context context) {
    File[] storages = ContextCompat.getExternalFilesDirs(context, null);
    if (storages.length > 1 && storages[0] != null && storages[1] != null)
        return true;
    else
        return false;
}

为什么要获取外部目录> 1,因为大多数 android 设备都将外部存储作为主目录,将可移动 SD 卡作为第二目录:

但您可以使用一种方法来获取可移动 microSD 卡的真实路径:

public static String getRemovableSDCardPath(Context context) {
    File[] storages = ContextCompat.getExternalFilesDirs(context, null);
    if (storages.length > 1 && storages[0] != null && storages[1] != null)
        return storages[1].toString();
    else
        return "";
}

那么就这样做吧:

File myDir = new File(getRemovableSDCardPath(getApplicationContext()),"test");
if(myDir.mkdirs()){
  Log.i(TAG, "Directory was succesfully create!");
}else{
  Log.i(TAG, "Error creating directory!");
}

例如使用方法:

   String pathSDCard = getRemovableSDCardPath(getApplicationContext());

因此,我得到了可移动 SD 卡的路径(如果我没有可移动 SD 卡,我的路径将是“”,因此您可以实施验证以避免创建文件夹):

/storage/extSdCard/Android/data/com.jorgesys.myapplication/files

现在在里面创建一个新文件夹:

    File myDir = new File(getRemovableSDCardPath(getApplicationContext()),"test");
    if(myDir.mkdirs()){
        Log.i(TAG, "Directory was succesfully create!");
    }else{
        Log.i(TAG, "Error creating directory!");
    }

现在我创建了目录/test

【讨论】:

  • 请为 -1 添加评论的原因,我愿意改进我的答案,为 OP 提供更好的帮助。
  • 感谢您的回答,但请参阅注 2:getExternalFilesDirs()not accessible when using target API 16 (added only in API level 19)。另外我不想访问/SDcard/Android/data/com.package.example/files/...,而是访问根文件夹:/SDcard/mp3/(即我不想被限制在/SDCard/Android/data/... 子文件夹中)。用例:假设您有一个 50 GB 的 MP3 库,您不希望它仅限于应用 X,但您还希望将它与应用 Y 或应用 Z 一起使用。
  • 问题是获得权限
【解决方案2】:

基于Doomsknight's answermineDave SmithMark Murphy blog posts: 123

  1. 理想情况下,使用Storage Access FrameworkDocumentFile 作为Jared Rummler pointed。或者:
  2. 使用your app specific path/storage/extSdCard/Android/data/com.myapp.example/files
  3. 为 pre-KitKat 添加read/write permission to manifest,为这条路径添加no permission required later
  4. 考虑KitKat and Samsung case,尝试使用point2目录和Doomsknight's methods
  5. 在 KitKat 之前过滤和使用 getStorageDirectories 点 2 路径和读/写权限。
  6. ContextCompat.getExternalFilesDirs 因为奇巧记得Samsung returns internal first

Write My Package 的 Android 数据目录可用于一级和二级存储:

在 KitKat 中,应用特定数据目录的完全所有权是 赋予应用程序的唯一用户 ID。这意味着,未来,没有 应用程序读取/写入其特定权限是必需的 外部存储上的目录...

android.permission.WRITE_EXTERNAL_STORAGE 权限现在授予 sdcard_rsdcard_rw 的成员...

无需权限即可使用任何外部存储卷 特定于应用程序的文件。

注意:ls -la /mnt/extSdCard/... 列表中,sdcard_r 组具有完整的 +rwx 权限,这在实践中显然不是真的......因为 FUSE 守护进程是修改应用权限的积极参与者到运行时的应用程序。

三星:案例研究

在 Android 4.3 中,三星模拟了主要的外部存储卷 在设备的内部闪存(不是可移动 SD 卡)上。标清 卡一直在内部被标记为辅助外部 存储介质...

在 Android 4.4 中,主外部存储卷仍处于开启状态 内部闪光灯。

三星选择包含一个 SD 卡插槽,但没有标记 作为主要的外部存储介质。


更新:

作为explained here 并回答您关于使用根路径共享文件的评论:

您可以在 Kikat 之前使用基于目标版本的Doomsknight's method 1if/else 代码,或building multiple APKs

自 KitKat 以来,第三方应用程序无法添加更多文件 他们自己在随机位置...

为什么是现在?

答案,简而言之,是 CTS.

规定二级存储卷不应由以下用户写入的规则 自 4.2 以来,应用程序也已在文档中

但是,new tests were added in CTS for 4.4 验证辅助存储是否具有适当的 非特定于应用程序的目录中的只读权限,大概是 因为新的 API 最终将这些路径公开给应用程序 开发商。一旦 CTS 包含这些规则,OEM 必须支持 他们可以继续运送带有 GMS(Google Play 等)的设备。

共享文件怎么样?

这是一个有效的问题。如果应用程序需要共享文件怎么办 它是在辅助外部存储卷上创建的吗?谷歌的 答案似乎是那些积极决定去的应用程序 除了主要的外部存储来写入内容还应该公开一个 安全的分享方式,使用content providernew Storage Access Framework

正如我在您请求我帮助的问题上解释的那样,我从未尝试过,但我的观点是,理论上您可以使用方法 1 在 KitKat 之前使用根路径,然后再使用这些替代方法。

【讨论】:

  • 非常感谢@albodelu。除了所有链接(我有点迷茫,因为我不知道要使用哪个确切版本),为了使它成为一个完整的答案,你是否有一些关于如何使它工作的最小代码示例:一个最小的代码那将创建一个文件<EXT_SDCARD>/books/hello.txt? (这将使用目标 API 16 = Android 4.1 进行编译,并且可以在无根的 Android 5.0 手机上运行)。提前十亿次感谢(可能更多,因为我花了这么多时间却没有任何成功,就像其他人可能的钱一样:))
  • 不客气。这个想法是使用this codeifsection 替换为ContextCompat.getExternalFilesDirselse 部分由Doomsknight 的方法1 或5。理论上,它将适用于API 16。我的旧设备已植根以将它们升级到KitKat,我可以不要尝试。如果您链接到 Github 中的演示/测试项目,我可以在下周尝试帮助您。无论如何,共享链接已经包含相关代码。关于路径,我希望其他开发人员尊重 Kitkat 限制。
  • 替换else部分的原因是三星设备使用getExternalFilesDir返回了错误的路径。你真的不需要使用根路径,这个/storage/extSdCard/Android/data/com.myapp.example/files解决了你的空间问题。
  • 不,@albodelu,/storage/extSdCard/Android/data/com.myapp.example/files 不能解决问题:想象一下您的 microSD 卡上有一个 50GB MP3 库。您希望 MP3Player 应用程序访问它,但也希望另一个应用程序 MP3_DJ 也访问它。所以文件夹不能是特定于应用程序的。这就是为什么我想访问(读/写)微型 SD 卡/sdcard/mp3/。在我对您提供的所有链接进行大量测试之前,您的解决方案是否适用于根文件夹?
  • This question 已经有两个关于 SAF 替代方案的理论和代码的答案。
【解决方案3】:

由于我在同一个问题上遇到了很多困难,所以我会分享我的一点。 非常感谢他们,我也从这些资源中获得了帮助:

DocumentFile Android DocsFrom Here

我已经在 5.1.16.0.1 上测试了代码,而不是在设备的其他部分我没有测试它,但它应该可以正常工作。

5.0.2 上要在外部设备上书写,您必须征得用户的许可。

使用下面的代码并在请求此权限之前,您需要指示用户选择根 sd 卡,以便您可以访问整个外部存储。

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 25);

现在在 onActivityResult 中保存 API 返回的 UriTree,因为您稍后会需要它。

 @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == 25&&resultCode == RESULT_OK) {
            getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    } 
    super.onActivityResult(requestCode, resultCode, data);
}

拥有根 UriTree 后,您可以从外部存储创建修改删除文件或目录,因为您将从 UriTree 获得 DocumentFile

使用以下代码获取 Document UriTree。

public static DocumentFile getDocumentFile(final File file) {
        String baseFolder = getExtSdCardFolder(file);
        String relativePath = null;

        if (baseFolder == null) {
            return null;
        }

        try {
            String fullPath = file.getCanonicalPath();
            relativePath = fullPath.substring(baseFolder.length() + 1);
        } catch (IOException e) {
            Logger.log(e.getMessage());
            return null;
        }
        Uri treeUri = Common.getInstance().getContentResolver().getPersistedUriPermissions().get(0).getUri();

        if (treeUri == null) {
            return null;
        }

        // start with root of SD card and then parse through document tree.
        DocumentFile document = DocumentFile.fromTreeUri(Common.getInstance(), treeUri);

        String[] parts = relativePath.split("\\/");

        for (String part : parts) {
            DocumentFile nextDocument = document.findFile(part);
            if (nextDocument != null) {
                document = nextDocument;
            }
        }

        return document;
    }


    public static String getExtSdCardFolder(File file) {
        String[] extSdPaths = getExtSdCardPaths();
        try {
            for (int i = 0; i < extSdPaths.length; i++) {
                if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
                    return extSdPaths[i];
                }
            }
        } catch (IOException e) {
            return null;
        }
        return null;
    }


@TargetApi(Build.VERSION_CODES.KITKAT)
public static String[] getExtSdCardPaths() {
    List<String> paths = new ArrayList<>();
    for (File file : Common.getInstance().getExternalFilesDirs("external")) {

        if (file != null && !file.equals(Common.getInstance().getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w("asd", "Unexpected external file dir: " + file.getAbsolutePath());
            } else {
                String path = file.getAbsolutePath().substring(0, index);
                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    // Keep non-canonical path.
                }
                paths.add(path);
            }
        }
    }
    return paths.toArray(new String[paths.size()]);
}

以上代码将返回任何文件的 DocumentFile 版本,您可以使用该版本执行所需的操作。

如果您想查看此代码的实际效果,请查看我在open source project 中使用此代码来修改外部存储上的 mp3 文件。

希望有帮助,如有疑问请告诉我。

忘了告诉我刚才问过同样的问题Here 是那个问题。

编辑: 使用此代码,您可以检查用户是否已授予权限

public static boolean hasPermission() {
    List<UriPermission> uriPermission = Common.getInstance().getContentResolver().getPersistedUriPermissions();
    return uriPermission != null && uriPermission.size() > 0;
}

如果权限被撤销,则不会有 UriTree,因此您必须再次请求权限。

【讨论】:

  • 它看起来很棒@ReyanshMishra,我会尽快尝试这个!两个小问题:1)您认为它会与目标 API16(Android 4.1)一起编译还是绝对需要更新版本?
  • 2) 用户每次都必须在Intent.ACTION_OPEN_DOCUMENT_TREE 对话框中选择根 SD 卡吗?每次手机重启后一个?还是一劳永逸(我们第一次打开应用程序时)?再次感谢您!
  • 是的,我应该在 API 16 上编译,对于第二个问题,我会更新答案。
  • 再次感谢@ReyanshMishra。如果用户什么都不做,权限会保留多久?假设我今天授予权限,我重启了几次手机,然后在 2018 年 1 月重新打开了应用程序,我是否需要重新授予权限,还是默认保留?
  • 只有两种情况下权限会被移除 1. 当用户移除 sdcard 2. 使用时会从应用设置中清除应用的缓存和数据。
猜你喜欢
  • 2018-04-17
  • 1970-01-01
  • 2021-02-18
  • 2016-12-21
  • 1970-01-01
  • 2012-12-08
  • 1970-01-01
  • 1970-01-01
  • 2017-09-17
相关资源
最近更新 更多