使用 MTP 通过 Android Storage Access Framework / DocumentProvider 遍历目录层次结构的问题

Posted

技术标签:

【中文标题】使用 MTP 通过 Android Storage Access Framework / DocumentProvider 遍历目录层次结构的问题【英文标题】:Issues traversing through directory hierarchy with Android Storage Access Framework / DocumentProvider using MTP 【发布时间】:2017-04-27 01:15:01 【问题描述】:

更新: 我最初的问题可能具有误导性,所以我想改写一下: 我想通过 android 的存储访问框架从 MTP 连接的设备遍历层次结构树。我似乎无法做到这一点,因为我得到一个SecurityException 说明子节点不是其父节点的后代。有没有办法解决这个问题?或者这是一个已知问题?谢谢。

我正在编写一个 Android 应用程序,它尝试使用 Android 的存储访问框架 (SAF) 通过 MtpDocumentsProvider 遍历和访问层次结构树中的文档。我或多或少遵循https://github.com/googlesamples/android-DirectorySelection 中描述的代码示例,了解如何从我的应用程序启动 SAF Picker,选择 MTP 数据源,然后在onActivityResult 中,使用返回的Uri 遍历层次结构.不幸的是,这似乎不起作用,因为一旦我访问一个子文件夹并尝试遍历它,我总是会得到一个 SecurityException 说明 document xx is not a descendant of yy

所以我的问题是,使用MtpDocumentProvider,我怎样才能成功地从我的应用程序遍历层次结构树并避免这个异常?

具体来说,在我的应用程序中,我首先调用以下方法来启动 SAF Picker:

private void launchStoragePicker() 
    Intent browseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    browseIntent.addFlags(
        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
            | Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    );
    startActivityForResult(browseIntent, REQUEST_CODE_OPEN_DIRECTORY);

Android SAF 选择器随后启动,我看到我连接的设备被识别为 MTP 数据源。我选择了上述数据源,然后从我的onActivityResult 中获得了Uri

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) 
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == Activity.RESULT_OK) 
        traverseDirectoryEntries(data.getData()); // getData() returns the root uri node
    

然后,使用返回的Uri,我调用DocumentsContract.buildChildDocumentsUriUsingTree 以获取Uri,然后我可以使用它来查询和访问树层次结构:

void traverseDirectoryEntries(Uri rootUri) 
    ContentResolver contentResolver = getActivity().getContentResolver();
    Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getTreeDocumentId(rootUri));

    // Keep track of our directory hierarchy
    List<Uri> dirNodes = new LinkedList<>();
    dirNodes.add(childrenUri);

    while(!dirNodes.isEmpty()) 
        childrenUri = dirNodes.remove(0); // get the item from top
        Log.d(TAG, "node uri: ", childrenUri);
        Cursor c = contentResolver.query(childrenUri, new String[]Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, null, null, null);
        try 
            while (c.moveToNext()) 
                final String docId = c.getString(0);
                final String name = c.getString(1);
                final String mime = c.getString(2);
                Log.d(TAG, "docId: " + id + ", name: " + name + ", mime: " + mime);
                if(isDirectory(mime)) 
                    final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId);
                    dirNodes.add(newNode);
                
            
         finally 
            closeQuietly(c);
        
    


// Util method to check if the mime type is a directory
private static boolean isDirectory(String mimeType) 
    return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);


// Util method to close a closeable
private static void closeQuietly(Closeable closeable) 
    if (closeable != null) 
        try 
            closeable.close();
         catch (RuntimeException re) 
            throw re;
         catch (Exception ignore) 
            // ignore exception
        
    

外部 while 循环的第一次迭代成功:对 query 的调用返回了一个有效的 Cursor 供我遍历。问题是第二次迭代:当我尝试查询 Uri(恰好是 rootUri 的子节点)时,我得到一个 SecurityException 说明文档 xx 不是 yy 的后代。

D/MyApp(19241): 节点 uri: content://com.android.mtp.documents/tree/2/document/2/children D/MyApp(19241):docId:4,名称:DCIM,mime:vnd.android.document/directory D/MyApp(19241): 节点 uri: content://com.android.mtp.documents/tree/2/document/4/children E/DatabaseUtils(20944):将异常写入包裹 E/DatabaseUtils(20944): java.lang.SecurityException: 文档 4 不是 2 的后代

谁能提供一些关于我做错了什么的见解?如果我使用不同的数据源提供程序,例如来自外部存储的数据源提供程序(即通过标准 USB OTG 读卡器连接的 SD 卡),一切正常。

附加信息: 我在 Nexus 6P、Android 7.1.1 上运行它,我的应用 minSdkVersion 是 19。

【问题讨论】:

