当前位置: 首页 > 知识库问答 >
问题:

如何使用新的LollipopAPI访问所有SD卡?

长孙硕
2023-03-14

从Lollipop开始,应用程序可以访问真正的SD卡(在Kitkat上无法访问,之前的版本还没有正式支持),正如我在这里询问的那样。

因为现在已经很少看到支持SD卡的Lollipop设备了,因为模拟器没有这个功能(或者真的有吗?)为了模拟SD卡支持,我花了很长时间来测试它。

不管怎样,似乎不是使用普通的文件类来访问SD卡(一旦获得了权限),而是需要使用URI,使用DocumentFile。

这限制了对正常路径的访问,因为我找不到一种方法将URI转换为路径,反之亦然(而且这很烦人)。这也意味着我不知道如何检查当前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;
    }
  }

以下是我检查可以访问哪些URI的方式:

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

>

是否有正式的方式在文档文件URI和实际路径之间转换?我已经找到了这个答案,但在我的情况下,它崩溃了,而且它看起来很粗糙。

是否可以向用户请求有关特定路径的权限?甚至可能只显示“你接受是/否”的对话框?

一旦授予权限,是否可以使用普通的文件应用编程接口而不是文档文件应用编程接口

给定一个文件/文件路径,是否可以只请求访问它的权限(并检查之前是否给出过权限)或其根路径?

有可能使模拟器有一个SD卡吗?目前,它已经提到了“SD卡”,但它作为主要的外部存储工作,我想使用次要的外部存储来测试它,以便尝试使用新的应用编程接口。

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

共有2个答案

呼延高超
2023-03-14

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

从API 19(KitKat)开始,非公开的Android类StorageVolume可用于通过反思获得问题的一些答案:

public static Map<String, String> getSecondaryMountedVolumesMap(Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Object[] volumes;

    StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
    Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList");
    volumes = (Object[])getVolumeListMethod.invoke(sm);

    Map<String, String> volumesMap = new HashMap<>();
    for (Object volume : volumes) {
        Method getStateMethod = volume.getClass().getMethod("getState");
        String mState = (String) getStateMethod.invoke(volume);

        Method isPrimaryMethod = volume.getClass().getMethod("isPrimary");
        boolean mPrimary = (Boolean) isPrimaryMethod.invoke(volume);

        if (!mPrimary && mState.equals("mounted")) {
            Method getPathMethod = volume.getClass().getMethod("getPath");
            String mPath = (String) getPathMethod.invoke(volume);

            Method getUuidMethod = volume.getClass().getMethod("getUuid");
            String mUuid = (String) getUuidMethod.invoke(volume);

            if (mUuid != null && mPath != null)
                volumesMap.put(mUuid, mPath);
        }
    }
    return volumesMap;
}

此方法为您提供所有已安装的非主存储(包括USB OTG),并将其UUID映射到其路径,这将帮助您将DocumentUri转换为真实路径(反之亦然):

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static String convertToPath(Uri treeUri, Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    String documentId = DocumentsContract.getTreeDocumentId(treeUri);
    if (documentId != null){
        String[] split = documentId.split(":");
        String uuid = null;
        if (split.length > 0)
            uuid = split[0];
        String pathToVolume = null;
        Map<String, String> volumesMap = getSecondaryMountedVolumesMap(context);
        if (volumesMap != null && uuid != null)
            pathToVolume = volumesMap.get(uuid);
        if (pathToVolume != null) {
            String pathInsideOfVolume = split.length == 2 ? IFile.SEPARATOR + split[1] : "";
            return pathToVolume + pathInsideOfVolume;
        }
    }
    return null;
}

谷歌似乎不会给我们更好的方式来处理他们丑陋的SAF。

编辑:似乎这种方法在Android 6.0中是无用的。。。

佘修为
2023-03-14

是否可以检查当前的SD卡是否可以访问,而哪些不能访问,并以某种方式要求用户获得对它们的许可?

这有三个部分:检测、有什么卡、检查卡是否已安装,以及请求访问。

如果设备制造商是好人,您可以通过调用带有null参数的getExternalFiles来获取“外部”存储列表(请参阅相关的javadoc)。

他们可能不是好人。并不是每个人都有最新、最热门、无错误的定期更新操作系统版本。所以可能有一些目录,没有列出那里(如OTG USB存储等)。每当有疑问时,您可以从文件/proc/自/挂载中获得操作系统挂载的完整列表。这个文件是Linux内核的一部分,它的格式记录在这里。您可以使用/proc/自己/挂载的内容作为备份:解析它,找到可用的文件系统(脂肪,ext3,ext4等)。)并通过输出getExtranalFilesDir删除重复。以一些不引人注目的名称向用户提供剩余的选项,比如“misc目录”。这将为您提供所有可能的外部,内部,任何存储空间。

编辑:在给出了以上建议之后,我终于试着自己去做了。到目前为止,它工作得还不错,但要注意,与其解析/proc/self/mounts,不如解析/pros/self/mountinfo(前者在现代Linux中仍然可用,但后来是一个更好、更健壮的替代品)。在根据挂载列表的内容进行假设时,还要确保考虑原子性问题。

您可以简单地检查目录是否可读/可写,方法是调用canRead并在上面写入。如果成功了,做额外的工作就没有意义了。如果没有,你要么有一个持久的Uri权限,要么没有。

