Glimpse: Commonize file action dialogs and snackbars

Change-Id: I6af661a1fa062e3bd6dbf7a64a7d6354b272d8a8
diff --git a/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt b/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt
index 37c6992..9e75421 100644
--- a/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt
+++ b/app/src/main/java/org/lineageos/glimpse/ViewActivity.kt
@@ -36,8 +36,6 @@
 import androidx.media3.common.Player
 import androidx.media3.exoplayer.ExoPlayer
 import androidx.viewpager2.widget.ViewPager2
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
@@ -52,6 +50,7 @@
 import org.lineageos.glimpse.models.UriMedia
 import org.lineageos.glimpse.recyclerview.MediaViewerAdapter
 import org.lineageos.glimpse.ui.MediaInfoBottomSheetDialog
+import org.lineageos.glimpse.utils.MediaDialogsUtils
 import org.lineageos.glimpse.utils.MediaStoreBuckets
 import org.lineageos.glimpse.utils.PermissionsGatedCallback
 import org.lineageos.glimpse.viewmodels.MediaViewerUIViewModel
@@ -137,8 +136,7 @@
     private var additionalMedias: Array<MediaStoreMedia>? = null
     private var secure = false
 
-    private var lastTrashedMedia: MediaStoreMedia? = null
-    private var undoTrashSnackbar: Snackbar? = null
+    private var lastProcessedMedia: MediaStoreMedia? = null
 
     /**
      * Check if we're showing a static set of medias.
@@ -149,62 +147,46 @@
     // Contracts
     private val deleteUriContract =
         registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
-            Snackbar.make(
+            val succeeded = it.resultCode != Activity.RESULT_CANCELED
+
+            MediaDialogsUtils.showDeleteForeverResultSnackbar(
+                this,
                 bottomSheetLinearLayout,
-                resources.getQuantityString(
-                    if (it.resultCode == Activity.RESULT_CANCELED) {
-                        R.plurals.delete_file_forever_unsuccessful
-                    } else {
-                        R.plurals.delete_file_forever_successful
-                    },
-                    1, 1
-                ),
-                Snackbar.LENGTH_LONG,
-            ).setAnchorView(bottomSheetLinearLayout).show()
+                succeeded, 1,
+                bottomSheetLinearLayout,
+            )
         }
 
     private val trashUriContract =
         registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
             val succeeded = it.resultCode != Activity.RESULT_CANCELED
 
-            Snackbar.make(
+            MediaDialogsUtils.showMoveToTrashResultSnackbar(
+                this,
                 bottomSheetLinearLayout,
-                resources.getQuantityString(
-                    if (succeeded) {
-                        R.plurals.move_file_to_trash_successful
-                    } else {
-                        R.plurals.move_file_to_trash_unsuccessful
-                    },
-                    1, 1
-                ),
-                Snackbar.LENGTH_LONG,
-            ).apply {
-                anchorView = bottomSheetLinearLayout
-                lastTrashedMedia?.takeIf { succeeded }?.let { trashedMedia ->
-                    setAction(R.string.move_file_to_trash_undo) {
-                        trashMedia(trashedMedia, false)
-                    }
-                }
-                undoTrashSnackbar = this
-            }.show()
+                succeeded, 1,
+                bottomSheetLinearLayout,
+                lastProcessedMedia?.let { trashedMedia ->
+                    { trashMedia(trashedMedia, false) }
+                },
+            )
 
-            lastTrashedMedia = null
+            lastProcessedMedia = null
         }
 
     private val restoreUriFromTrashContract =
         registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
-            Snackbar.make(
+            val succeeded = it.resultCode != Activity.RESULT_CANCELED
+
+            MediaDialogsUtils.showRestoreFromTrashResultSnackbar(
+                this,
                 bottomSheetLinearLayout,
-                resources.getQuantityString(
-                    if (it.resultCode == Activity.RESULT_CANCELED) {
-                        R.plurals.restore_file_from_trash_unsuccessful
-                    } else {
-                        R.plurals.restore_file_from_trash_successful
-                    },
-                    1, 1
-                ),
-                Snackbar.LENGTH_LONG,
-            ).setAnchorView(bottomSheetLinearLayout).show()
+                succeeded, 1,
+                bottomSheetLinearLayout,
+                lastProcessedMedia?.let { trashedMedia ->
+                    { trashMedia(trashedMedia, true) }
+                },
+            )
         }
 
     private val favoriteContract =
@@ -399,23 +381,9 @@
 
         deleteButton.setOnLongClickListener {
             mediaViewerAdapter.getItemAtPosition(viewPager.currentItem).let {
-                MaterialAlertDialogBuilder(this)
-                    .setTitle(R.string.file_action_delete_forever)
-                    .setMessage(
-                        resources.getQuantityString(
-                            R.plurals.delete_file_forever_confirm_message, 1, 1
-                        )
-                    ).setPositiveButton(android.R.string.ok) { _, _ ->
-                        deleteUriContract.launch(
-                            contentResolver.createDeleteRequest(
-                                it.uri
-                            )
-                        )
-                    }
-                    .setNegativeButton(android.R.string.cancel) { _, _ ->
-                        // Do nothing
-                    }
-                    .show()
+                MediaDialogsUtils.openDeleteForeverDialog(this, it.uri) { uris ->
+                    deleteUriContract.launch(contentResolver.createDeleteRequest(*uris))
+                }
 
                 true
             }
@@ -520,7 +488,7 @@
 
     private fun trashMedia(media: MediaStoreMedia, trash: Boolean = !media.isTrashed) {
         if (trash) {
-            lastTrashedMedia = media
+            lastProcessedMedia = media
         }
 
         val contract = when (trash) {
diff --git a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt
index 216e11b..69013a6 100644
--- a/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt
+++ b/app/src/main/java/org/lineageos/glimpse/fragments/AlbumViewerFragment.kt
@@ -38,9 +38,7 @@
 import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.appbar.AppBarLayout
 import com.google.android.material.appbar.MaterialToolbar
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.shape.MaterialShapeDrawable
-import com.google.android.material.snackbar.Snackbar
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import org.lineageos.glimpse.R
@@ -51,6 +49,7 @@
 import org.lineageos.glimpse.recyclerview.ThumbnailAdapter
 import org.lineageos.glimpse.recyclerview.ThumbnailItemDetailsLookup
 import org.lineageos.glimpse.recyclerview.ThumbnailLayoutManager
+import org.lineageos.glimpse.utils.MediaDialogsUtils
 import org.lineageos.glimpse.utils.MediaStoreBuckets
 import org.lineageos.glimpse.utils.PermissionsGatedCallback
 import org.lineageos.glimpse.viewmodels.AlbumViewerViewModel
@@ -154,47 +153,25 @@
             selectionTracker?.selection?.toList()?.toTypedArray()?.takeUnless {
                 it.isEmpty()
             }?.let { selection ->
-                val count = selection.count()
-
                 when (item?.itemId) {
                     R.id.deleteForever -> {
-                        MaterialAlertDialogBuilder(requireContext())
-                            .setTitle(R.string.file_action_delete_forever)
-                            .setMessage(
-                                resources.getQuantityString(
-                                    R.plurals.delete_file_forever_confirm_message, count, count
+                        MediaDialogsUtils.openDeleteForeverDialog(requireContext(), *selection) {
+                            deleteForeverContract.launch(
+                                requireContext().contentResolver.createDeleteRequest(
+                                    *it.map { media ->
+                                        media.uri
+                                    }.toTypedArray()
                                 )
-                            ).setPositiveButton(android.R.string.ok) { _, _ ->
-                                deleteForeverContract.launch(
-                                    requireContext().contentResolver.createDeleteRequest(
-                                        *selection.map { media ->
-                                            media.uri
-                                        }.toTypedArray()
-                                    )
-                                )
-                            }
-                            .setNegativeButton(android.R.string.cancel) { _, _ ->
-                                // Do nothing
-                            }
-                            .show()
+                            )
+                        }
 
                         true
                     }
 
                     R.id.restoreFromTrash -> {
-                        MaterialAlertDialogBuilder(requireContext())
-                            .setTitle(R.string.file_action_restore_from_trash)
-                            .setMessage(
-                                resources.getQuantityString(
-                                    R.plurals.restore_file_from_trash_confirm_message, count, count
-                                )
-                            ).setPositiveButton(android.R.string.ok) { _, _ ->
-                                trashMedias(false, *selection)
-                            }
-                            .setNegativeButton(android.R.string.cancel) { _, _ ->
-                                // Do nothing
-                            }
-                            .show()
+                        MediaDialogsUtils.openRestoreFromTrashDialog(requireContext(), *selection) {
+                            trashMedias(false, *selection)
+                        }
 
                         true
                     }
@@ -206,19 +183,9 @@
                     }
 
                     R.id.moveToTrash -> {
-                        MaterialAlertDialogBuilder(requireContext())
-                            .setTitle(R.string.file_action_move_to_trash)
-                            .setMessage(
-                                resources.getQuantityString(
-                                    R.plurals.move_file_to_trash_confirm_message, count, count
-                                )
-                            ).setPositiveButton(android.R.string.ok) { _, _ ->
-                                trashMedias(true, *selection)
-                            }
-                            .setNegativeButton(android.R.string.cancel) { _, _ ->
-                                // Do nothing
-                            }
-                            .show()
+                        MediaDialogsUtils.openMoveToTrashDialog(requireContext(), *selection) {
+                            trashMedias(true, *selection)
+                        }
 
                         true
                     }
@@ -242,24 +209,17 @@
 
     // Contracts
     private var lastProcessedSelection: Array<out MediaStoreMedia>? = null
-    private var undoTrashSnackbar: Snackbar? = null
 
     private val deleteForeverContract =
         registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
+            val succeeded = it.resultCode != Activity.RESULT_CANCELED
             val count = lastProcessedSelection?.count() ?: 1
 
-            Snackbar.make(
+            MediaDialogsUtils.showDeleteForeverResultSnackbar(
+                requireContext(),
                 requireView(),
-                resources.getQuantityString(
-                    if (it.resultCode == Activity.RESULT_CANCELED) {
-                        R.plurals.delete_file_forever_unsuccessful
-                    } else {
-                        R.plurals.delete_file_forever_successful
-                    },
-                    count, count
-                ),
-                Snackbar.LENGTH_LONG,
-            ).show()
+                succeeded, count,
+            )
 
             lastProcessedSelection = null
             selectionTracker?.clearSelection()
@@ -270,25 +230,16 @@
             val succeeded = it.resultCode != Activity.RESULT_CANCELED
             val count = lastProcessedSelection?.count() ?: 1
 
-            Snackbar.make(
+            MediaDialogsUtils.showMoveToTrashResultSnackbar(
+                requireContext(),
                 requireView(),
-                resources.getQuantityString(
-                    if (succeeded) {
-                        R.plurals.move_file_to_trash_successful
-                    } else {
-                        R.plurals.move_file_to_trash_unsuccessful
-                    },
-                    count, count
-                ),
-                Snackbar.LENGTH_LONG,
-            ).apply {
-                lastProcessedSelection?.takeIf { succeeded }?.let { trashedMedias ->
-                    setAction(R.string.move_file_to_trash_undo) {
+                succeeded, count,
+                actionCallback = lastProcessedSelection?.let { trashedMedias ->
+                    {
                         trashMedias(false, *trashedMedias)
                     }
                 }
-                undoTrashSnackbar = this
-            }.show()
+            )
 
             lastProcessedSelection = null
             selectionTracker?.clearSelection()
@@ -296,20 +247,19 @@
 
     private val restoreFromTrashContract =
         registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
+            val succeeded = it.resultCode != Activity.RESULT_CANCELED
             val count = lastProcessedSelection?.count() ?: 1
 
-            Snackbar.make(
+            MediaDialogsUtils.showRestoreFromTrashResultSnackbar(
+                requireContext(),
                 requireView(),
-                resources.getQuantityString(
-                    if (it.resultCode == Activity.RESULT_CANCELED) {
-                        R.plurals.restore_file_from_trash_unsuccessful
-                    } else {
-                        R.plurals.restore_file_from_trash_successful
-                    },
-                    count, count
-                ),
-                Snackbar.LENGTH_LONG,
-            ).show()
+                succeeded, count,
+                actionCallback = lastProcessedSelection?.let { trashedMedias ->
+                    {
+                        trashMedias(true, *trashedMedias)
+                    }
+                }
+            )
 
             lastProcessedSelection = null
             selectionTracker?.clearSelection()
@@ -346,29 +296,19 @@
                 R.id.emptyTrash -> {
                     val selection = thumbnailAdapter.currentList.mapNotNull {
                         AlbumViewerViewModel.DataType.Thumbnail::class.safeCast(it)?.media
-                    }
+                    }.toTypedArray()
                     val count = selection.size
 
                     if (count > 0) {
-                        MaterialAlertDialogBuilder(requireContext())
-                            .setTitle(R.string.file_action_delete_forever)
-                            .setMessage(
-                                resources.getQuantityString(
-                                    R.plurals.delete_file_forever_confirm_message, count, count
+                        MediaDialogsUtils.openDeleteForeverDialog(requireContext(), *selection) {
+                            deleteForeverContract.launch(
+                                requireContext().contentResolver.createDeleteRequest(
+                                    *it.mapNotNull { media ->
+                                        MediaStoreMedia::class.safeCast(media)?.uri
+                                    }.toTypedArray()
                                 )
-                            ).setPositiveButton(android.R.string.ok) { _, _ ->
-                                deleteForeverContract.launch(
-                                    requireContext().contentResolver.createDeleteRequest(
-                                        *selection.mapNotNull { media ->
-                                            MediaStoreMedia::class.safeCast(media)?.uri
-                                        }.toTypedArray()
-                                    )
-                                )
-                            }
-                            .setNegativeButton(android.R.string.cancel) { _, _ ->
-                                // Do nothing
-                            }
-                            .show()
+                            )
+                        }
                     }
 
                     true
diff --git a/app/src/main/java/org/lineageos/glimpse/utils/MediaDialogsUtils.kt b/app/src/main/java/org/lineageos/glimpse/utils/MediaDialogsUtils.kt
new file mode 100644
index 0000000..a399c5d
--- /dev/null
+++ b/app/src/main/java/org/lineageos/glimpse/utils/MediaDialogsUtils.kt
@@ -0,0 +1,146 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.glimpse.utils
+
+import android.content.Context
+import android.view.View
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import org.lineageos.glimpse.R
+
+object MediaDialogsUtils {
+    private fun <T> openDialog(
+        context: Context,
+        vararg uris: T,
+        onPositiveCallback: (uris: Array<out T>) -> Unit,
+        @StringRes titleStringRes: Int,
+        @PluralsRes confirmMessagePluralsRes: Int,
+    ) {
+        val count = uris.size
+
+        MaterialAlertDialogBuilder(context)
+            .setTitle(titleStringRes)
+            .setMessage(
+                context.resources.getQuantityString(
+                    confirmMessagePluralsRes, count, count
+                )
+            ).setPositiveButton(android.R.string.ok) { _, _ ->
+                onPositiveCallback(uris)
+            }
+            .setNegativeButton(android.R.string.cancel) { _, _ ->
+                // Do nothing
+            }
+            .show()
+    }
+
+    private fun showResultSnackbar(
+        context: Context,
+        view: View,
+        succeeded: Boolean,
+        count: Int,
+        anchorView: View? = null,
+        undoActionCallback: (() -> Unit)? = null,
+        @PluralsRes titleSuccessfulPluralsRes: Int,
+        @PluralsRes titleUnsuccessfulPluralsRes: Int,
+    ) = Snackbar.make(
+        view,
+        context.resources.getQuantityString(
+            if (succeeded) {
+                titleSuccessfulPluralsRes
+            } else {
+                titleUnsuccessfulPluralsRes
+            },
+            count, count
+        ),
+        Snackbar.LENGTH_LONG,
+    ).apply {
+        anchorView?.let {
+            this.anchorView = it
+        }
+
+        undoActionCallback?.takeIf { succeeded }?.let {
+            setAction(R.string.file_action_undo_action) { it() }
+        }
+
+        show()
+    }
+
+    // Move to trash
+
+    fun <T> openMoveToTrashDialog(
+        context: Context,
+        vararg uris: T,
+        onPositiveCallback: (uris: Array<out T>) -> Unit,
+    ) = openDialog(
+        context, *uris, onPositiveCallback = onPositiveCallback,
+        titleStringRes = R.string.file_action_move_to_trash,
+        confirmMessagePluralsRes = R.plurals.move_file_to_trash_confirm_message,
+    )
+
+    fun showMoveToTrashResultSnackbar(
+        context: Context,
+        view: View,
+        succeeded: Boolean,
+        count: Int,
+        anchorView: View? = null,
+        actionCallback: (() -> Unit)? = null,
+    ) = showResultSnackbar(
+        context, view, succeeded, count, anchorView, actionCallback,
+        titleSuccessfulPluralsRes = R.plurals.move_file_to_trash_successful,
+        titleUnsuccessfulPluralsRes = R.plurals.move_file_to_trash_unsuccessful,
+    )
+
+    // Restore from trash
+
+    fun <T> openRestoreFromTrashDialog(
+        context: Context,
+        vararg uris: T,
+        onPositiveCallback: (uris: Array<out T>) -> Unit,
+    ) = openDialog(
+        context, *uris, onPositiveCallback = onPositiveCallback,
+        titleStringRes = R.string.file_action_restore_from_trash,
+        confirmMessagePluralsRes = R.plurals.restore_file_from_trash_confirm_message,
+    )
+
+    fun showRestoreFromTrashResultSnackbar(
+        context: Context,
+        view: View,
+        succeeded: Boolean,
+        count: Int,
+        anchorView: View? = null,
+        actionCallback: (() -> Unit)? = null,
+    ) = showResultSnackbar(
+        context, view, succeeded, count, anchorView, actionCallback,
+        titleSuccessfulPluralsRes = R.plurals.restore_file_from_trash_successful,
+        titleUnsuccessfulPluralsRes = R.plurals.restore_file_from_trash_unsuccessful,
+    )
+
+    // Delete forever
+
+    fun <T> openDeleteForeverDialog(
+        context: Context,
+        vararg uris: T,
+        onPositiveCallback: (uris: Array<out T>) -> Unit,
+    ) = openDialog(
+        context, *uris, onPositiveCallback = onPositiveCallback,
+        titleStringRes = R.string.file_action_delete_forever,
+        confirmMessagePluralsRes = R.plurals.delete_file_forever_confirm_message,
+    )
+
+    fun showDeleteForeverResultSnackbar(
+        context: Context,
+        view: View,
+        succeeded: Boolean,
+        count: Int,
+        anchorView: View? = null,
+    ) = showResultSnackbar(
+        context, view, succeeded, count, anchorView,
+        titleSuccessfulPluralsRes = R.plurals.restore_file_from_trash_successful,
+        titleUnsuccessfulPluralsRes = R.plurals.restore_file_from_trash_unsuccessful,
+    )
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e8f99cc..6321293 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -52,7 +52,6 @@
         <item quantity="one">File couldn\'t be moved to trash</item>
         <item quantity="other">%d files couldn\'t be moved to trash</item>
     </plurals>
-    <string name="move_file_to_trash_undo">Undo</string>
 
     <!-- File restoring from trash -->
     <plurals name="restore_file_from_trash_confirm_message">
@@ -94,6 +93,9 @@
     <string name="file_action_restore_from_trash">Restore from trash</string>
     <string name="file_action_share">Share</string>
 
+    <!-- File actions - undo -->
+    <string name="file_action_undo_action">Undo</string>
+
     <!-- No media -->
     <string name="no_media">No media</string>