and I see my connected device recognized as the MTP data source.。你能详细说明一下吗?中期计划?您是否在 Android 设备上连接了设备?如何?什么样的设备? if(isDirectory(mime)) 。您没有发布该功能的代码。你也没有解释。 你有 Uri rootUri 和 Uri uri。但后者没有解释。 closeQuietly(childCursor);childCursor? @greenapps 嗨,是的,我使用 USB-C 到 USB-C 电缆在我的 Android 手机上连接了相机设备。 Android 成功检测到设备并确定我可以通过 MTP 访问内容。 isDirectorycloseQuietly 方法只是辅助方法。我在我编辑的帖子中添加了代码。 rootUri 是从 onActivityResult 返回的 Uri。我犯了一个复制/粘贴错误,我已经修复了。 【参考方案1】:

我正在使用您的代码通过添加行进入子文件夹:

Uri childrenUri;
try 
    //for childs and sub child dirs
    childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri));
 catch (Exception e) 
    // for parent dir
    childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));

【讨论】:

【参考方案2】:

经过不同的尝试,我不确定是否有办法绕过这个SecurityException 用于 MTP 数据源(除非有人可以反驳我)。查看DocumentProvider.java 源代码和堆栈跟踪,似乎在DocumentProvider#enforceTree 中对DocumentProvider#isChildDocument 的调用可能没有在Android 框架的MtpDocumentProvider 实现中被正确覆盖。默认实现总是返回false。 #悲伤的脸

【讨论】:

也许吧。但是,如果您像我一样只使用 DocumentFile,则不会使用这些功能。那么异常会从哪里来呢? 问题是当您在Uri 查询相关项目时。要访问DocumentFile 的内容,您仍然需要通过DocumentFile#getUri 获取它的Uri,或者在内部DocumentFile 使用这个Uri 引用来完成它需要完成的事情(例如,调用@ 987654333@ 抛出相同的异常)。所以我的猜测是,DocumentFile 通过内容解析器查询这个 Uri,后者又在内部调用queryChildren 的具体实现,最终调用enforceTree,然后抛出异常。这是我的猜测。 遗憾的是我没有去测试。此外,处理 mtp 是否为 Android 7 之后的其他版本实现? may not have been properly overriden in the MtpDocumentProvider implementation in the Android framework。抱歉,但我无法想象使用此代码。提供商在您的 go pro 相机上,您怎么知道它是如何在那里实现的?【参考方案3】:

在开始活动之前为结果添加的标志没有任何作用。

我不明白您使用 DocumentsContract。用户选择一个目录。从获得的 uri 中,您可以为该目录构造一个 DocumentFile

之后在该实例上使用DocumentFile::listFiles() 以获取子目录和文件的列表。

【讨论】:

我想遍历从rootUri开始的树层次结构。我调用 DocumentsContract 方法来构建子 uri,这样我就可以递归地遍历其下的目录。所以假设我有一个根节点 A,并且有子目录 B 和 C。而 B 有名为 B1、B2、B3 的文件。最终我想访问 B1、B2、B3(它们可以是图像文件),但问题是我无法遍历 B 的子节点,因为只要我请求 B 的子节点,它就会给我@ 987654325@. 你不必再解释你想要什么和你做什么。我已经理解了这一点,并尝试了适用于 sd 卡的代码。你所有的代码对我来说都是新的,所以很有趣。但我想知道的是为什么你不使用我建议的代码。你甚至没有对我的提议做出反应。使用我的代码,您也可以遍历所有内容。否则我没有发布它。 因为我想直接访问Uri,我不想简单地列出它们。如果我想打开上述目录的内容,例如图像文件,并通过流打开它,我应该可以做到。但是,是的,让我试一试你的方法,让你知道会发生什么。我只是回复 cmets 以确保我的意图是明确的。 如前所述,您可以使用 DocumentFile 浏览和列出所有内容。并使用 getContentResolver()。 openInputStream(docfile.getUri()) 读取文件的内容。这是消费者的正常方式。同时,您还没有解释为什么要为消费者使用提供者的代码。 我尝试使用DocumentFile#listFiles(),但也没有用。我得到同样的行为。每当我尝试访问子Uri 时,例如在使用getContentResolver().openInputStream 在流中打开它时,甚至调用DocumentFile#isDirectory() 时,它都会抛出相同的SecurityException。生成的Uri 是一样的,无论我使用DocumentFile 还是通过提供程序。同样,这仅适用于数据源是 MTP 数据源的情况。如果它来自外部存储(即 SD 卡),则工作正常

以上是关于使用 MTP 通过 Android Storage Access Framework / DocumentProvider 遍历目录层次结构的问题的主要内容,如果未能解决你的问题,请参考以下文章

与 Android 平板电脑刷新 MTP 连接?

Android MTP 文件浏览Demo

如何将 Android M 默认 USB 配置设置为 MTP 而不是“仅充电”?

如何为 mtp 设备构建路径(可在文件夹浏览对话框中使用)?

无法访问 Android (MTP) 中公用文件夹中存储的文件

Android 8.0 删除 MTP模式