android - 如何使用新的 Lollipop API 访问所有 SD 卡?

标签 android file uri android-sdcard documentfile

背景

从 Lollipop 开始,应用程序可以访问真正的 SD 卡(在 Kitkat 上无法访问它之后,并且在以前的版本上还没有正式支持),正如我问过的 here .

问题

因为很少看到支持 SD 卡的 Lollipop 设备,而且仿真器实际上没有能力(或者有吗?)模拟 SD 卡支持,所以我花了很长时间来测试它.

无论如何,似乎不是使用普通的 File 类来访问 SD 卡(一旦你获得了它的许可),你需要使用 Uris,使用 DocumentFile 。

这限制了对正常路径的访问,因为我找不到将 Uris 转换为路径的方法,反之亦然(而且这很烦人)。这也意味着我不知道如何检查当前的 SD 卡是否可以访问,所以我不知道何时向用户请求读/写(或向他们)的权限。

我试过的

目前,这是我获取所有 SD 卡路径的方式:

  /**
   * returns a list of all available sd cards paths, or null if not found.
   *
   * @param includePrimaryExternalStorage set to true if you wish to also include the path of the primary external storage
   */
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  public static List<String> getExternalStoragePaths(final Context context,final boolean includePrimaryExternalStorage)
    {
    final File primaryExternalStorageDirectory=Environment.getExternalStorageDirectory();
    final List<String> result=new ArrayList<>();
    final File[] externalCacheDirs=ContextCompat.getExternalCacheDirs(context);
    if(externalCacheDirs==null||externalCacheDirs.length==0)
      return result;
    if(externalCacheDirs.length==1)
      {
      if(externalCacheDirs[0]==null)
        return result;
      final String storageState=EnvironmentCompat.getStorageState(externalCacheDirs[0]);
      if(!Environment.MEDIA_MOUNTED.equals(storageState))
        return result;
      if(!includePrimaryExternalStorage&&VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB&&Environment.isExternalStorageEmulated())
        return result;
      }
    if(includePrimaryExternalStorage||externalCacheDirs.length==1)
      {
      if(primaryExternalStorageDirectory!=null)
        result.add(primaryExternalStorageDirectory.getAbsolutePath());
      else
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[0]));
      }
    for(int i=1;i<externalCacheDirs.length;++i)
      {
      final File file=externalCacheDirs[i];
      if(file==null)
        continue;
      final String storageState=EnvironmentCompat.getStorageState(file);
      if(Environment.MEDIA_MOUNTED.equals(storageState))
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[i]));
      }
    return result;
    }


private static String getRootOfInnerSdCardFolder(File file)
  {
  if(file==null)
    return null;
  final long totalSpace=file.getTotalSpace();
  while(true)
    {
    final File parentFile=file.getParentFile();
    if(parentFile==null||parentFile.getTotalSpace()!=totalSpace)
      return file.getAbsolutePath();
    file=parentFile;
    }
  }

这是我检查我可以访问的 Uris 的方法:
final List<UriPermission> persistedUriPermissions=getContentResolver().getPersistedUriPermissions();

这是访问 SD 卡的方法:
startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),42);

