androidstorage-access-frameworkdocumentfile

Storage Access Framework Getting Correct Uri Path Delete/Edit/Get File


TL:DR; I explained how to use create folders and subfolders using DocumentFile and how to delete file created using this class. Uri returned from onActvityResult() and documentFile.getUri.toString() are not same. My question is how to get a valid Uri to manipulate folders and files without using SAF UI, if possible not without using hack.

Let me share what i've learned so far and ask my questions. If you want get Uri of folder and work on it, you should use Intent with ACTION_OPEN_DOCUMENT_TREE to get an Uri to access folders and set W/R permission for that uri.

Persistible permission granted onActivityResult with:

final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);

If you select the main folder of device:

Uri treeUri = data.getData();
treeUri.toString()

Returns: content://com.android.externalstorage.documents/tree/primary:

File mediaStorageDir = new File(Environment.getExternalStorageDirectory(), "");

Returns: storage/emulated/0

new File(treeUri.toString()).getAbsolutePath();

Returns: content:/com.android.externalstorage.documents/tree/primary:

If you use the DocumentFile class for getting path of the main folder you get

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

Returns: file:///storage/emulated/0

My first question is how can get the Uri with content using DocumentFile class.

I'm building a photography app and as default i'd like to set an initial folder for images using DocumentFile class.

 @TargetApi(19)
protected DocumentFile getSaveDirMainMemory() {
    DocumentFile saveDir = null;
    saveDir = DocumentFile.fromFile(Environment.getExternalStorageDirectory());
    // saveDir =
    // DocumentFile.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
    // saveDir =
    // DocumentFile.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES));

    DocumentFile newDir = null;
    /*
     * Check or create Main Folder
     */

    // Check if main folder exist
    newDir = saveDir.findFile(DIR_MAIN);

    // Folder does not exist, create it
    if (newDir == null || !newDir.exists()) {
        newDir = saveDir.createDirectory(DIR_MAIN);
    }
    /*
     * Check or create Sub-Folder
     */
    DocumentFile newSubDir = null;

    // Check if sub-folder exist
    newSubDir = newDir.findFile(DIR_SUB);


    // Folder does not exist, create it
    if (newSubDir == null || !newSubDir.exists()) {
        newSubDir = newDir.createDirectory(DIR_SUB);
    }

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

This method creates DIR_MAIN/DIR_SUB inside main memory of the device or PICTURES or DCIM folder depending on choice. Using this default folder i save images to this created sub folder. I get newSubDir.getUri().toString(): file:///storage/emulated/0/MainFolder/SubFolder I named DIR_MAIN MainFolder, DIR_SUB: SubFolder to test.

To access or delete images i use this path and image name i created as

DocumentFile imageToDeletePath = DocumentFile.fromFile(new File(lastSavedImagePath));
DocumentFile imageToDelete = imageToDeletePath.findFile(lastSavedImageName);

imageDelete returns null because Uri is not in correct format.

If i open SAF ui and get UI onActivityResult and save it as string i use this method to get a directory and check Uri permissions

@TargetApi(19)
protected DocumentFile getSaveDirNew(String uriString) {
    DocumentFile saveDir = null;

    boolean canWrite = isUriWritePermission(uriString);

    if (canWrite) {
        try {
            saveDir = DocumentFile.fromTreeUri(MainActivity.this, Uri.parse(uriString));
        } catch (Exception e) {
            saveDir = null;
        }
    }

    return saveDir;
}

Check if Uri from string has write permission, it may not have if you don't take or remove persistable permissions.

private boolean isUriWritePermission(String uriString) {
    boolean canWrite = false;

    List<UriPermission> perms = getContentResolver().getPersistedUriPermissions();
    for (UriPermission p : perms) {
        if (p.getUri().toString().equals(uriString) && p.isWritePermission()) {
            Toast.makeText(this, "canWrite() can write URI::  " + p.getUri().toString(), Toast.LENGTH_LONG).show();
            canWrite = true;
            break;
        }
    }
    return canWrite;
}

After saving image with valid uri and using

DocumentFile imageToDeletePath = DocumentFile.fromTreeUri(this, Uri.parse(lastSavedImagePath));
DocumentFile imageToDelete = imageToDeletePath.findFile(lastSavedImageName);

Solution

  • Uri.fromFile() and DocumentFile.fromTreeUri() create uris from two different worlds.

    While they currently look very similar, this is just coincidence and could change with any future Android release.

    There is no "non-hacky" way to convert from one to the other. If you want a dirty solution, you can go for reflection (view the source code of DocumentFile.fromTreeUri and possibly use the Storage class on newer Android versions.

    Also see: Android - Storage Access Framework - Uri into local file