Как запросить удаление файла в 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()
преобразовать это MediaStore
Uri
в SAF Uri
указывая на тот же контент
Шаг № 3: Положите этот SAF Uri
как 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 неясно, так как я еще не пробовал это (хотя это в моем списке дел в начале следующей недели...).
Мои окончательные выводы.
Для 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
}
}
}