6

在 Android Q 中,不是默认文件管理器或图库的应用程序只能修改和/或删除他们拥有的图像文件,因此,这些图像文件是应用程序创建的。

授予读/写权限不允许修改或删除任何不属于应用程序的文件。

这意味着不仅其他应用程序创建的文件无法访问,而且如果一个应用程序被卸载然后重新安装,那么这个应用程序将失去对该应用程序先前创建的所有公共文件的所有权。因此,重新安装后它不能再修改或删除它们。

当想要修改 1 个图像文件或删除大量多个图像文件时,这些文件以前属于某个应用程序,但由于重新安装而失去了所有权,那么实现此类操作(删除或修改)的程序是什么?

最好的解决方案是不使用 SAF 文件选择器,以避免请求用户通过 SAF 选择和授予位置。

而如果唯一的解决方案是使用SAF文件选择器,那么如何触发直接提示删除一组已知的特定文件而不请求树访问,也不必告诉用户浏览、搜索和自己做呢?

4

3 回答 3

8

我的最终结论。

对于 API >= 29,在没有用户交互的情况下无法删除非拥有文件,并且无法绕过这一事实。

Android 10/Q (API 29)中,必须捕获RecoverableSecurityException ,然后请求用户权限,最后如果被授予执行删除。

Android 11/R (API 30)中得到了极大的改进。甚至可以在同一批次中合并已经拥有的文件,也可以批量删除。请求后无需处理任何事情,如果用户允许,系统会负责删除。 限制是它只处理媒体文件(图像、视频、音频)。对于其他文件类型,抛出 IllegalArgumentException并显示以下消息:“所有请求的项目必须由特定 ID 引用”,(请在 MediaStore 源代码中查看此消息)。

请注意,在 API 30 中有一个新的MANAGE_EXTERNAL_STORAGE权限,但它的使用需要在开发人员控制台中执行额外的步骤,例如解释为什么需要该权限。

例子:

public static void delete(final Activity activity, final Uri[] uriList, final int requestCode)
        throws SecurityException, IntentSender.SendIntentException, IllegalArgumentException
{
    final ContentResolver resolver = activity.getContentResolver();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
    {
        // WARNING: if the URI isn't a MediaStore Uri and specifically
        // only for media files (images, videos, audio) then the request
        // will throw an IllegalArgumentException, with the message:
        // 'All requested items must be referenced by specific ID'

        // No need to handle 'onActivityResult' callback, when the system returns
        // from the user permission prompt the files will be already deleted.
        // Multiple 'owned' and 'not-owned' files can be combined in the 
        // same batch request. The system will automatically delete them 
        // using the same prompt dialog, making the experience homogeneous.

        final List<Uri> list = new ArrayList<>();
        Collections.addAll(list, uriList);

        final PendingIntent pendingIntent = MediaStore.createDeleteRequest(resolver, list);
        activity.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, null);
    }
    else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q)
    {
        try
        {
            // In Android == Q a RecoverableSecurityException is thrown for not-owned.
            // For a batch request the deletion will stop at the failed not-owned
            // file, so you may want to restrict deletion in Android Q to only
            // 1 file at a time, to make the experience less ugly.
            // Fortunately this gets solved in Android R.

            for (final Uri uri : uriList)
            {
                resolver.delete(uri, null, null);
            }
        }
        catch (RecoverableSecurityException ex)
        {
            final IntentSender intent = ex.getUserAction()
                    .getActionIntent()
                    .getIntentSender();

            // IMPORTANT: still need to perform the actual deletion
            // as usual, so again getContentResolver().delete(...),
            // in your 'onActivityResult' callback, as in Android Q
            // all this extra code is necessary 'only' to get the permission,
            // as the system doesn't perform any actual deletion at all.
            // The onActivityResult doesn't have the target Uri, so you
            // need to cache it somewhere.
            activity.startIntentSenderForResult(intent, requestCode, null, 0, 0, 0, null);
        }
    }
    else
    {
        // As usual for older APIs
        
        for (final Uri uri : uriList)
        {
            resolver.delete(uri, null, null);
        }
    }
}
于 2020-12-24T10:37:34.617 回答
2

实现此类操作(删除或修改)的程序是什么?

AFAIK,您唯一的选择是使用 SAF 并以这种方式获得权利。

最好的解决方案是不使用 SAF 文件选择器,以避免请求用户通过 SAF 选择和授予位置。

那是不可能的。如果是这样,那将是一个安全漏洞。请理解,虽然您认为这些是您的文件,但从操作系统的角度来看,它们只是设备上的文件。如果应用程序可以对任意文件进行任意修改访问,那将是我们之前相当不安全的东西的倒退。

怎么触发直接提示删除一组已知的特定文件

SAF 中没有删除文档或删除树 UI 选项,尽管这不是一个坏主意。

不必告诉用户浏览、搜索和自己做?

也许可以解决这个问题。你可以试试这个:

第 1 步:Uri为其中一个MediaStore条目获取 a(例如,使用a 中ContentUris的一个 IDquery()用于您的内容)

步骤#2:用于getDocumentUri()将其变形为指向相同内容MediaStore Uri的 SAFUri

步骤#3:将该 SAFUri作为EXTRA_INITIAL_URI值放入 中ACTION_OPEN_DOCUMENT_TREE Intent,并使用它尝试将树选择器预填充到您的内容目录

第 4 步:验证Uri您返回的ACTION_OPEN_DOCUMENT_TREE是否是您所期望的(它有您的文件,它与EXTRA_INITIAL_URI. 或类似的东西相匹配)

此时,您现在可以删除DocumentFile.fromTreeUri()用于获取DocumentFile树的文件,并从那里列出树中的文件并删除它们。

Uri您从第 2 步获得的内容是否适用EXTRA_INITIAL_URI于第 3 步尚不清楚,因为我还没有尝试过(尽管它在我下周初的待办事项清单上......)。

于 2019-06-07T16:11:50.007 回答
0

要从媒体商店中删除单个文件,请执行以下操作,如果该文件不是您的应用程序的一部分,则意图将开始获得许可

    val uri: String? = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString()
    val where = MediaStore.Audio.Media._ID + "=?"
    val selectionArgs = arrayOf(mId)

    try {
        val deleted = mActivity.contentResolver.delete(Uri.parse(uri), where, selectionArgs)

        return deleted >= 0

    } catch (securityException: SecurityException) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val recoverableSecurityException =
                    securityException as? RecoverableSecurityException
                            ?: throw SecurityException()

            val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender

            intentSender?.let {
                mActivity.startIntentSenderForResult(intentSender, 0, null, 0, 0, 0, null)
            }
        } else {
            throw SecurityException()
        }
    }

要添加到媒体商店,请执行以下操作...

val values = ContentValues().apply {
                put(MediaStore.Audio.Media.TITLE, song?.title)
                put(MediaStore.MediaColumns.DISPLAY_NAME, song?.title)
                put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
                put(MediaStore.Audio.Media.MIME_TYPE, song?.mimeType)
            }

            val resolver = mContext.contentResolver

            val uri = resolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values)

            // Download file to Media Store
            uri?.let { mUri ->
                resolver.openOutputStream(mUri).use { mOutputStream ->
                    mOutputStream?.let {
                        // Download to output stream using the url we just created
                    }
                }
            }
于 2019-12-17T11:04:46.507 回答