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

and*_*per 113 android android-sdcard android-5.0-lollipop documentfile

背景

Android 4.4(KitKat)上,谷歌已经限制了对SD卡的访问.

从Android Lollipop(5.0)开始,开发人员可以使用新的API,要求用户确认允许访问特定文件夹,如此Google-Groups帖子所述.

问题

该帖子指导您访问两个网站:

这看起来像是一个内部示例(可能稍后会在API演示中显示),但很难理解发生了什么.

这是新API的官方文档,但它没有详细说明如何使用它.

这是它告诉你的:

如果您确实需要完全访问整个文档子树,请首先启动ACTION_OPEN_DOCUMENT_TREE以允许用户选择目录.然后将生成的getData()传递给fromTreeUri(Context,Uri)以开始使用用户选择的树.

在导航DocumentFile实例树时,您始终可以使用getUri()来获取表示该对象的基础文档的Uri,以便与openInputStream(Uri)等一起使用.

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

问题

我对新API有几个问题:

  1. 你是如何真正使用它的?
  2. 根据帖子,操作系统会记住应用程序被授予访问文件/文件夹的权限.你如何检查你是否可以访问文件/文件夹?是否有一个函数可以返回我可以访问的文件/文件夹列表?
  3. 你如何在Kitkat上处理这个问题?它是支持库的一部分吗?
  4. 操作系统上是否有设置屏幕,显示哪些应用可以访问哪些文件/文件夹?
  5. 如果在同一设备上为多个用户安装了应用程序,会发生什么?
  6. 是否有关于这个新API的任何其他文档/教程?
  7. 权限可以被撤销吗?如果是这样,是否有意图被发送到应用程序?
  8. 是否会在所选文件夹上递归请求权限工作?
  9. 使用该权限是否也允许用户有机会根据用户的选择进行多次选择?或者应用程序是否需要专门告知意图允许哪些文件/文件夹?
  10. 模拟器上有没有办法尝试新的API?我的意思是,它有SD卡分区,但它作为主要外部存储器,所以已经给出了所有访问权限(使用简单的权限).
  11. 当用户用另一张SD卡替换SD卡时会发生什么?

Jef*_*key 139

很多好问题,让我们深入研究.:)

你如何使用它?

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

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

与Lollipop中的新API交互非常相似.要提示用户选择目录树,您可以启动这样的意图:

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, 42);
Run Code Online (Sandbox Code Playgroud)

然后在onActivityResult()中,您可以将用户选择的Uri传递给新的DocumentFile助手类.这是一个快速示例,列出了已挑选目录中的文件,然后创建一个新文件:

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

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

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

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

默认情况下,通过存储访问框架返回的Uris意图不会在重新启动后保留.该平台"提供"持久许可的能力,但如果您需要,您仍需要"获取"权限.在上面的示例中,您将调用:

    getContentResolver().takePersistableUriPermission(treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION |
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Run Code Online (Sandbox Code Playgroud)

您始终可以通过ContentResolver.getPersistedUriPermissions()API 找出您的应用可以访问的持久授权.如果您不再需要访问持久性Uri,可以使用它来释放它ContentResolver.releasePersistableUriPermission().

KitKat上有这个吗?

不,我们无法追溯性地向旧版本的平台添加新功能.

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

目前没有显示此功能的UI,但您可以在输出的"授予Uri权限"部分中找到详细信息adb shell dumpsys activity providers.

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

Uri权限授予是基于每个用户隔离的,就像所有其他多用户平台功能一样.也就是说,在两个不同用户下运行的相同应用程序没有重叠或共享的Uri权限授予.

权限可以被撤销吗?

后备DocumentProvider可以随时撤消权限,例如删除基于云的文档时.发现这些撤销权限的最常见方式是它们从ContentResolver.getPersistedUriPermissions()上面提到的消失.

每当为授权中涉及的应用程序清除应用程序数据时,权限也会被撤销.

是否会在所选文件夹上递归请求权限工作?

是的,ACTION_OPEN_DOCUMENT_TREE意图使您可以递归访问现有和新创建的文件和目录.

这是否允许多种选择?

是的,自KitKat以来一直支持多种选择,您可以通过设置EXTRA_ALLOW_MULTIPLE启动ACTION_OPEN_DOCUMENT意图来允许它.您可以使用Intent.setType()EXTRA_MIME_TYPES缩小可以选择的文件类型:

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

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

是的,主要的共享存储设备应该出现在选择器中,即使在模拟器上也是如此.如果您的应用仅使用存储访问框架来访问共享存储,则您根本不再需要READ/WRITE_EXTERNAL_STORAGE权限,可以删除它们或使用该android:maxSdkVersion功能仅在较旧的平台版本上请求它们.

当用户用另一张SD卡替换SD卡时会发生什么?

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

如果用户换入第二张卡,则需要提示访问新卡.由于系统会记住基于每个UUID的授权,如果用户稍后重新插入,您将继续拥有以前授予的对原始卡的访问权限.

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

  • @JeffSharkey有没有办法为OPEN_DOCUMENT_TREE提供起始位置提示?用户并不总是最擅长导航到应用程序需要访问的内容. (7认同)
  • 我知道了。所以决定是不要向已知的(文件的)API 添加更多内容,而是使用不同的 API。好的。您能否回答其他问题(写在第一条评论中)?BTW,谢谢你回答所有这些。 (2认同)
  • 有没有办法,如何在File类中更改Last Modified Timestamp(**setLastModified()**方法)?这对于档案馆等应用程序来说非常重要. (2认同)

Jör*_*eld 42

在我下面链接的Github的Android项目中,你可以找到允许在Android 5中写入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);
}
Run Code Online (Sandbox Code Playgroud)

处理来自Storage Access Framework的响应:

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

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

DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
    getContentResolver().openOutputStream(targetDocument.getUri());
Run Code Online (Sandbox Code Playgroud)

这使用以下帮助器方法:

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

参考完整代码

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

  • @JörgEisfeld:我有一个使用"文件"254次的应用程序.你能想象修复吗?Android正在成为开发人员的噩梦,因为它完全缺乏向后兼容性.我仍然没有找到任何地方解释为什么谷歌采取所有这些关于外部存储的愚蠢决定.有些人声称"安全",但当然是无稽之谈,因为任何应用都会搞乱内部存储.我的猜测是试图强迫我们使用他们的云服务.值得庆幸的是,rooting解决了这些问题......至少对于Android <6 .... (14认同)