19

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.

Martin Serrano
  • 3,727
  • 1
  • 35
  • 48
Joe Walton
  • 191
  • 1
  • 1
  • 4

6 Answers6

49

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

Anonymous
  • 4,470
  • 3
  • 36
  • 67
  • 1
    downvotes are not very helpful if they don't come with an explanation. This method worked for me. What problem does the downvoter see ? – Anonymous Apr 06 '16 at 14:11
  • Why would someone down vote this? This actually works for my situation. Thanks a lot – Dante May 23 '16 at 16:25
  • @Anonymous Would you please help me with question at :- http://stackoverflow.com/q/39054454/5309039 I am really in need to find answer to this. – Ankesh kumar Jaisansaria Aug 23 '16 at 14:10
  • 9
    This is working answer. Google engineers over-engineered the File class, so we have to use hacks like this. Note that API24 added StorageVolume class, this removes need to use the reflection code. And needed to note that above code assumes some specific format of ID returned from DocumentsContract.getTreeDocumentId which is not documented, it works so as of API 25, but is not guaranteed to be same in future. – Pointer Null Mar 24 '17 at 17:19
  • Note: This works perfectly for the Internal Storage and SD Card, but fails for OTG Devices (returns `"/"`) – Spikatrix May 17 '19 at 09:04
  • Doesn't work on "Downloads" folder nor "SD card" Api 26+ – ahmed galal Feb 06 '20 at 23:43
  • @ahmedgalal I tried in android 10 (SDK 29) and it works fine...perhaps you are misusing it – Anonymous Feb 29 '20 at 18:38
  • When selecting a location from the 'Downloads' option in the sidebar rather than the 'Internal Storage' option, this returns `FILE.separator` which is probably what ahmed was saying – Spikatrix Jun 25 '20 at 13:42
  • 1
    It would be great if this could be turned into a library and have the issues mentioned (ala "Downloads" and OTG fails) turned into GH issues. Does anyone know if this has already been done? If not, I'd like to take that on – Crutchcorn Jun 26 '20 at 01:58
  • @Spikatrix I updated the answer regarding the Downloads issue. – Anonymous Jun 28 '20 at 19:30
  • 1
    Worth mentioning that the proposed solution for Downloads will work only for the Downloads folder itself. It you select a folder inside the Downloads folder, this will not work as the `treeUri` is something along the lines of `content://com.android.providers.downloads.documents/tree/msd%3A5018`. I'm not sure if the path can be decoded in cases like this. Also, Thank you for following up on this! – Spikatrix Jun 29 '20 at 04:58
  • Broken in Android 11. They are close some non-SDK methods. Any alternatives? – DuhVir Nov 17 '20 at 14:45
  • Ok, I'll paste my quickfix below in answer. If you have time, correct your class with this or with your method – DuhVir Nov 17 '20 at 15:07
  • This works for me targeting 11 but running on 10 including on the external SD card. The problem is it returns the path of the tree even if you give it a Uri of a document in the tree. I tried fixing it to use DocumentsContract.getDocumentId, not getDocumentTreeId. It then gets an exception in DocumentsContract.getDocumentId for the tree, but works as desired for the document. I think it is a bug in DocumentsContract (1.0.1). Only difference in ids is longer path part for the document. I made two versions. You need the non-tree version for the tree. (Subdirectories not tested.) – Kenneth Evans Dec 03 '20 at 00:53
  • Corrections: The 1.0.1 I mentioned is for 'implementation "androidx.documentfile:documentfile:1.0.1"' and is not relevant. (I stopped using it. DocumentsContract is better.) Also a treeUri does get exceptions for isDocumentUri and getDocumentId (i.e. not a bug). You can make a docUri from a treeUri with buildDocumentUriUsingTree. I fixed FileUtil.getFullPathFromUri to call getFullPathFromTreeUri if !isDocumentUri. It now gets the full path for either. Still learning. – Kenneth Evans Dec 05 '20 at 22:05
  • This Method worked for me. We are usually not ware of which directory user grants permission to. This method works perfectly for me as I can know which directory user is granting permission from treeUri. Thanks a Lot!!! – Gokula Krishnan May 19 '21 at 09:43
  • I get an exception for `String uuid = (String) getUuid.invoke(storageVolumeElement);` in `getVolumePathBeforeAndroid11`. Any idea what could be causing it? – eri0o Jan 09 '22 at 00:26
3

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.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
3

It's addition to @Anonymous answer for 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;
        }
    }
DuhVir
  • 447
  • 1
  • 4
  • 15
0

I was trying to add a default save directory before or if user does not select a custom directory using SAF UI in preferences screen of my app. It's possible for users to miss selecting a folder and app may crash. To add a default folder in device memory you should

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

This snippet will create a directory inside main memory of device and grant read/write permissions for that folder and sub-folders. You can't directly create MyFolder/MySubFolder hierarchy, you should create another directory again.

You can check if that directory has permission to write, as far i seen on 3 devices, it returns true if it's created using DocumentFileinstead of File class. This is a simple method for creating and granting write permission for Android >= 5.0 without using ACTION_OPEN_DOCUMENT_TREE

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • You can also set main directory as `Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)` or any public directory you wish as long as it exists. – Thracian Sep 18 '17 at 15:09
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;
}

this function gives me the absolute path from tree uri. this solution working on most of device i tested in more than 1000 devices. it also gives us the right absolute path of folder which contained in memory card or OTG.

How it Works?

basically most of devices have the path that starts with /Storage/ prefix. and the middle part of path contains mounted point name i.e /emulated/0/ for internal Storage, or some string like /C0V54440/ etc (just example). and the last segment is path from root of storage to folder like /movie/piratesofthecarribian

so, the path we constructed from :- /tree/primary:movie/piratesofthecarribian is :- /storage/emulated/0/movie/piratesofthecarribian

You can find more information on my github repo. visit there to get the android code about how to convert tree uri to actual absolute path.

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

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;
    }
Rishabh Dhiman
  • 159
  • 2
  • 9