#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.Binder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
|
@ -734,7 +733,7 @@ abstract class ImageProvider {
|
||||||
targetUri: Uri,
|
targetUri: Uri,
|
||||||
targetPath: String
|
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")
|
val targetStream = StorageUtils.openOutputStream(context, targetUri, mimeType) ?: throw Exception("failed to open output stream for uri=$targetUri")
|
||||||
DocumentFileCompat.fromFile(sourceFile).copyTo(targetStream)
|
DocumentFileCompat.fromFile(sourceFile).copyTo(targetStream)
|
||||||
} else {
|
} else {
|
||||||
|
@ -758,14 +757,17 @@ abstract class ImageProvider {
|
||||||
// used when skipping a move/creation op because the target file already exists
|
// used when skipping a move/creation op because the target file already exists
|
||||||
val skippedFieldMap: HashMap<String, Any?> = hashMapOf("skipped" to true)
|
val skippedFieldMap: HashMap<String, Any?> = hashMapOf("skipped" to true)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
fun isMediaUriPermissionGranted(context: Context, uri: Uri, mimeType: String): Boolean {
|
fun isMediaUriPermissionGranted(context: Context, uri: Uri, mimeType: String): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val safeUri = StorageUtils.getMediaStoreScopedStorageSafeUri(uri, mimeType)
|
val safeUri = StorageUtils.getMediaStoreScopedStorageSafeUri(uri, mimeType)
|
||||||
|
|
||||||
val pid = Binder.getCallingPid()
|
val pid = Binder.getCallingPid()
|
||||||
val uid = Binder.getCallingUid()
|
val uid = Binder.getCallingUid()
|
||||||
val flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
val flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
return context.checkUriPermission(safeUri, pid, uid, flags) == PackageManager.PERMISSION_GRANTED
|
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.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.commonsware.cwac.document.DocumentFileCompat
|
import com.commonsware.cwac.document.DocumentFileCompat
|
||||||
import deckers.thibault.aves.MainActivity
|
import deckers.thibault.aves.MainActivity
|
||||||
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
|
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
|
// `uri` is a media URI, not a document URI
|
||||||
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) {
|
override suspend fun delete(activity: Activity, uri: Uri, path: String?, mimeType: String) {
|
||||||
if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
path ?: throw Exception("failed to delete file because path is null")
|
||||||
&& isMediaUriPermissionGranted(activity, uri, mimeType))
|
|
||||||
|
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()`
|
// 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,
|
// 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
|
// 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)) {
|
|
||||||
Log.d(LOG_TAG, "delete document at uri=$uri path=$path")
|
Log.d(LOG_TAG, "delete document at uri=$uri path=$path")
|
||||||
val df = StorageUtils.getDocumentFile(activity, path, uri)
|
val df = StorageUtils.getDocumentFile(activity, path, uri)
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
if (df != null && df.delete()) return
|
if (df != null && df.delete()) {
|
||||||
throw Exception("failed to delete file with df=$df")
|
scanObsoletePath(activity, path, mimeType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw Exception("failed to delete document with df=$df")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
if (activity.contentResolver.delete(uri, null, null) > 0) return
|
||||||
|
throw Exception("failed to delete row from content provider")
|
||||||
} catch (securityException: SecurityException) {
|
} catch (securityException: SecurityException) {
|
||||||
// even if the app has access permission granted on the containing directory,
|
// even if the app has access permission granted on the containing directory,
|
||||||
// the delete request may yield a `RecoverableSecurityException` on Android 10+
|
// the delete request may yield a `RecoverableSecurityException` on Android 10+
|
||||||
|
@ -291,7 +302,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
throw securityException
|
throw securityException
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw Exception("failed to delete row from content provider")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun moveMultiple(
|
override suspend fun moveMultiple(
|
||||||
|
@ -518,26 +528,29 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return skippedFieldMap
|
return skippedFieldMap
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
return if (isMediaUriPermissionGranted(activity, oldMediaUri, mimeType)) {
|
||||||
&& isMediaUriPermissionGranted(activity, oldMediaUri, mimeType)
|
|
||||||
) {
|
|
||||||
renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
|
renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
|
||||||
} else {
|
} else {
|
||||||
renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
|
renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
private suspend fun renameSingleByMediaStore(
|
private suspend fun renameSingleByMediaStore(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
mediaUri: Uri,
|
mediaUri: Uri,
|
||||||
newFile: File
|
newFile: File
|
||||||
): FieldMap {
|
): FieldMap {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
throw Exception("unsupported Android version")
|
||||||
|
}
|
||||||
|
|
||||||
val uri = StorageUtils.getMediaStoreScopedStorageSafeUri(mediaUri, mimeType)
|
val uri = StorageUtils.getMediaStoreScopedStorageSafeUri(mediaUri, mimeType)
|
||||||
|
|
||||||
// `IS_PENDING` is necessary for `TITLE`, not for `DISPLAY_NAME`
|
// `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) {
|
if (activity.contentResolver.update(uri, tempValues, null, null) == 0) {
|
||||||
throw Exception("failed to update fields for uri=$uri")
|
throw Exception("failed to update fields for uri=$uri")
|
||||||
}
|
}
|
||||||
|
@ -658,11 +671,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newUri == null) {
|
if (newUri != null) {
|
||||||
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
|
|
||||||
return@scanFile
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentUri: Uri? = null
|
var contentUri: Uri? = null
|
||||||
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
|
// `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")
|
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
|
||||||
|
@ -683,6 +692,9 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
} else {
|
} else {
|
||||||
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
|
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -394,6 +394,8 @@ object StorageUtils {
|
||||||
* Misc
|
* Misc
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fun canEditByFile(context: Context, path: String) = !requireAccessPermission(context, path)
|
||||||
|
|
||||||
fun requireAccessPermission(context: Context, anyPath: String): Boolean {
|
fun requireAccessPermission(context: Context, anyPath: String): Boolean {
|
||||||
// on Android R, we should always require access permission, even on primary volume
|
// on Android R, we should always require access permission, even on primary volume
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||||
|
|
Loading…
Reference in a new issue