You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android N定制设备自定义Document Provider无法识别USB OTG求助

问题分析与解决方案

看起来你的自定义Document Provider没显示USB内容的核心问题在于路径指向错误和Document ID与实际USB存储路径未关联,咱们一步步拆解修复:

1. 核心问题定位

  • 你的queryChildDocuments方法里,把父路径拼到了APP内部存储的getFilesDir()下,这完全不是USB存储的位置,自然看不到USB文件
  • queryRoots中设置的COLUMN_DOCUMENT_ID为"/",没有和实际的USB挂载路径绑定,导致后续无法正确定位USB目录

2. 修复步骤与代码示例

第一步:正确获取USB存储的挂载路径

Android N上USB OTG存储通常挂载在/storage/[USB名称]或/mnt/media_rw/[UUID]目录下,我们需要先拿到这个路径:

private List<String> getUsbMountPaths() {
    List<String> usbPaths = new ArrayList<>();
    StorageManager storageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
    
    if (storageManager != null) {
        List<StorageVolume> volumes = storageManager.getStorageVolumes();
        for (StorageVolume volume : volumes) {
            // 判断是否为可移除的USB存储(非模拟SD卡)
            if (volume.isRemovable() && !volume.isEmulated()) {
                try {
                    // Android N需用反射获取路径(API26+才有getPath()方法)
                    Method pathMethod = StorageVolume.class.getMethod("getPath");
                    String usbPath = (String) pathMethod.invoke(volume);
                    usbPaths.add(usbPath);
                } catch (Exception e) {
                    e.printStackTrace();
                    // 备选方案:遍历/mnt/media_rw目录找USB挂载点
                    File mediaRwDir = new File("/mnt/media_rw");
                    if (mediaRwDir.exists() && mediaRwDir.isDirectory()) {
                        for (File dir : mediaRwDir.listFiles()) {
                            if (dir.isDirectory() && !dir.getName().startsWith("emulated")) {
                                usbPaths.add(dir.getAbsolutePath());
                            }
                        }
                    }
                }
            }
        }
    }
    return usbPaths;
}

第二步:修正queryRoots方法,关联USB路径

把根节点指向USB存储,用自定义格式的Document ID绑定实际路径:

@Override 
public Cursor queryRoots(String[] projection) throws FileNotFoundException { 
    final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
    List<String> usbPaths = getUsbMountPaths();

    // 遍历所有连接的USB设备(这里示例处理第一个,多设备可循环添加)
    if (!usbPaths.isEmpty()) {
        String usbRootPath = usbPaths.get(0);
        MatrixCursor.RowBuilder row = result.newRow();
        row.add(Root.COLUMN_ROOT_ID, "usb_root_0"); // 唯一标识USB根节点
        row.add(Root.COLUMN_ICON, R.drawable.ic_menu_manage);
        row.add(Root.COLUMN_TITLE, "USB驱动器");
        row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_LOCAL_ONLY);
        row.add(Root.COLUMN_SUMMARY, "已连接的USB存储");
        row.add(Root.COLUMN_DOCUMENT_ID, "usb:" + usbRootPath); // 用"usb:"前缀标识USB路径
        row.add(Root.COLUMN_MIME_TYPES, "*/*");
    }

    Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
    getContext().getContentResolver().notifyChange(rootsUri, null);
    return result; 
}

第三步:修正queryChildDocuments方法,解析USB路径

根据Document ID找到对应的USB目录,遍历其中的文件:

@Override 
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { 
    final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    String parentPath;

    // 解析Document ID对应的实际路径
    if (parentDocumentId.startsWith("usb:")) {
        parentPath = parentDocumentId.substring(4); // 去掉"usb:"前缀
    } else {
        // 子目录的Document ID格式为"usb:/storage/UsbDriveA/xxx"
        parentPath = parentDocumentId.substring(4);
    }

    File dir = new File(parentPath);
    if (!dir.exists() || !dir.isDirectory()) {
        return result;
    }

    for (File file : dir.listFiles()) {
        String documentId = "usb:" + file.getAbsolutePath();
        includeFile(result, documentId, file); // 完善文件属性填充
    }
    return result; 
}

第四步:完善includeFile方法,正确填充文件属性

需要把文件的名称、大小、类型等信息正确写入Cursor:

private void includeFile(MatrixCursor result, String documentId, File file) {
    MatrixCursor.RowBuilder row = result.newRow();
    row.add(Document.COLUMN_DOCUMENT_ID, documentId);
    row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
    row.add(Document.COLUMN_SIZE, file.length());
    // 区分目录和文件的MIME类型
    String mimeType = file.isDirectory() 
        ? DocumentsContract.Document.MIME_TYPE_DIR 
        : getFileMimeType(file);
    row.add(Document.COLUMN_MIME_TYPE, mimeType);
    row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
    // 设置文件/目录的操作权限
    int flags = file.isDirectory() 
        ? Document.FLAG_DIR_SUPPORTS_CREATE 
        : Document.FLAG_SUPPORTS_READ;
    row.add(Document.COLUMN_FLAGS, flags);
}

private String getFileMimeType(File file) {
    String extension = MimeTypeMap.getFileExtensionFromUrl(file.getAbsolutePath());
    String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    return mimeType != null ? mimeType : "*/*";
}

第五步:权限与启动Intent优化

  • 在AndroidManifest.xml中添加必要权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
  • 启动SAF时,用ACTION_OPEN_DOCUMENT_TREE更适合浏览目录:
if (id == R.id.nav_file_explorer) {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, 1);
}

3. 额外说明

  • 如果设备支持多USB同时接入,可以在queryRoots中循环添加多个根节点
  • Android N对外部存储访问限制较严,确保你的APP已获得存储权限
  • 测试时可以先手动确认USB的实际挂载路径(比如通过adb shell查看),验证路径是否正确

内容的提问来源于stack exchange,提问作者reverb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:50:26