mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-11-02 15:53:55 +01:00
Issue 5811: folder deletion
This commit is contained in:
parent
d03e7bcf70
commit
764028d624
8 changed files with 233 additions and 275 deletions
|
|
@ -1,59 +1,136 @@
|
|||
package fr.free.nrw.commons.customselector.helper
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.RecoverableSecurityException
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.filepicker.Constants
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
object FolderDeletionHelper {
|
||||
|
||||
/**
|
||||
* Main function to confirm and delete a folder.
|
||||
* Prompts the user to confirm deletion of a specified folder and, if confirmed, deletes it.
|
||||
*
|
||||
* @param context The context used to show the confirmation dialog and manage deletion.
|
||||
* @param folder The folder to be deleted.
|
||||
* @param onDeletionComplete Callback invoked with `true` if the folder was
|
||||
* successfully deleted, `false` otherwise.
|
||||
*/
|
||||
fun confirmAndDeleteFolder(context: Context, folder: File, onDeletionComplete: (Boolean) -> Unit) {
|
||||
val itemCount = countItemsInFolder(context, folder)
|
||||
val folderPath = folder.absolutePath
|
||||
|
||||
// Show confirmation dialog
|
||||
//don't show this dialog on API 30+, it's handled automatically using MediaStore
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val success = deleteFolderMain(context, folder)
|
||||
onDeletionComplete(success)
|
||||
|
||||
} else {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("Confirm Deletion")
|
||||
.setMessage("Are you sure you want to delete the folder?\n\nPath: $folderPath\nItems: $itemCount")
|
||||
.setPositiveButton("Delete") { _, _ ->
|
||||
// Proceed with deletion if user confirms
|
||||
val success = deleteFolder(context, folder)
|
||||
.setTitle(context.getString(R.string.custom_selector_confirm_deletion_title))
|
||||
.setMessage(context.getString(R.string.custom_selector_confirm_deletion_message, folderPath, itemCount))
|
||||
.setPositiveButton(context.getString(R.string.custom_selector_delete)) { _, _ ->
|
||||
|
||||
//proceed with deletion if user confirms
|
||||
val success = deleteFolderMain(context, folder)
|
||||
onDeletionComplete(success)
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
.setNegativeButton(context.getString(R.string.custom_selector_cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDeletionComplete(false) // Return false if the user cancels
|
||||
onDeletionComplete(false)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the folder based on the Android version.
|
||||
* Deletes the specified folder, handling different Android storage models based on the API
|
||||
*
|
||||
* @param context The context used to manage storage operations.
|
||||
* @param folder The folder to delete.
|
||||
* @return `true` if the folder deletion was successful, `false` otherwise.
|
||||
*/
|
||||
fun deleteFolder(context: Context, folder: File): Boolean {
|
||||
private fun deleteFolderMain(context: Context, folder: File): Boolean {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> deleteFolderScopedStorage(context, folder)
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> deleteFolderMediaStore(context, folder)
|
||||
//for API 30 and above, use MediaStore
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> trashFolderContents(context, folder)
|
||||
|
||||
//for API 29 ('requestLegacyExternalStorage' is set to true in Manifest)
|
||||
// and below use file system
|
||||
else -> deleteFolderLegacy(folder)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of items in the folder, including subfolders.
|
||||
* Moves all contents of a specified folder to the trash on devices running
|
||||
* Android 11 (API level 30) and above.
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folder The folder whose contents are to be moved to the trash.
|
||||
* @return `true` if the trash request was initiated successfully, `false` otherwise.
|
||||
*/
|
||||
private fun trashFolderContents(context: Context, folder: File): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
|
||||
|
||||
val contentResolver = context.contentResolver
|
||||
val folderPath = folder.absolutePath
|
||||
val urisToTrash = mutableListOf<Uri>()
|
||||
|
||||
// Use URIs specific to media items
|
||||
val mediaUris = listOf(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
)
|
||||
|
||||
for (mediaUri in mediaUris) {
|
||||
val selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%")
|
||||
|
||||
contentResolver.query(mediaUri, arrayOf(MediaStore.MediaColumns._ID), selection,
|
||||
selectionArgs, null)
|
||||
?.use{ cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val fileUri = ContentUris.withAppendedId(mediaUri, id)
|
||||
urisToTrash.add(fileUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//proceed with trashing if we have valid URIs
|
||||
if (urisToTrash.isNotEmpty()) {
|
||||
try {
|
||||
val trashRequest = MediaStore.createTrashRequest(contentResolver, urisToTrash, true)
|
||||
(context as? Activity)?.startIntentSenderForResult(
|
||||
trashRequest.intentSender,
|
||||
Constants.RequestCodes.DELETE_FOLDER_REQUEST_CODE,
|
||||
null, 0, 0, 0
|
||||
)
|
||||
return true
|
||||
} catch (e: SecurityException) {
|
||||
Timber.tag("DeleteFolder").e(context.getString(R.string.custom_selector_error_trashing_folder_contents, e.message))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Counts the number of items in a specified folder, including items in subfolders.
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folder The folder in which to count items.
|
||||
* @return The total number of items in the folder.
|
||||
*/
|
||||
private fun countItemsInFolder(context: Context, folder: File): Int {
|
||||
val contentResolver = context.contentResolver
|
||||
|
|
@ -62,116 +139,52 @@ object FolderDeletionHelper {
|
|||
val selection = "${MediaStore.Images.Media.DATA} LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%")
|
||||
|
||||
return contentResolver.query(uri, arrayOf(MediaStore.Images.Media._ID), selection, selectionArgs, null)?.use { cursor ->
|
||||
return contentResolver.query(
|
||||
uri,
|
||||
arrayOf(MediaStore.Images.Media._ID),
|
||||
selection,
|
||||
selectionArgs,
|
||||
null)?.use { cursor ->
|
||||
cursor.count
|
||||
} ?: 0
|
||||
}
|
||||
/**
|
||||
* Deletes a folder using the Scoped Storage API for Android 11 (API level 30) and above.
|
||||
*/
|
||||
private fun deleteFolderScopedStorage(context: Context, folder: File): Boolean {
|
||||
Timber.tag("FolderAction").d("Deleting folder using Scoped Storage API")
|
||||
|
||||
// Implement deletion with Scoped Storage; fallback to recursive delete
|
||||
return folder.deleteRecursively().also {
|
||||
if (!it) {
|
||||
Timber.tag("FolderAction").e("Failed to delete folder with Scoped Storage API")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a folder using the MediaStore API for Android 10 (API level 29).
|
||||
* Refreshes the MediaStore for a specified folder, updating the system to recognize any changes
|
||||
*
|
||||
* @param context The context used to access the MediaScannerConnection.
|
||||
* @param folder The folder to refresh in the MediaStore.
|
||||
*/
|
||||
fun deleteFolderMediaStore(context: Context, folder: File): Boolean {
|
||||
Timber.tag("FolderAction").d("Deleting folder using MediaStore API on Android 10")
|
||||
|
||||
val contentResolver = context.contentResolver
|
||||
val folderPath = folder.absolutePath
|
||||
var deletionSuccessful = true
|
||||
|
||||
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
val selection = "${MediaStore.Images.Media.DATA} LIKE ?"
|
||||
val selectionArgs = arrayOf("$folderPath/%")
|
||||
|
||||
contentResolver.query(uri, arrayOf(MediaStore.Images.Media._ID), selection, selectionArgs, null)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
|
||||
try {
|
||||
val rowsDeleted = contentResolver.delete(imageUri, null, null)
|
||||
if (rowsDeleted <= 0) {
|
||||
Timber.tag("FolderAction").e("Failed to delete image with URI: $imageUri")
|
||||
deletionSuccessful = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Handle RecoverableSecurityException only for API 29 and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && e is RecoverableSecurityException) {
|
||||
handleRecoverableSecurityException(context, e)
|
||||
deletionSuccessful = false
|
||||
} else {
|
||||
Timber.tag("FolderAction").e("Error deleting file: ${e.message}")
|
||||
deletionSuccessful = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deletionSuccessful
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the RecoverableSecurityException for deletion requests requiring user confirmation.
|
||||
*/
|
||||
private fun handleRecoverableSecurityException(context: Context, e: RecoverableSecurityException) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
try {
|
||||
val intentSender = e.userAction.actionIntent.intentSender
|
||||
(context as? Activity)?.startIntentSenderForResult(
|
||||
intentSender,
|
||||
2,
|
||||
null, 0, 0, 0
|
||||
)
|
||||
} catch (ex: IntentSender.SendIntentException) {
|
||||
Timber.tag("FolderAction").e("Error sending intent for deletion: ${ex.message}")
|
||||
}
|
||||
} else {
|
||||
Timber.tag("FolderAction").e("RecoverableSecurityException requires API 29 or higher")
|
||||
}
|
||||
}
|
||||
fun refreshMediaStore(context: Context, folder: File) {
|
||||
MediaScannerConnection.scanFile(
|
||||
context,
|
||||
arrayOf(folder.absolutePath),
|
||||
null
|
||||
) { path, uri ->
|
||||
Timber.tag("FolderAction").d("MediaStore updated for path: $path, URI: $uri")
|
||||
) { _, _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles deletion for devices running Android 9 (API level 28) and below.
|
||||
* Deletes a specified folder and all of its contents on devices running
|
||||
* Android 10 (API level 29) and below.
|
||||
*
|
||||
* @param folder The `File` object representing the folder to be deleted.
|
||||
* @return `true` if the folder and all contents were deleted successfully; `false` otherwise.
|
||||
*/
|
||||
private fun deleteFolderLegacy(folder: File): Boolean {
|
||||
return folder.deleteRecursively().also {
|
||||
if (it) {
|
||||
Timber.tag("FolderAction").d("Folder deleted successfully")
|
||||
} else {
|
||||
Timber.tag("FolderAction").e("Failed to delete folder")
|
||||
}
|
||||
}
|
||||
return folder.deleteRecursively()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the path of the folder with the specified ID from the MediaStore.
|
||||
* Retrieves the absolute path of a folder given its unique identifier (bucket ID).
|
||||
*
|
||||
* @param context The context used to access the content resolver.
|
||||
* @param folderId The unique identifier (bucket ID) of the folder.
|
||||
* @return The absolute path of the folder as a `String`, or `null` if the folder is not found.
|
||||
*/
|
||||
fun getFolderPath(context: Context, folderId: Long): String? {
|
||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||
|
|
@ -190,36 +203,35 @@ object FolderDeletionHelper {
|
|||
return File(fullPath).parent
|
||||
}
|
||||
}
|
||||
Timber.tag("FolderDeletion").d("Path is null for folder ID: $folderId")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
fun printCurrentPermissions(context: Context) {
|
||||
val permissions = mutableListOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissions.add(Manifest.permission.READ_MEDIA_IMAGES)
|
||||
permissions.add(Manifest.permission.ACCESS_MEDIA_LOCATION)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
permissions.add(Manifest.permission.ACCESS_MEDIA_LOCATION)
|
||||
} else {
|
||||
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
/**
|
||||
* Displays an error message to the user and logs it for debugging purposes.
|
||||
*
|
||||
* @param context The context used to display the Toast.
|
||||
* @param message The error message to display and log.
|
||||
* @param folderName The name of the folder to delete.
|
||||
*/
|
||||
fun showError(context: Context, message: String, folderName: String) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.custom_selector_folder_deleted_failure, folderName),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("DeleteFolder").e(message)
|
||||
}
|
||||
|
||||
for (permission in permissions) {
|
||||
val status = if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||
"GRANTED"
|
||||
} else {
|
||||
"DENIED"
|
||||
}
|
||||
Timber.tag("PermissionsStatus").d("$permission: $status")
|
||||
}
|
||||
/**
|
||||
* Displays a success message to the user.
|
||||
*
|
||||
* @param context The context used to display the Toast.
|
||||
* @param message The success message to display.
|
||||
* @param folderName The name of the folder to delete.
|
||||
*/
|
||||
fun showSuccess(context: Context, message: String, folderName: String) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.custom_selector_folder_deleted_success, folderName),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("DeleteFolder").d(message)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,19 +6,14 @@ import android.app.Dialog
|
|||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -43,19 +38,12 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.MultiplePermissionsReport
|
||||
import com.karumi.dexter.PermissionToken
|
||||
import com.karumi.dexter.listener.PermissionRequest
|
||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants.SHOULD_REFRESH
|
||||
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper
|
||||
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper.getFolderPath
|
||||
import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper.refreshMediaStore
|
||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
|
|
@ -67,18 +55,17 @@ import fr.free.nrw.commons.media.ZoomableActivity
|
|||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||
import fr.free.nrw.commons.utils.PermissionUtils
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.lang.Integer.max
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* Custom Selector Activity.
|
||||
*/
|
||||
|
|
@ -164,7 +151,7 @@ class CustomSelectorActivity :
|
|||
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* Show delete button on folder
|
||||
* Show delete button in folder
|
||||
*/
|
||||
private var showOverflowMenu = false
|
||||
|
||||
|
|
@ -266,24 +253,19 @@ class CustomSelectorActivity :
|
|||
imageFragment?.passSelectedImages(selectedImages, shouldRefresh)
|
||||
}
|
||||
|
||||
if (requestCode == 2) { // Consistent with handleRecoverableSecurityException
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Timber.tag("FolderAction").d("User confirmed deletion")
|
||||
// Retry deletion for the pending files
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId)
|
||||
if (folderPath != null) {
|
||||
val folder = File(folderPath)
|
||||
FolderDeletionHelper.deleteFolder(this, folder)
|
||||
navigateToMainScreen()
|
||||
} else {
|
||||
Timber.tag("FolderAction").e("User denied deletion request")
|
||||
}
|
||||
}
|
||||
if (requestCode == Constants.RequestCodes.DELETE_FOLDER_REQUEST_CODE &&
|
||||
resultCode == Activity.RESULT_OK) {
|
||||
|
||||
FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName)
|
||||
navigateToCustomSelector()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show Custom Selector Welcome Dialog.
|
||||
*/
|
||||
|
|
@ -480,11 +462,10 @@ class CustomSelectorActivity :
|
|||
val popupMenu = PopupMenu(this, anchorView)
|
||||
popupMenu.menuInflater.inflate(R.menu.menu_custom_selector, popupMenu.menu)
|
||||
|
||||
// Handle menu item clicks
|
||||
popupMenu.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_delete_folder -> {
|
||||
deleteFolderWithPermissions()
|
||||
deleteFolder()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
|
@ -493,121 +474,63 @@ class CustomSelectorActivity :
|
|||
popupMenu.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers folder deletion after permission checks.
|
||||
*/
|
||||
private fun deleteFolderWithPermissions() {
|
||||
val permissions = PermissionUtils.PERMISSIONS_STORAGE
|
||||
if (PermissionUtils.hasPermission(this, permissions)) {
|
||||
deleteFolderByApiVersion()
|
||||
} else {
|
||||
requestPermissionsIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and requests necessary permissions using Dexter library.
|
||||
*/
|
||||
private fun requestPermissionsIfNeeded() {
|
||||
val permissions = PermissionUtils.PERMISSIONS_STORAGE
|
||||
|
||||
Dexter.withActivity(this)
|
||||
.withPermissions(*permissions)
|
||||
.withListener(object : MultiplePermissionsListener {
|
||||
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
|
||||
if (report.areAllPermissionsGranted()) {
|
||||
deleteFolderByApiVersion()
|
||||
} else if (report.isAnyPermissionPermanentlyDenied) {
|
||||
showSettingsDialog()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@CustomSelectorActivity,
|
||||
"Permissions required to delete folder.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPermissionRationaleShouldBeShown(
|
||||
permissions: List<PermissionRequest>,
|
||||
token: PermissionToken
|
||||
) {
|
||||
token.continuePermissionRequest()
|
||||
}
|
||||
}).check()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a dialog directing the user to settings if permissions are permanently denied.
|
||||
*/
|
||||
private fun showSettingsDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Need Permissions")
|
||||
.setMessage("This app needs storage permissions to delete folders. You can grant them in app settings.")
|
||||
.setPositiveButton("Go to Settings") { _, _ ->
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.fromParts("package", packageName, null)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes folder based on Android API version.
|
||||
*/
|
||||
private fun deleteFolderByApiVersion() {
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId)
|
||||
if (folderPath != null) {
|
||||
val folder = File(folderPath)
|
||||
private fun deleteFolder() {
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: run {
|
||||
FolderDeletionHelper.showError(this, "Failed to retrieve folder path", bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
val folder = File(folderPath)
|
||||
if (!folder.exists() || !folder.isDirectory) {
|
||||
FolderDeletionHelper.showError(this,"Folder not found or is not a directory", bucketName)
|
||||
return
|
||||
}
|
||||
|
||||
if (folder.exists() && folder.isDirectory) {
|
||||
FolderDeletionHelper.confirmAndDeleteFolder(this, folder) { success ->
|
||||
if (success) {
|
||||
Toast.makeText(this, "Folder deleted", Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("FolderAction").d("Folder deleted successfully")
|
||||
// For API 30+, navigation is handled in 'onActivityResult'
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName)
|
||||
navigateToCustomSelector()
|
||||
}
|
||||
} else {
|
||||
FolderDeletionHelper.showError(this, "Failed to delete folder", bucketName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Navigate back to main FolderFragment after deletion
|
||||
navigateToMainScreen()
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to delete folder", Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("FolderAction").e("Failed to delete folder")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Folder not found", Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("FolderAction").e("Folder not found")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "Folder path not found", Toast.LENGTH_SHORT).show()
|
||||
Timber.tag("FolderDeletion").e("Failed to retrieve folder path for bucket ID: $bucketId")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate back to the main FolderFragment.
|
||||
* Navigates back to the main `FolderFragment`, refreshes the MediaStore, resets UI states,
|
||||
* and reloads folder data.
|
||||
*/
|
||||
private fun navigateToMainScreen() {
|
||||
private fun navigateToCustomSelector() {
|
||||
|
||||
val folderPath = getFolderPath(this, bucketId) ?: ""
|
||||
val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: ""
|
||||
val folder = File(folderPath)
|
||||
|
||||
supportFragmentManager.popBackStack(null, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
supportFragmentManager.popBackStack(null,
|
||||
androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
|
||||
// Refresh MediaStore for the deleted folder path to ensure metadata updates
|
||||
refreshMediaStore(this, folder)
|
||||
// Replace the current fragment with FolderFragment to go back to the main screen
|
||||
//refresh MediaStore for the deleted folder path to ensure metadata updates
|
||||
FolderDeletionHelper.refreshMediaStore(this, folder)
|
||||
|
||||
//replace the current fragment with FolderFragment to go back to the main screen
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, FolderFragment.newInstance())
|
||||
.commitAllowingStateLoss() // Ensure this transaction executes, even if the activity state is not fully stable
|
||||
.commitAllowingStateLoss()
|
||||
|
||||
// Reset toolbar and flags
|
||||
//reset toolbar and flags
|
||||
isImageFragmentOpen = false
|
||||
showOverflowMenu = false
|
||||
setUpToolbar()
|
||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||
|
||||
// Fetch updated folder data
|
||||
//fetch updated folder data
|
||||
fetchData()
|
||||
}
|
||||
|
||||
|
|
@ -634,7 +557,7 @@ class CustomSelectorActivity :
|
|||
bucketName = folderName
|
||||
isImageFragmentOpen = true
|
||||
|
||||
// Show the overflow menu only when a folder is clicked
|
||||
//show the overflow menu only when a folder is clicked
|
||||
showOverflowMenu = true
|
||||
setUpToolbar()
|
||||
|
||||
|
|
@ -821,7 +744,9 @@ fun partialStorageAccessIndicator(
|
|||
border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
|
||||
Row(modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "You've given access to a select number of photos",
|
||||
modifier = Modifier.weight(1f),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public interface Constants {
|
|||
int CAPTURE_VIDEO = FILE_PICKER_IMAGE_IDENTIFICATOR + (1 << 14);
|
||||
|
||||
int RECEIVE_DATA_FROM_FULL_SCREEN_MODE = 1 << 9;
|
||||
|
||||
int DELETE_FOLDER_REQUEST_CODE = 1 << 16;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -34,10 +34,15 @@ public class PermissionUtils {
|
|||
return new String[]{ Manifest.permission.READ_MEDIA_IMAGES,
|
||||
Manifest. permission.ACCESS_MEDIA_LOCATION };
|
||||
}
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||
return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_MEDIA_LOCATION };
|
||||
}
|
||||
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.ACCESS_MEDIA_LOCATION };
|
||||
}
|
||||
return new String[]{
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE };
|
||||
|
|
|
|||
|
|
@ -40,30 +40,34 @@
|
|||
app:layout_constraintStart_toEndOf="@+id/back"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Warning Icon (image_limit_error) -->
|
||||
<ImageButton
|
||||
android:id="@+id/image_limit_error"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#00FFFFFF"
|
||||
android:padding="@dimen/standard_gap"
|
||||
android:contentDescription="@string/custom_selector_limit_error_desc"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/menu_overflow"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
app:layout_constraintStart_toEndOf="@id/title"
|
||||
app:srcCompat="@drawable/ic_error_red_24dp" />
|
||||
|
||||
<!-- Overflow Menu Icon (menu_overflow) -->
|
||||
<ImageButton
|
||||
android:id="@+id/menu_overflow"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#00FFFFFF"
|
||||
android:padding="@dimen/standard_gap"
|
||||
android:contentDescription="@string/menu_overflow_desc"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@id/title"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
app:layout_constraintStart_toEndOf="@id/image_limit_error"
|
||||
app:srcCompat="@drawable/ic_overflow" />
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_delete_folder"
|
||||
android:title="@string/delete_folder"
|
||||
android:title="@string/custom_selector_delete_folder"
|
||||
android:icon="@drawable/ic_delete_grey_700_24dp"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
|
|
@ -832,5 +832,15 @@ Upload your first media by tapping on the add button.</string>
|
|||
<string name="pending">Pending</string>
|
||||
<string name="failed">Failed</string>
|
||||
<string name="could_not_load_place_data">Could not load place data</string>
|
||||
<string name="delete_folder">Delete Folder</string>
|
||||
|
||||
<string name="custom_selector_delete_folder">Delete Folder</string>
|
||||
<string name="custom_selector_confirm_deletion_title">Confirm Deletion</string>
|
||||
<string name="custom_selector_confirm_deletion_message">Are you sure you want to delete folder %1$s containing %2$d items?</string>
|
||||
<string name="custom_selector_delete">Delete</string>
|
||||
<string name="custom_selector_cancel">Cancel</string>
|
||||
<string name="custom_selector_folder_deleted_success">Folder %1$s deleted successfully</string>
|
||||
<string name="custom_selector_folder_deleted_failure">Failed to delete folder %1$s</string>
|
||||
<string name="custom_selector_error_trashing_folder_contents">Error in trashing folder contents: %1$s</string>
|
||||
<string name="custom_selector_folder_not_found_error">Failed to retrieve folder path for bucket ID: %1$d</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue