如何使用为Android 5.0(棒棒糖)提供的新的SD卡访问API?

背景

在Android 4.4 (KitKat)上,Google已经对SD卡进行了相当的限制。

从Android Lollipop(5.0)开始,开发人员可以使用一个新的API,要求用户确认是否允许访问特定的文件夹,就像写在这个Google-Groups文章中一样

问题

这篇文章指导你访问两个网站:

  • https://android.googlesource.com/platform/development/+/android-5.0.0_r2/samples/Vault/src/com/example/android/vault/VaultProvider.java#258

这看起来像是一个内部的例子(可能稍后会在API演示中显示),但是很难理解正在发生的事情。

  • http://developer.android.com/reference/android/support/v4/provider/DocumentFile.html

这是新的API的官方文档,但它没有告诉足够的细节如何使用它。

以下是它告诉你的:

如果您确实需要完全访问文档的整个子树,请启动ACTION_OPEN_DOCUMENT_TREE以让用户select一个目录。 然后将得到的getData()传入fromTreeUri(Context,Uri)开始使用用户select的树。

在浏览DocumentFile实例的树时,可以始终使用getUri()来获取表示该对象的基础文档的Uri,以用于openInputStream(Uri)等。

为了简化运行KITKAT或更早版本的设备上的代码,可以使用fromFile(File)来模拟DocumentsProvider的行为。

问题

我有一些关于新API的问题:

  1. 你怎么用它?
  2. 根据这篇文章,操作系统会记住应用程序被授予访问文件/文件夹的权限。 你如何检查你是否可以访问文件/文件夹? 有没有一个函数返回我可以访问的文件/文件夹的列表?
  3. 你如何处理Kitkat上的这个问题? 它是支持库的一部分吗?
  4. 操作系统上是否有设置屏幕,显示哪些应用程序可以访问哪些文件/文件夹?
  5. 如果在同一台设备上为多个用户安装应用程序会发生什么情况?
  6. 有没有关于这个新API的其他文档/教程?
  7. 权限可以被撤销吗? 如果是这样,是否有意向被发送到应用程序?
  8. 会要求权限工作recursion在选定的文件夹?
  9. 使用权限还允许给用户一个由用户select多重select的机会吗? 还是应用程序需要明确告诉意图哪些文件/文件夹允许?
  10. 有没有在模拟器上尝试新API的方法? 我的意思是,它有SD卡分区,但它作为主要的外部存储,所以所有的访问权限已经给(使用一个简单的权限)。
  11. 当用户用另一个replaceSD卡时会发生什么?

很多很好的问题,让我们深入。:)

你如何使用它?

这里有一个与KitKat中的存储访问框架交互的很好的教程:

https://developer.android.com/guide/topics/providers/document-provider.html#client

与Lollipop中的新API交互非常相似。 要提示用户select一个目录树,你可以启动一个这样的意图:

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

然后在onActivityResult()中,可以将用户select的Uri传递给新的DocumentFile辅助类。 下面是一个简单的例子,列出所选目录中的文件,然后创build一个新文件:

 public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (resultCode == RESULT_OK) { Uri treeUri = resultData.getData(); DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri); // List all existing files inside picked directory for (DocumentFile file : pickedDir.listFiles()) { Log.d(TAG, "Found file " + file.getName() + " with size " + file.length()); } // Create a new file and write into it DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel"); OutputStream out = getContentResolver().openOutputStream(newFile.getUri()); out.write("A long time ago...".getBytes()); out.close(); } } 

DocumentFile.getUri()返回的Uri具有足够的灵活性,可以与不同的平台API一起使用。 例如,您可以使用Intent.setData()使用Intent.setData()来共享它。

如果要从本机代码访问该Uri,可以调用ContentResolver.openFileDescriptor() ,然后使用ParcelFileDescriptor.getFd()detachFd()获取传统的POSIX文件描述符整数。

你如何检查你是否可以访问文件/文件夹?

默认情况下,通过存储访问框架返回的Uris 不会在重新启动时持续存在。 该平台“提供”持续许可的能力,但是如果需要,您仍然需要“取得”许可。 在我们上面的例子中,你会打电话给:

  getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 

您始终可以通过ContentResolver.getPersistedUriPermissions() API找出您的应用程序可以访问的持续授予内容。 如果您不再需要访问持续的Uri,则可以使用ContentResolver.releasePersistableUriPermission()将其释放。

这是在KitKat上可用吗?

不,我们不能追溯性地向旧版本的平台添加新的function。

我可以看到哪些应用程序可以访问文件/文件夹吗?

