@@ -30,22 +30,29 @@ object FolderDeletionHelper {
30
30
folder : File ,
31
31
trashFolderLauncher : ActivityResultLauncher <IntentSenderRequest >,
32
32
onDeletionComplete : (Boolean ) -> Unit ) {
33
- val itemCount = countItemsInFolder(context, folder)
34
- val folderPath = folder.absolutePath
35
33
36
34
// don't show this dialog on API 30+, it's handled automatically using MediaStore
37
35
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
38
- val success = deleteFolderMain (context, folder, trashFolderLauncher)
36
+ val success = trashImagesInFolder (context, folder, trashFolderLauncher)
39
37
onDeletionComplete(success)
40
-
41
38
} else {
39
+ val imagePaths = listImagesInFolder(context, folder)
40
+ val imageCount = imagePaths.size
41
+ val folderPath = folder.absolutePath
42
+
42
43
AlertDialog .Builder (context)
43
44
.setTitle(context.getString(R .string.custom_selector_confirm_deletion_title))
44
- .setMessage(context.getString(R .string.custom_selector_confirm_deletion_message, folderPath, itemCount))
45
+ .setMessage(
46
+ context.getString(
47
+ R .string.custom_selector_confirm_deletion_message,
48
+ folderPath,
49
+ imageCount
50
+ )
51
+ )
45
52
.setPositiveButton(context.getString(R .string.custom_selector_delete)) { _, _ ->
46
53
47
54
// proceed with deletion if user confirms
48
- val success = deleteFolderMain(context, folder, trashFolderLauncher )
55
+ val success = deleteImagesLegacy(imagePaths )
49
56
onDeletionComplete(success)
50
57
}
51
58
.setNegativeButton(context.getString(R .string.custom_selector_cancel)) { dialog, _ ->
@@ -57,38 +64,16 @@ object FolderDeletionHelper {
57
64
}
58
65
59
66
/* *
60
- * Deletes the specified folder, handling different Android storage models based on the API
61
- *
62
- * @param context The context used to manage storage operations.
63
- * @param folder The folder to delete.
64
- * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request.
65
- * @return `true` if the folder deletion was successful, `false` otherwise.
66
- */
67
- private fun deleteFolderMain (
68
- context : Context ,
69
- folder : File ,
70
- trashFolderLauncher : ActivityResultLauncher <IntentSenderRequest >): Boolean
71
- {
72
- return when {
73
- // for API 30 and above, use MediaStore
74
- Build .VERSION .SDK_INT >= Build .VERSION_CODES .R -> trashFolderContents(context, folder, trashFolderLauncher)
75
-
76
- // for API 29 ('requestLegacyExternalStorage' is set to true in Manifest)
77
- // and below use file system
78
- else -> deleteFolderLegacy(folder)
79
- }
80
- }
81
-
82
- /* *
83
- * Moves all contents of a specified folder to the trash on devices running
84
- * Android 11 (API level 30) and above.
67
+ * Moves all images in a specified folder (but not within its subfolders) to the trash on
68
+ * devices running Android 11 (API level 30) and above.
85
69
*
86
70
* @param context The context used to access the content resolver.
87
- * @param folder The folder whose contents are to be moved to the trash.
88
- * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request.
71
+ * @param folder The folder whose top-level images are to be moved to the trash.
72
+ * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash
73
+ * request.
89
74
* @return `true` if the trash request was initiated successfully, `false` otherwise.
90
75
*/
91
- private fun trashFolderContents (
76
+ private fun trashImagesInFolder (
92
77
context : Context ,
93
78
folder : File ,
94
79
trashFolderLauncher : ActivityResultLauncher <IntentSenderRequest >): Boolean
@@ -99,66 +84,74 @@ object FolderDeletionHelper {
99
84
val folderPath = folder.absolutePath
100
85
val urisToTrash = mutableListOf<Uri >()
101
86
102
- // Use URIs specific to media items
103
- val mediaUris = listOf (
104
- MediaStore .Images .Media .EXTERNAL_CONTENT_URI ,
105
- MediaStore .Video .Media .EXTERNAL_CONTENT_URI ,
106
- MediaStore .Audio .Media .EXTERNAL_CONTENT_URI
107
- )
108
-
109
- for (mediaUri in mediaUris) {
110
- val selection = " ${MediaStore .MediaColumns .DATA } LIKE ?"
111
- val selectionArgs = arrayOf(" $folderPath /%" )
112
-
113
- contentResolver.query(mediaUri, arrayOf(MediaStore .MediaColumns ._ID ), selection,
114
- selectionArgs, null )
115
- ?.use{ cursor ->
116
- val idColumn = cursor.getColumnIndexOrThrow(MediaStore .MediaColumns ._ID )
117
- while (cursor.moveToNext()) {
118
- val id = cursor.getLong(idColumn)
119
- val fileUri = ContentUris .withAppendedId(mediaUri, id)
120
- urisToTrash.add(fileUri)
121
- }
87
+ val mediaUri = MediaStore .Images .Media .EXTERNAL_CONTENT_URI
88
+
89
+ // select images contained in the folder but not within subfolders
90
+ val selection =
91
+ " ${MediaStore .MediaColumns .DATA } LIKE ? AND ${MediaStore .MediaColumns .DATA } NOT LIKE ?"
92
+ val selectionArgs = arrayOf(" $folderPath /%" , " $folderPath /%/%" )
93
+
94
+ contentResolver.query(
95
+ mediaUri, arrayOf(MediaStore .MediaColumns ._ID ), selection,
96
+ selectionArgs, null
97
+ )?.use { cursor ->
98
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore .MediaColumns ._ID )
99
+ while (cursor.moveToNext()) {
100
+ val id = cursor.getLong(idColumn)
101
+ val fileUri = ContentUris .withAppendedId(mediaUri, id)
102
+ urisToTrash.add(fileUri)
122
103
}
123
104
}
124
105
106
+
125
107
// proceed with trashing if we have valid URIs
126
108
if (urisToTrash.isNotEmpty()) {
127
109
try {
128
110
val trashRequest = MediaStore .createTrashRequest(contentResolver, urisToTrash, true )
129
- val intentSenderRequest = IntentSenderRequest .Builder (trashRequest.intentSender).build()
111
+ val intentSenderRequest =
112
+ IntentSenderRequest .Builder (trashRequest.intentSender).build()
130
113
trashFolderLauncher.launch(intentSenderRequest)
131
114
return true
132
115
} catch (e: SecurityException ) {
133
- Timber .tag(" DeleteFolder" ).e(context.getString(R .string.custom_selector_error_trashing_folder_contents, e.message))
116
+ Timber .tag(" DeleteFolder" ).e(
117
+ context.getString(
118
+ R .string.custom_selector_error_trashing_folder_contents,
119
+ e.message
120
+ )
121
+ )
134
122
}
135
123
}
136
124
return false
137
125
}
138
126
139
127
140
128
/* *
141
- * Counts the number of items in a specified folder, including items in subfolders.
129
+ * Lists all image file paths in the specified folder, excluding any subfolders.
142
130
*
143
131
* @param context The context used to access the content resolver.
144
- * @param folder The folder in which to count items .
145
- * @return The total number of items in the folder.
132
+ * @param folder The folder whose top-level images are to be listed .
133
+ * @return A list of file paths (as Strings) pointing to the images in the specified folder.
146
134
*/
147
- private fun countItemsInFolder (context : Context , folder : File ): Int {
135
+ private fun listImagesInFolder (context : Context , folder : File ): List < String > {
148
136
val contentResolver = context.contentResolver
149
137
val folderPath = folder.absolutePath
150
- val uri = MediaStore .Images .Media .EXTERNAL_CONTENT_URI
151
- val selection = " ${MediaStore .Images .Media .DATA } LIKE ?"
152
- val selectionArgs = arrayOf(" $folderPath /%" )
153
-
154
- return contentResolver.query(
155
- uri,
156
- arrayOf(MediaStore .Images .Media ._ID ),
157
- selection,
158
- selectionArgs,
159
- null )?.use { cursor ->
160
- cursor.count
161
- } ? : 0
138
+ val mediaUri = MediaStore .Images .Media .EXTERNAL_CONTENT_URI
139
+ val selection =
140
+ " ${MediaStore .MediaColumns .DATA } LIKE ? AND ${MediaStore .MediaColumns .DATA } NOT LIKE ?"
141
+ val selectionArgs = arrayOf(" $folderPath /%" , " $folderPath /%/%" )
142
+ val imagePaths = mutableListOf<String >()
143
+
144
+ contentResolver.query(
145
+ mediaUri, arrayOf(MediaStore .MediaColumns .DATA ), selection,
146
+ selectionArgs, null
147
+ )?.use { cursor ->
148
+ val dataColumn = cursor.getColumnIndexOrThrow(MediaStore .MediaColumns .DATA )
149
+ while (cursor.moveToNext()) {
150
+ val imagePath = cursor.getString(dataColumn)
151
+ imagePaths.add(imagePath)
152
+ }
153
+ }
154
+ return imagePaths
162
155
}
163
156
164
157
@@ -180,14 +173,20 @@ object FolderDeletionHelper {
180
173
181
174
182
175
/* *
183
- * Deletes a specified folder and all of its contents on devices running
176
+ * Deletes a list of image files specified by their paths, on
184
177
* Android 10 (API level 29) and below.
185
178
*
186
- * @param folder The `File` object representing the folder to be deleted.
187
- * @return `true` if the folder and all contents were deleted successfully; `false` otherwise.
179
+ * @param imagePaths A list of absolute file paths to image files that need to be deleted.
180
+ * @return `true` if all the images are successfully deleted, `false` otherwise.
188
181
*/
189
- private fun deleteFolderLegacy (folder : File ): Boolean {
190
- return folder.deleteRecursively()
182
+ private fun deleteImagesLegacy (imagePaths : List <String >): Boolean {
183
+ var result = true
184
+ imagePaths.forEach {
185
+ val imageFile = File (it)
186
+ val deleted = imageFile.exists() && imageFile.delete()
187
+ result = result && deleted
188
+ }
189
+ return result
191
190
}
192
191
193
192
0 commit comments