@@ -30,22 +30,29 @@ object FolderDeletionHelper {
3030 folder : File ,
3131 trashFolderLauncher : ActivityResultLauncher <IntentSenderRequest >,
3232 onDeletionComplete : (Boolean ) -> Unit ) {
33- val itemCount = countItemsInFolder(context, folder)
34- val folderPath = folder.absolutePath
3533
3634 // don't show this dialog on API 30+, it's handled automatically using MediaStore
3735 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
38- val success = deleteFolderMain (context, folder, trashFolderLauncher)
36+ val success = trashImagesInFolder (context, folder, trashFolderLauncher)
3937 onDeletionComplete(success)
40-
4138 } else {
39+ val imagePaths = listImagesInFolder(context, folder)
40+ val imageCount = imagePaths.size
41+ val folderPath = folder.absolutePath
42+
4243 AlertDialog .Builder (context)
4344 .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+ )
4552 .setPositiveButton(context.getString(R .string.custom_selector_delete)) { _, _ ->
4653
4754 // proceed with deletion if user confirms
48- val success = deleteFolderMain(context, folder, trashFolderLauncher )
55+ val success = deleteImagesLegacy(imagePaths )
4956 onDeletionComplete(success)
5057 }
5158 .setNegativeButton(context.getString(R .string.custom_selector_cancel)) { dialog, _ ->
@@ -57,38 +64,16 @@ object FolderDeletionHelper {
5764 }
5865
5966 /* *
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.
8569 *
8670 * @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.
8974 * @return `true` if the trash request was initiated successfully, `false` otherwise.
9075 */
91- private fun trashFolderContents (
76+ private fun trashImagesInFolder (
9277 context : Context ,
9378 folder : File ,
9479 trashFolderLauncher : ActivityResultLauncher <IntentSenderRequest >): Boolean
@@ -99,66 +84,74 @@ object FolderDeletionHelper {
9984 val folderPath = folder.absolutePath
10085 val urisToTrash = mutableListOf<Uri >()
10186
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)
122103 }
123104 }
124105
106+
125107 // proceed with trashing if we have valid URIs
126108 if (urisToTrash.isNotEmpty()) {
127109 try {
128110 val trashRequest = MediaStore .createTrashRequest(contentResolver, urisToTrash, true )
129- val intentSenderRequest = IntentSenderRequest .Builder (trashRequest.intentSender).build()
111+ val intentSenderRequest =
112+ IntentSenderRequest .Builder (trashRequest.intentSender).build()
130113 trashFolderLauncher.launch(intentSenderRequest)
131114 return true
132115 } 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+ )
134122 }
135123 }
136124 return false
137125 }
138126
139127
140128 /* *
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.
142130 *
143131 * @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.
146134 */
147- private fun countItemsInFolder (context : Context , folder : File ): Int {
135+ private fun listImagesInFolder (context : Context , folder : File ): List < String > {
148136 val contentResolver = context.contentResolver
149137 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
162155 }
163156
164157
@@ -180,14 +173,20 @@ object FolderDeletionHelper {
180173
181174
182175 /* *
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
184177 * Android 10 (API level 29) and below.
185178 *
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.
188181 */
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
191190 }
192191
193192
0 commit comments