Как запросить удаление файла в Android Q для несобственных файлов

В Android Q приложения, которые не являются файловым менеджером или галереей по умолчанию, могут только изменять и / или удалять файлы изображений, которыми они владеют, то есть созданные приложением.

Предоставление разрешений на чтение / запись не позволяет изменять или удалять файлы, не принадлежащие приложению.

Это означает, что не только файлы, созданные другими приложениями, находятся вне досягаемости, но также, если приложение удаляется, а затем переустанавливается, то этот файл теряет право собственности на все общедоступные файлы, созданные приложением ранее. Таким образом, после переустановки он не может изменить или удалить их больше.

Если требуется изменить 1 файл изображения или удалить большую часть файлов с несколькими изображениями, которые ранее принадлежали приложению, но утратили право собственности из-за переустановки, то какова процедура для выполнения таких действий (удаление или изменение)?

Предпочтительным решением было бы не использовать средство выбора файла SAF, чтобы избежать запроса пользователя на выбор и предоставление местоположения через SAF.

И если единственным решением является использование средства выбора файлов SAF, то как можно инициировать прямую подсказку об удалении набора известных конкретных файлов без запроса доступа к дереву, а также не нужно указывать пользователю просматривать, искать и делать это самому?

3 ответа

Решение

Какова процедура для достижения таких действий (удалить или изменить)?

AFAIK, ваш единственный вариант - использовать SAF и получить права таким образом.

Предпочтительным решением было бы не использовать средство выбора файла SAF, чтобы избежать запроса пользователя на выбор и предоставление местоположения через SAF.

Это невозможно. Это было бы ошибкой безопасности, если бы это было. Пожалуйста, поймите, что, хотя вы и думаете, что это ваши файлы, с точки зрения ОС, это всего лишь файлы на устройстве. Если бы приложения могли получить произвольный доступ к произвольным модифицированным файлам, это было бы шагом назад по сравнению с довольно небезопасными вещами, которые мы имели ранее.

как может быть вызван прямой запрос на удаление набора известных конкретных файлов

В SAF нет опции пользовательского интерфейса delete-document или delete-tree, хотя это неплохая идея.

Не нужно ли указывать пользователю просматривать, искать и делать это самому?

Что вы можете обойти. Вы можете попробовать это:

Шаг № 1: Получить Uri для одного из MediaStore записи (например, использовать ContentUris и один из идентификаторов из query() для вашего контента)

Шаг № 2: Используйте getDocumentUri() преобразовать это MediaStoreUri в SAF Uri указывая на тот же контент

Шаг № 3: Положите этот SAF Uri как EXTRA_INITIAL_URI значение в ACTION_OPEN_DOCUMENT_TREEIntent и используйте это, чтобы попытаться предварительно заполнить средство выбора дерева в каталоге вашего контента

Шаг № 4: Подтвердите, что Uri ты вернулся из ACTION_OPEN_DOCUMENT_TREE это тот, который вы ожидаете (он имеет ваши файлы, он соответствует EXTRA_INITIAL_URI, Или что-то вдоль этих линий)

Теперь вы можете удалить файлы, используя DocumentFile.fromTreeUri() чтобы получить DocumentFile для дерева, и оттуда перечислите файлы в дереве и удалите их.

Будь то Uri что вы получите от шага № 2 будет работать для EXTRA_INITIAL_URI на шаге 3 неясно, так как я еще не пробовал это (хотя это в моем списке дел в начале следующей недели...).

Мои окончательные выводы.

Для API>= 29 невозможно удалить чужие файлы без взаимодействия с пользователем, и этот факт нельзя обойти.

В Android 10 / Q (API 29), RecoverableSecurityException должен быть пойман, а затем запросить разрешение пользователя, и , наконец , в случае предоставления выполнить удаление.

В Android 11 / R (API 30) значительно улучшено. Можно удалить массово, даже объединив уже имеющиеся файлы в одном пакете. Нет необходимости обрабатывать что-либо после запроса, система позаботится об удалении, если это разрешено пользователем. Ограничение заключается в том, что он обрабатывает только мультимедийные файлы (изображения, видео, аудио) . Для других типов файлов генерируется исключение IllegalArgumentException с сообщением: «На все запрошенные элементы необходимо ссылаться по определенному идентификатору» (проверьте это сообщение в исходном коде 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), 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 
        // 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 if 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);
        }
    }
}

Чтобы удалить один файл из магазина мультимедиа, сделайте что-то вроде этого, если файл не является частью вашего приложения, тогда намерение начнет получать разрешение

    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
                    }
                }
            }
Другие вопросы по тегам