Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ class CtaViewModelTest {
verify(mockPixel).fire(eq(SURVEY_CTA_LAUNCHED), any())
}

@Test
fun whenCtaSecondaryButonClickedPixelIsFired() {
val secondaryButtonCta = mock<SecondaryButtonCta>()
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)))
Expand Down
20 changes: 14 additions & 6 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.*
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1388,7 +1390,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope {
daxDialog?.dismiss()
daxDialog = configuration.createCta(activity).apply {
setHideClickListener {
dismiss()
getDaxDialog().dismiss()
launchHideTipsDialog(activity, configuration)
}
setDismissListener {
Expand All @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -903,6 +904,10 @@ class BrowserTabViewModel(
}
}

fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) {
ctaViewModel.onUserClickCtaSecondaryButton(cta)
}

fun onUserDismissedCta() {
val cta = ctaViewState.value?.cta ?: return

Expand Down
32 changes: 22 additions & 10 deletions app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -71,6 +71,12 @@ interface Cta {
fun pixelOkParameters(): Map<String, String?>
}

interface SecondaryButtonCta {
val secondaryButtonPixel: Pixel.PixelName?

fun pixelSecondaryButtonParameters(): Map<String, String?>
}

sealed class DaxDialogCta(
override val ctaId: CtaId,
@AnyRes open val description: Int,
Expand All @@ -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<String, String?> = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam)

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<View>(R.id.privacyGradeButton)
onAnimationFinishedListener {
if (isFromSameNetworkDomain()) {
Expand All @@ -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()
}
Expand All @@ -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<View>(R.id.fire)
onAnimationFinishedListener {
startHighlightViewAnimation(fireButton)
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ class CtaViewModel @Inject constructor(
}
}

fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) {
cta.secondaryButtonPixel?.let {
pixel.fire(it, cta.pixelSecondaryButtonParameters())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this ok being launched in the main thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiBasedPixel:Pixel will excecute it in a background thread. So we are good here.

}
}

suspend fun refreshCta(dispatcher: CoroutineContext, isBrowserShowing: Boolean, site: Site? = null): Cta? {
surveyCta()?.let {
return it
Expand Down
110 changes: 77 additions & 33 deletions app/src/main/java/com/duckduckgo/app/global/view/DaxDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onAnimationFinishedListener is specific to the typing animation and is triggered when it finishes. I would suggest to move it to another interface or make it specific to the TypewriteDaxDialog class but this would make DaxDialogHighlightView a bit messier.
The other option is to assume the DaxDialog always is going to have the typing animation (which it does for now) and maybe rename TypewriteDaxDialog to something more generic? Having TypewriteDaxDialog makes me think there is another version without the typing animation when in reality there isn't.
I don't think is massively important to stop the PR but it feels a very specific name for something that's always used like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, onAnimationFinishedListener can be moved into an interface and DaxDialogHighlightViewwill not be affected. The only part that will be affected is where we create those dialogs. But I liked that you commented on that. I think we can delay that change till we introduce a different Dax dialog, that is also the reason why I didn't segregate that interface into different ones.
As per the name, I'm fine changing the name or using this concrete one (because the interface takes the generic name), so I'm open to suggestions here. Tbh, I could come up with something else.

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 = { }

Expand All @@ -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
Expand All @@ -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() {
Expand All @@ -129,6 +145,13 @@ class DaxDialog(
primaryCtaClickListener()
}

secondaryCtaClickListener?.let {
secondaryCta.setOnClickListener {
dialogText.cancelAnimation()
it()
}
}

if (dismissible) {
dialogContainer.setOnClickListener {
dialogText.cancelAnimation()
Expand All @@ -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)

Expand All @@ -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
}
}
Expand Down
Loading