Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android 5.0 DocumentFile from tree URI

Alright, I've searched and searched and no one has my exact answer, or I missed it. I'm having my users select a directory by:

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

In my activity I want to capture the actual path, which seems to be impossible.

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

The folder I selected is at:

Device storage/test/

I've tried all of the following ways to get an exact path name, but to no avail.

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

Basically I need to turn /tree/1AF6-3708: into /storage/emulated/0/ or whatever each device calls it's storage location. All other available options return /tree/1AF6-37u08: also.

There are 2 reasons I want to do it this way.

1) In my app I store the file location as a shared preference because it is user specific. I have quite a bit of data that will be downloaded and stored and I want the user to be able to place it where they want, especially if they have an additional storage location. I do set a default, but I want versatility, rather than the dedicated location of:

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

2) In 5.0 I want to enable the user to get read/write permissions to that folder and this seems the only way to do that. If I can get read/write permissions from a string that would fix this issue.

All solutions I've been able to find relate to Mediastore, which doesn't help me exactly. I have to be missing something somewhere or I must have glazed over it. Any help would be appreciated. Thanks.

like image 457
Joe Walton Avatar asked Jan 21 '16 15:01

Joe Walton


2 Answers

This will give you the actual path of the selected folder It will work ONLY for files/folders that belong in local storage.

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

where FileUtil is the following class

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

UPDATE:

To address the Downloads issue mentioned in the comments: If you select Downloads from the left drawer in the default Android file picker you are not actually selecting a directory. Downloads is a provider. A normal folder tree uri looks something like this:

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

The tree uri of Downloads is

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

You can see that the one says externalstorage while the other one says providers. That is why it cannot be matched to a directory in the file system. Because it is not a directory.

SOLUTION: You can add an equality check and if the tree uri is equal to that then return the default download folder path which can be retrieved like this:

Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); 

And do something similar for all the providers if you wish to. And it would work correctly most of the time I assume. But I imagine that there are edge cases where it wouldn't.

thanx to @DuhVir for supporting the Android R case

like image 50
Anonymous Avatar answered Sep 20 '22 12:09

Anonymous


In my activity I want to capture the actual path, which seems to be impossible.

That's is because there may not be an actual path, let alone one you can access. There are many possible document providers, few of which will have all their documents locally on the device, and few of those that do will have the files on external storage, where you can work with them.

I have quite a bit of data that will be downloaded and stored and I want the user to be able to place it where they want

Then use the Storage Access Framework APIs, rather than thinking that documents/trees that you get from the Storage Access Framework are always local. Or, do not use ACTION_OPEN_DOCUMENT_TREE.

In 5.0 I want to enable the user to get read/write permissions to that folder

That is handled by the storage provider, as part of how the user interacts with that storage provider. You are not involved.

like image 38
CommonsWare Avatar answered Sep 18 '22 12:09

CommonsWare