125

背景

Android 4.4 (KitKat) 上,Google 对 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 卡时会发生什么?
4

3 回答 3

149

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

你如何使用它?

这是与 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);

然后在您的 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();
    }
}

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

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

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

默认情况下,通过存储访问框架意图返回的 Uris不会在重新启动后保留。平台“提供”了持久化权限的能力,但如果你愿意,你仍然需要“获取”权限。在我们上面的例子中,你会调用:

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

ContentResolver.getPersistedUriPermissions()您始终可以通过API找出您的应用程序可以访问的持久授权。如果您不再需要访问持久化的 Uri,您可以使用ContentResolver.releasePersistableUriPermission().

这在 KitKat 上可用吗?

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

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

目前没有显示此内容的 UI,但您可以在adb shell dumpsys activity providers输出的“Granted Uri Permissions”部分中找到详细信息。

如果在同一设备上为多个用户安装应用程序会怎样?

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

于 2014-11-05T19:53:49.717 回答
48

在我在 Github 上的 Android 项目中,链接如下,您可以找到允许在 Android 5 中写入 extSdCard 的工作代码。它假定用户授予对整个 SD 卡的访问权限,然后允许您在此卡上的任何地方写入。(如果您只想访问单个文件,事情会变得更容易。)

主要代码片段

触发存储访问框架:

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

通过存储访问框架获取文件的输出流(利用存储的 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

于 2015-05-28T18:20:43.677 回答
1

SimpleStorage通过简化跨 API 级别的存储访问框架来帮助您。它也适用于范围存储。例如:

val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Downloads/MyMovie.mp4")

val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "9016-4EF8", basePath = "Downloads/MyMovie.mp4")

使用此库授予 SD 卡的 URI 权限、选择文件和文件夹更简单:

class MainActivity : AppCompatActivity() {

    private lateinit var storageHelper: SimpleStorageHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        storageHelper = SimpleStorageHelper(this, savedInstanceState)
        storageHelper.onFolderSelected = { requestCode, folder ->
            // do stuff
        }
        storageHelper.onFileSelected = { requestCode, file ->
            // do stuff
        }

        btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
        btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
        btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        storageHelper.storage.onActivityResult(requestCode, resultCode, data)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        storageHelper.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        storageHelper.onRestoreInstanceState(savedInstanceState)
    }
}
于 2018-05-11T20:33:41.790 回答