目前没有用户界面显示,但可以在adb shell dumpsys activity providers输出的“授予的Uri权限”部分find详细信息。

如果在同一台设备上为多个用户安装了应用程序,会发生什么情况?

与所有其他多用户平台function一样,Uri权限授予也是基于每个用户进行隔离的。 也就是说,在两个不同的用户下运行的同一个应用没有重叠或共享的Uri权限授予。

权限可以被撤销吗?

支持DocumentProvider可以随时撤销权限,例如当删除基于云的文档时。 发现这些被撤销的权限的最常见的方式是当它们从ContentResolver.getPersistedUriPermissions()中消失时。

只要应用程序数据被清除,任何涉及授权的应用程序,权限也将被撤销。

会要求权限工作recursion在选定的文件夹?

是的, ACTION_OPEN_DOCUMENT_TREE意图让您recursion访问现有和新创build的文件和目录。

这是否允许多个select?

是的,从KitKat开始支持多选,并且可以在开始ACTION_OPEN_DOCUMENT意图时通过设置EXTRA_ALLOW_MULTIPLE来允许。 您可以使用Intent.setType()EXTRA_MIME_TYPES来缩小可以select的文件的types:

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

有没有在模拟器上尝试新API的方法?

是的,即使在仿真器上,主共享存储设备也应该出现在select器中。 如果您的应用程序仅使用存储访问框架访问共享存储,则不再需要READ/WRITE_EXTERNAL_STORAGE权限可以删除它们或使用android:maxSdkVersionfunction仅在较旧的平台版本上请求它们。

当用户用另一个replaceSD卡时会发生什么?

当涉及物理介质时,底层介质的UUID(例如FAT序列号)总是被烧入返回的Uri中。 即使用户在多个插槽之间交换媒体,系统也会使用此选项将您连接到用户最初select的媒体。

如果用户使用第二张卡交换,则需要提示访问新卡。 由于系统会根据每个UUID记忆赠款,因此如果用户稍后将其重新插入,您将继续先前授予对原始卡的访问权限。

http://en.wikipedia.org/wiki/Volume_serial_number

在我的Github的Android项目中,链接如下,你可以find工作代码,允许在Android5的extSdCard上写。它假定用户访问整个SD卡,然后让你写在这张卡上的任何地方。 (如果你只想访问单个文件,事情会变得更简单。)

主要代码snipplets

触发存储访问框架:

 @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void triggerStorageAccessFramework() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS); } 

处理来自存储访问框架的响应:

 @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) { if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) { Uri treeUri = null; if (resultCode == Activity.RESULT_OK) { // Get Uri from Storage Access Framework. treeUri = resultData.getData(); // Persist URI in shared preference so that you can use it later. // Use your own framework here instead of PreferenceUtil. PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri); // Persist access permissions. final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags); } } } 

通过Storage Access Framework获取文件的outputStream(使用存储的URL,假设这是外部SD卡的根文件夹的URL)

 DocumentFile targetDocument = getDocumentFile(file, false); OutputStream outStream = Application.getAppContext(). getContentResolver().openOutputStream(targetDocument.getUri()); 

这使用以下辅助方法:

 public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) { String baseFolder = getExtSdCardFolder(file); if (baseFolder == null) { return null; } String relativePath = null; try { String fullPath = file.getCanonicalPath(); relativePath = fullPath.substring(baseFolder.length() + 1); } catch (IOException e) { return null; } Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard); if (treeUri == null) { return null; } // start with root of SD card and then parse through document tree. DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri); String[] parts = relativePath.split("\\/"); for (int i = 0; i < parts.length; i++) { DocumentFile nextDocument = document.findFile(parts[i]); if (nextDocument == null) { if ((i < parts.length - 1) || isDirectory) { nextDocument = document.createDirectory(parts[i]); } else { nextDocument = document.createFile("image", parts[i]); } } document = nextDocument; } return document; } public static String getExtSdCardFolder(final 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; } /** * Get a list of external SD card paths. (Kitkat or higher.) * * @return A list of external SD card paths. */ @TargetApi(Build.VERSION_CODES.KITKAT) private static String[] getExtSdCardPaths() { List<String> paths = new ArrayList<>(); for (File file : Application.getAppContext().getExternalFilesDirs("external")) { if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) { int index = file.getAbsolutePath().lastIndexOf("/Android/data"); if (index < 0) { Log.w(Application.TAG, "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()]); } /** * Retrieve the application context. * * @return The (statically stored) application context */ public static Context getAppContext() { return Application.mApplication.getApplicationContext(); } 

参考完整的代码

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java