diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 70db0f0ac6a8..a9fc912d8559 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -173,6 +173,14 @@ class CtaViewModelTest { verify(mockPixel).fire(eq(SURVEY_CTA_LAUNCHED), any()) } + @Test + fun whenCtaSecondaryButonClickedPixelIsFired() { + val secondaryButtonCta = mock() + whenever(secondaryButtonCta.secondaryButtonPixel).thenReturn(ONBOARDING_DAX_ALL_CTA_HIDDEN) + testee.onUserClickCtaSecondaryButton(secondaryButtonCta) + verify(mockPixel).fire(eq(ONBOARDING_DAX_ALL_CTA_HIDDEN), any()) + } + @Test fun whenCtaDismissedPixelIsFired() { testee.onUserDismissedCta(HomePanelCta.Survey(Survey("abc", "http://example.com", 1, SCHEDULED))) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index bf2c102402f8..4e56d784f7f1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -49,6 +49,7 @@ import androidx.core.view.isEmpty import androidx.core.view.isNotEmpty import androidx.core.view.isVisible import androidx.core.view.postDelayed +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.* import androidx.recyclerview.widget.LinearLayoutManager @@ -76,6 +77,7 @@ import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta import com.duckduckgo.app.cta.ui.DaxDialogCta +import com.duckduckgo.app.cta.ui.SecondaryButtonCta import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.view.* @@ -235,7 +237,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { private val logoHidingListener by lazy { LogoHidingLayoutChangeLifecycleListener(ddgLogo) } private var alertDialog: AlertDialog? = null - private var daxDialog: DaxDialog? = null + private var daxDialog: DialogFragment? = null override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) @@ -1388,7 +1390,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { daxDialog?.dismiss() daxDialog = configuration.createCta(activity).apply { setHideClickListener { - dismiss() + getDaxDialog().dismiss() launchHideTipsDialog(activity, configuration) } setDismissListener { @@ -1402,16 +1404,22 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { if (configuration is DaxDialogCta.DaxMainNetworkCta) { setPrimaryCtaClickListener { viewModel.onUserClickCtaOkButton() - dismiss() + getDaxDialog().dismiss() } configuration.setSecondDialog(this, activity) viewModel.onManualCtaShown(configuration) } else { - dismiss() + getDaxDialog().dismiss() } } - show(activity.supportFragmentManager, DAX_DIALOG_DIALOG_TAG) - } + if (configuration is SecondaryButtonCta) { + setSecondaryCtaClickListener { + viewModel.onUserClickCtaSecondaryButton(configuration) + getDaxDialog().dismiss() + } + } + getDaxDialog().show(activity.supportFragmentManager, DAX_DIALOG_DIALOG_TAG) + }.getDaxDialog() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index defa29874ced..a6acce4cf495 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -55,6 +55,7 @@ import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthen import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.CtaViewModel +import com.duckduckgo.app.cta.ui.SecondaryButtonCta import com.duckduckgo.app.global.* import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory @@ -903,6 +904,10 @@ class BrowserTabViewModel( } } + fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) { + ctaViewModel.onUserClickCtaSecondaryButton(cta) + } + fun onUserDismissedCta() { val cta = ctaViewState.value?.cta ?: return diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 24beca873e7f..0b569d965d0f 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -30,6 +30,8 @@ import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.global.view.DaxDialog +import com.duckduckgo.app.global.view.DaxDialogHighlightView +import com.duckduckgo.app.global.view.TypewriterDaxDialog import com.duckduckgo.app.global.view.hide import com.duckduckgo.app.global.view.html import com.duckduckgo.app.global.view.show @@ -38,9 +40,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.trackerdetection.model.TrackingEvent import kotlinx.android.synthetic.main.include_cta_buttons.view.* import kotlinx.android.synthetic.main.include_cta_content.view.* -import kotlinx.android.synthetic.main.include_dax_dialog_cta.view.dialogTextCta -import kotlinx.android.synthetic.main.include_dax_dialog_cta.view.hiddenTextCta -import kotlinx.android.synthetic.main.include_dax_dialog_cta.view.primaryCta +import kotlinx.android.synthetic.main.include_dax_dialog_cta.view.* interface DialogCta { fun createCta(activity: FragmentActivity): DaxDialog @@ -71,6 +71,12 @@ interface Cta { fun pixelOkParameters(): Map } +interface SecondaryButtonCta { + val secondaryButtonPixel: Pixel.PixelName? + + fun pixelSecondaryButtonParameters(): Map +} + sealed class DaxDialogCta( override val ctaId: CtaId, @AnyRes open val description: Int, @@ -83,7 +89,7 @@ sealed class DaxDialogCta( override val appInstallStore: AppInstallStore ) : Cta, DialogCta, DaxCta { - override fun createCta(activity: FragmentActivity) = DaxDialog(getDaxText(activity), activity.resources.getString(okButton)) + override fun createCta(activity: FragmentActivity): DaxDialog = TypewriterDaxDialog(getDaxText(activity), activity.resources.getString(okButton)) override fun pixelCancelParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) @@ -123,7 +129,7 @@ sealed class DaxDialogCta( ) { override fun createCta(activity: FragmentActivity): DaxDialog = - DaxDialog(getDaxText(activity), activity.resources.getString(okButton), false) + TypewriterDaxDialog(daxText = getDaxText(activity), primaryButtonText = activity.resources.getString(okButton), toolbarDimmed = false) override fun getDaxText(context: Context): String { val trackersFiltered = trackers.asSequence() @@ -172,7 +178,9 @@ sealed class DaxDialogCta( } override fun createCta(activity: FragmentActivity): DaxDialog { - return DaxDialog(getDaxText(activity), activity.resources.getString(okButton)).apply { + return DaxDialogHighlightView( + TypewriterDaxDialog(daxText = getDaxText(activity), primaryButtonText = activity.resources.getString(okButton)) + ).apply { val privacyGradeButton = activity.findViewById(R.id.privacyGradeButton) onAnimationFinishedListener { if (isFromSameNetworkDomain()) { @@ -184,8 +192,8 @@ sealed class DaxDialogCta( fun setSecondDialog(dialog: DaxDialog, activity: FragmentActivity) { ctaPixelParam = Pixel.PixelValues.DAX_NETWORK_CTA_2 - dialog.daxText = activity.resources.getString(R.string.daxMainNetworkStep2CtaText, firstParagraph(activity), network) - dialog.buttonText = activity.resources.getString(R.string.daxDialogGotIt) + dialog.setDaxText(activity.resources.getString(R.string.daxMainNetworkStep2CtaText, firstParagraph(activity), network)) + dialog.setButtonText(activity.resources.getString(R.string.daxDialogGotIt)) dialog.onAnimationFinishedListener { } dialog.setDialogAndStartAnimation() } @@ -211,9 +219,13 @@ sealed class DaxDialogCta( onboardingStore, appInstallStore ) { - override fun createCta(activity: FragmentActivity): DaxDialog { - return DaxDialog(getDaxText(activity), activity.resources.getString(okButton)).apply { + return DaxDialogHighlightView( + TypewriterDaxDialog( + daxText = getDaxText(activity), + primaryButtonText = activity.resources.getString(okButton) + ) + ).apply { val fireButton = activity.findViewById(R.id.fire) onAnimationFinishedListener { startHighlightViewAnimation(fireButton) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 0191c42cc414..911a1eca29ea 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -111,6 +111,12 @@ class CtaViewModel @Inject constructor( } } + fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) { + cta.secondaryButtonPixel?.let { + pixel.fire(it, cta.pixelSecondaryButtonParameters()) + } + } + suspend fun refreshCta(dispatcher: CoroutineContext, isBrowserShowing: Boolean, site: Site? = null): Cta? { surveyCta()?.let { return it diff --git a/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt b/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt index 2df766e4b166..3a99557438ed 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt @@ -22,30 +22,44 @@ import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.os.Bundle +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.DialogFragment -import com.duckduckgo.app.browser.R -import android.view.Gravity -import kotlinx.android.synthetic.main.content_dax_dialog.* import android.view.animation.Animation import android.view.animation.OvershootInterpolator import android.view.animation.ScaleAnimation import android.widget.ImageView import android.widget.RelativeLayout import androidx.core.content.ContextCompat.getColor +import androidx.fragment.app.DialogFragment +import com.duckduckgo.app.browser.R +import kotlinx.android.synthetic.main.content_dax_dialog.* -class DaxDialog( - var daxText: String, - var buttonText: String, +interface DaxDialog { + fun setDaxText(daxText: String) + fun setButtonText(buttonText: String) + fun setDialogAndStartAnimation() + fun onAnimationFinishedListener(onAnimationFinished: () -> Unit) + fun setPrimaryCtaClickListener(clickListener: () -> Unit) + fun setSecondaryCtaClickListener(clickListener: () -> Unit) + fun setHideClickListener(clickListener: () -> Unit) + fun setDismissListener(clickListener: () -> Unit) + fun getDaxDialog(): DialogFragment +} + +class TypewriterDaxDialog( + private var daxText: String, + private var primaryButtonText: String, + private var secondaryButtonText: String? = "", private val toolbarDimmed: Boolean = true, private val dismissible: Boolean = true, private val typingDelayInMs: Long = 20 -) : DialogFragment() { +) : DialogFragment(), DaxDialog { private var onAnimationFinished: () -> Unit = {} private var primaryCtaClickListener: () -> Unit = { dismiss() } + private var secondaryCtaClickListener: (() -> Unit)? = null private var hideClickListener: () -> Unit = { dismiss() } private var dismissListener: () -> Unit = { } @@ -60,11 +74,13 @@ class DaxDialog( attributes?.gravity = Gravity.BOTTOM window?.attributes = attributes window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - setStyle(STYLE_NO_TITLE, android.R.style.Theme_DeviceDefault_Light_NoActionBar) - return dialog } + override fun getTheme(): Int { + return R.style.DaxDialogFragment + } + override fun onStart() { super.onStart() dialog?.window?.attributes?.dimAmount = 0f @@ -82,40 +98,40 @@ class DaxDialog( super.onDismiss(dialog) } - fun setDialogAndStartAnimation() { + override fun getDaxDialog(): DialogFragment = this + + override fun setDaxText(daxText: String) { + this.daxText = daxText + } + + override fun setButtonText(buttonText: String) { + this.primaryButtonText = buttonText + } + + override fun setDialogAndStartAnimation() { setDialog() setListeners() dialogText.startTypingAnimation(daxText, true, onAnimationFinished) } - fun onAnimationFinishedListener(onAnimationFinished: () -> Unit) { + override fun onAnimationFinishedListener(onAnimationFinished: () -> Unit) { this.onAnimationFinished = onAnimationFinished } - fun setPrimaryCtaClickListener(clickListener: () -> Unit) { + override fun setPrimaryCtaClickListener(clickListener: () -> Unit) { primaryCtaClickListener = clickListener } - fun setHideClickListener(clickListener: () -> Unit) { - hideClickListener = clickListener - } - - fun setDismissListener(clickListener: () -> Unit) { - dismissListener = clickListener + override fun setSecondaryCtaClickListener(clickListener: () -> Unit) { + secondaryCtaClickListener = clickListener } - fun startHighlightViewAnimation(targetView: View, duration: Long = 400, timesBigger: Float = 0f) { - val highlightImageView = addHighlightView(targetView, timesBigger) - val scaleAnimation = buildScaleAnimation(duration) - highlightImageView?.startAnimation(scaleAnimation) + override fun setHideClickListener(clickListener: () -> Unit) { + hideClickListener = clickListener } - private fun buildScaleAnimation(duration: Long = 400): Animation { - val scaleAnimation = ScaleAnimation(0f, 1f, 0f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) - scaleAnimation.duration = duration - scaleAnimation.fillAfter = true - scaleAnimation.interpolator = OvershootInterpolator(OVERSHOOT_TENSION) - return scaleAnimation + override fun setDismissListener(clickListener: () -> Unit) { + dismissListener = clickListener } private fun setListeners() { @@ -129,6 +145,13 @@ class DaxDialog( primaryCtaClickListener() } + secondaryCtaClickListener?.let { + secondaryCta.setOnClickListener { + dialogText.cancelAnimation() + it() + } + } + if (dismissible) { dialogContainer.setOnClickListener { dialogText.cancelAnimation() @@ -147,14 +170,35 @@ class DaxDialog( val toolbarColor = if (toolbarDimmed) getColor(it, R.color.dimmed) else getColor(it, android.R.color.transparent) toolbarDialogLayout.setBackgroundColor(toolbarColor) hiddenText.text = daxText.html(it) - primaryCta.text = buttonText + primaryCta.text = primaryButtonText + secondaryCta.text = secondaryButtonText + secondaryCta.visibility = if (secondaryButtonText.isNullOrEmpty()) View.GONE else View.VISIBLE dialogText.typingDelayInMs = typingDelayInMs } } +} + +class DaxDialogHighlightView( + daxDialog: TypewriterDaxDialog +) : DaxDialog by daxDialog { + + fun startHighlightViewAnimation(targetView: View, duration: Long = 400, timesBigger: Float = 0f) { + val highlightImageView = addHighlightView(targetView, timesBigger) + val scaleAnimation = buildScaleAnimation(duration) + highlightImageView?.startAnimation(scaleAnimation) + } + + private fun buildScaleAnimation(duration: Long = 400): Animation { + val scaleAnimation = ScaleAnimation(0f, 1f, 0f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f) + scaleAnimation.duration = duration + scaleAnimation.fillAfter = true + scaleAnimation.interpolator = OvershootInterpolator(OVERSHOOT_TENSION) + return scaleAnimation + } private fun addHighlightView(targetView: View, timesBigger: Float): View? { - return activity?.let { - val highlightImageView = ImageView(context) + return getDaxDialog().activity?.let { + val highlightImageView = ImageView(getDaxDialog().context) highlightImageView.id = View.generateViewId() highlightImageView.setImageResource(R.drawable.ic_circle) @@ -174,7 +218,7 @@ class DaxDialog( val params = RelativeLayout.LayoutParams((width + timesBiggerX).toInt(), (height + timesBiggerY).toInt()) params.leftMargin = locationOnScreen[0] - (timesBiggerX / 2).toInt() params.topMargin = (locationOnScreen[1] - statusBarHeight) - (timesBiggerY / 2).toInt() - dialogContainer.addView(highlightImageView, params) + getDaxDialog().dialogContainer.addView(highlightImageView, params) highlightImageView } } diff --git a/app/src/main/res/layout/content_dax_dialog.xml b/app/src/main/res/layout/content_dax_dialog.xml index d8ba15bf902a..25fa94e12b6a 100644 --- a/app/src/main/res/layout/content_dax_dialog.xml +++ b/app/src/main/res/layout/content_dax_dialog.xml @@ -18,7 +18,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/dialogContainer" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> - + android:orientation="vertical"> - + + + - + + + android:layout_marginTop="10dp" /> - + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 017257dae5fb..9f722dad52c5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -49,5 +49,7 @@ + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 81f21c71b3ff..4981378d2fb7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -325,5 +325,21 @@ 9dp + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e17fb5f48918..901716551567 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -61,6 +61,8 @@ @color/white @color/grayishThree @color/charcoalGrey + @color/white + @color/white @color/grayish @color/almostBlack @@ -119,6 +121,8 @@ @color/almostBlack @color/brownishGrayTwo @color/white + @color/grayishBrown + @color/subtleGrayTwo @color/warmerGray @color/whiteFive @@ -165,4 +169,8 @@ ?attr/settingsSwitchBackgroundColor + +