I know this question is a bit old but here is a working solution to organize your files anywhere except the root phone directory or gain persistant access to your directory of choice (so you don't have to prompt over and over again)
First In Your build.gradle file, implement the SAF framework's DocumentFile class:
implementation 'androidx.documentfile:documentfile:1.0.1'
Next Call this method which request permissions for the SAF to operate (You will only need to do this once on user install):
private void requestDocumentTreePermissions() {
// Choose a directory using the system's file picker.
new AlertDialog.Builder(this)
.setMessage("*Please Select A Folder For The App To Organize The Videos*")
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void onClick(DialogInterface dialog, int which) {
StorageManager sm = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
String startDir = "Documents";
Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
String scheme = uri.toString();
scheme = scheme.replace("/root/", "/document/");
scheme += "%3A" + startDir;
uri = Uri.parse(scheme);
Uri rootUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
uri.toString()
);
Uri treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
uri.toString()
);
uri = Uri.parse(scheme);
Uri treeUri2 = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
uri.toString()
);
List<Uri> uriTreeList = new ArrayList<>();
uriTreeList.add(treeUri);
uriTreeList.add(treeUri2);
getPrimaryVolume().createOpenDocumentTreeIntent()
.putExtra(EXTRA_INITIAL_URI, rootUri);
Intent intent2 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when it loads.
intent2.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
intent2.putExtra(EXTRA_INITIAL_URI, rootUri);
startActivityForResult(intent2, 99);
}
})
.setCancelable(false)
.show();
}
Next Store some Persistant Permissions:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 99 && resultCode == RESULT_OK) {
//get back the document tree URI (in this case we expect the documents root directory)
Uri uri = data.getData();
//now we grant permanent persistant permissions to our contentResolver and we are free to open up sub directory Uris as we please until the app is uninstalled
getSharedPreferences().edit().putString(ACCESS_FOLDER, uri.toString()).apply();
final int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getApplicationContext().getContentResolver().takePersistableUriPermission(uri, takeFlags);
//simply recreate the activity although you could call some function at this point
recreate();
}
}
You can call the documentFile's rename method on the correct file
DocumentFile df = DocumentFile.fromTreeUri(MainActivity.this, uri);
df = df.findFile("CurrentName")
df.renameTo("NewName");
You Can also open InputStreams and OutputStreams using your content resolver because of the persistant URI permissions granted to your content resolver for that DocumentFile using the following snippet:
getContentResolver().openInputStream(df.getUri());
getContentResolver().openOutputStream(df.getUri());
InputStreams are for reading and OutputStreams are for saving
You can list files using
df.listFiles();
Or You can list out files using:
public static DocumentFile findFileInDirectoryMatchingName(Context mContext, Uri mUri, String name) {
final ContentResolver resolver = mContext.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mUri,
DocumentsContract.getDocumentId(mUri));
Cursor c = null;
try {
c = resolver.query(childrenUri, new String[]{
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED
}, DocumentsContract.Document.COLUMN_DISPLAY_NAME + " LIKE '?%'", new String[]{name}, null);
c.moveToFirst();
while (!c.isAfterLast()) {
final String filename = c.getString(1);
final String mimeType = c.getString(2);
final Long lastModified = c.getLong(3);
if (filename.contains(name)) {
final String documentId = c.getString(0);
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
documentId);
return DocumentFile.fromTreeUri(mContext, documentUri);
}
c.moveToNext();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (c != null) {
c.close();
}
}
return null;
}
Which will run faster than the df.listFiles() method
Src (This is my own implementation but here is the original SF question)
Renaming Video / Image While Targeting Android 11 (Api 30)