Android 5.0中的DocumentFile来自树形URI

19

好的,我已经搜索了很久,但没有人给出我需要的确切答案,或者我错过了。 我要求我的用户通过以下方式选择一个目录:

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

在我的活动中,我想捕获实际路径,但似乎不可能。

protected void onActivityResult(int requestCode, int resultCode, Intent intent){
    super.onActivityResult(requestCode, resultCode, intent);
    if (resultCode == RESULT_OK) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
            //Marshmallow 

        } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP){
            //Set directory as default in preferences
            Uri treeUri = intent.getData();
            //grant write permissions
            getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            //File myFile = new File(uri.getPath()); 
            DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);

我选择的文件夹在:

Device storage/test/

我已经尝试过以下所有方法来获取准确的路径名,但都没有成功。

File myFile = new File (uri.getPath());
//returns: /tree/1AF6-3708:test

treeUri.getPath();
//returns: /tree/1AF6-3708:test/

pickedDir.getName()
//returns: test

pickedDir.getParentFile()
//returns: null

基本上我需要将/tree/1AF6-3708:转换为/storage/emulated/0/或任何设备称为其存储位置的内容。所有其他可用选项也返回/tree/1AF6-37u08:

我之所以希望这样做有两个原因:

1)在我的应用程序中,我将文件位置存储为共享首选项,因为它是特定于用户的。我将下载和存储相当多的数据,并希望用户能够将其放置在他们想要的地方,特别是如果他们有额外的存储位置。我设置了一个默认值,但我想要的是灵活性,而不是专用位置如下:

Device storage/Android/data/com.app.name/

2) 在5.0版本中,我希望用户能够获得对该文件夹的读/写权限,这似乎是唯一的解决方法。如果我可以从一个字符串中获得读/写权限,那么这个问题就可以解决。

我能找到的所有解决方案都与Mediastore有关,这对我并没有什么帮助。我一定是漏掉了某些东西或者看漏了。任何帮助将不胜感激。谢谢。


1
你搞定了吗? - sam_k
6个回答

50

这将为您提供所选文件夹的实际路径,它仅适用于属于本地存储的文件/文件夹。

Uri treeUri = data.getData();
String path = FileUtil.getFullPathFromTreeUri(treeUri,this); 

其中FileUtil是以下类

public final class FileUtil {

    private static final String PRIMARY_VOLUME_NAME = "primary";

