#112 use File API to delete, when possible
This commit is contained in:
parent
0bd3e509fa
commit
41c3e08925
3 changed files with 62 additions and 46 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue