From 07ea3d8a48b8697b47b8780c0e20f7670145b24b Mon Sep 17 00:00:00 2001 From: mike a Date: Sun, 19 Dec 2021 19:38:51 -0800 Subject: [PATCH] [components] Closes https://github.com/mozilla-mobile/android-components/issues/11411: Cancel downloads prompt --- .../ui/DownloadCancelDialogFragment.kt | 216 ++++++++++++++++++ .../main/res/layout/mozac_download_cancel.xml | 76 ++++++ .../downloads/src/main/res/values/strings.xml | 9 + .../DownloadCancelDialogFragmentTest.kt | 144 ++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt create mode 100644 android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml create mode 100644 android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadCancelDialogFragmentTest.kt diff --git a/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt b/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt new file mode 100644 index 000000000000..c2cdcfbcf2c4 --- /dev/null +++ b/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/ui/DownloadCancelDialogFragment.kt @@ -0,0 +1,216 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.downloads.ui + +import android.app.Dialog +import android.content.DialogInterface +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable +import android.os.Bundle +import android.os.Parcelable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.widget.LinearLayout +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.content.ContextCompat +import kotlinx.parcelize.Parcelize +import mozilla.components.feature.downloads.R +import mozilla.components.feature.downloads.databinding.MozacDownloadCancelBinding + +/** + * The dialog warns the user that closing last private tab leads to cancellation of active private + * downloads. + */ +class DownloadCancelDialogFragment : AppCompatDialogFragment() { + + var onAcceptClicked: ((tabId: String?, source: String?) -> Unit)? = null + var onDenyClicked: (() -> Unit)? = null + + private val safeArguments get() = requireNotNull(arguments) + private val downloadCount by lazy { safeArguments.getInt(KEY_DOWNLOAD_COUNT) } + private val tabId by lazy { safeArguments.getString(KEY_TAB_ID) } + private val source by lazy { safeArguments.getString(KEY_SOURCE) } + private val promptStyling by lazy { safeArguments.getParcelable(KEY_STYLE) ?: PromptStyling() } + private val promptText by lazy { safeArguments.getParcelable(KEY_TEXT) ?: PromptText() } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return Dialog(requireContext()).apply { + requestWindowFeature(Window.FEATURE_NO_TITLE) + setCanceledOnTouchOutside(true) + + setContainerView(promptStyling.shouldWidthMatchParent, createContainer()) + + window?.apply { + setGravity(promptStyling.gravity) + + if (promptStyling.shouldWidthMatchParent) { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + // This must be called after addContentView, or it won't fully fill to the edge. + setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDenyClicked?.invoke() + } + + @Suppress("NestedBlockDepth") + private fun Dialog.setContainerView(dialogShouldWidthMatchParent: Boolean, rootView: View) { + if (dialogShouldWidthMatchParent) { + setContentView(rootView) + } else { + addContentView( + rootView, + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + ) + } + } + + @Suppress("InflateParams", "NestedBlockDepth") + private fun createContainer() = LayoutInflater.from(requireContext()).inflate( + R.layout.mozac_download_cancel, + null, + false + ).apply { + with(MozacDownloadCancelBinding.bind(this)) { + acceptButton.setOnClickListener { + onAcceptClicked?.invoke(tabId, source) + dismiss() + } + + denyButton.setOnClickListener { + onDenyClicked?.invoke() + dismiss() + } + + with(promptText) { + title.text = getString(titleText) + body.text = buildWarningText(downloadCount, bodyText) + acceptButton.text = getString(acceptText) + denyButton.text = getString(denyText) + } + + with(promptStyling) { + positiveButtonBackgroundColor?.let { + val backgroundTintList = ContextCompat.getColorStateList(requireContext(), it) + acceptButton.backgroundTintList = backgroundTintList + + // It appears there is not guaranteed way to get background color of a button, + // there are always nullable types, hence the code changing the positiveButtonRadius + // executes only if positiveButtonBackgroundColor is provided + positiveButtonRadius?.let { + val shape = GradientDrawable() + shape.shape = GradientDrawable.RECTANGLE + shape.setColor( + ContextCompat.getColor( + requireContext(), + positiveButtonBackgroundColor + ) + ) + shape.cornerRadius = positiveButtonRadius + acceptButton.background = shape + } + } + + positiveButtonTextColor?.let { + val color = ContextCompat.getColor(requireContext(), it) + acceptButton.setTextColor(color) + } + } + } + } + + @VisibleForTesting + internal fun buildWarningText(downloadCount: Int, @StringRes stringId: Int) = String.format( + getString(stringId), + downloadCount + ) + + companion object { + private const val KEY_DOWNLOAD_COUNT = "KEY_DOWNLOAD_COUNT" + private const val KEY_TAB_ID = "KEY_TAB_ID" + private const val KEY_SOURCE = "KEY_SOURCE" + private const val KEY_STYLE = "KEY_STYLE" + private const val KEY_TEXT = "KEY_TEXT" + + /** + * Returns a new instance of [DownloadCancelDialogFragment]. + * @param downloadCount The number of currently active downloads. + * @param promptStyling Styling properties for the dialog. + * @param onPositiveButtonClicked A lambda called when the allow button is clicked. + * @param onNegativeButtonClicked A lambda called when the deny button is clicked. + */ + fun newInstance( + downloadCount: Int, + tabId: String? = null, + source: String? = null, + promptText: PromptText? = null, + promptStyling: PromptStyling? = null, + onPositiveButtonClicked: ((tabId: String?, source: String?) -> Unit)? = null, + onNegativeButtonClicked: (() -> Unit)? = null + ): DownloadCancelDialogFragment { + return DownloadCancelDialogFragment().apply { + this.arguments = Bundle().apply { + putInt(KEY_DOWNLOAD_COUNT, downloadCount) + tabId?.let { putString(KEY_TAB_ID, it) } + source?.let { putString(KEY_SOURCE, it) } + promptText?.let { putParcelable(KEY_TEXT, it) } + promptStyling?.let { putParcelable(KEY_STYLE, it) } + } + this.onAcceptClicked = onPositiveButtonClicked + this.onDenyClicked = onNegativeButtonClicked + } + } + } + + /** + * Styling for the downloads cancellation dialog. + * Note that for [positiveButtonRadius] to be applied, + * specifying [positiveButtonBackgroundColor] is necessary. + */ + + @Parcelize + data class PromptStyling( + val gravity: Int = Gravity.BOTTOM, + val shouldWidthMatchParent: Boolean = true, + @ColorRes + val positiveButtonBackgroundColor: Int? = null, + @ColorRes + val positiveButtonTextColor: Int? = null, + val positiveButtonRadius: Float? = null + ) : Parcelable + + /** + * The class gives an option to override string resources used by [DownloadCancelDialogFragment]. + */ + + @Parcelize + data class PromptText( + @StringRes + val titleText: Int = R.string.cancel_active_downloads_warning_content_title, + @StringRes + val bodyText: Int = R.string.cancel_active_private_downloads_warning_content_body, + @StringRes + val acceptText: Int = R.string.cancel_active_downloads_accept, + @StringRes + val denyText: Int = R.string.cancel_active_private_downloads_deny + ) : Parcelable +} diff --git a/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml b/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml new file mode 100644 index 000000000000..252f545dd9ed --- /dev/null +++ b/android-components/components/feature/downloads/src/main/res/layout/mozac_download_cancel.xml @@ -0,0 +1,76 @@ + + + + + + + + + +