From a5f983972061bc5fa93b30b2e012bf87796b7ccb Mon Sep 17 00:00:00 2001 From: cmonfortep Date: Wed, 19 Feb 2020 14:03:07 +0100 Subject: [PATCH 1/4] Dax dialog with two buttons (#713) * Daxdialog layout changes to simplify required design. Removing one layer of ConstraintLayout for one LinearLayout to simplify working with margins between buttons. * Avoid showing a dark statusbar when dialog is shown * Style created for each Dax dialog button. --- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 8 ++ .../app/browser/BrowserTabFragment.kt | 20 +++- .../app/browser/BrowserTabViewModel.kt | 5 + .../java/com/duckduckgo/app/cta/ui/Cta.kt | 32 +++-- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 6 + .../duckduckgo/app/global/view/DaxDialog.kt | 110 ++++++++++++------ .../main/res/layout/content_dax_dialog.xml | 88 +++++++------- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/styles.xml | 16 +++ app/src/main/res/values/themes.xml | 8 ++ 10 files changed, 199 insertions(+), 96 deletions(-) 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 + + From 3b0b83b2e6fb2509c4b36bd3e0ed40941cbfdc55 Mon Sep 17 00:00:00 2001 From: cmonfortep Date: Wed, 19 Feb 2020 15:03:13 +0100 Subject: [PATCH 2/4] New variant: Showing DefaultBrowserCta and SearchWidgetCta as Dax dialogs (#714) * Introducing new variants for second experiment. * Renamed some features making explicit to what part of the app they are related * Avoid allocating users before referrer logic has been executed. --- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 14 +++--- .../ui/OnboardingPageManagerTest.kt | 2 +- .../page/DefaultBrowserPageViewModelTest.kt | 10 ++-- .../app/statistics/VariantManagerTest.kt | 48 +++++++++++-------- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 4 +- .../app/global/DuckDuckGoApplication.kt | 2 +- .../java/com/duckduckgo/app/global/Theming.kt | 12 +---- .../onboarding/ui/OnboardingPageManager.kt | 6 +-- .../ui/page/DefaultBrowserPageViewModel.kt | 2 +- .../app/statistics/VariantManager.kt | 46 +++++++++++++----- 10 files changed, 82 insertions(+), 64 deletions(-) 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 a9fc912d8559..cc85b49fbefb 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 @@ -38,7 +38,7 @@ import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.VariantManager.VariantFeature.ConceptTest -import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressWidgetCta +import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressHomeTabWidgetCta import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import com.duckduckgo.app.survey.db.SurveyDao @@ -294,7 +294,7 @@ class CtaViewModelTest { @Test fun whenRefreshCtaOnHomeTabAndSuppressWidgetCtaFeatureThenDoNotShowWidgetAutoCta() = runBlockingTest { - setSuppressWidgetCtaFeature() + setSuppressHomeTabWidgetCtaFeature() whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) @@ -306,7 +306,7 @@ class CtaViewModelTest { @Test fun whenRefreshCtaOnHomeTabAndSuppressWidgetCtaFeatureActiveThenDoNotShowWidgetInstructionsCta() = runBlockingTest { - setSuppressWidgetCtaFeature() + setSuppressHomeTabWidgetCtaFeature() whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false) @@ -342,7 +342,7 @@ class CtaViewModelTest { @Test fun whenRefreshCtaOnHomeTabAndSuppressWidgetCtaFeatureActiveThenReturnNullWhenTryngToShowWidgetCta() = runBlockingTest { - setSuppressWidgetCtaFeature() + setSuppressHomeTabWidgetCtaFeature() whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) @@ -354,7 +354,7 @@ class CtaViewModelTest { @Test fun whenRefreshCtaOnHomeTabAndSuppressWidgetCtaFeatureActiveThenReturnNullWhenTryngToShowWidgetInstructionsCta() = runBlockingTest { - setSuppressWidgetCtaFeature() + setSuppressHomeTabWidgetCtaFeature() whenever(mockSettingsDataStore.hideTips).thenReturn(true) whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false) @@ -439,9 +439,9 @@ class CtaViewModelTest { ) } - private fun setSuppressWidgetCtaFeature() { + private fun setSuppressHomeTabWidgetCtaFeature() { whenever(mockVariantManager.getVariant()).thenReturn( - Variant("test", features = listOf(SuppressWidgetCta), filterBy = { true }) + Variant("test", features = listOf(SuppressHomeTabWidgetCta), filterBy = { true }) ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt index 01117c48251c..69485ff16ce8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManagerTest.kt @@ -78,7 +78,7 @@ class OnboardingPageManagerTest { whenever(mockVariantManager.getVariant()).thenReturn( Variant("test", features = listOf( VariantManager.VariantFeature.ConceptTest, - VariantManager.VariantFeature.SuppressDefaultBrowserContinueScreen + VariantManager.VariantFeature.SuppressOnboardingDefaultBrowserContinueScreen ), filterBy = { true }) ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModelTest.kt index 360e2b04f358..401b8ec7ca88 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModelTest.kt @@ -258,7 +258,7 @@ class DefaultBrowserPageViewModelTest { @Test fun whenUserSetDDGAsDefaultFromDialogAndSuppressContinueScreenVariantEnabledThenContinueToBrowserAndFirePixel() { - givenSuppressDefaultBrowserContinueScreen() + givenSuppressOnboardingDefaultBrowserContinueScreen() val params = mapOf( Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(), Pixel.PixelParameter.DEFAULT_BROWSER_SET_ORIGIN to DEFAULT_BROWSER_DIALOG @@ -359,7 +359,7 @@ class DefaultBrowserPageViewModelTest { Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to true.toString(), Pixel.PixelParameter.DEFAULT_BROWSER_SET_ORIGIN to Pixel.PixelValues.DEFAULT_BROWSER_EXTERNAL ) - givenSuppressDefaultBrowserContinueScreen() + givenSuppressOnboardingDefaultBrowserContinueScreen() testee.loadUI() testee.onDefaultBrowserClicked() whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(true) @@ -387,7 +387,7 @@ class DefaultBrowserPageViewModelTest { @Test fun whenUserWasTakenToSettingsAndSelectedDDGAsDefaultAndSuppressContinueScreenVariantEnabledThenContinueToBrowser() { - givenSuppressDefaultBrowserContinueScreen() + givenSuppressOnboardingDefaultBrowserContinueScreen() whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false) whenever(mockDefaultBrowserDetector.hasDefaultBrowser()).thenReturn(true) testee.loadUI() @@ -513,9 +513,9 @@ class DefaultBrowserPageViewModelTest { ) } - private fun givenSuppressDefaultBrowserContinueScreen() { + private fun givenSuppressOnboardingDefaultBrowserContinueScreen() { whenever(mockVariantManager.getVariant()).thenReturn( - Variant("test", features = listOf(VariantManager.VariantFeature.SuppressDefaultBrowserContinueScreen), filterBy = { true }) + Variant("test", features = listOf(VariantManager.VariantFeature.SuppressOnboardingDefaultBrowserContinueScreen), filterBy = { true }) ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt index 416531f357d4..077e0c7fa532 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt @@ -54,8 +54,8 @@ class VariantManagerTest { val variant = variants.firstOrNull { it.key == "md" } assertEqualsDouble(0.0, variant!!.weight) assertEquals(2, variant!!.features.size) - assertTrue(variant.hasFeature(SuppressWidgetCta)) - assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) + assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) } @Test @@ -64,8 +64,8 @@ class VariantManagerTest { assertEqualsDouble(0.0, variant!!.weight) assertEquals(3, variant!!.features.size) assertTrue(variant.hasFeature(ConceptTest)) - assertTrue(variant.hasFeature(SuppressWidgetCta)) - assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) + assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) } // CTA Validation experiments @@ -78,19 +78,19 @@ class VariantManagerTest { } @Test - fun ctaSuppressDefaultBrowserVariantIsInactiveAndHasSuppressDefaultBrowserFeature() { + fun ctaSuppressOnboardingDefaultBrowserVariantIsInactiveAndHasSuppressDefaultBrowserFeature() { val variant = variants.firstOrNull { it.key == "mr" } assertEqualsDouble(0.0, variant!!.weight) assertEquals(1, variant!!.features.size) - assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) } @Test - fun ctaSuppressWidgetVariantIsInactiveAndHasSuppressWidgetCtaFeature() { + fun ctaSuppressHomeWidgetVariantIsInactiveAndHasSuppressWidgetCtaFeature() { val variant = variants.firstOrNull { it.key == "ms" } assertEqualsDouble(0.0, variant!!.weight) assertEquals(1, variant!!.features.size) - assertTrue(variant.hasFeature(SuppressWidgetCta)) + assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) } @Test @@ -98,35 +98,41 @@ class VariantManagerTest { val variant = variants.firstOrNull { it.key == "mt" } assertEqualsDouble(0.0, variant!!.weight) assertEquals(2, variant!!.features.size) - assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) - assertTrue(variant.hasFeature(SuppressWidgetCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) + assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) } // CTA on Concept Test experiments @Test - fun insertCtaControlVariantIsActiveAndHasNoFeatures() { - val variant = variants.firstOrNull { it.key == "mu" } + fun insertCtaConceptControlVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { + val variant = variants.firstOrNull { it.key == "mj" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(0, variant.features.size) + assertEquals(3, variant!!.features.size) + assertTrue(variant.hasFeature(ConceptTest)) + assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) } @Test - fun insertCtaConceptTestVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "mv" } + fun insertCtaConceptTestWithAllCtaExperimentalVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { + val variant = variants.firstOrNull { it.key == "ml" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(3, variant!!.features.size) + assertEquals(2, variant!!.features.size) assertTrue(variant.hasFeature(ConceptTest)) - assertTrue(variant.hasFeature(SuppressWidgetCta)) - assertTrue(variant.hasFeature(SuppressDefaultBrowserCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserContinueScreen)) } @Test - fun insertCtaConceptTestWithAllCtaExperimentalVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "mz" } + fun insertCtaConceptTestWithCtasAsDaxDialogsExperimentalVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { + val variant = variants.firstOrNull { it.key == "mh" } assertEqualsDouble(1.0, variant!!.weight) - assertEquals(2, variant!!.features.size) + assertEquals(5, variant!!.features.size) assertTrue(variant.hasFeature(ConceptTest)) + assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) + assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) + assertTrue(variant.hasFeature(DefaultBrowserDaxCta)) + assertTrue(variant.hasFeature(SearchWidgetDaxCta)) } @Test 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 911a1eca29ea..0f7c58aad055 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 @@ -31,7 +31,7 @@ import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.VariantManager.VariantFeature.ConceptTest -import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressWidgetCta +import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressHomeTabWidgetCta import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.survey.db.SurveyDao import com.duckduckgo.app.survey.model.Survey @@ -172,7 +172,7 @@ class CtaViewModel @Inject constructor( return widgetCapabilities.supportsStandardWidgetAdd && !widgetCapabilities.hasInstalledWidgets && !dismissedCtaDao.exists(CtaId.ADD_WIDGET) && - !variant().hasFeature(SuppressWidgetCta) + !variant().hasFeature(SuppressHomeTabWidgetCta) } @WorkerThread diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt index 85633f7918cc..40cda41d353a 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt @@ -186,7 +186,7 @@ open class DuckDuckGoApplication : HasActivityInjector, HasServiceInjector, HasS } recordInstallationTimestamp() - initializeTheme(settingsDataStore, variantManager) + initializeTheme(settingsDataStore) loadTrackerData() configureDataDownloader() scheduleOfflinePixels() diff --git a/app/src/main/java/com/duckduckgo/app/global/Theming.kt b/app/src/main/java/com/duckduckgo/app/global/Theming.kt index 4d9eab2dc90a..9ce90e12f409 100644 --- a/app/src/main/java/com/duckduckgo/app/global/Theming.kt +++ b/app/src/main/java/com/duckduckgo/app/global/Theming.kt @@ -34,13 +34,9 @@ enum class DuckDuckGoTheme { object Theming { - fun initializeTheme(settingsDataStore: SettingsDataStore, variantManager: VariantManager) { + fun initializeTheme(settingsDataStore: SettingsDataStore) { if (settingsDataStore.theme == null) { - if (variantManager.getVariant().hasFeature(VariantManager.VariantFeature.ConceptTest)) { - settingsDataStore.theme = DuckDuckGoTheme.LIGHT - } else { - settingsDataStore.theme = DuckDuckGoTheme.DARK - } + settingsDataStore.theme = DuckDuckGoTheme.LIGHT } } @@ -61,10 +57,6 @@ fun DuckDuckGoActivity.applyTheme(): BroadcastReceiver? { return registerForThemeChangeBroadcast() } -fun DuckDuckGoActivity.appTheme(): DuckDuckGoTheme? { - return settingsDataStore.theme -} - private fun DuckDuckGoActivity.registerForThemeChangeBroadcast(): BroadcastReceiver { val manager = LocalBroadcastManager.getInstance(applicationContext) val receiver = object : BroadcastReceiver() { diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt index 100908875bf1..aac11937c9cd 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingPageManager.kt @@ -28,7 +28,7 @@ import com.duckduckgo.app.onboarding.ui.page.OnboardingPageFragment import com.duckduckgo.app.onboarding.ui.page.UnifiedSummaryPage import com.duckduckgo.app.onboarding.ui.page.WelcomePage import com.duckduckgo.app.statistics.VariantManager -import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressDefaultBrowserCta +import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressOnboardingDefaultBrowserCta interface OnboardingPageManager { fun pageCount(): Int @@ -85,13 +85,13 @@ class OnboardingPageManagerWithTrackerBlocking( private fun shouldShowDefaultBrowserPage(): Boolean { return defaultWebBrowserCapability.deviceSupportsDefaultBrowserConfiguration() - && !variantManager.getVariant().hasFeature(SuppressDefaultBrowserCta) + && !variantManager.getVariant().hasFeature(SuppressOnboardingDefaultBrowserCta) && !isDefaultBrowserAndSuppressedContinuationScreen() } private fun isDefaultBrowserAndSuppressedContinuationScreen(): Boolean { return defaultWebBrowserCapability.isDefaultBrowser() - && variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SuppressDefaultBrowserContinueScreen) + && variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SuppressOnboardingDefaultBrowserContinueScreen) } private fun isFinalPage(position: Int) = position == pageCount() - 1 diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModel.kt index 3406fa5c4940..86765d3c2e37 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/DefaultBrowserPageViewModel.kt @@ -146,7 +146,7 @@ class DefaultBrowserPageViewModel( } private fun shouldShowContinueScreen(): Boolean { - return !variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SuppressDefaultBrowserContinueScreen) + return !variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SuppressOnboardingDefaultBrowserContinueScreen) } private fun handleOriginInternalBrowser(): Boolean { diff --git a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt index 91f3b6fa9c96..3e9800cc6064 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt @@ -30,9 +30,11 @@ interface VariantManager { // variant-dependant features listed here sealed class VariantFeature { object ConceptTest : VariantFeature() - object SuppressWidgetCta : VariantFeature() - object SuppressDefaultBrowserCta : VariantFeature() - object SuppressDefaultBrowserContinueScreen : VariantFeature() + object SuppressHomeTabWidgetCta : VariantFeature() + object SuppressOnboardingDefaultBrowserCta : VariantFeature() + object SuppressOnboardingDefaultBrowserContinueScreen : VariantFeature() + object DefaultBrowserDaxCta : VariantFeature() + object SearchWidgetDaxCta : VariantFeature() } companion object { @@ -53,25 +55,43 @@ interface VariantManager { Variant( key = "me", weight = 0.0, - features = listOf(ConceptTest, SuppressWidgetCta, SuppressDefaultBrowserCta), + features = listOf(ConceptTest, SuppressHomeTabWidgetCta, SuppressOnboardingDefaultBrowserCta), + filterBy = { isEnglishLocale() }), + Variant( + key = "md", + weight = 0.0, + features = listOf(SuppressHomeTabWidgetCta, SuppressOnboardingDefaultBrowserCta), filterBy = { isEnglishLocale() }), - Variant(key = "md", weight = 0.0, features = listOf(SuppressWidgetCta, SuppressDefaultBrowserCta), filterBy = { isEnglishLocale() }), // Validate CTAs experiment Variant(key = "mq", weight = 0.0, features = emptyList(), filterBy = { noFilter() }), - Variant(key = "mr", weight = 0.0, features = listOf(SuppressDefaultBrowserCta), filterBy = { noFilter() }), - Variant(key = "ms", weight = 0.0, features = listOf(SuppressWidgetCta), filterBy = { noFilter() }), - Variant(key = "mt", weight = 0.0, features = listOf(SuppressWidgetCta, SuppressDefaultBrowserCta), filterBy = { noFilter() }), + Variant(key = "mr", weight = 0.0, features = listOf(SuppressOnboardingDefaultBrowserCta), filterBy = { noFilter() }), + Variant(key = "ms", weight = 0.0, features = listOf(SuppressHomeTabWidgetCta), filterBy = { noFilter() }), + Variant( + key = "mt", + weight = 0.0, + features = listOf(SuppressHomeTabWidgetCta, SuppressOnboardingDefaultBrowserCta), + filterBy = { noFilter() }), // Insert CTAs on Concept test experiment - Variant(key = "mu", weight = 1.0, features = emptyList(), filterBy = { isEnglishLocale() }), - Variant(key = "mv", weight = 1.0, - features = listOf(ConceptTest, SuppressWidgetCta, SuppressDefaultBrowserCta), + Variant(key = "mj", weight = 1.0, + features = listOf(ConceptTest, SuppressHomeTabWidgetCta, SuppressOnboardingDefaultBrowserCta), + filterBy = { isEnglishLocale() }), + Variant( + key = "ml", + weight = 1.0, + features = listOf(ConceptTest, SuppressOnboardingDefaultBrowserContinueScreen), filterBy = { isEnglishLocale() }), Variant( - key = "mz", + key = "mh", weight = 1.0, - features = listOf(ConceptTest, SuppressDefaultBrowserContinueScreen), + features = listOf( + ConceptTest, + SuppressHomeTabWidgetCta, + SuppressOnboardingDefaultBrowserCta, + DefaultBrowserDaxCta, + SearchWidgetDaxCta + ), filterBy = { isEnglishLocale() }) // All groups in an experiment (control and variants) MUST use the same filters From 77fe816340bfdf59404f54d747bb0bb7e367a4eb Mon Sep 17 00:00:00 2001 From: cmonfortep Date: Tue, 25 Feb 2020 21:53:50 +0100 Subject: [PATCH 3/4] Insert DefaultBrowser Cta and SearchWidget Cta as Dax dialog during Dax Journey (#716) * create defaultBrowser dax dialog * show defaultBrowser cta after TrackersBlockedCta * When dismiss a cta, rely on dimissed cta instead of current value in Livedata. * Separate implementation details of view highlighting from base Dax dialog class. Not all Dax dialogs use view highlighting. * Created functional dax dialogs with default browser cta and search widget cta * Send pixels when secondary button is clicked using secondaryPixelName from SecondaryButtonCta * navigation to settings extracted to a new class * Toast instructions extracted to a new class * Introducing new variants for second experiment. * Avoid allocating users before referrer logic has been executed. * Fence new dax dialogs ctas based on variantmanager * Track dbo=e only when user selects another browser as default --- .../app/browser/BrowserTabViewModelTest.kt | 170 +++++++++++--- .../app/cta/ui/CtaViewModelNextCtaTest.kt | 209 ++++++++++++++++++ .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 7 +- .../duckduckgo/app/cta/ui/DaxDialogCtaTest.kt | 82 +++++++ .../app/browser/BrowserTabFragment.kt | 64 ++++-- .../app/browser/BrowserTabViewModel.kt | 112 ++++++++-- .../DefaultBrowserNavigator.kt | 48 ++++ .../defaultbrowsing/TopInstructionsCard.kt | 34 +++ .../duckduckgo/app/cta/model/DismissedCta.kt | 2 + .../java/com/duckduckgo/app/cta/ui/Cta.kt | 126 ++++++++++- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 37 +++- .../duckduckgo/app/global/ViewModelFactory.kt | 2 + .../app/global/view/TypeAnimationTextView.kt | 1 - .../duckduckgo/app/statistics/pixels/Pixel.kt | 5 + .../main/res/layout/content_dax_dialog.xml | 2 +- ...ontent_onboarding_default_browser_card.xml | 4 +- .../main/res/layout/fragment_browser_tab.xml | 10 + .../main/res/values/string-untranslated.xml | 6 + 18 files changed, 849 insertions(+), 72 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt create mode 100644 app/src/androidTest/java/com/duckduckgo/app/cta/ui/DaxDialogCtaTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserNavigator.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/TopInstructionsCard.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index d260c40b4ed9..d818830af5cb 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -41,6 +41,7 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.Command.Navigate import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.DownloadFile import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.OpenInNewTab import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.favicon.FaviconDownloader import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest @@ -51,6 +52,7 @@ import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.DismissedCta 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.HomePanelCta import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore @@ -175,6 +177,9 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockPrivacySettingsStore: PrivacySettingsStore + @Mock + private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector + private lateinit var mockAutoCompleteApi: AutoCompleteApi private lateinit var ctaViewModel: CtaViewModel @@ -207,7 +212,8 @@ class BrowserTabViewModelTest { mockVariantManager, mockSettingsStore, mockOnboardingStore, - mockPrivacySettingsStore + mockPrivacySettingsStore, + mockDefaultBrowserDetector ) val siteFactory = SiteFactory(mockPrivacyPractices, mockEntityLookup) @@ -239,7 +245,9 @@ class BrowserTabViewModelTest { searchCountDao = mockSearchCountDao, pixel = mockPixel, variantManager = mockVariantManager, - dispatchers = coroutineRule.testDispatcherProvider + dispatchers = coroutineRule.testDispatcherProvider, + defaultBrowserDetector = mockDefaultBrowserDetector, + installStore = mockAppInstallStore ) testee.loadData("abc", null, false) @@ -1359,7 +1367,7 @@ class BrowserTabViewModelTest { @Test fun whenSurveyCtaDismissedAndNoOtherCtaPossibleCtaIsNull() { testee.onSurveyChanged(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) - testee.onUserDismissedCta() + testee.onUserDismissedCta(testee.ctaViewState.value!!.cta!!) assertNull(testee.ctaViewState.value!!.cta) } @@ -1370,7 +1378,7 @@ class BrowserTabViewModelTest { whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(false) testee.onSurveyChanged(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) - testee.onUserDismissedCta() + testee.onUserDismissedCta(testee.ctaViewState.value!!.cta!!) assertEquals(HomePanelCta.AddWidgetAuto, testee.ctaViewState.value!!.cta) } @@ -1465,75 +1473,171 @@ class BrowserTabViewModelTest { @Test fun whenUserClickedCtaButtonThenFirePixel() { val cta = DaxBubbleCta.DaxIntroCta(mockOnboardingStore, mockAppInstallStore) - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserClickCtaOkButton() + testee.onUserClickCtaOkButton(cta) verify(mockPixel).fire(cta.okPixel!!, cta.pixelOkParameters()) } @Test - fun whenUserClickedCtaButtonThenLaunchSurveyCommand() { + fun whenUserClickedSurveyCtaButtonThenLaunchSurveyCommand() { val cta = HomePanelCta.Survey(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserClickCtaOkButton() + testee.onUserClickCtaOkButton(cta) assertCommandIssued() } @Test - fun whenUserClickedCtaButtonThenLaunchAddWidgetCommand() { + fun whenUserClickedAddWidgetCtaButtonThenLaunchAddWidgetCommand() { val cta = HomePanelCta.AddWidgetAuto - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserClickCtaOkButton() + testee.onUserClickCtaOkButton(cta) assertCommandIssued() } @Test - fun whenUserClickedCtaButtonThenLaunchLegacyAddWidgetCommand() { + fun whenUserClickedLegacyAddWidgetCtaButtonThenLaunchLegacyAddWidgetCommand() { val cta = HomePanelCta.AddWidgetInstructions - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) + testee.onUserClickCtaOkButton(cta) + assertCommandIssued() + } - testee.onUserClickCtaOkButton() + @Test + fun whenUserClickedDefaultBrowserCtaButtonWithoutDefaultBrowserThenLaunchDialogCommand() { + whenever(mockDefaultBrowserDetector.hasDefaultBrowser()).thenReturn(false) + val cta = DaxDialogCta.DefaultBrowserCta(mockDefaultBrowserDetector, mock(), mock()) + testee.onUserClickCtaOkButton(cta) + assertCommandIssued() + } + + @Test + fun whenUserClickedDefaultBrowserCtaButtonWithDefaultBrowserThenOpenDefaultBrowserSettings() { + whenever(mockDefaultBrowserDetector.hasDefaultBrowser()).thenReturn(true) + val cta = DaxDialogCta.DefaultBrowserCta(mockDefaultBrowserDetector, mock(), mock()) + testee.onUserClickCtaOkButton(cta) + assertCommandIssued() + } + + @Test + fun whenUserClickedDaxSearchWidgetCtaButtonWithWidgetCapabilitiesThenLaunchAddWidget() { + whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) + val cta = DaxDialogCta.SearchWidgetCta(mockWidgetCapabilities, mock(), mock()) + testee.onUserClickCtaOkButton(cta) + assertCommandIssued() + } + + @Test + fun whenUserClickedDaxSearchWidgetCtaButtonWithoutWidgetCapabilitiesThenLaunchLegacyAddWidget() { + whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false) + val cta = DaxDialogCta.SearchWidgetCta(mockWidgetCapabilities, mock(), mock()) + testee.onUserClickCtaOkButton(cta) assertCommandIssued() } @Test fun whenUserDismissedCtaThenFirePixel() { val cta = HomePanelCta.Survey(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserDismissedCta() + testee.onUserDismissedCta(cta) verify(mockPixel).fire(cta.cancelPixel!!, cta.pixelCancelParameters()) } @Test fun whenUserDismissedCtaThenRegisterInDatabase() { val cta = HomePanelCta.AddWidgetAuto - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserDismissedCta() + testee.onUserDismissedCta(cta) verify(mockDismissedCtaDao).insert(DismissedCta(cta.ctaId)) } @Test fun whenUserDismissedSurveyCtaThenDoNotRegisterInDatabase() { val cta = HomePanelCta.Survey(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserDismissedCta() + testee.onUserDismissedCta(cta) verify(mockDismissedCtaDao, never()).insert(DismissedCta(cta.ctaId)) } @Test fun whenUserDismissedSurveyCtaThenCancelScheduledSurveys() { val cta = HomePanelCta.Survey(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) - testee.ctaViewState.value = BrowserTabViewModel.CtaViewState(cta = cta) - - testee.onUserDismissedCta() + testee.onUserDismissedCta(cta) verify(mockSurveyDao).cancelScheduledSurveys() } + @Test + fun whenUserFailedSettingDdgAsDefaultBrowserFromSettingsThenFirePixelAndUpdateInstallStore() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false) + + testee.onUserTriedToSetAsDefaultBrowserFromSettings() + + verify(mockAppInstallStore).defaultBrowser = false + verify(mockPixel).fire(Pixel.PixelName.DEFAULT_BROWSER_NOT_SET, defaultBrowserPixelParams(Pixel.PixelValues.DEFAULT_BROWSER_SETTINGS)) + } + + @Test + fun whenUserHasSetDdgAsDefaultBrowserFromSettingsThenFirePixelAndUpdateInstallStore() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(true) + + testee.onUserTriedToSetAsDefaultBrowserFromSettings() + + verify(mockAppInstallStore).defaultBrowser = true + verify(mockPixel).fire(Pixel.PixelName.DEFAULT_BROWSER_SET, defaultBrowserPixelParams(Pixel.PixelValues.DEFAULT_BROWSER_SETTINGS)) + } + + @Test + fun whenUserFailedSettingDdgAsDefaultBrowserFromDialogOnceThenOpenDefaultBrowserDialogAgain() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false) + + testee.onUserTriedToSetAsDefaultBrowserFromDialog() + + assertCommandIssued() + } + + @Test + fun whenUserFailedSettingDdgAsDefaultBrowserFromDialogTwiceThenFirePixelAndUpdateInstallStore() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false) + + testee.onUserTriedToSetAsDefaultBrowserFromDialog() + testee.onUserTriedToSetAsDefaultBrowserFromDialog() + + verify(mockAppInstallStore, times(2)).defaultBrowser = false + verify(mockPixel).fire( + Pixel.PixelName.DEFAULT_BROWSER_NOT_SET, + defaultBrowserPixelParams(Pixel.PixelValues.DEFAULT_BROWSER_JUST_ONCE_MAX) + ) + } + + @Test + fun whenUserHasSetDdgAsDefaultBrowserFromDialogThenFirePixelAndUpdateInstallStore() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(true) + + testee.onUserTriedToSetAsDefaultBrowserFromDialog() + + verify(mockAppInstallStore).defaultBrowser = true + verify(mockPixel).fire(Pixel.PixelName.DEFAULT_BROWSER_SET, defaultBrowserPixelParams(Pixel.PixelValues.DEFAULT_BROWSER_DIALOG)) + } + + @Test + fun whenUserDismissesDefaultBrowserDialogThenFirePixelAndUpdateInstallStore() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false) + + testee.onUserDismissedDefaultBrowserDialog() + + verify(mockAppInstallStore).defaultBrowser = false + verify(mockPixel).fire( + Pixel.PixelName.DEFAULT_BROWSER_NOT_SET, + defaultBrowserPixelParams(Pixel.PixelValues.DEFAULT_BROWSER_DIALOG_DISMISSED) + ) + } + + @Test + fun whenDefaultBrowserDialogDismissedAfterSelectingDefaultExternalBrowserAlwThenFirePixelAndUpdateInstallStore() { + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(false) + whenever(mockDefaultBrowserDetector.hasDefaultBrowser()).thenReturn(true) + + testee.onUserDismissedDefaultBrowserDialog() + + verify(mockAppInstallStore).defaultBrowser = false + verify(mockPixel).fire( + Pixel.PixelName.DEFAULT_BROWSER_NOT_SET, + defaultBrowserPixelParams(Pixel.PixelValues.DEFAULT_BROWSER_EXTERNAL) + ) + } + private inline fun assertCommandIssued(instanceAssertions: T.() -> Unit = {}) { verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) val issuedCommand = commandCaptor.allValues.find { it is T } @@ -1546,6 +1650,12 @@ class BrowserTabViewModelTest { Pixel.PixelParameter.BOOKMARK_CAPABLE to bookmarkCapable.toString() ) + private fun defaultBrowserPixelParams(origin: String) = mapOf( + Pixel.PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to false.toString(), + Pixel.PixelParameter.DEFAULT_BROWSER_SET_ORIGIN to origin + ) + + private fun givenExpectedCtaAddWidgetInstructions() { setBrowserShowing(false) whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt new file mode 100644 index 000000000000..3673bf68f5ea --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.cta.ui + +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector +import com.duckduckgo.app.cta.db.DismissedCtaDao +import com.duckduckgo.app.cta.model.CtaId +import com.duckduckgo.app.statistics.Variant +import com.duckduckgo.app.statistics.VariantManager +import com.duckduckgo.app.widget.ui.WidgetCapabilities +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue +import org.junit.experimental.theories.DataPoint +import org.junit.experimental.theories.Theories +import org.junit.experimental.theories.Theory +import org.junit.runner.RunWith + +@RunWith(Theories::class) +class CtaViewModelNextCtaTest { + + private val mockWidgetCapabilities = mock() + private val mockDismissedCtaDao = mock() + private val mockVariantManager = mock() + private val mockDefaultBrowserDetector = mock() + + private val testee = CtaViewModel( + mock(), + mock(), + mock(), + mockWidgetCapabilities, + mockDismissedCtaDao, + mockVariantManager, + mock(), + mock(), + mock(), + mockDefaultBrowserDetector + ) + + @Theory + fun whenPreviousCtaIsDaxSerpCtaButConditionsAreValidThenExpectedNextCtaIsSearchWidgetCta( + supportsStandardWidgetAdd: Boolean, + hasInstalledWidget: Boolean, + hasSearchWidgetDaxCtaFeature: Boolean, + daxSearchWidgetShown: Boolean + ) { + val previousCta = DaxDialogCta.DaxSerpCta(mock(), mock()) + assumeTrue(supportsStandardWidgetAdd) + assumeFalse(hasInstalledWidget) + assumeTrue(hasSearchWidgetDaxCtaFeature) + assumeFalse(daxSearchWidgetShown) + givenSearchWidgetScenario(supportsStandardWidgetAdd, hasInstalledWidget, hasSearchWidgetDaxCtaFeature, daxSearchWidgetShown) + + val nextCta = testee.obtainNextCta(previousCta) + + assertTrue(nextCta is DaxDialogCta.SearchWidgetCta) + } + + @Theory + fun whenPreviousCtaIsDaxSerpCtaButConditionsAreInvalidThenExpectedNextCtaIsNull( + supportsStandardWidgetAdd: Boolean, + hasInstalledWidget: Boolean, + hasSearchWidgetDaxCtaFeature: Boolean, + daxSearchWidgetShown: Boolean + ) { + val previousCta = DaxDialogCta.DaxSerpCta(mock(), mock()) + assumeFalse(supportsStandardWidgetAdd && !hasInstalledWidget && hasSearchWidgetDaxCtaFeature && !daxSearchWidgetShown) + givenSearchWidgetScenario(supportsStandardWidgetAdd, hasInstalledWidget, hasSearchWidgetDaxCtaFeature, daxSearchWidgetShown) + + val nextCta = testee.obtainNextCta(previousCta) + + assertNull(nextCta) + } + + @Theory + fun whenPreviousCtaIsDaxTrackersBlockedCtaAndConditionsAreValidThenExpectedNextCtaIsDefaultBrowserCta( + deviceSupportsDefaultBrowserConfiguration: Boolean, + isDefaultBrowser: Boolean, + hasDefaultBrowserDaxCtaFeature: Boolean, + daxDefaultBrowserShown: Boolean + ) { + val previousCta = DaxDialogCta.DaxTrackersBlockedCta(mock(), mock(), emptyList(), "") + assumeTrue(deviceSupportsDefaultBrowserConfiguration) + assumeFalse(isDefaultBrowser) + assumeTrue(hasDefaultBrowserDaxCtaFeature) + assumeFalse(daxDefaultBrowserShown) + givenDefaultBrowserScenario( + deviceSupportsDefaultBrowserConfiguration, + isDefaultBrowser, + hasDefaultBrowserDaxCtaFeature, + daxDefaultBrowserShown + ) + + val nextCta = testee.obtainNextCta(previousCta) + + assertTrue(nextCta is DaxDialogCta.DefaultBrowserCta) + } + + @Theory + fun whenPreviousCtaIsDaxTrackersBlockedCtaCtaButConditionsAreInvalidThenExpectedNextCtaIsNull( + deviceSupportsDefaultBrowserConfiguration: Boolean, + isDefaultBrowser: Boolean, + hasDefaultBrowserDaxCtaFeature: Boolean, + daxDefaultBrowserShown: Boolean + ) { + val previousCta = DaxDialogCta.DaxTrackersBlockedCta(mock(), mock(), emptyList(), "") + assumeFalse(deviceSupportsDefaultBrowserConfiguration && !isDefaultBrowser && hasDefaultBrowserDaxCtaFeature && !daxDefaultBrowserShown) + givenDefaultBrowserScenario( + deviceSupportsDefaultBrowserConfiguration, + isDefaultBrowser, + hasDefaultBrowserDaxCtaFeature, + daxDefaultBrowserShown + ) + + val nextCta = testee.obtainNextCta(previousCta) + + assertNull(nextCta) + } + + private fun givenDefaultBrowserScenario( + deviceSupportsDefaultBrowserConfiguration: Boolean, + isDefaultBrowser: Boolean, + hasDefaultBrowserDaxCtaFeature: Boolean, + daxDefaultBrowserShown: Boolean + ) { + whenever(mockDefaultBrowserDetector.deviceSupportsDefaultBrowserConfiguration()).thenReturn(deviceSupportsDefaultBrowserConfiguration) + whenever(mockDefaultBrowserDetector.isDefaultBrowser()).thenReturn(isDefaultBrowser) + whenever(mockVariantManager.getVariant()).thenReturn( + Variant("test", features = getFeatures(false, hasDefaultBrowserDaxCtaFeature), filterBy = { true }) + ) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_DEFAULT_BROWSER)).thenReturn(daxDefaultBrowserShown) + } + + private fun givenSearchWidgetScenario( + supportsStandardWidgetAdd: Boolean, + hasInstalledWidget: Boolean, + hasSearchWidgetDaxCtaFeature: Boolean, + daxSearchWidgetShown: Boolean + ) { + whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(supportsStandardWidgetAdd) + whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(hasInstalledWidget) + whenever(mockVariantManager.getVariant()).thenReturn( + Variant("test", features = getFeatures(hasSearchWidgetDaxCtaFeature, false), filterBy = { true }) + ) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SEARCH_WIDGET)).thenReturn(daxSearchWidgetShown) + } + + private fun getFeatures(searchWidgetFeature: Boolean, defaultBrowserFeature: Boolean): List { + val featureList = mutableListOf() + if (searchWidgetFeature) { + featureList.add(VariantManager.VariantFeature.SearchWidgetDaxCta) + } + if (defaultBrowserFeature) { + featureList.add(VariantManager.VariantFeature.DefaultBrowserDaxCta) + } + return featureList + } + + companion object { + @JvmStatic + @DataPoint + fun supportsStandardWidgetAdd() = listOf(true, false) + + @JvmStatic + @DataPoint + fun hasInstalledWidget() = listOf(true, false) + + @JvmStatic + @DataPoint + fun hasSearchWidgetDaxCtaFeature() = listOf(true, false) + + @JvmStatic + @DataPoint + fun daxSearchWidgetShown() = listOf(true, false) + + @JvmStatic + @DataPoint + fun deviceSupportsDefaultBrowserConfiguration() = listOf(true, false) + + @JvmStatic + @DataPoint + fun isDefaultBrowser() = listOf(true, false) + + @JvmStatic + @DataPoint + fun hasDefaultBrowserDaxCtaFeature() = listOf(true, false) + + @JvmStatic + @DataPoint + fun daxDefaultBrowserShown() = listOf(true, false) + } +} \ No newline at end of file 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 cc85b49fbefb..0af6ab2fa8d2 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 @@ -22,6 +22,7 @@ import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.InstantSchedulersRule +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta @@ -103,6 +104,9 @@ class CtaViewModelTest { @Mock private lateinit var mockPrivacySettingsStore: PrivacySettingsStore + @Mock + private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector + private lateinit var testee: CtaViewModel @Before @@ -126,7 +130,8 @@ class CtaViewModelTest { mockVariantManager, mockSettingsDataStore, mockOnboardingStore, - mockPrivacySettingsStore + mockPrivacySettingsStore, + mockDefaultBrowserDetector ) } diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/DaxDialogCtaTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/DaxDialogCtaTest.kt new file mode 100644 index 000000000000..da9a759df2a2 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/DaxDialogCtaTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.cta.ui + +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector +import com.duckduckgo.app.cta.ui.DaxDialogCta.DefaultBrowserCta.DefaultBrowserCtaBehavior +import com.duckduckgo.app.cta.ui.DaxDialogCta.SearchWidgetCta.SearchWidgetCtaBehavior +import com.duckduckgo.app.widget.ui.WidgetCapabilities +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import org.junit.Assert.assertEquals +import org.junit.Test + +class DaxDialogCtaTest { + + private val mockDefaultBrowserDetector: DefaultBrowserDetector = mock() + private val mockWidgetCapabilities: WidgetCapabilities = mock() + + @Test + fun whenUserHasDefaultBrowserThenDefaultBrowserCtaBehaviorIsSettings() { + whenever(mockDefaultBrowserDetector.hasDefaultBrowser()).thenReturn(true) + + val testee = DaxDialogCta.DefaultBrowserCta( + mockDefaultBrowserDetector, mock(), mock() + ) + + assertEquals(testee.primaryAction, DefaultBrowserCtaBehavior.Settings.action) + assertEquals(testee.ctaPixelParam, DefaultBrowserCtaBehavior.Settings.ctaPixelParam) + assertEquals(testee.okButton, DefaultBrowserCtaBehavior.Settings.primaryButtonStringRes) + } + + @Test + fun whenUserDoesNotHaveDefaultBrowserThenDefaultBrowserCtaBehaviorIsDialog() { + whenever(mockDefaultBrowserDetector.hasDefaultBrowser()).thenReturn(false) + + val testee = DaxDialogCta.DefaultBrowserCta( + mockDefaultBrowserDetector, mock(), mock() + ) + + assertEquals(testee.primaryAction, DefaultBrowserCtaBehavior.Dialog.action) + assertEquals(testee.ctaPixelParam, DefaultBrowserCtaBehavior.Dialog.ctaPixelParam) + assertEquals(testee.okButton, DefaultBrowserCtaBehavior.Dialog.primaryButtonStringRes) + } + + @Test + fun whenUserDeviceSupportsAutomaticWidgetAddThenSearchWidgetCtaBehaviorIsAuto() { + whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(true) + + val testee = DaxDialogCta.SearchWidgetCta( + mockWidgetCapabilities, mock(), mock() + ) + + assertEquals(testee.primaryAction, SearchWidgetCtaBehavior.Automatic.action) + assertEquals(testee.ctaPixelParam, SearchWidgetCtaBehavior.Automatic.ctaPixelParam) + } + + @Test + fun whenUserDeviceDoesNotSupportAutomaticWidgetAddThenSearchWidgetCtaBehaviorIsManual() { + whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false) + + val testee = DaxDialogCta.SearchWidgetCta( + mockWidgetCapabilities, mock(), mock() + ) + + assertEquals(testee.primaryAction, SearchWidgetCtaBehavior.Manual.action) + assertEquals(testee.ctaPixelParam, SearchWidgetCtaBehavior.Manual.ctaPixelParam) + } +} \ No newline at end of file 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 3fa2e2211f9c..2a2261462f85 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -39,6 +39,7 @@ import android.webkit.WebView.HitTestResult import android.webkit.WebView.HitTestResult.* import android.widget.EditText import android.widget.TextView +import android.widget.Toast import androidx.annotation.AnyThread import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog @@ -57,6 +58,8 @@ import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestio import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.browser.BrowserTabViewModel.* import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserNavigator +import com.duckduckgo.app.browser.defaultbrowsing.TopInstructionsCard import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager import com.duckduckgo.app.browser.downloader.FileDownloader import com.duckduckgo.app.browser.downloader.FileDownloader.PendingFileDownload @@ -72,15 +75,11 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment import com.duckduckgo.app.browser.useragent.UserAgentProvider -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.DaxBubbleCta -import com.duckduckgo.app.cta.ui.DaxDialogCta -import com.duckduckgo.app.cta.ui.SecondaryButtonCta +import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.view.* +import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPage import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.renderer.icon import com.duckduckgo.app.privacy.store.PrivacySettingsStore @@ -94,9 +93,9 @@ import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import com.duckduckgo.widget.SearchWidgetLight import com.google.android.material.snackbar.Snackbar import dagger.android.support.AndroidSupportInjection -import kotlinx.android.synthetic.main.include_dax_dialog_cta.* import kotlinx.android.synthetic.main.fragment_browser_tab.* import kotlinx.android.synthetic.main.include_cta_buttons.view.* +import kotlinx.android.synthetic.main.include_dax_dialog_cta.* import kotlinx.android.synthetic.main.include_find_in_page.* import kotlinx.android.synthetic.main.include_new_browser_tab.* import kotlinx.android.synthetic.main.include_omnibar_toolbar.* @@ -111,7 +110,6 @@ import javax.inject.Inject import kotlin.concurrent.thread import kotlin.coroutines.CoroutineContext - class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { private val supervisorJob = SupervisorJob() @@ -170,6 +168,9 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { @Inject lateinit var privacySettingsStore: PrivacySettingsStore + @Inject + lateinit var defaultBrowserNavigation: DefaultBrowserNavigator + val tabId get() = arguments!![TAB_ID_ARG] as String lateinit var userAgentProvider: UserAgentProvider @@ -218,6 +219,13 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { private var webView: WebView? = null + private var instructionsCard: Toast? = null + get() { + field?.cancel() + field = TopInstructionsCard(requireContext(), Toast.LENGTH_SHORT) + return field + } + private val errorSnackbar: Snackbar by lazy { Snackbar.make(browserLayout, R.string.crashedWebViewErrorMessage, Snackbar.LENGTH_INDEFINITE) .setBehavior(NonDismissibleBehavior()) @@ -539,9 +547,26 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { is Command.GenerateWebViewPreviewImage -> generateWebViewPreviewImage() is Command.LaunchTabSwitcher -> launchTabSwitcher() is Command.ShowErrorWithAction -> showErrorSnackbar(it) + is Command.OpenDefaultBrowserSettings -> openDefaultBrowserSettings() + is Command.OpenDefaultBrowserDialog -> openDefaultBrowserDialog(it.url) } } + private fun openDefaultBrowserDialog(url: String) { + instructionsCard?.show() + defaultCard?.show() + defaultCard?.animate()?.alpha(1f)?.setDuration(INSTRUCTIONS_CARD_ANIMATION_DURATION)?.start() + defaultBrowserNavigation.openDefaultBrowserDialog(this, url, DEFAULT_BROWSER_REQUEST_CODE_DIALOG) + } + + private fun hideInstructionsCard() { + defaultCard?.animate()?.alpha(0f)?.setDuration(INSTRUCTIONS_CARD_ANIMATION_DURATION)?.start() + } + + private fun openDefaultBrowserSettings() { + defaultBrowserNavigation.navigateToSettings(this, DEFAULT_BROWSER_REQUEST_CODE_SETTINGS) + } + private fun showErrorSnackbar(command: Command.ShowErrorWithAction) { //Snackbar is global and it should appear only the foreground fragment if (!errorSnackbar.view.isAttachedToWindow && isVisible) { @@ -631,6 +656,15 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_CODE_CHOOSE_FILE) { handleFileUploadResult(resultCode, data) + } else if (requestCode == DEFAULT_BROWSER_REQUEST_CODE_SETTINGS) { + viewModel.onUserTriedToSetAsDefaultBrowserFromSettings() + } else if (requestCode == DEFAULT_BROWSER_REQUEST_CODE_DIALOG) { + hideInstructionsCard() + if (resultCode == DefaultBrowserPage.DEFAULT_BROWSER_RESULT_CODE_DIALOG_INTERNAL) { + viewModel.onUserTriedToSetAsDefaultBrowserFromDialog() + } else { + viewModel.onUserDismissedDefaultBrowserDialog() + } } } @@ -1109,7 +1143,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { } companion object { - + private const val DEFAULT_BROWSER_REQUEST_CODE_DIALOG = 191 + private const val DEFAULT_BROWSER_REQUEST_CODE_SETTINGS = 190 private const val TAB_ID_ARG = "TAB_ID_ARG" private const val URL_EXTRA_ARG = "URL_EXTRA_ARG" private const val SKIP_HOME_ARG = "SKIP_HOME_ARG" @@ -1129,6 +1164,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { private const val MAX_PROGRESS = 100 private const val TRACKERS_INI_DELAY = 500L private const val TRACKERS_SECONDARY_DELAY = 200L + private const val INSTRUCTIONS_CARD_ANIMATION_DURATION: Long = 100 fun newInstance(tabId: String, query: String? = null, skipHome: Boolean): BrowserTabFragment { val fragment = BrowserTabFragment() @@ -1391,13 +1427,13 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { if (configuration is DaxDialogCta.DaxTrackersBlockedCta) { animatorHelper.finishTrackerAnimation(omnibarViews(), container) } - viewModel.onUserDismissedCta() + viewModel.onUserDismissedCta(configuration) } setPrimaryCtaClickListener { - viewModel.onUserClickCtaOkButton() + viewModel.onUserClickCtaOkButton(configuration) if (configuration is DaxDialogCta.DaxMainNetworkCta) { setPrimaryCtaClickListener { - viewModel.onUserClickCtaOkButton() + viewModel.onUserClickCtaOkButton(configuration) getDaxDialog().dismiss() } configuration.setSecondDialog(this, activity) @@ -1469,11 +1505,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { configuration.showCta(ctaContainer) ctaContainer.ctaOkButton.setOnClickListener { - viewModel.onUserClickCtaOkButton() + viewModel.onUserClickCtaOkButton(configuration) } ctaContainer.ctaDismissButton.setOnClickListener { - viewModel.onUserDismissedCta() + viewModel.onUserDismissedCta(cta) } ConstraintSet().also { 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 d9dfe115cb26..fa4a5b0b7135 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -29,7 +29,10 @@ import android.webkit.WebView import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting import androidx.core.net.toUri -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.duckduckgo.app.autocomplete.api.AutoCompleteApi import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteResult import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.AutoCompleteSuggestion @@ -45,6 +48,7 @@ import com.duckduckgo.app.browser.LongPressHandler.RequiredAction import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.IntentType import com.duckduckgo.app.browser.WebNavigationStateChange.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.browser.favicon.FaviconDownloader import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest @@ -52,11 +56,9 @@ import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener -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.cta.ui.* import com.duckduckgo.app.global.* +import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.model.domainMatchesUrl @@ -100,6 +102,8 @@ class BrowserTabViewModel( private val ctaViewModel: CtaViewModel, private val searchCountDao: SearchCountDao, private val pixel: Pixel, + private val installStore: AppInstallStore, + private val defaultBrowserDetector: DefaultBrowserDetector, private val variantManager: VariantManager, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() ) : WebViewClientListener, EditBookmarkListener, HttpAuthenticationListener, ViewModel() { @@ -193,6 +197,8 @@ class BrowserTabViewModel( object GenerateWebViewPreviewImage : Command() object LaunchTabSwitcher : Command() class ShowErrorWithAction(val action: () -> Unit) : Command() + class OpenDefaultBrowserDialog(val url: String = DEFAULT_URL) : Command() + object OpenDefaultBrowserSettings : Command() } val autoCompleteViewState: MutableLiveData = MutableLiveData() @@ -220,6 +226,7 @@ class BrowserTabViewModel( private var site: Site? = null private lateinit var tabId: String private var webNavigationState: WebNavigationState? = null + private var defaultBrowserAttempt: Int = 1 init { initializeViewStates() @@ -898,29 +905,86 @@ class BrowserTabViewModel( ctaViewModel.registerDaxBubbleCtaDismissed(cta) } - fun onUserClickCtaOkButton() { - val cta = ctaViewState.value?.cta ?: return + fun onUserClickCtaOkButton(cta: Cta) { ctaViewModel.onUserClickCtaOkButton(cta) + viewModelScope.launch { + withContext(dispatchers.io()) { + ctaViewModel.obtainNextCta(previousCta = cta) + }?.let { ctaViewState.value = currentCtaViewState().copy(cta = it) } + produceNewCommand(cta) + } + } + + fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) { + ctaViewModel.onUserClickCtaSecondaryButton(cta) + } + + fun onUserDismissedCta(dismissedCta: Cta) { + ctaViewModel.onUserDismissedCta(dismissedCta) + if (dismissedCta is HomePanelCta) { + refreshCta() + } else { + ctaViewState.value = currentCtaViewState().copy(cta = null) + } + } + + private fun produceNewCommand(cta: Cta) { command.value = when (cta) { is HomePanelCta.Survey -> LaunchSurvey(cta.survey) is HomePanelCta.AddWidgetAuto -> LaunchAddWidget is HomePanelCta.AddWidgetInstructions -> LaunchLegacyAddWidget + is DaxDialogCta.DefaultBrowserCta -> cta.primaryAction.mapToCommand() + is DaxDialogCta.SearchWidgetCta -> cta.primaryAction.mapToCommand() else -> return } } - fun onUserClickCtaSecondaryButton(cta: SecondaryButtonCta) { - ctaViewModel.onUserClickCtaSecondaryButton(cta) + fun onUserTriedToSetAsDefaultBrowserFromSettings() { + val isDefaultBrowser = defaultBrowserDetector.isDefaultBrowser() + installStore.defaultBrowser = isDefaultBrowser + firePixelDefaultBrowserCtaUserAction(isDefaultBrowser, origin = Pixel.PixelValues.DEFAULT_BROWSER_SETTINGS) } - fun onUserDismissedCta() { - val cta = ctaViewState.value?.cta ?: return + fun onUserTriedToSetAsDefaultBrowserFromDialog() { + val isDefaultBrowser = defaultBrowserDetector.isDefaultBrowser() + installStore.defaultBrowser = isDefaultBrowser + if (defaultBrowserDetector.isDefaultBrowser()) { + defaultBrowserAttempt = 1 + firePixelDefaultBrowserCtaUserAction(true, origin = Pixel.PixelValues.DEFAULT_BROWSER_DIALOG) + } else { + if (defaultBrowserAttempt < MAX_DIALOG_ATTEMPTS) { + defaultBrowserAttempt++ + command.value = OpenDefaultBrowserDialog() + } else { + firePixelDefaultBrowserCtaUserAction(false, origin = Pixel.PixelValues.DEFAULT_BROWSER_JUST_ONCE_MAX) + } + } + } - ctaViewModel.onUserDismissedCta(cta) - if (cta is HomePanelCta) { - refreshCta() + fun onUserDismissedDefaultBrowserDialog() { + val isDefaultBrowser = defaultBrowserDetector.isDefaultBrowser() + val hasDefaultBrowser = defaultBrowserDetector.hasDefaultBrowser() + + installStore.defaultBrowser = isDefaultBrowser + + val origin = if (!isDefaultBrowser && hasDefaultBrowser) { + Pixel.PixelValues.DEFAULT_BROWSER_EXTERNAL } else { - ctaViewState.value = currentCtaViewState().copy(cta = null) + Pixel.PixelValues.DEFAULT_BROWSER_DIALOG_DISMISSED + } + firePixelDefaultBrowserCtaUserAction(isDefaultBrowser, origin = origin) + } + + private fun firePixelDefaultBrowserCtaUserAction(isDefaultBrowser: Boolean, origin: String) { + val params = mapOf( + PixelParameter.DEFAULT_BROWSER_SET_FROM_ONBOARDING to false.toString(), + PixelParameter.DEFAULT_BROWSER_SET_ORIGIN to origin + ) + + if (isDefaultBrowser) { + pixel.fire(PixelName.DEFAULT_BROWSER_SET, params) + } else { + pixel.fire(PixelName.DEFAULT_BROWSER_NOT_SET, params) } } @@ -993,7 +1057,23 @@ class BrowserTabViewModel( command.value = OpenInNewTab(query) } + private fun DaxDialogCta.DefaultBrowserCta.DefaultBrowserAction.mapToCommand(): Command { + return when (this) { + is DaxDialogCta.DefaultBrowserCta.DefaultBrowserAction.ShowSettings -> OpenDefaultBrowserSettings + is DaxDialogCta.DefaultBrowserCta.DefaultBrowserAction.ShowSystemDialog -> OpenDefaultBrowserDialog() + } + } + + private fun DaxDialogCta.SearchWidgetCta.SearchWidgetAction.mapToCommand(): Command { + return when (this) { + is DaxDialogCta.SearchWidgetCta.SearchWidgetAction.AddAutomatic -> LaunchAddWidget + is DaxDialogCta.SearchWidgetCta.SearchWidgetAction.AddManually -> LaunchLegacyAddWidget + } + } + companion object { private const val FIXED_PROGRESS = 50 + private const val MAX_DIALOG_ATTEMPTS = 2 + private const val DEFAULT_URL = "https://duckduckgo.com" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserNavigator.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserNavigator.kt new file mode 100644 index 000000000000..35cda4887cf2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserNavigator.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.defaultbrowsing + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Build +import androidx.fragment.app.Fragment +import com.duckduckgo.app.browser.BrowserActivity +import com.duckduckgo.app.browser.R +import timber.log.Timber +import javax.inject.Inject + +class DefaultBrowserNavigator @Inject constructor() { + + fun navigateToSettings(fragment: Fragment, requestCode: Int = 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val intent = DefaultBrowserSystemSettings.intent() + try { + fragment.startActivityForResult(intent, requestCode) + } catch (e: ActivityNotFoundException) { + Timber.w(e, fragment.getString(R.string.cannotLaunchDefaultAppSettings)) + } + } + } + + fun openDefaultBrowserDialog(fragment: Fragment, url: String, requestCode: Int = 0) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.putExtra(BrowserActivity.LAUNCH_FROM_DEFAULT_BROWSER_DIALOG, true) + fragment.startActivityForResult(intent, requestCode) + } + +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/TopInstructionsCard.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/TopInstructionsCard.kt new file mode 100644 index 000000000000..c56c6e27fe67 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/TopInstructionsCard.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.defaultbrowsing + +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.widget.Toast +import com.duckduckgo.app.browser.R + +class TopInstructionsCard(context: Context, duration: Int = LENGTH_LONG) : Toast(context) { + + init { + val inflater = LayoutInflater.from(context) + val inflatedView = inflater.inflate(R.layout.content_onboarding_default_browser_card, null) + view = inflatedView + setGravity(Gravity.TOP or Gravity.FILL_HORIZONTAL, 0, 0) + this.duration = duration + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt index 537b9f0b8316..72d1a83b48d8 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/model/DismissedCta.kt @@ -26,6 +26,8 @@ enum class CtaId { ADD_WIDGET, DAX_INTRO, DAX_DIALOG_SERP, + DAX_DIALOG_DEFAULT_BROWSER, + DAX_DIALOG_SEARCH_WIDGET, DAX_DIALOG_TRACKERS_FOUND, DAX_DIALOG_NETWORK, DAX_DIALOG_OTHER, 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 0b569d965d0f..227262791aad 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 @@ -24,20 +24,17 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.fragment.app.FragmentActivity import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.ui.DaxCta.Companion.MAX_DAYS_ALLOWED 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 +import com.duckduckgo.app.global.view.* import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.trackerdetection.model.TrackingEvent +import com.duckduckgo.app.widget.ui.WidgetCapabilities 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.* @@ -111,6 +108,123 @@ sealed class DaxDialogCta( appInstallStore ) + class DefaultBrowserCta( + defaultBrowserDetector: DefaultBrowserDetector, + override val onboardingStore: OnboardingStore, + override val appInstallStore: AppInstallStore, + private val defaultBrowserCtaBehavior: DefaultBrowserCtaBehavior = getDefaultBrowserCtaBehavior(defaultBrowserDetector) + ) : DaxDialogCta( + ctaId = CtaId.DAX_DIALOG_DEFAULT_BROWSER, + description = R.string.daxDefaultBrowserCtaText, + okButton = defaultBrowserCtaBehavior.primaryButtonStringRes, + shownPixel = Pixel.PixelName.ONBOARDING_DAX_CTA_SHOWN, + okPixel = Pixel.PixelName.ONBOARDING_DAX_CTA_OK_BUTTON, + cancelPixel = null, + ctaPixelParam = defaultBrowserCtaBehavior.ctaPixelParam, + onboardingStore = onboardingStore, + appInstallStore = appInstallStore + ), SecondaryButtonCta { + + override val secondaryButtonPixel: Pixel.PixelName = Pixel.PixelName.ONBOARDING_DAX_CTA_CANCEL_BUTTON + + val primaryAction: DefaultBrowserAction + get() = defaultBrowserCtaBehavior.action + + override fun createCta(activity: FragmentActivity): DaxDialog { + return TypewriterDaxDialog( + daxText = getDaxText(activity), + primaryButtonText = activity.resources.getString(okButton), + secondaryButtonText = activity.resources.getString(R.string.daxDialogMaybeLater), + toolbarDimmed = true + ) + } + + override fun pixelSecondaryButtonParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) + + sealed class DefaultBrowserAction { + object ShowSettings : DefaultBrowserAction() + object ShowSystemDialog : DefaultBrowserAction() + } + + sealed class DefaultBrowserCtaBehavior(val primaryButtonStringRes: Int, val ctaPixelParam: String, val action: DefaultBrowserAction) { + object Settings : DefaultBrowserCtaBehavior( + primaryButtonStringRes = R.string.daxDialogSettings, + ctaPixelParam = Pixel.PixelValues.DAX_DEFAULT_BROWSER_CTA_SETTINGS, + action = DefaultBrowserAction.ShowSettings + ) + + object Dialog : DefaultBrowserCtaBehavior( + primaryButtonStringRes = R.string.daxDialogYes, + ctaPixelParam = Pixel.PixelValues.DAX_DEFAULT_BROWSER_CTA_DIALOG, + action = DefaultBrowserAction.ShowSystemDialog + ) + } + + companion object { + private fun getDefaultBrowserCtaBehavior(defaultBrowserDetector: DefaultBrowserDetector): DefaultBrowserCtaBehavior { + return if (defaultBrowserDetector.hasDefaultBrowser()) { + DefaultBrowserCtaBehavior.Settings + } else { + DefaultBrowserCtaBehavior.Dialog + } + } + } + } + + class SearchWidgetCta( + widgetCapabilities: WidgetCapabilities, + override val onboardingStore: OnboardingStore, + override val appInstallStore: AppInstallStore, + private val searchWidgetCtaBehavior: SearchWidgetCtaBehavior = getSearchWidgetBehavior(widgetCapabilities) + ) : DaxDialogCta( + ctaId = CtaId.DAX_DIALOG_SEARCH_WIDGET, + description = R.string.daxSearchWidgetCtaText, + okButton = R.string.daxDialogAddWidget, + shownPixel = Pixel.PixelName.ONBOARDING_DAX_CTA_SHOWN, + okPixel = Pixel.PixelName.ONBOARDING_DAX_CTA_OK_BUTTON, + cancelPixel = null, + ctaPixelParam = searchWidgetCtaBehavior.ctaPixelParam, + onboardingStore = onboardingStore, + appInstallStore = appInstallStore + ), SecondaryButtonCta { + + override val secondaryButtonPixel: Pixel.PixelName = Pixel.PixelName.ONBOARDING_DAX_CTA_CANCEL_BUTTON + + val primaryAction: SearchWidgetAction + get() = searchWidgetCtaBehavior.action + + override fun createCta(activity: FragmentActivity): DaxDialog { + return TypewriterDaxDialog( + daxText = getDaxText(activity), + primaryButtonText = activity.resources.getString(okButton), + secondaryButtonText = activity.resources.getString(R.string.daxDialogMaybeLater), + toolbarDimmed = true + ) + } + + override fun pixelSecondaryButtonParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) + + sealed class SearchWidgetAction { + object AddAutomatic : SearchWidgetAction() + object AddManually : SearchWidgetAction() + } + + sealed class SearchWidgetCtaBehavior(val ctaPixelParam: String, val action: SearchWidgetAction) { + object Automatic : SearchWidgetCtaBehavior(Pixel.PixelValues.DAX_SEARCH_WIDGET_CTA_AUTO, SearchWidgetAction.AddAutomatic) + object Manual : SearchWidgetCtaBehavior(Pixel.PixelValues.DAX_SEARCH_WIDGET_CTA_MANUAL, SearchWidgetAction.AddManually) + } + + companion object { + private fun getSearchWidgetBehavior(widgetCapabilities: WidgetCapabilities): SearchWidgetCtaBehavior { + return if (widgetCapabilities.supportsAutomaticWidgetAdd) { + SearchWidgetCtaBehavior.Automatic + } else { + SearchWidgetCtaBehavior.Manual + } + } + } + } + class DaxTrackersBlockedCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, 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 0f7c58aad055..48f4d11fe4a5 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 @@ -18,6 +18,7 @@ package com.duckduckgo.app.cta.ui import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData +import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta @@ -53,7 +54,8 @@ class CtaViewModel @Inject constructor( private val variantManager: VariantManager, private val settingsDataStore: SettingsDataStore, private val onboardingStore: OnboardingStore, - private val settingsPrivacySettingsStore: PrivacySettingsStore + private val settingsPrivacySettingsStore: PrivacySettingsStore, + private val defaultBrowserDetector: DefaultBrowserDetector ) { val surveyLiveData: LiveData = surveyDao.getLiveScheduled() @@ -222,6 +224,35 @@ class CtaViewModel @Inject constructor( } } + @WorkerThread + fun obtainNextCta(previousCta: Cta): Cta? { + return when { + previousCta is DaxDialogCta.DaxTrackersBlockedCta && canShowDefaultBrowserDaxCta() -> { + DaxDialogCta.DefaultBrowserCta(defaultBrowserDetector, onboardingStore, appInstallStore) + } + previousCta is DaxDialogCta.DaxSerpCta && canShowWidgetDaxCta() -> { + DaxDialogCta.SearchWidgetCta(widgetCapabilities, onboardingStore, appInstallStore) + } + else -> null + } + } + + @WorkerThread + private fun canShowDefaultBrowserDaxCta(): Boolean { + return defaultBrowserDetector.deviceSupportsDefaultBrowserConfiguration() && + !defaultBrowserDetector.isDefaultBrowser() && + variantManager.getVariant().hasFeature(VariantManager.VariantFeature.DefaultBrowserDaxCta) && + !daxDefaultBrowserShown() + } + + @WorkerThread + private fun canShowWidgetDaxCta(): Boolean { + return widgetCapabilities.supportsStandardWidgetAdd && + !widgetCapabilities.hasInstalledWidgets && + variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SearchWidgetDaxCta) && + !daxSearchWidgetShown() + } + private fun hasTrackersInformation(events: List): Boolean = events.asSequence() .filter { it.entity?.isMajor == true } @@ -247,5 +278,9 @@ class CtaViewModel @Inject constructor( private fun daxDialogNetworkShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_NETWORK) + private fun daxDefaultBrowserShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_DEFAULT_BROWSER) + + private fun daxSearchWidgetShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_SEARCH_WIDGET) + private fun isSerpUrl(url: String): Boolean = url.contains(DaxDialogCta.SERP) } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index 0b43cbc7c090..39e1bc359b75 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -183,6 +183,8 @@ class ViewModelFactory @Inject constructor( addToHomeCapabilityDetector = addToHomeCapabilityDetector, ctaViewModel = ctaViewModel, searchCountDao = searchCountDao, + installStore = appInstallStore, + defaultBrowserDetector = defaultBrowserDetector, pixel = pixel, variantManager = variantManager ) diff --git a/app/src/main/java/com/duckduckgo/app/global/view/TypeAnimationTextView.kt b/app/src/main/java/com/duckduckgo/app/global/view/TypeAnimationTextView.kt index aded40cdd637..acf9ca2e05bd 100644 --- a/app/src/main/java/com/duckduckgo/app/global/view/TypeAnimationTextView.kt +++ b/app/src/main/java/com/duckduckgo/app/global/view/TypeAnimationTextView.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.global.view import android.content.Context import android.util.AttributeSet -import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index e9520dc14e71..13977e179d07 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -62,6 +62,7 @@ interface Pixel { ONBOARDING_DAX_CTA_SHOWN("m_odc_s"), ONBOARDING_DAX_ALL_CTA_HIDDEN("m_odc_h"), ONBOARDING_DAX_CTA_OK_BUTTON("m_odc_ok"), + ONBOARDING_DAX_CTA_CANCEL_BUTTON("m_odc_c"), PRIVACY_DASHBOARD_OPENED("mp"), PRIVACY_DASHBOARD_SCORECARD("mp_c"), @@ -173,6 +174,10 @@ interface Pixel { const val DAX_INITIAL_CTA = "i" const val DAX_END_CTA = "e" const val DAX_SERP_CTA = "s" + const val DAX_DEFAULT_BROWSER_CTA_DIALOG = "dbd" + const val DAX_DEFAULT_BROWSER_CTA_SETTINGS = "dbs" + const val DAX_SEARCH_WIDGET_CTA_AUTO = "wa" + const val DAX_SEARCH_WIDGET_CTA_MANUAL = "wm" const val DAX_NETWORK_CTA_1 = "n" const val DAX_NETWORK_CTA_2 = "n2" const val DAX_TRACKERS_BLOCKED_CTA = "t" diff --git a/app/src/main/res/layout/content_dax_dialog.xml b/app/src/main/res/layout/content_dax_dialog.xml index 25fa94e12b6a..38a39cf567a5 100644 --- a/app/src/main/res/layout/content_dax_dialog.xml +++ b/app/src/main/res/layout/content_dax_dialog.xml @@ -110,7 +110,7 @@ + +
I\'ll block trackers so they can\'t spy on you. I\'ll also upgrade the security of your connection if possible. 🔒]]>
Remember: every time you browse with me a creepy ad loses its wings. 👍]]>
Your DuckDuckGo searches are anonymous and I never store your search history. Ever. 🙌 +
Want tracker protection when you open links from other apps?

