Skip to content

Commit

Permalink
Closes mozilla-mobile#6915: Add addon installation confirmation dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
psymoon committed May 11, 2020
1 parent 16b7808 commit 49d2be0
Show file tree
Hide file tree
Showing 6 changed files with 623 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/* 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.addons.ui

import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.mozac_feature_addons_fragment_dialog_addon_installed.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.amo.AddonCollectionProvider
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import java.io.IOException

private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY"
private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT"
private const val KEY_CONFIRM_BUTTON_BACKGROUND_COLOR = "KEY_CONFIRM_BUTTON_BACKGROUND_COLOR"
private const val KEY_CONFIRM_BUTTON_TEXT_COLOR = "KEY_CONFIRM_BUTTON_TEXT_COLOR"
private const val KEY_CONFIRM_BUTTON_RADIUS = "KEY_CONFIRM_BUTTON_RADIUS"
private const val DEFAULT_VALUE = Int.MAX_VALUE

/**
* A dialog that shows [Addon] installation confirmation.
*/
class AddonInstallationDialogFragment(
private val addonCollectionProvider: AddonCollectionProvider
) : AppCompatDialogFragment() {
private val scope = CoroutineScope(Dispatchers.IO)
private val logger = Logger("AddonInstallationDialogFragment")
/**
* A lambda called when the confirm button is clicked.
*/
var onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null

private val safeArguments get() = requireNotNull(arguments)

internal val addon get() = requireNotNull(safeArguments.getParcelable<Addon>(KEY_ADDON))
private var allowPrivateBrowsing: Boolean = false

internal val confirmButtonRadius
get() =
safeArguments.getFloat(KEY_CONFIRM_BUTTON_RADIUS, DEFAULT_VALUE.toFloat())

internal val dialogGravity: Int
get() =
safeArguments.getInt(
KEY_DIALOG_GRAVITY,
DEFAULT_VALUE
)
internal val dialogShouldWidthMatchParent: Boolean
get() =
safeArguments.getBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT)

internal val confirmButtonBackgroundColor
get() =
safeArguments.getInt(
KEY_CONFIRM_BUTTON_BACKGROUND_COLOR,
DEFAULT_VALUE
)

internal val confirmButtonTextColor
get() =
safeArguments.getInt(
KEY_CONFIRM_BUTTON_TEXT_COLOR,
DEFAULT_VALUE
)

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val sheetDialog = Dialog(requireContext())
sheetDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
sheetDialog.setCanceledOnTouchOutside(true)

val rootView = createContainer()

sheetDialog.setContainerView(rootView)

sheetDialog.window?.apply {
if (dialogGravity != DEFAULT_VALUE) {
setGravity(dialogGravity)
}

if (dialogShouldWidthMatchParent) {
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)
}
}

return sheetDialog
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onConfirmButtonClicked?.invoke(addon, allowPrivateBrowsing)
}

