Renaming Video / Image While Targeting Android 11 (Api 30)

Viewed 820

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>
1 Answers

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.

Related