Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

In Android Q, apps that aren't the default file manager or gallery can only modify and/or delete image files which they own, so, which an app has created.

Granting the read/write permissions doesn’t allow to modify or to delete any file that isn’t owned by an app.

This implies that not only files created by other apps are out of reach, but also if an app gets uninstalled and then reinstalled, then this one loses the ownership over all the public files that the app previously created. So, after the re-installation it can’t modify or delete them anymore.

When wanting to modify 1 image file or to delete a bulk of multiple images files, which were previously owned by an app, but lost ownership due to a re-installation, then what is the procedure to achieve such actions (delete or modify)?

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.

And if the only solution is to use the SAF file picker, then how can be triggered to directly prompt to delete a set of known specific files without requesting tree access, neither having to tell the user to browse, search, and do it himself?

like image 853
PerracoLabs Avatar asked Jun 07 '19 14:06

PerracoLabs


Video Answer


2 Answers

My final conclusions.

For APIs >= 29 is not possible to delete non-owned files without user interaction, and there is no way around this fact.

In Android 10/Q (API 29), a RecoverableSecurityException must be caught, then request the user permission, and finally if granted perform the deletion.

In Android 11/R (API 30) is greatly improved. Can delete in bulk even combining already owned files in the same batch. No need to handle anything after the request, the system takes care of the deletion if granted by the user. The limitation is that it only handles media files (images, videos, audio). For other file types an IllegalArgumentException is thrown with the message: "All requested items must be referenced by specific ID", (check this message in MediaStore source code).

Note that in API 30 there is a new MANAGE_EXTERNAL_STORAGE permission, but its usage requires extra steps in the developer's console, such as to explain why the permission is needed.

Example:

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);
        }
    }
}
like image 80
PerracoLabs Avatar answered Sep 28 '22 08:09

PerracoLabs


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

AFAIK, your only option is to use the SAF and get rights that way.

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.

That's not possible. It would be a security flaw if it were. Please understand that while you think that these are your files, from the OS' standpoint, they are just files on the device. If apps could get arbitrary modification access to arbitrary files, that would be a step backwards from the fairly insecure stuff we had previously.

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

There is no delete-document or delete-tree UI option in SAF, though it's not a bad idea.

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

That you might be able to work around. You can try this:

Step #1: Get a Uri for one of the MediaStore entries (e.g., use ContentUris and one of the IDs from a query() for your content)

Step #2: Use getDocumentUri() to transmogrify that MediaStore Uri into an SAF Uri pointing to the same content

Step #3: Put that SAF Uri as the EXTRA_INITIAL_URI value in an ACTION_OPEN_DOCUMENT_TREE Intent, and use that to try to pre-populate the tree picker to your content's directory

Step #4: Validate that the Uri you get back from ACTION_OPEN_DOCUMENT_TREE is the one you are expecting (it has your files, it matches the EXTRA_INITIAL_URI, or something along those lines)

At this point, you now can delete the files using DocumentFile.fromTreeUri() to get a DocumentFile for the tree, and from there list the files in the tree and delete them.

Whether the Uri that you get from Step #2 will work for EXTRA_INITIAL_URI in Step #3 is unclear, as I haven't tried this yet (though it's on my to-do list for early next week...).

like image 23
CommonsWare Avatar answered Sep 28 '22 06:09

CommonsWare