“获得许可”是一个丑陋的部分。好吧,在SAF的基础设施中,没有办法干净地做到这一点<代码>意图。ACTION_PICK听起来可能有用(因为它接受一个Uri,可以从中进行选择),但它不起作用。也许,这可以被认为是一个bug,应该向Android bug tracker报告。

给定一个文件/文件路径,是否可以只请求访问它的权限(并检查之前是否给出过权限)或其根路径?

这就是ACTION_PICK的作用。同样,SAF选择器不支持开箱即用的ACTION_PICK。第三方文件管理器可能会,但其中很少有人会真正授予您访问权限。如果你愿意,你也可以报告这是错误。

编辑:这个答案是在Android牛轧糖问世之前写的。截至API 24,仍然无法请求对特定目录的细粒度访问,但至少可以动态请求对整个卷的访问:确定包含文件的卷,并使用带有null参数的getAccessIntent(对于辅助卷)或通过请求WRITE_EXTERNAL_STORAGE权限(对于主卷)请求访问。

是否有正式的方式在文档文件URI和实际路径之间转换?我已经找到了这个答案,但在我的情况下,它崩溃了,而且它看起来很粗糙。

不,从来没有。你将永远服从存储访问框架创建者的奇思妙想!恶笑

实际上,有一种更简单的方法:只需打开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;
  }
}

如果上述方法返回一个位置,您仍然需要访问才能将其与文件一起使用。如果未返回位置,则所讨论的Uri不会引用文件(即使是临时文件)。它可能是一个网络流、Unix管道等等。你可以从这个答案中获得上面的旧版本Android的方法版本。它适用于任何Uri、任何内容提供者,而不仅仅是SAF,只要可以使用openFileDescriptor打开Uri(例如,它来自Intent.CATEGORY_OPENABLE)。

请注意,如果您认为官方LinuxAPI的任何部分是官方的,那么上面的方法可以被视为官方的。许多Linux软件使用它,我也看到它被一些AOSP代码使用(例如在Launcher3测试中)。

编辑:Android牛轧糖引入了许多安全更改,最显著的更改应用程序私有目录的权限。这意味着,由于API 24,当Uri引用应用程序私有目录中的文件时,上面的代码片段将始终在Exc0019中失败。这是原意:你不再被期望知道这条路。即使您以其他方式确定了文件系统路径,您也不能使用该路径访问文件。即使其他应用程序与您合作并将文件的权限更改为全局可读,您仍然无法访问它。这是因为Linux不允许访问文件,如果您没有路径中目录之一的搜索访问权限。因此,从ContentProvider接收文件描述符是访问它们的唯一方法。

一旦授予权限,是否可以使用普通的文件应用编程接口而不是文档文件应用编程接口?

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

通过使用从Uri创建的文件描述符,您可以获得一定程度的访问权限(几乎任何东西,都可以通过File完成)。我建议你阅读这个答案,它可能有一些有用的信息,关于使用文件解码器在一般和有关Android.

 类似资料:
  • 在Android4.4(KitKat)上,谷歌对SD卡的访问进行了严格的限制。 从Android Lollipop(5.0)开始,开发人员可以使用一个新的API,它要求用户确认是否允许访问特定的文件夹,正如Google-Groups的帖子所写的那样。 null null 关于新的API我有几个问题: 如何真正使用它? 根据这篇文章,操作系统会记住应用程序被授予了访问文件/文件夹的权限。如何检查是否

  • 问题内容: 我正在尝试访问所有显示的URL,但它首先访问的不是全部 Google搜索网址https://www.google.co.uk/search?q=Rashmi&oq=Rashmi&aqs=chrome..69i57j69i60l3.6857j0j1&sourceid=chrome&ie=UTF-8#q=Rashmi+Custom+Tailors 问题答案: 您的问题是使用以下方法导致的

  • 我想做一个像相册一样的lib改编成Android Q 由于范围存储,不推荐使用; 我们不能直接通过这样的路径读取文件 没有URI之类的值,因此我无法通过ContentProvider获取图片。 我们可以通过这种方式只打开一个图片(下面的代码),并且在回调中接收到一个URI; 但是我想访问所有的图片,那么,我如何扫描所有的图片在Android Q?

  • 问题内容: 我有包含Gmail登录信息的Cookie,因此Chrome会自动打开我的Gmail。 我尝试了以下代码,但是没有用: 我检查了它的目录是可以的。这里有什么问题? 问题答案: 我以前没有注意到的Chrome驱动程序官方Wiki页面上有一个“ 已知问题”部分: 已知的问题 3。无法指定自定义配置文件 现在,我不知道这是不是过时的。我找不到为此的错误报告。的确,您不能通过(自2013年7月起

  • 问题内容: 我有一个地图对象,该对象放置在控制器中的Spring 中,并转发到jsp视图以填充选择。第一次填充后,我想用我使用jquery AJAX检索的json对象替换用于填充select的map对象,并使用jQuery.parseJSON转换为对象。我可以用json对象的内容动态替换select的全部内容吗? 问题答案: 对于实际修改选项,您实际上并不需要jQuery。您可以通过分配框属性的属

  • 问题内容: 我在数据库中收集了300个对象。我可以通过MongoDB的交互式外壳轻松地与该集合进行交互;但是,当我尝试在express.js应用程序中通过Mongoose获取集合时,我得到了一个空数组。 我的问题是,如何才能访问此现有数据集而不是快速创建它?这是一些代码: 输出: 问题答案: 猫鼬增加了在模式下指定集合名称的功能,或在声明模型时作为第三个参数的功能。否则,它将使用您映射到模型的名称