Make DuckDuckGo Your Default Browser!]]>
+
Search privately even faster! Add the DuckDuckGo search widget to your home screen.]]>

Want to get fancy? Try clearing your data by hitting the fire button.]]>

%s runs a major tracking network.

A low privacy grade will tip you off to sites like this.]]>

%s is owned by %s, which runs a major tracking network.]]>
@@ -41,6 +43,10 @@ HIDE Phew! + OK! + Add Widget + Maybe Later + Go to settings Next High Five! Got It From 5baeadc45afceef0b1479129072988b2314fa4af Mon Sep 17 00:00:00 2001 From: cmonfortep Date: Thu, 27 Feb 2020 17:43:48 +0100 Subject: [PATCH 4/4] New control group and changes on when to show search widget cta (#723) * Moving as control group variant of concept test + CTAs * Removing from experiment concept test as control group * Show search widget cta only if at least one nonSerp Cta has been shown --- .../app/cta/ui/CtaViewModelNextCtaTest.kt | 59 +++++++++++++++++-- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 47 +++++++++++++-- .../app/statistics/VariantManagerTest.kt | 58 +++++++++--------- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 24 ++++++-- .../app/statistics/VariantManager.kt | 5 +- 5 files changed, 144 insertions(+), 49 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt index 3673bf68f5ea..3fa1cb5b9ce7 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelNextCtaTest.kt @@ -59,14 +59,26 @@ class CtaViewModelNextCtaTest { supportsStandardWidgetAdd: Boolean, hasInstalledWidget: Boolean, hasSearchWidgetDaxCtaFeature: Boolean, - daxSearchWidgetShown: Boolean + daxSearchWidgetShown: Boolean, + daxNetworkDialogShown: Boolean, + daxTrackersDialogShown: Boolean, + daxOtherDialogShown: Boolean ) { val previousCta = DaxDialogCta.DaxSerpCta(mock(), mock()) assumeTrue(supportsStandardWidgetAdd) assumeFalse(hasInstalledWidget) assumeTrue(hasSearchWidgetDaxCtaFeature) assumeFalse(daxSearchWidgetShown) - givenSearchWidgetScenario(supportsStandardWidgetAdd, hasInstalledWidget, hasSearchWidgetDaxCtaFeature, daxSearchWidgetShown) + assumeTrue(daxNetworkDialogShown || daxTrackersDialogShown || daxOtherDialogShown) + givenSearchWidgetScenario( + supportsStandardWidgetAdd, + hasInstalledWidget, + hasSearchWidgetDaxCtaFeature, + daxSearchWidgetShown, + daxNetworkDialogShown, + daxTrackersDialogShown, + daxOtherDialogShown + ) val nextCta = testee.obtainNextCta(previousCta) @@ -78,11 +90,28 @@ class CtaViewModelNextCtaTest { supportsStandardWidgetAdd: Boolean, hasInstalledWidget: Boolean, hasSearchWidgetDaxCtaFeature: Boolean, - daxSearchWidgetShown: Boolean + daxSearchWidgetShown: Boolean, + daxNetworkDialogShown: Boolean, + daxTrackersDialogShown: Boolean, + daxOtherDialogShown: Boolean ) { val previousCta = DaxDialogCta.DaxSerpCta(mock(), mock()) - assumeFalse(supportsStandardWidgetAdd && !hasInstalledWidget && hasSearchWidgetDaxCtaFeature && !daxSearchWidgetShown) - givenSearchWidgetScenario(supportsStandardWidgetAdd, hasInstalledWidget, hasSearchWidgetDaxCtaFeature, daxSearchWidgetShown) + assumeFalse( + supportsStandardWidgetAdd && + !hasInstalledWidget && + hasSearchWidgetDaxCtaFeature && + !daxSearchWidgetShown && + (daxNetworkDialogShown || daxTrackersDialogShown || daxOtherDialogShown) + ) + givenSearchWidgetScenario( + supportsStandardWidgetAdd, + hasInstalledWidget, + hasSearchWidgetDaxCtaFeature, + daxSearchWidgetShown, + daxNetworkDialogShown, + daxTrackersDialogShown, + daxOtherDialogShown + ) val nextCta = testee.obtainNextCta(previousCta) @@ -152,7 +181,10 @@ class CtaViewModelNextCtaTest { supportsStandardWidgetAdd: Boolean, hasInstalledWidget: Boolean, hasSearchWidgetDaxCtaFeature: Boolean, - daxSearchWidgetShown: Boolean + daxSearchWidgetShown: Boolean, + daxNetworkDialogShown: Boolean, + daxTrackersDialogShown: Boolean, + daxOtherDialogShown: Boolean ) { whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(supportsStandardWidgetAdd) whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(hasInstalledWidget) @@ -160,6 +192,9 @@ class CtaViewModelNextCtaTest { Variant("test", features = getFeatures(hasSearchWidgetDaxCtaFeature, false), filterBy = { true }) ) whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SEARCH_WIDGET)).thenReturn(daxSearchWidgetShown) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_NETWORK)).thenReturn(daxNetworkDialogShown) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_TRACKERS_FOUND)).thenReturn(daxTrackersDialogShown) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_OTHER)).thenReturn(daxOtherDialogShown) } private fun getFeatures(searchWidgetFeature: Boolean, defaultBrowserFeature: Boolean): List { @@ -205,5 +240,17 @@ class CtaViewModelNextCtaTest { @JvmStatic @DataPoint fun daxDefaultBrowserShown() = listOf(true, false) + + @JvmStatic + @DataPoint + fun daxNetworkDialogShown() = listOf(true, false) + + @JvmStatic + @DataPoint + fun daxTrackersDialogShown() = listOf(true, false) + + @JvmStatic + @DataPoint + fun daxOtherDialogShown() = listOf(true, false) } } \ No newline at end of file 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 0af6ab2fa8d2..ade8fa532096 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 @@ -38,8 +38,7 @@ import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager -import com.duckduckgo.app.statistics.VariantManager.VariantFeature.ConceptTest -import com.duckduckgo.app.statistics.VariantManager.VariantFeature.SuppressHomeTabWidgetCta +import com.duckduckgo.app.statistics.VariantManager.VariantFeature.* import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import com.duckduckgo.app.survey.db.SurveyDao @@ -398,7 +397,7 @@ class CtaViewModelTest { } @Test - fun whenRefreshCtaOnWhileBrowsingAndConceptTestFeatureActiveThenReturnSerpCta() = runBlockingTest { + fun whenRefreshCtaOnSerpWhileBrowsingAndConceptTestFeatureActiveThenReturnSerpCta() = runBlockingTest { setConceptTestFeature() val site = site(url = "http://www.duckduckgo.com") val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) @@ -406,6 +405,36 @@ class CtaViewModelTest { assertTrue(value is DaxDialogCta.DaxSerpCta) } + @Test + fun whenRefreshCtaOnSerpWhereSerpAndAnyNonSerpCtaShownAndWidgetCtaActiveThenReturnWidgetCta() = runBlockingTest { + setConceptTestFeature(SearchWidgetDaxCta) + givenSerpCtaShown() + givenAtLeastOneNonSerpCtaShown() + whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) + whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(false) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SEARCH_WIDGET)).thenReturn(false) + val site = site(url = "http://www.duckduckgo.com") + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) + + assertTrue(value is DaxDialogCta.SearchWidgetCta) + } + + @Test + fun whenRefreshCtaOnSerpSiteWhereSerpCtaShownAndWidgetCtaShownAndConceptTestActiveThenReturnNull() = runBlockingTest { + setConceptTestFeature(SearchWidgetDaxCta) + givenSerpCtaShown() + givenAtLeastOneNonSerpCtaShown() + whenever(mockWidgetCapabilities.supportsStandardWidgetAdd).thenReturn(true) + whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(false) + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SEARCH_WIDGET)).thenReturn(true) + val site = site(url = "http://www.duckduckgo.com") + + val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) + + assertNull(value) + } + @Test fun whenRefreshCtaWhileBrowsingAndConceptFeatureActiveTestThenReturnNoSerpCta() = runBlockingTest { setConceptTestFeature() @@ -438,9 +467,9 @@ class CtaViewModelTest { whenever(mockVariantManager.getVariant()).thenReturn(Variant("test", features = emptyList(), filterBy = { true })) } - private fun setConceptTestFeature() { + private fun setConceptTestFeature(vararg features: VariantManager.VariantFeature) { whenever(mockVariantManager.getVariant()).thenReturn( - Variant("test", features = listOf(ConceptTest), filterBy = { true }) + Variant("test", features = listOf(ConceptTest) + features, filterBy = { true }) ) } @@ -450,6 +479,14 @@ class CtaViewModelTest { ) } + private fun givenSerpCtaShown() { + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP)).thenReturn(true) + } + + private fun givenAtLeastOneNonSerpCtaShown() { + whenever(mockDismissedCtaDao.exists(CtaId.DAX_DIALOG_TRACKERS_FOUND)).thenReturn(true) + } + private fun site( url: String = "http://www.test.com", uri: Uri? = Uri.parse(url), diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt index 41f15c76d4cf..1b78f0762523 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt @@ -16,27 +16,31 @@ package com.duckduckgo.app.statistics +import com.duckduckgo.app.statistics.VariantManager.Companion.DEFAULT_VARIANT +import com.duckduckgo.app.statistics.VariantManager.Companion.RESERVED_EU_AUCTION_VARIANT import com.duckduckgo.app.statistics.VariantManager.VariantFeature.* import org.junit.Assert.* import org.junit.Test class VariantManagerTest { - private val variants = VariantManager.ACTIVE_VARIANTS + private val variants = VariantManager.ACTIVE_VARIANTS + + variantWithKey(RESERVED_EU_AUCTION_VARIANT) + + DEFAULT_VARIANT // SERP Experiment(s) @Test fun serpControlVariantIsInactiveAndHasNoFeatures() { - val variant = variants.firstOrNull { it.key == "sc" } - assertEqualsDouble(0.0, variant!!.weight) + val variant = variants.first { it.key == "sc" } + assertEqualsDouble(0.0, variant.weight) assertEquals(0, variant.features.size) } @Test fun serpExperimentalVariantIsInactiveAndHasNoFeatures() { - val variant = variants.firstOrNull { it.key == "se" } - assertEqualsDouble(0.0, variant!!.weight) + val variant = variants.first { it.key == "se" } + assertEqualsDouble(0.0, variant.weight) assertEquals(0, variant.features.size) } @@ -44,15 +48,15 @@ class VariantManagerTest { @Test fun conceptTestControlVariantIsInactiveAndHasNoFeatures() { - val variant = variants.firstOrNull { it.key == "mc" } - assertEqualsDouble(0.0, variant!!.weight) + val variant = variants.first { it.key == "mc" } + assertEqualsDouble(0.0, variant.weight) assertEquals(0, variant.features.size) } @Test fun conceptTestNoCtaVariantIsInactiveAndHasSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "md" } - assertEqualsDouble(0.0, variant!!.weight) + val variant = variants.first { it.key == "md" } + assertEqualsDouble(0.0, variant.weight) assertEquals(2, variant.features.size) assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) @@ -60,8 +64,8 @@ class VariantManagerTest { @Test fun conceptTestExperimentalVariantIsInactiveAndHasConceptTestAndSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "me" } - assertEqualsDouble(0.0, variant!!.weight) + val variant = variants.first { it.key == "me" } + assertEqualsDouble(0.0, variant.weight) assertEquals(3, variant.features.size) assertTrue(variant.hasFeature(ConceptTest)) assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) @@ -71,29 +75,19 @@ class VariantManagerTest { // CTA on Concept Test experiments @Test - fun insertCtaConceptControlVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "mj" } - assertEqualsDouble(1.0, variant!!.weight) - assertEquals(3, variant.features.size) - assertTrue(variant.hasFeature(ConceptTest)) - assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) - assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) - } - - @Test - fun insertCtaConceptTestWithAllCtaExperimentalVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "ml" } - assertEqualsDouble(1.0, variant!!.weight) + fun insertCtaConceptTestControlVariantIsActiveAndHasConceptTestAndHasExpectedFeatures() { + val variant = variants.first { it.key == "mj" } + assertEqualsDouble(1.0, variant.weight) assertEquals(2, variant.features.size) assertTrue(variant.hasFeature(ConceptTest)) assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserContinueScreen)) } @Test - fun insertCtaConceptTestWithCtasAsDaxDialogsExperimentalVariantIsActiveAndHasConceptTestAndSuppressCtaFeatures() { - val variant = variants.firstOrNull { it.key == "mh" } - assertEqualsDouble(1.0, variant!!.weight) - assertEquals(5, variant!!.features.size) + fun insertCtaConceptTestWithCtasAsDaxDialogsExperimentalVariantIsActiveAndHasExpectedFeatures() { + val variant = variants.first { it.key == "mh" } + assertEqualsDouble(1.0, variant.weight) + assertEquals(5, variant.features.size) assertTrue(variant.hasFeature(ConceptTest)) assertTrue(variant.hasFeature(SuppressHomeTabWidgetCta)) assertTrue(variant.hasFeature(SuppressOnboardingDefaultBrowserCta)) @@ -111,7 +105,6 @@ class VariantManagerTest { } } - @Suppress("SameParameterValue") private fun assertEqualsDouble(expected: Double, actual: Double) { val comparison = expected.compareTo(actual) @@ -119,4 +112,9 @@ class VariantManagerTest { fail("Doubles are not equal. Expected $expected but was $actual") } } -} \ No newline at end of file + + @Suppress("SameParameterValue") + private fun variantWithKey(key: String): Variant { + return DEFAULT_VARIANT.copy(key = key) + } +} 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 48f4d11fe4a5..152f609dc48d 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 @@ -209,10 +209,14 @@ class CtaViewModel @Inject constructor( } } } - // Is Serp - if (!daxDialogSerpShown() && isSerpUrl(it.url)) { - return DaxDialogCta.DaxSerpCta(onboardingStore, appInstallStore) + + if (isSerpUrl(it.url)) { + val ctaOnSerp = getCtaOnSerp() + if (ctaOnSerp != null) { + return ctaOnSerp + } } + // Trackers blocked return if (!daxDialogTrackersFoundShown() && !isSerpUrl(it.url) && hasTrackersInformation(it.trackingEvents) && host != null) { DaxDialogCta.DaxTrackersBlockedCta(onboardingStore, appInstallStore, it.trackingEvents, host) @@ -237,6 +241,15 @@ class CtaViewModel @Inject constructor( } } + @WorkerThread + private fun getCtaOnSerp(): DaxDialogCta? { + return when { + !daxDialogSerpShown() -> DaxDialogCta.DaxSerpCta(onboardingStore, appInstallStore) + canShowWidgetDaxCta() -> DaxDialogCta.SearchWidgetCta(widgetCapabilities, onboardingStore, appInstallStore) + else -> null + } + } + @WorkerThread private fun canShowDefaultBrowserDaxCta(): Boolean { return defaultBrowserDetector.deviceSupportsDefaultBrowserConfiguration() && @@ -250,7 +263,8 @@ class CtaViewModel @Inject constructor( return widgetCapabilities.supportsStandardWidgetAdd && !widgetCapabilities.hasInstalledWidgets && variantManager.getVariant().hasFeature(VariantManager.VariantFeature.SearchWidgetDaxCta) && - !daxSearchWidgetShown() + !daxSearchWidgetShown() && + daxNonSerpDialogShown() } private fun hasTrackersInformation(events: List): Boolean = @@ -278,6 +292,8 @@ class CtaViewModel @Inject constructor( private fun daxDialogNetworkShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_NETWORK) + private fun daxNonSerpDialogShown(): Boolean = daxDialogNetworkShown() || daxDialogTrackersFoundShown() || daxDialogOtherShown() + private fun daxDefaultBrowserShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_DEFAULT_BROWSER) private fun daxSearchWidgetShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_SEARCH_WIDGET) diff --git a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt index ef1e15cfef79..7d3b38739278 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt @@ -64,11 +64,8 @@ interface VariantManager { filterBy = { isEnglishLocale() }), // Insert CTAs on Concept test experiment - Variant(key = "mj", weight = 1.0, - features = listOf(ConceptTest, SuppressHomeTabWidgetCta, SuppressOnboardingDefaultBrowserCta), - filterBy = { isEnglishLocale() }), Variant( - key = "ml", + key = "mj", weight = 1.0, features = listOf(ConceptTest, SuppressOnboardingDefaultBrowserContinueScreen), filterBy = { isEnglishLocale() }),