    @Nullable
    public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
        if (treeUri == null) return null;
        String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con);
        if (volumePath == null) return File.separator;
        if (volumePath.endsWith(File.separator))
            volumePath = volumePath.substring(0, volumePath.length() - 1);

        String documentPath = getDocumentPathFromTreeUri(treeUri);
        if (documentPath.endsWith(File.separator))
            documentPath = documentPath.substring(0, documentPath.length() - 1);

        if (documentPath.length() > 0) {
            if (documentPath.startsWith(File.separator))
                return volumePath + documentPath;
            else
                return volumePath + File.separator + documentPath;
        }
        else return volumePath;
    }


    private static String getVolumePath(final String volumeId, Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
            return null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            return getVolumePathForAndroid11AndAbove(volumeId, context);
        else
            return getVolumePathBeforeAndroid11(volumeId, context);
    }


    private static String getVolumePathBeforeAndroid11(final String volumeId, Context context){
        try {
            StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
            Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
            Method getUuid = storageVolumeClazz.getMethod("getUuid");
            Method getPath = storageVolumeClazz.getMethod("getPath");
            Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
            Object result = getVolumeList.invoke(mStorageManager);

            final int length = Array.getLength(result);
            for (int i = 0; i < length; i++) {
                Object storageVolumeElement = Array.get(result, i);
                String uuid = (String) getUuid.invoke(storageVolumeElement);
                Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);

                if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))    // primary volume?
                    return (String) getPath.invoke(storageVolumeElement);

                if (uuid != null && uuid.equals(volumeId))    // other volumes?
                    return (String) getPath.invoke(storageVolumeElement);
            }
            // not found.
            return null;
        } catch (Exception ex) {
            return null;
        }
    }

    @TargetApi(Build.VERSION_CODES.R)
    private static String getVolumePathForAndroid11AndAbove(final String volumeId, Context context) {
        try {
            StorageManager mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
            for (StorageVolume storageVolume : storageVolumes) {
                // primary volume?
                if (storageVolume.isPrimary() && PRIMARY_VOLUME_NAME.equals(volumeId))
                    return storageVolume.getDirectory().getPath();

                // other volumes?
                String uuid = storageVolume.getUuid();
                if (uuid != null && uuid.equals(volumeId))
                    return storageVolume.getDirectory().getPath();

            }
            // not found.
            return null;
        } catch (Exception ex) {
            return null;
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private static String getVolumeIdFromTreeUri(final Uri treeUri) {
        final String docId = DocumentsContract.getTreeDocumentId(treeUri);
        final String[] split = docId.split(":");
        if (split.length > 0) return split[0];
        else return null;
    }


    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private static String getDocumentPathFromTreeUri(final Uri treeUri) {
        final String docId = DocumentsContract.getTreeDocumentId(treeUri);
        final String[] split = docId.split(":");
        if ((split.length >= 2) && (split[1] != null)) return split[1];
        else return File.separator;
    }
}

更新:

针对评论中提到的下载问题:如果您在默认的Android文件选择器中从左侧抽屉中选择下载,则实际上并未选择一个目录。 下载是一个提供程序。 正常的文件夹树URI看起来像这样:

content://com.android.externalstorage.documents/tree/primary%3ADCIM

下载页面的树形 URI 是:

content://com.android.providers.downloads.documents/tree/downloads 

你可以看到一个说externalstorage,而另一个说providers。这就是为什么它无法与文件系统中的目录匹配。因为它不是一个目录

解决方案:你可以添加一个相等检查,如果树形uri等于那个值,则返回默认的下载文件夹路径,可以像这样检索:

Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 

如果您愿意,可以对所有提供商执行类似操作。我认为它会在大多数情况下正常工作。但我想象中也有一些边缘情况它可能无法正常工作。

感谢@DuhVir支持Android R案例。


1
如果没有解释,downvotes就没有太大的帮助。这种方法对我很有效。downvoter看到了什么问题? - Anonymous
为什么有人会点踩这个?实际上,这对我的情况非常有效。非常感谢。 - Dante
9
这是可用的答案。Google的工程师过度设计了File类,因此我们不得不像这样使用一些技巧。请注意,API24添加了StorageVolume类,这消除了使用反射代码的需要。需要注意上面的代码假设从DocumentsContract.getTreeDocumentId返回的ID具有某些特定格式,而这并没有记录在文档中,目前在API 25中可以正常工作,但不能保证将来会保持相同。 - Pointer Null
1
如果能够将此内容变成一个库,并解决所提到的问题(如“下载”和OTG失败),那就太好了。有人知道是否已经完成了这项工作吗?如果没有,我想承担这项任务。 - Crutchcorn
1
值得一提的是,针对下载文件夹的建议解决方案仅适用于下载文件夹本身。如果您选择下载文件夹内部的文件夹,则此解决方案将无法正常工作,因为“treeUri”类似于“content://com.android.providers.downloads.documents/tree/msd%3A5018”。我不确定在这种情况下路径是否可以被解码。另外,感谢您对此事的跟进! - Spikatrix
显示剩余12条评论

3
在我的活动中,我想要获取实际路径,但这似乎是不可能的。这是因为可能没有实际路径,更别说你能访问了。有许多可能的文档提供程序,其中很少有所有文件都在设备上本地存储的,而且少数那些在外部存储器上有文件的提供程序,你可以使用它们。
如果要下载和存储大量数据,并希望用户能够将其放置在他们想要的位置,则应使用 Storage Access Framework APIs,而不是认为从 Storage Access Framework 获取的文档/树总是本地的。或者,不要使用 ACTION_OPEN_DOCUMENT_TREE
在 5.0 中,我想让用户获得对该文件夹的读写权限。这由存储提供程序处理,作为用户与该存储提供程序交互的一部分。你不需要参与其中。

3
这是对@Anonymous在Android 11方面回答的补充。
@TargetApi(Build.VERSION_CODES.R)
    private static String getVolumePath_SDK30(final String volumeId, Context context) {
        try {
            StorageManager mStorageManager =
                    (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            if (mStorageManager == null) return null;
            List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
            for (StorageVolume storageVolume : storageVolumes) {
                String uuid = storageVolume.getUuid();
                Boolean primary = storageVolume.isPrimary();
                if (primary == null) return null;
                // primary volume?
                if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
                    return storageVolume.getDirectory().getPath();
                // other volumes?
                if (uuid != null && uuid.equals(volumeId))
                    return storageVolume.getDirectory().getPath();
            }
            // not found.
            return null;
        } catch (Exception ex) {
            return null;
        }
    }

很酷,我有时间会去看一下并进行编辑。谢谢! - Anonymous

0

我试图在我的应用程序的首选项屏幕中,在用户未选择自定义目录时或之前,使用SAF UI添加默认保存目录。用户可能会忘记选择文件夹,导致应用程序崩溃。为了在设备内存中添加默认文件夹,您应该

    DocumentFile saveDir = null;
    saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
    String uriString = saveDir.getUri().toString();

    List<UriPermission> perms = getContentResolver().getPersistedUriPermissions();
    for (UriPermission p : perms) {
        if (p.getUri().toString().equals(uriString) && p.isWritePermission()) {
            canWrite = true;
            break;
        }
    }
    // Permitted to create a direct child of parent directory
    DocumentFile newDir = null;
    if (canWrite) {
         newDir = saveDir.createDirectory("MyFolder");
    }

    if (newDir != null && newDir.exists()) {
        return newDir;
    }

这段代码将在设备的主内存中创建一个目录,并为该文件夹和子文件夹授予读写权限。您无法直接创建 MyFolder/MySubFolder 层次结构,您应该再次创建另一个目录。

您可以检查该目录是否具有写入权限,根据我在 3 台设备上的观察,如果使用 DocumentFile 而不是 File 类创建,则返回 true。这是一种简单的方法,可在 Android >= 5.0 上创建并授予写入权限,而不使用 ACTION_OPEN_DOCUMENT_TREE


您还可以将主目录设置为Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)或任何公共目录,只要它存在即可。 - Thracian

0
public static String findFullPath(String path) {
        String actualResult="";
        path=path.substring(5);
        int index=0;
        StringBuilder result = new StringBuilder("/storage");
        for (int i = 0; i < path.length(); i++) {
            if (path.charAt(i) != ':') {
                result.append(path.charAt(i));
            } else {
                index = ++i;
                result.append('/');
                break;
            }
        }
        for (int i = index; i < path.length(); i++) {
            result.append(path.charAt(i));
        }
        if (result.substring(9, 16).equalsIgnoreCase("primary")) {
            actualResult = result.substring(0, 8) + "/emulated/0/" + result.substring(17);
        } else {
            actualResult = result.toString();
        }
        return actualResult;
    }

0
public static String findFullPath(String path) {
    String actualResult="";
    path=path.substring(5);
    int index=0;
    StringBuilder result = new StringBuilder("/storage");
    for (int i = 0; i < path.length(); i++) {
        if (path.charAt(i) != ':') {
            result.append(path.charAt(i));
        } else {
            index = ++i;
            result.append('/');
            break;
        }
    }
    for (int i = index; i < path.length(); i++) {
        result.append(path.charAt(i));
    }
    if (result.substring(9, 16).equalsIgnoreCase("primary")) {
        actualResult = result.substring(0, 8) + "/emulated/0/" + result.substring(17);
    } else {
        actualResult = result.toString();
    }
    return actualResult;
}

这个函数可以从树形URI中获取绝对路径。 我在1000多个设备上测试过,这个解决方案适用于大多数设备。 它还可以给我们提供存储卡或OTG中包含的文件夹的正确绝对路径。

How it Works?

基本上大多数设备的路径都以/Storage/前缀开头,路径的中间部分包含挂载点名称,例如内部存储的/emulated/0/或类似/C0V54440/的一些字符串(仅为示例)。最后一段是从存储根目录到文件夹的路径,例如/movie/piratesofthecarribian。

因此,我们从/tree/primary:movie/piratesofthecarribian构建的路径是/storage/emulated/0/movie/piratesofthecarribian。

您可以在我的github资料库中找到更多信息。访问那里以获取有关如何将tree uri转换为实际绝对路径的Android代码。

gihub(https://github.com/chetanborase/TreeUritoAbsolutePath)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接