public void onActivityResult(int requestCode,int resultCode,Intent resultData)
  {
  if(resultCode!=RESULT_OK)
    return;
  Uri treeUri=resultData.getData();
  DocumentFile pickedDir=DocumentFile.fromTreeUri(this,treeUri);
  grantUriPermission(getPackageName(),treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  getContentResolver().takePersistableUriPermission(treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }

问题
  • 是否可以检查当前的 SD 卡是否可访问,哪些不可访问,并以某种方式要求用户获得对它们的许可?
  • 是否有在 DocumentFile uris 和真实路径之间进行转换的官方方法?我找到了 this answer ,但它在我的情况下崩溃了,而且看起来很糟糕。
  • 是否可以向用户请求有关特定路径的许可?甚至可能只显示“你接受是/否吗?”的对话框?
  • 一旦授予权限,是否可以使用普通的 File API 而不是 DocumentFile API?
  • 给定一个文件/文件路径,是否可以只请求访问它的权限(并检查它之前是否已给出)或其根路径?
  • 是否可以使模拟器具有 SD 卡?目前,它提到了“SD 卡”,但它用作主要外部存储,我想使用辅助外部存储对其进行测试,以尝试使用新的 API。

  • 我认为对于其中一些问题,回答另一个问题很有帮助。

    最佳答案

    Is it possible to check if the current SD-cards are accessible, which aren't , and somehow ask the user to get permission to them?



    这有 3 个部分:检测、有哪些卡、检查是否安装了卡以及请求访问。

    如果设备制造商是好人,您可以通过拨打 getExternalFiles 获得“外部”存储列表。与 null参数(请参阅相关的 javadoc)。

    他们可能不是好人。并不是每个人都拥有最新、 HitTest 门、无错误且定期更新的操作系统版本。所以可能有一些目录没有在那里列出(例如 OTG USB 存储等)。如有疑问,您可以从文件 /proc/self/mounts 中获取完整的操作系统安装列表。 .该文件是 Linux 内核的一部分,其格式记录在 here .您可以使用 /proc/self/mounts 的内容作为备份:解析它,找到可用的文件系统(fat、ext3、ext4 等)并删除重复的输出 getExternalFilesDirs .以一些不显眼的名称为用户提供剩余的选项,比如“杂项目录”。这将为您提供所有可能的外部、内部、任何存储空间。

    编辑 : 在给出上述建议后,我终于尝试自己遵循它。到目前为止它运行良好,但要注意不是 /proc/self/mounts你最好解析 /pros/self/mountinfo (前者在现代 Linux 中仍然可用,但后来是更好、更强大的替代品)。还要确保占atomicity issues根据挂载列表的内容进行假设时。

    您可以通过调用 canRead 天真地检查目录是否可读/可写。和 canWrite在上面。如果这成功了,那么做额外的工作就没有意义了。如果没有,您要么拥有持久的 Uri 权限,要么没有。

    “获得许可”是一个丑陋的部分。 AFAIK,没有办法在 SAF 基础设施中完全做到这一点。 Intent.ACTION_PICK听起来像是可以工作的东西(因为它接受一个可以从中挑选的 Uri),但事实并非如此。也许,这可以被认为是一个错误,应该像这样报告给 Android 错误跟踪器。

    Given a file/filepath, is it possible to just request a permission to access it (and check if it's given before), or its root path ?



    这是什么ACTION_PICK是为了。同样,SAF 选择器不支持 ACTION_PICK开箱即用。第三方文件管理器可能会,但实际上很少会授予您真正的访问权限。如果您愿意,也可以将此报告为错误。

    编辑 :这个答案是在Android Nougat 出来之前写的。从 API 24 开始,仍然无法请求对特定目录的细粒度访问,但至少可以动态请求访问整个卷:determine the volume ,包含文件,并使用 getAccessIntent 请求访问与 null参数(用于二级卷)或通过请求 WRITE_EXTERNAL_STORAGE权限(对于主卷)。

    Is there an official way to convert between the DocumentFile uris and real paths? I've found this answer, but it crashed in my case, plus it looks hack-y.



    没有永不。您将永远屈从于存储访问框架创建者的奇思妙想!邪恶的笑声

    实际上,有一种更简单的方法:只需打开 Uri 并检查创建的描述符的文件系统位置(为简单起见,仅 Lollipop 版本):
    public String getFilesystemPath(Context context, Uri uri) {
      ContentResolver res = context.getContentResolver();
    
      String resolved;
      try (ParcelFileDescriptor fd = res.openFileDescriptor(someSafUri, "r")) {
        final File procfsFdFile = new File("/proc/self/fd/" + fd.getFd());
    
        resolved = Os.readlink(procfsFdFile.getAbsolutePath());
    
        if (TextUtils.isEmpty(resolved)
              || resolved.charAt(0) != '/'
              || resolved.startsWith("/proc/")
              || resolved.startsWith("/fd/"))
        return null;
      } catch (Exception errnoe) {
        return null;
      }
    }
    

    如果上述方法返回一个位置,您仍然需要通过 File 访问才能使用它。 .如果它不返回位置,则有问题的 Uri 不会引用文件(即使是临时文件)。它可能是网络流、Unix 管道等等。您可以从 this answer 获取旧 Android 版本的上述方法版本。 .它适用于任何 Uri、任何 ContentProvider——不仅仅是 SAF——只要 Uri 可以用 openFileDescriptor 打开。 (例如,它来自 Intent.CATEGORY_OPENABLE )。

    请注意,如果您将官方 Linux API 的任何部分视为此类,则上述方法可以被视为官方方法。很多 Linux 软件都使用它,我也看到它被一些 AOSP 代码使用(例如在 Launcher3 测试中)。

    编辑 :Android Nougat 推出数量security changes ,最显着的是更改了应用程序私有(private)目录的权限。这意味着,自 API 24 起,当 Uri 引用应用程序私有(private)目录中的文件时,上面的代码段将始终失败并出现异常。这符合预期:您根本不再需要知道该路径。即使您通过其他方式以某种方式确定文件系统路径,您也无法使用该路径访问文件。即使其他应用程序与您合作并将文件的权限更改为世界可读,您仍然无法访问它。这是因为 Linux does not allow access to files, if you don't have search access to one of directories in path .因此,从 ContentProvider 接收文件描述符是访问它们的唯一方法。

    Is it possible to use the normal File API instead of the DocumentFile API once the permission was granted?



    你不能。至少不是牛轧糖。普通文件 API 进入 Linux 内核以获得权限。根据 Linux 内核,您的外部 SD 卡具有限制性权限,这会阻止您的应用程序使用它。 Storage Access Framework 给出的权限由 SAF(IIRC 存储在一些 xml 文件中)管理,内核对它们一无所知。您必须使用中间方(存储访问框架)来访问外部存储。请注意,Linux 内核有自己的机制来管理对目录子树的访问(称为 bind-mounts),但是存储访问框架的创建者要么不知道,要么不想使用它。

    通过使用从 Uri 创建的文件描述符,您可以获得一定程度的访问权限(几乎任何事情,都可以通过 File 完成)。我建议你阅读 this answer ,它可能有一些关于使用一般文件描述符以及与 Android 相关的有用信息。

    关于android - 如何使用新的 Lollipop API 访问所有 SD 卡?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31662055/

    相关文章:

    uri - Wiremock 中的多值查询字符串

    android - 如何在 android 和 ios 上运行 angular 7 应用程序?

    android - Firebase crashlytics 中应用内消息传递崩溃?

    android - 我应该忽略用户的这个异常吗

    php - 从 url codeigniter 获取值参数

    jsp - URI 在识别 JSTL 标签库中起什么作用?

    带有自定义内容的 Android 默认样式通知

    php - 如何通过 PHP 从 MySQL 数据库中导出 20,000 个联系人以导入到桌面地址簿中?

    C fprintf 十六进制错误输出

    android - 如何增加 Android App .apk 的文件大小