如何在 Android Q 中请求删除不属于自己的文件

How to request file deletion in Android Q for Not-Owned files

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

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

这意味着不仅无法访问其他应用程序创建的文件,而且如果某个应用程序被卸载然后重新安装,那么这个应用程序将失去所有 public 的所有权该应用先前创建的文件。所以,重新安装后就不能修改删除了。

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

更好的解决方案是不使用 SAF 文件选择器,因为这可以避免向用户请求 select 并通过 SAF 授予位置。

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

what is the procedure to achieve such actions (delete or modify)?

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

The preferable solution would be not to use the SAF file picker, in the sense of avoid requesting to the user to select and grant a location through SAF.

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

how can be triggered to directly prompt to delete a set of known specific files

SAF 中没有 delete-document 或 delete-tree UI 选项,尽管这不是一个坏主意。

neither having to tell the user to browse, search, and do it himself?

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

第 1 步:为 MediaStore 条目之一获取 Uri(例如,使用 ContentUris 和来自 query() 的 ID 之一作为您的内容)

第 2 步:使用 getDocumentUri()MediaStore Uri 转变为指向相同内容的 SAF Uri

第 3 步:将 SAF Uri 作为 EXTRA_INITIAL_URI 值放入 ACTION_OPEN_DOCUMENT_TREE Intent,并使用它来尝试 pre-populate树选择器到您的内容目录

第 4 步:验证您从 ACTION_OPEN_DOCUMENT_TREE 返回的 Uri 是否是您期望的那个(它有您的文件,它与 EXTRA_INITIAL_URI 匹配,或者其他东西那些行)

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

您从步骤 #2 获得的 Uri 是否适用于步骤 #3 中的 EXTRA_INITIAL_URI 尚不清楚,因为我还没有尝试过(尽管它在我的 to-do 下周初的清单...)。

要从 Media Store 中删除单个文件,请执行类似这样的操作,如果该文件不是您应用程序的一部分,那么 intent 将开始获取权限

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

我的最终结论。

因为 APIs >= 29 无法在没有用户交互的情况下删除非拥有的文件,并且没有办法绕过这个事实。

Android10/Q(API29)中,必须抓到一个RecoverableSecurityException,然后请求用户权限,并且最后,如果被允许执行删除。

Android11/R(API30)中有很大的提高。可以批量删除甚至合并同一批中已有的文件。请求后无需处理任何事情,如果用户允许,系统会负责删除。 限制是它只能处理媒体文件(图像、视频、音频)。对于其他文件类型,会抛出 IllegalArgumentException 消息:"All requested items must be referenced by specific ID", (check this message in MediaStore source code ).

请注意,在API30中有一个新的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);
        }
    }
}