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

3

11

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?

Hoyle answered 7/6, 2019 at 14:58 Comment(6)
"then this one loses the ownership over all the public files that the app previously created" -- how did you create them? SAF? MediaStore? something else?Chieftain
The files are created using the MediaStore, by inserting a new record using the ContentResolver. After uninstalling an app Android sets the field MediaStore.MediaColumns.OWNER_PACKAGE_NAME to Null, so logically when reinstalling the app this one has no longer ownership.Hoyle
OK, but then how do you have "a set of known specific files"? How is installation #2 of your app going to distinguish content created by installation #1 of your app from content created by other apps or copied over to the device by the user? You have no way to reliably derive SAF Uri values for those, and you would not have permissions for them anyway. However, if they are MediaStore entries, you might be able to delete them using the MediaStore itself, if you hold WRITE_EXTERNAL_STORAGE. The docs cite being able to read the contents with READ_EXTERNAL_STORAGE.Chieftain
The way we delete files in all Android versions is through the MediaStore, by using ContentResolver.delete(uri, ...), but such operation fails in Android Q for not owned files, regardless of the Write/Read permissions.Hoyle
The files are images saved into a specific folder with our product name. The way we know that the files were previously owned and can query for them is by checking that the BUCKET_DISPLAY_NAME field has the app name. We intent to also check in Android Q if the RELATIVE_PATH contains also the name. Of course, this isn't bulletproof, but since the app name is uncommon and even trademarked, then the chances that another app has the same name are practically none, without taking into account that maybe a user could create another folder with such name, but we can't think of any reason for this.Hoyle
some good info here youtube.com/watch?v=UnJ3amzJM94&t=1788sPajamas
C
3

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...).

Chieftain answered 7/6, 2019 at 16:11 Comment(3)
Thanks for the answer, Android Q is getting complicated. In my opinion, the same way that Android has a common sharing panel, a common prompt for permissions, etc., then with all the new file restrictions in Android Q, it would had been good that at least the Android Team had implemented some kind of new delete prompt panel at system level, which could had made the delete files action safer, homogeneous, and consistent across apps, so keeping security in-place and make developer’s life easier.Hoyle
What about using DocumentFile.fromSingleUri i managed too delete a single file with Uri documentUri = MediaStore.getDocumentUri(getActivity(), mediaUri); and then DocumentFile documentFile = DocumentFile.fromSingleUri(getActivity(), documentUri); and then i just call delete on the documentFile.Fogged
@VinceVD: If that deletes the underlying content, great! In this case, the OP had a directory of content ("The files are images saved into a specific folder with our product name") and I was focused on working the directory angle.Chieftain
H
14

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);
        }
    }
}
Hoyle answered 24/12, 2020 at 10:37 Comment(3)
Thanks for very helpful answer. But how to create an Uri for file on API28- ? MediaStore.MediaColumns.RELATIVE_PATH is only for API29+ accessibleHornet
Yes, RELATIVE_PATH was introduced in API 29, so not accessible below it at all. About how to create a URI, you must use ContentResolver.insert(..) which returns a Uri. Look at the next example: #56904985Hoyle
Working like charm..thanks for answer.Morceau
C
3

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...).

Chieftain answered 7/6, 2019 at 16:11 Comment(3)
Thanks for the answer, Android Q is getting complicated. In my opinion, the same way that Android has a common sharing panel, a common prompt for permissions, etc., then with all the new file restrictions in Android Q, it would had been good that at least the Android Team had implemented some kind of new delete prompt panel at system level, which could had made the delete files action safer, homogeneous, and consistent across apps, so keeping security in-place and make developer’s life easier.Hoyle
What about using DocumentFile.fromSingleUri i managed too delete a single file with Uri documentUri = MediaStore.getDocumentUri(getActivity(), mediaUri); and then DocumentFile documentFile = DocumentFile.fromSingleUri(getActivity(), documentUri); and then i just call delete on the documentFile.Fogged
@VinceVD: If that deletes the underlying content, great! In this case, the OP had a directory of content ("The files are images saved into a specific folder with our product name") and I was focused on working the directory angle.Chieftain
P
0

To delete a Single file from the Media Store do something like this, if the file is not part of your app then an intent will start to get permission

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

To add to the Media Store do something like this...

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
                    }
                }
            }
Pajamas answered 17/12, 2019 at 11:4 Comment(1)
This won't delete the file, just the entry from the MediaStore.Fogged

© 2022 - 2024 — McMap. All rights reserved.