diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/helper/FolderDeletionHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/FolderDeletionHelper.kt index 589b0ee99..01f3196f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/helper/FolderDeletionHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/FolderDeletionHelper.kt @@ -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 - 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) - onDeletionComplete(success) - } - .setNegativeButton("Cancel") { dialog, _ -> - dialog.dismiss() - onDeletionComplete(false) // Return false if the user cancels - } - .show() + //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(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(context.getString(R.string.custom_selector_cancel)) { dialog, _ -> + dialog.dismiss() + 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() + + // 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) - } - - for (permission in permissions) { - val status = if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) { - "GRANTED" - } else { - "DENIED" - } - Timber.tag("PermissionsStatus").d("$permission: $status") - } + /** + * 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) } + + /** + * 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) + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 956fc25bb..a85d48ed1 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -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, - 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 + } - 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") + val folder = File(folderPath) + if (!folder.exists() || !folder.isDirectory) { + FolderDeletionHelper.showError(this,"Folder not found or is not a directory", bucketName) + return + } - // 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") - } + FolderDeletionHelper.confirmAndDeleteFolder(this, folder) { success -> + if (success) { + // 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 { - Toast.makeText(this, "Folder not found", Toast.LENGTH_SHORT).show() - Timber.tag("FolderAction").e("Folder not found") + FolderDeletionHelper.showError(this, "Failed to delete folder", bucketName) } - } 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. - */ - private fun navigateToMainScreen() { - val folderPath = getFolderPath(this, bucketId) ?: "" + + /** + * Navigates back to the main `FolderFragment`, refreshes the MediaStore, resets UI states, + * and reloads folder data. + */ + private fun navigateToCustomSelector() { + + 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), diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java index 3c9299c1a..503d385ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java @@ -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; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java index 9082c1f0f..c51d9dd40 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java @@ -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 }; diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index 02c864422..44e494cb9 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -11,7 +11,7 @@ android:id="@+id/partial_access_indicator" android:layout_width="match_parent" android:layout_height="0dp" - app:layout_constraintTop_toBottomOf="@id/toolbar_layout"/> + app:layout_constraintTop_toBottomOf="@id/toolbar_layout" /> + + diff --git a/app/src/main/res/menu/menu_custom_selector.xml b/app/src/main/res/menu/menu_custom_selector.xml index 981c59469..fc432439e 100644 --- a/app/src/main/res/menu/menu_custom_selector.xml +++ b/app/src/main/res/menu/menu_custom_selector.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 014cc5a6e..99a3495ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -832,5 +832,15 @@ Upload your first media by tapping on the add button. Pending Failed Could not load place data - Delete Folder + + Delete Folder + Confirm Deletion + Are you sure you want to delete folder %1$s containing %2$d items? + Delete + Cancel + Folder %1$s deleted successfully + Failed to delete folder %1$s + Error in trashing folder contents: %1$s + Failed to retrieve folder path for bucket ID: %1$d +