4

I'm having difficulty simply renaming a file created by the app but was put into the documents folder.

EDIT:

As it so happens the videos are not created by the application but are expected to be renamed by the application. The user drops the videos into the documents folder manually at the start. My mistake.

Here is my code:

public static boolean renameVideoFile(Context c, File from, File to) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        try {
            Uri fromUri = FileProvider.getUriForFile(c, c.getPackageName() + ".provider", new File(FileUtils.getVideosDir(), from.getName()));
            ContentResolver contentResolver = c.getContentResolver();
            ContentValues contentValues = new ContentValues();

            contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 1);
            contentResolver.update(fromUri, contentValues, null, null);
            contentValues.clear();
            contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, to.getName());
            contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
            contentResolver.update(fromUri, contentValues, null, null);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    } else {
        if (from.renameTo(to)) {
            removeMedia(c, from);
            addMedia(c, to);
            return true;
        } else {
            return false;
        }
    }
}

I progressed through a few errors but my final error is:

java.lang.UnsupportedOperationException: No external updates

Which is an internal problem with the FileProvider at

at androidx.core.content.FileProvider.update(FileProvider.java:523)

EDIT #2 Also here are my provider declarations in the manifest:

 <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths">
        </meta-data>
    </provider>

And here is my pathing declarations. Again this is causing no issues for saving:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
    name="internal_images"
    path="files/Pictures" />
<external-files-path
    name="internal_images_alternate"
    path="Pictures" />
<external-path
    name="external"
    path="." />
<external-files-path
    name="external_files"
    path="." />
<cache-path
    name="cache"
    path="." />
<external-cache-path
    name="external_cache"
    path="." />
<files-path
    name="files"
    path="." />
</paths>
KoalaKoalified
  • 687
  • 4
  • 15
  • When an image / video goes into the documents folder is it doomed to remain however it is named for all time? – KoalaKoalified Aug 03 '21 at 18:21
  • First tell how you created that file. The code you posted mixes File and MediaStore class and can never work certainly where you also use a FileProvider. All makes no sense. – blackapps Aug 03 '21 at 18:26
  • The old code works fine and the file creation has no issues. The problem occurs when renaming the file. Also the videos are dropped into the documents folder – KoalaKoalified Aug 03 '21 at 18:27
  • Your code dies not make sense. And you did not even show how you called renameVideoFile. – blackapps Aug 03 '21 at 18:48
  • contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, to.getName()); renames the video – KoalaKoalified Aug 03 '21 at 18:49
  • That code makes no sense and you did not give the info i asked for. – blackapps Aug 03 '21 at 20:01

2 Answers2

0

EDIT: The external folder I chose was the documents folder FYI

So I did finally get it working. Here is the code to rename a video (it may not be the best but it does the trick!)

private static void tryAddVideosToMediaStore(Activity context) {
    List<File> files = MediaUtils.getVideoFilesFromDirectory();
    for (File file : files) {
        try {
            Uri fromUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);

            if (getRealPathFromURI(context, fromUri) == null) {

                String nameWoExtension = MediaUtils.getNameWithoutStatus(file.getAbsolutePath());
                ContentValues values = new ContentValues(3);
                values.put(MediaStore.Video.Media.TITLE, nameWoExtension);
                values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
                values.put(MediaStore.Video.Media.DATA, file.getAbsolutePath());
                context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

public static String getRealPathFromURI(Context context, Uri contentUri) {
    Cursor cursor = null;
    try {
        String[] proj = {MediaStore.Images.Media.DATA};
        cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
        cursor.moveToFirst();
        return cursor.getString(column_index);
    } catch(Exception e) {
        return null;
    }finally {
        if (cursor != null) {
            cursor.close();
        }
    }
}

And then the calling methods

public static String getVideoNameFromPath(String path) {
    return path.substring(path.lastIndexOf("/") + 1, path.indexOf(".mp4"));
}

public static boolean renameVideoFile(MainActivityViewModel viewModel, SharedPreferenceHelper sharedPreferenceHelper, Activity c, File from, File to) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        tryAddVideosToMediaStore(c);
        Uri fromUri = MediaUtils.getVideoUriFromFS(c, from);
        try {
            ContentResolver contentResolver = c.getContentResolver();
            ContentValues contentValues = new ContentValues();

            contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 1);
            contentResolver.update(fromUri, contentValues, null, null);
            contentValues.clear();
            contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, to.getName());
            contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
            contentResolver.update(fromUri, contentValues, null, null);
            return true;
        } catch (Exception securityException) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                sharedPreferenceHelper.get().edit().putString("from", from.getAbsolutePath()).putString("to", to.getAbsolutePath()).apply();
                RecoverableSecurityException recoverableSecurityException;
                viewModel.setContentUri(fromUri);

                if (securityException instanceof RecoverableSecurityException) {
                    recoverableSecurityException =
                            (RecoverableSecurityException) securityException;
                } else {
                    requestVideoWritePermissions(c, Uri.parse(MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "/" + MediaUtils.getVideoId(c, from)));
                    return false;
                }
                IntentSender intentSender = recoverableSecurityException.getUserAction()
                        .getActionIntent().getIntentSender();

                try {
                    c.startIntentSenderForResult(intentSender, 55,
                            null, 0, 0, 0);
                } catch (Exception e) {
                    e.printStackTrace();
                    return false;
                }
            } else {
                throw new RuntimeException(
                        securityException.getMessage(), securityException);
            }
        }
        return false;
    } else {
        if (from.renameTo(to)) {
            removeMedia(c, from);
            addMedia(c, to);
            return true;
        } else {
            return false;
        }
    }
}