private fun Dialog.setContainerView(rootView: View) {
if (dialogShouldWidthMatchParent) {
setContentView(rootView)
} else {
addContentView(
rootView,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
}
}

@SuppressLint("InflateParams")
private fun createContainer(): View {
val rootView = LayoutInflater.from(requireContext()).inflate(
R.layout.mozac_feature_addons_fragment_dialog_addon_installed,
null,
false
)

rootView.findViewById<TextView>(R.id.title).text =
requireContext().getString(
R.string.mozac_feature_addons_installed_dialog_title,
addon.translatedName,
requireContext().appName
)

fetchIcon(addon, rootView.icon)

val allowedInPrivateBrowsing = rootView.findViewById<AppCompatCheckBox>(R.id.allow_in_private_browsing)
allowedInPrivateBrowsing.setOnCheckedChangeListener { _, isChecked ->
allowPrivateBrowsing = isChecked
}

val confirmButton = rootView.findViewById<Button>(R.id.confirm_button)
confirmButton.setOnClickListener {
onConfirmButtonClicked?.invoke(addon, allowPrivateBrowsing)
dismiss()
}

if (confirmButtonBackgroundColor != DEFAULT_VALUE) {
val backgroundTintList =
ContextCompat.getColorStateList(requireContext(), confirmButtonBackgroundColor)
confirmButton.backgroundTintList = backgroundTintList
}

if (confirmButtonTextColor != DEFAULT_VALUE) {
val color = ContextCompat.getColor(requireContext(), confirmButtonTextColor)
confirmButton.setTextColor(color)
}

if (confirmButtonRadius != DEFAULT_VALUE.toFloat()) {
val shape = GradientDrawable()
shape.shape = GradientDrawable.RECTANGLE
shape.setColor(
ContextCompat.getColor(
requireContext(),
confirmButtonBackgroundColor
)
)
shape.cornerRadius = confirmButtonRadius
confirmButton.background = shape
}

return rootView
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun fetchIcon(addon: Addon, iconView: ImageView, scope: CoroutineScope = this.scope): Job {
return scope.launch {
try {
val iconBitmap = addonCollectionProvider.getAddonIconBitmap(addon)
iconBitmap?.let {
scope.launch(Dispatchers.Main) {
iconView.setImageDrawable(BitmapDrawable(iconView.resources, it))
}
}
} catch (e: IOException) {
scope.launch(Dispatchers.Main) {
val context = iconView.context
val att = context.theme.resolveAttribute(android.R.attr.textColorPrimary)
iconView.setColorFilter(ContextCompat.getColor(context, att))
iconView.setImageDrawable(context.getDrawable(R.drawable.mozac_ic_extensions))
}
logger.error("Attempt to fetch the ${addon.id} icon failed", e)
}
}
}

@Suppress("LongParameterList")
companion object {
/**
* Returns a new instance of [AddonInstallationDialogFragment].
* @param addon The addon to show in the dialog.
* @param promptsStyling Styling properties for the dialog.
* @param onConfirmButtonClicked A lambda called when the allow button is clicked.
*/
fun newInstance(
addon: Addon,
addonCollectionProvider: AddonCollectionProvider,
promptsStyling: PromptsStyling? = PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true
),
onConfirmButtonClicked: ((Addon, Boolean) -> Unit)? = null
): AddonInstallationDialogFragment {

val fragment = AddonInstallationDialogFragment(addonCollectionProvider)
val arguments = fragment.arguments ?: Bundle()

arguments.apply {
putParcelable(KEY_ADDON, addon)

promptsStyling?.gravity?.apply {
putInt(KEY_DIALOG_GRAVITY, this)
}
promptsStyling?.shouldWidthMatchParent?.apply {
putBoolean(KEY_DIALOG_WIDTH_MATCH_PARENT, this)
}
promptsStyling?.confirmButtonBackgroundColor?.apply {
putInt(KEY_CONFIRM_BUTTON_BACKGROUND_COLOR, this)
}

promptsStyling?.confirmButtonTextColor?.apply {
putInt(KEY_CONFIRM_BUTTON_TEXT_COLOR, this)
}
}
fragment.onConfirmButtonClicked = onConfirmButtonClicked
fragment.arguments = arguments
return fragment
}
}

/**
* Styling for the addon installation dialog.
*/
data class PromptsStyling(
val gravity: Int,
val shouldWidthMatchParent: Boolean = false,
@ColorRes
val confirmButtonBackgroundColor: Int? = null,
@ColorRes
val confirmButtonTextColor: Int? = null,
val confirmButtonRadius: Float? = null
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!-- 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/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:orientation="vertical"
tools:ignore="Overdraw">

<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentTop="true"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
app:srcCompat="@drawable/mozac_ic_extensions" />

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/icon"
android:layout_alignParentTop="true"
android:layout_marginStart="3dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="11dp"
android:layout_toEndOf="@id/icon"
android:paddingStart="5dp"
android:paddingTop="4dp"
android:paddingEnd="5dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
tools:text="@string/mozac_feature_addons_installed_dialog_title"
tools:textColor="#000000" />

<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_alignStart="@id/title"
android:layout_marginTop="16dp"
android:paddingStart="5dp"
android:paddingTop="4dp"
android:paddingEnd="5dp"
android:textColor="?android:attr/textColorPrimary"
android:text="@string/mozac_feature_addons_installed_dialog_description" />

<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/description"
android:layout_below="@id/title"
android:layout_marginTop="16dp"
app:tint="?android:attr/textColorPrimary"
app:srcCompat="@drawable/mozac_ic_menu"
/>

<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/allow_in_private_browsing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/description"
android:layout_alignStart="@id/title"
android:layout_marginTop="16dp"
android:paddingStart="5dp"
android:paddingTop="4dp"
android:paddingEnd="5dp"
android:text="@string/mozac_feature_addons_settings_allow_in_private_browsing" />

<Button
android:id="@+id/confirm_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/allow_in_private_browsing"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/mozac_feature_addons_installed_dialog_okay_button"
android:textAllCaps="false" />

</RelativeLayout>
8 changes: 7 additions & 1 deletion components/feature/addons/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,10 @@
<string name="mozac_feature_addons_updater_dialog_last_attempt">Last attempt:</string>
<!-- Displayed in the "Status" field for the updater when an add-on has been correctly updated -->
<string name="mozac_feature_addons_updater_dialog_status">Status:</string>
</resources>
<!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. %2$s is the app name. -->
<string name="mozac_feature_addons_installed_dialog_title">%1$s has been added to %2$s</string>
<!-- Text shown in the dialog when add-on installation is completed. %1$s is the add-on name. -->
<string name="mozac_feature_addons_installed_dialog_description">Open it in the menu</string>
<!-- Confirmation button text for the dialog when add-on installation is completed. -->
<string name="mozac_feature_addons_installed_dialog_okay_button">Okay, Got It</string>
</resources>
Loading

0 comments on commit 49d2be0

Please sign in to comment.