在针对 Android 11 (Api 30) 进行开发时,如何重命名视频/图像?

4

我在尝试重命名一个由应用程序创建但放置于“文档”文件夹中的文件,但遇到了困难。

编辑:

实际上,这些视频不是由应用程序创建的,而是由应用程序期望进行重命名。用户在开始时手动将视频拖放到“文档”文件夹中。是我的错误。

以下是我的代码:

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

我在解决几个错误后遇到了最终的错误:

java.lang.UnsupportedOperationException: 没有外部更新

这是FileProvider的内部问题,位于

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

编辑#2 以下是我的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>

这里是我的路径声明。同样,这并不会导致保存时出现任何问题:

<?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
首先告诉我你是如何创建那个文件的。你发布的代码混合了File和MediaStore类,而且在你还使用FileProvider的情况下肯定无法正常工作。这一切都没有意义。 - blackapps
旧代码运行良好,文件创建没有问题。问题出现在重命名文件时。此外,视频被放置在文档文件夹中。 - KoalaKoalified
你的代码没有意义。而且你甚至没有展示如何调用renameVideoFile函数。 - blackapps
contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, to.getName()); 重命名视频。 - KoalaKoalified
那段代码毫无意义,而且你没有提供我所要求的信息。 - blackapps
2个回答

0

编辑:我选择的外部文件夹是文档文件夹,供参考。

最终我终于让它工作了。这里是重命名视频的代码(可能不是最好的,但它能完成任务!)

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

然后是调用方法

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

如果我忘了什么,请告诉我,我会在这里发布。 搜寻了几个小时才找到这个解决方案,对于谷歌引入的简单性与复杂性之间的差异感到相当沮丧。
编辑:我想我忘记了这个非常重要的方法。
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;
}

我还应该提到每个视频都是这种方式的提示。用户可以选择是否允许您覆盖每个视频,这不太理想。我希望我可以通过外部访问来处理整个文件夹,但我猜测这在作用域存储更改中不会发生。


Uri fromUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);如果您使用上述代码,它是没有意义的。无论您发明多少个URI,它都会返回null。请多次调用它,结果仍然是null。 - blackapps
1
你又没有展示如何调用renameVideoFile()函数。这篇文章不是关于重命名的吗? - blackapps
我希望我可以一次处理整个文件夹。很遗憾你没有对你的代码做一个小介绍,比如添加到MediaStore和捕获SecurityException是正确的方法。而且谷歌承诺添加一个函数来处理一次批量的urie。很可能它已经存在了。 - blackapps
感谢您的努力,因为我在Android 11上重命名视频时遇到了麻烦。但是这并没有帮助我解决问题。有很多未定义的代码,似乎是您自己的实现。 - private static
你能告诉我关于 presenter.getFromFilePath() 函数的信息吗? - swiftbegineer
显示剩余2条评论

0

由于我看到这是一个非常流行的问题,我将更新一下我所做的更改,因为一些代码已经过时或不起作用。

首先,在您的build.gradle文件中,实现SAF框架的DocumentFile类:

implementation 'androidx.documentfile:documentfile:1.0.1'

接下来调用此方法请求权限以使 SAF 正常运行(用户安装后只需执行一次):

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


}

接下来存储一些持久化的权限:

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

最后,在正确的文件上调用documentFile的rename方法。
DocumentFile df = DocumentFile.fromTreeUri(MainActivity.this, uri);
df = df.findFile("CurrentName")
df.renameTo("NewName");

您还可以使用内容解析器打开InputStreams和OutputStreams,因为该DocumentFile的持久URI权限已授予您的内容解析器,使用以下代码片段:

getContentResolver().openInputStream(df.getUri());
getContentResolver().openOutputStream(df.getUri());

您可以使用以下方式列出文件

df.listFiles();

或者您可以使用以下命令列出文件:

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

哪个方法比df.listFiles()更快?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接