public static Uri getVideoUriFromFS(Context c, File file) {
    long id = getFilePathToMediaID(file, c);
    Uri fromUri = ContentUris.withAppendedId( MediaStore.Video.Media.EXTERNAL_CONTENT_URI,id);
    return fromUri;
}



public static long getFilePathToMediaID(File videoPath, Context context)
{
    Uri mainUri;
    Cursor cursor1 = context.getContentResolver().query(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            new String[]{MediaStore.Video.Media._ID},
            MediaStore.Video.Media.DATA + "=? ",
            new String[]{videoPath.getAbsolutePath()}, null);
    long id = 0;
    if (cursor1 != null && cursor1.moveToFirst()) {

        id = cursor1.getLong(cursor1.getColumnIndex(MediaStore.MediaColumns._ID));
        cursor1.close();
    }
    return id;
}


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 55) { //rename video request code
        if (resultCode == RESULT_OK) {
            //update UI
            String from = presenter.getFromFilePath();
            String to = presenter.getToFilePath();
            if (from != null && to != null) {
                Uri fromUri = MediaUtils.getVideoUriFromFS(this, new File(from));
                ContentResolver contentResolver = getContentResolver();
                    ContentValues contentValues = new ContentValues();

                    contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 1);
                    contentResolver.update(fromUri, contentValues, null, null);
                    contentValues.clear();
                    contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, new File(to).getName());
                    contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
                    contentResolver.update(fromUri, contentValues, null, null);
                    //update UI
            }
        }
    }
}

If I forgot something please let me know and I will post it here. It took literally hours of searching to find this solution. I'm quite upset at the simplicity vs the complexity that google has introduced.

EDIT: I Think I forgot this method which was very important

public static boolean requestVideoWritePermissions(Activity activity, Uri fromUri) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

        boolean hasPermission = true;
        if (activity.checkUriPermission(fromUri, Binder.getCallingPid(), Binder.getCallingUid(),
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PackageManager.PERMISSION_GRANTED) {
            hasPermission = false;
        }

        List<Uri> uriList = new ArrayList<>();
        uriList.add(fromUri);

        if (!hasPermission) {
            PendingIntent pi = MediaStore.createWriteRequest(activity.getContentResolver(), uriList);
            try {
                activity.startIntentSenderForResult(pi.getIntentSender(), 55, null, 0, 0, 0);
            } catch (IntentSender.SendIntentException e) {
                e.printStackTrace();
            }
            return false;
        }
        return true;
    }
    return true;
}

I should also mention each video is a prompt this way. The user chooses whether or not to allow you to overwrite each video which was less than optimal. I wish I could just do a whole folder of external access but I'm guessing that's not going to happen with scoped storage changes.

KoalaKoalified
  • 687
  • 4
  • 15
  • `Uri fromUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file); if (getRealPathFromURI(context, fromUri) == null) { ` That code makes no sense. Of course it returns null. For al uries you might invent. Call it several times and it stays null. – blackapps Aug 04 '21 at 06:53
  • 1
    And again you are not showing how you call renameVideoFile(). And wasnt this post about renaming? – blackapps Aug 04 '21 at 06:55
  • `I wish I could just do a whole folder of` It is a pitty that you did not give a small introduction to what you do in your code like adding to mediastore and catching a securityException which is the way to go. And further google promised to add a function to handle a batch of uries in one go. Pretty possible it is already there. – blackapps Aug 04 '21 at 07:05
  • And you could have used SAF to let the user choose the directory. After that you would have full access to all files and in a small loop you could rename all. – blackapps Aug 04 '21 at 07:07
  • And that snippet does exactly what it's supposed to do. It renames files that were not added by my app – KoalaKoalified Aug 04 '21 at 07:37
  • Appreciate your effort because I'm having trouble renaming videos on Android 11. But this was not helpful. A lot of undefined codes which seem to be your own implementation. – private static Jan 09 '22 at 10:23
  • Can you tell me about the presenter.getFromFilePath() function? – swiftbegineer Aug 20 '22 at 04:38
0

Since I see this is a very popular question, I'm going to go ahead and update what I changed to be doing since some of this code is deprecated or not working.

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

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

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

KoalaKoalified
  • 687
  • 4
  • 15