#112 use File API to delete, when possible

This commit is contained in:
Thibault Deckers 2021-11-03 12:23:11 +09:00
parent 0bd3e509fa
commit 41c3e08925
3 changed files with 62 additions and 46 deletions

View file

@ -9,7 +9,6 @@ import android.net.Uri
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
@ -734,7 +733,7 @@ abstract class ImageProvider {
targetUri: Uri,
targetPath: String
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaUriPermissionGranted(context, targetUri, mimeType)) {
if (isMediaUriPermissionGranted(context, targetUri, mimeType)) {
val targetStream = StorageUtils.openOutputStream(context, targetUri, mimeType) ?: throw Exception("failed to open output stream for uri=$targetUri")
DocumentFileCompat.fromFile(sourceFile).copyTo(targetStream)
} else {
@ -758,14 +757,17 @@ abstract class ImageProvider {
// used when skipping a move/creation op because the target file already exists
val skippedFieldMap: HashMap<String, Any?> = hashMapOf("skipped" to true)
@RequiresApi(Build.VERSION_CODES.Q)
fun isMediaUriPermissionGranted(context: Context, uri: Uri, mimeType: String): Boolean {
val safeUri = StorageUtils.getMediaStoreScopedStorageSafeUri(uri, mimeType)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val safeUri = StorageUtils.getMediaStoreScopedStorageSafeUri(uri, mimeType)
val pid = Binder.getCallingPid()
val uid = Binder.getCallingUid()
val flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
return context.checkUriPermission(safeUri, pid, uid, flags) == PackageManager.PERMISSION_GRANTED
val pid = Binder.getCallingPid()
val uid = Binder.getCallingUid()
val flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.checkUriPermission(safeUri, pid, uid, flags) == PackageManager.PERMISSION_GRANTED
} else {
false
}
}
}
}

View file

@ -12,7 +12,6 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import androidx.annotation.RequiresApi
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
@ -247,26 +246,38 @@ class MediaStoreImageProvider : ImageProvider() {
// `uri` is a media URI, not a document URI
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) {
if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& isMediaUriPermissionGranted(activity, uri, mimeType))
) {
// if the file is on SD card, calling the content resolver `delete()`
// removes the entry from the Media Store but it doesn't delete the file,
// even when the app has the permission, so we manually delete the document file
path ?: throw Exception("failed to delete file because path is null")
if (File(path).exists() && StorageUtils.requireAccessPermission(activity, path)) {
path ?: throw Exception("failed to delete file because path is null")
val file = File(path)
if (file.exists()) {
if (StorageUtils.canEditByFile(activity, path)) {
Log.d(LOG_TAG, "delete file at uri=$uri path=$path")
if (file.delete()) {
scanObsoletePath(activity, path, mimeType)
return
}
} else if (!isMediaUriPermissionGranted(activity, uri, mimeType)
&& StorageUtils.requireAccessPermission(activity, path)
) {
// if the file is on SD card, calling the content resolver `delete()`
// removes the entry from the Media Store but it doesn't delete the file,
// even when the app has the permission, so we manually delete the document file
Log.d(LOG_TAG, "delete document at uri=$uri path=$path")
val df = StorageUtils.getDocumentFile(activity, path, uri)
@Suppress("BlockingMethodInNonBlockingContext")
if (df != null && df.delete()) return
throw Exception("failed to delete file with df=$df")
if (df != null && df.delete()) {
scanObsoletePath(activity, path, mimeType)
return
}
throw Exception("failed to delete document with df=$df")
}
}
try {
Log.d(LOG_TAG, "delete content at uri=$uri")
Log.d(LOG_TAG, "delete content at uri=$uri path=$path")
if (activity.contentResolver.delete(uri, null, null) > 0) return
throw Exception("failed to delete row from content provider")
} catch (securityException: SecurityException) {
// even if the app has access permission granted on the containing directory,
// the delete request may yield a `RecoverableSecurityException` on Android 10+
@ -291,7 +302,6 @@ class MediaStoreImageProvider : ImageProvider() {
throw securityException
}
}
throw Exception("failed to delete row from content provider")
}
override suspend fun moveMultiple(
@ -518,26 +528,29 @@ class MediaStoreImageProvider : ImageProvider() {
return skippedFieldMap
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& isMediaUriPermissionGranted(activity, oldMediaUri, mimeType)
) {
return if (isMediaUriPermissionGranted(activity, oldMediaUri, mimeType)) {
renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
} else {
renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private suspend fun renameSingleByMediaStore(
activity: Activity,
mimeType: String,
mediaUri: Uri,
newFile: File
): FieldMap {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
throw Exception("unsupported Android version")
}
val uri = StorageUtils.getMediaStoreScopedStorageSafeUri(mediaUri, mimeType)
// `IS_PENDING` is necessary for `TITLE`, not for `DISPLAY_NAME`
val tempValues = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 1) }
val tempValues = ContentValues().apply {
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
if (activity.contentResolver.update(uri, tempValues, null, null) == 0) {
throw Exception("failed to update fields for uri=$uri")
}
@ -658,30 +671,29 @@ class MediaStoreImageProvider : ImageProvider() {
return null
}
if (newUri == null) {
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
return@scanFile
}
var contentUri: Uri? = null
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, contentId)
if (newUri != null) {
var contentUri: Uri? = null
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, contentId)
}
}
}
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
if (newFields != null) {
cont.resume(newFields)
if (newFields != null) {
cont.resume(newFields)
} else {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
}
} else {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
}
}
}

View file

@ -394,6 +394,8 @@ object StorageUtils {
* Misc
*/
fun canEditByFile(context: Context, path: String) = !requireAccessPermission(context, path)
fun requireAccessPermission(context: Context, anyPath: String): Boolean {
// on Android R, we should always require access permission, even on primary volume
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {