8000 Fire button education (#916) · chmodawk/Android@04410e8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 04410e8

Browse files
authored
Fire button education (duckduckgo#916)
* New FireDialog with Cta replaces old FireDialog * FireDialog communicates with CtaViewModel to make cta appear * Pulse animation * Show PulseAnimation logic implemented. * Emit flow from database * Add experemient variants enabled * show/hide fire button animation if variant active * On Fire education experiment, fire cta is included as required cta to complete onboarding * Notify when fire dialog shows/hides to avoid showing a browser CTA when Visible * Avoid hiding browser with clearingInProgressView. - We want to show browser in the background while fire animation - FireDialog will be non cancellable when clearing data process runs to avoid undesired user actions * Include new fire animation and add lottie view to FireDialog xml * Fire dialog runs fire animation as part of clearing data process. - No activity restart till both (animation and clearing data process) finish - If animation is cancelled, then clearing data process unique step before app restart * show animation full screen * speed up animation if clearing data process finished * extract logic to set all parents FitsSystemWindows value into View extension * Update speed up flag based on FireDialogEvent * extract into listener logic to accelerate animation when clearing data process finishes * create extension fun to set all parent clipChildren and clipToPadding * Use latest Lottie version * Rising flame animation with background * Force setRenderMode to Software and enableMergePath on LottieView * pause animation if app goes to background * init Lottie cache based on Activity lifecycle to avoid conflicts between variant allocation and installation referrer. * Avoid showing white navigation bar while restarting app
1 parent 624e316 commit 04410e8

29 files changed

+813
-134
lines changed

app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ ext {
120120
retrofit = "2.8.1"
121121
ankoVersion = "0.10.4"
122122
glide = "4.11.0"
123-
lottieVersion = "2.6.0-beta19"
123+
lottieVersion = "3.4.0"
124124
okHttp = "3.14.7"
125125
rxJava = "2.1.10"
126126
rxAndroid = "2.0.2"
@@ -174,6 +174,7 @@ dependencies {
174174
// ViewModel and LiveData
175175
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle"
176176
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle"
177+
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle"
177178

178179
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle"
179180
testImplementation "androidx.arch.core:core-testing:$coreTesting"

app/src/androidTest/java/com/duckduckgo/app/CoroutineTestRule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCorout
4848
}
4949

5050
@ExperimentalCoroutinesApi
51-
fun CoroutineTestRule.runBlocking(block: suspend () -> Unit) = this.testDispatcher.runBlockingTest {
51+
fun CoroutineTestRule.runBlocking(block: suspend TestCoroutineScope.() -> Unit) = this.testDispatcher.runBlockingTest {
5252
block()
5353
}

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.duckduckgo.app.bookmarks.db.BookmarkEntity
3939
import com.duckduckgo.app.bookmarks.db.BookmarksDao
4040
import com.duckduckgo.app.browser.BrowserTabViewModel.Command
4141
import com.duckduckgo.app.browser.BrowserTabViewModel.Command.Navigate
42+
import com.duckduckgo.app.browser.BrowserTabViewModel.FireButton
4243
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.DownloadFile
4344
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.OpenInNewTab
4445
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
@@ -105,6 +106,7 @@ import com.nhaarman.mockitokotlin2.*
105106
import io.reactivex.Observable
106107
import io.reactivex.Single
107108
import kotlinx.coroutines.ExperimentalCoroutinesApi
109+
import kotlinx.coroutines.flow.emptyFlow
108110
import kotlinx.coroutines.runBlocking
109111
import kotlinx.coroutines.test.runBlockingTest
110112
import org.junit.After
@@ -250,6 +252,8 @@ class BrowserTabViewModelTest {
250252

251253
mockAutoCompleteApi = AutoCompleteApi(mockAutoCompleteService, mockBookmarksDao)
252254

255+
whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(emptyFlow())
256+
253257
ctaViewModel = CtaViewModel(
254258
mockAppInstallStore,
255259
mockPixel,
@@ -796,31 +800,31 @@ class BrowserTabViewModelTest {
796800

797801
@Test
798802
fun whenInitialisedThenFireButtonIsShown() {
799-
assertTrue(browserViewState().showFireButton)
803+
assertTrue(browserViewState().fireButton is FireButton.Visible)
800804
}
801805

802806
@Test
803807
fun whenOmnibarInputDoesNotHaveFocusAndHasQueryThenFireButtonIsShown() {
804808
testee.onOmnibarInputStateChanged("query", false, hasQueryChanged = false)
805-
assertTrue(browserViewState().showFireButton)
809+
assertTrue(browserViewState().fireButton is FireButton.Visible)
806810
}
807811

808812
@Test
809813
fun whenOmnibarInputDoesNotHaveFocusOrQueryThenFireButtonIsShown() {
810814
testee.onOmnibarInputStateChanged("", false, hasQueryChanged = false)
811-
assertTrue(browserViewState().showFireButton)
815+
assertTrue(browserViewState().fireButton is FireButton.Visible)
812816
}
813817

814818
@Test
815819
fun whenOmnibarInputHasFocusAndNoQueryThenFireButtonIsShown() {
816820
testee.onOmnibarInputStateChanged("", true, hasQueryChanged = false)
817-
assertTrue(browserViewState().showFireButton)
821+
assertTrue(browserViewState().fireButton is FireButton.Visible)
818822
}
819823

820824
@Test
821825
fun whenOmnibarInputHasFocusAndQueryThenFireButtonIsHidden() {
822826
testee.onOmnibarInputStateChanged("query", true, hasQueryChanged = false)
823-
assertFalse(browserViewState().showFireButton)
827+
assertTrue(browserViewState().fireButton is FireButton.Gone)
824828
}
825829

826830
@Test

app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt

Lines changed: 186 additions & 4 deletions
< EA94 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ import com.duckduckgo.app.InstantSchedulersRule
2525
import com.duckduckgo.app.cta.db.DismissedCtaDao
2626
import com.duckduckgo.app.cta.model.CtaId
2727
import com.duckduckgo.app.cta.model.DismissedCta
28-
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
2928
import com.duckduckgo.app.global.db.AppDatabase
30-
import com.duckduckgo.app.global.install.AppInstallStore
31-
import com.duckduckgo.app.global.model.Site
3229
import com.duckduckgo.app.global.events.db.UserEventEntity
33-
import com.duckduckgo.app.global.events.db.UserEventsStore
3430
import com.duckduckgo.app.global.events.db.UserEventKey
31+
import com.duckduckgo.app.global.events.db.UserEventsStore
32+
import com.duckduckgo.app.global.install.AppInstallStore
33+
import com.duckduckgo.app.global.model.Site
3534
import com.duckduckgo.app.global.useourapp.UseOurAppDetector
35+
import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
3636
import com.duckduckgo.app.onboarding.store.AppStage
3737
import com.duckduckgo.app.onboarding.store.OnboardingStore
3838
import com.duckduckgo.app.onboarding.store.UserStageStore
@@ -43,7 +43,9 @@ import com.duckduckgo.app.privacy.model.PrivacyPractices
4343
import com.duckduckgo.app.privacy.model.TestEntity
4444
import com.duckduckgo.app.runBlocking
4545
import com.duckduckgo.app.settings.db.SettingsDataStore
46+
import com.duckduckgo.app.statistics.Variant
4647
import com.duckduckgo.app.statistics.VariantManager
48+
import com.duckduckgo.app.statistics.VariantManager.Companion.DEFAULT_VARIANT
4749
import com.duckduckgo.app.statistics.pixels.Pixel
4850
import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.*
4951
import com.duckduckgo.app.survey.db.SurveyDao
@@ -54,6 +56,11 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent
5456
import com.duckduckgo.app.widget.ui.WidgetCapabilities
5557
import com.nhaarman.mockitokotlin2.*
5658
import kotlinx.coroutines.ExperimentalCoroutinesApi
59+
import kotlinx.coroutines.channels.Channel
60+
import kotlinx.coroutines.flow.collect
61+
import kotlinx.coroutines.flow.consumeAsFlow
62+
import kotlinx.coroutines.flow.first
63+
import kotlinx.coroutines.launch
5764
import kotlinx.coroutines.test.runBlockingTest
5865
import org.junit.After
5966
import org.junit.Assert.*
@@ -114,6 +121,8 @@ class CtaViewModelTest {
114121
@Mock
115122
private lateinit var mockUserEventsStore: UserEventsStore
116123

124+
private val dismissedCtaDaoChannel = Channel<List<DismissedCta>>()
125+
117126
private val requiredDaxOnboardingCtas: List<CtaId> = listOf(
118127
CtaId.DAX_INTRO,
119128
CtaId.DAX_DIALOG_SERP,
@@ -122,6 +131,15 @@ class CtaViewModelTest {
122131
CtaId.DAX_END
123132
)
124133

134+
private val requiredFireEducationDaxOnboardingCtas: List<CtaId> = listOf(
135+
CtaId.DAX_INTRO,
136+
CtaId.DAX_DIALOG_SERP,
137+
CtaId.DAX_DIALOG_TRACKERS_FOUND,
138+
CtaId.DAX_DIALOG_NETWORK,
139+
CtaId.DAX_FIRE_BUTTON,
140+
CtaId.DAX_END
141+
)
142+
125143
private lateinit var testee: CtaViewModel
126144

127145
@Before
@@ -135,6 +153,8 @@ class CtaViewModelTest {
135153

136154
whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1))
137155
whenever(mockUserWhitelistDao.contains(any())).thenReturn(false)
156+
whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow())
157+
givenControlGroup()
138158

139159
testee = CtaViewModel(
140160
mockAppInstallStore,
@@ -155,6 +175,7 @@ class CtaViewModelTest {
155175

156176
@After
157177
fun after() {
178+
dismissedCtaDaoChannel.close()
158179
db.close()
159180
}
160181

@@ -231,6 +252,33 @@ class CtaViewModelTest {
231252
verify(mockUserStageStore).stageCompleted(AppStage.DAX_ONBOARDING)
232253
}
233254

255+
@Test
256+
fun whenFireEducationEnabledCtaDismissedAndUserHasPendingOnboardingCtasThenStageNotCompleted() = coroutineRule.runBlocking {
257+
givenFireButtonEducationActive()
258+
givenOnboardingActive()
259+
givenShownDaxOnboardingCtas(emptyList())
260+
testee.onUserDismissedCta(DaxBubbleCta.DaxEndCta(mockOnboardingStore, mockAppInstallStore))
261+
verify(mockUserStageStore, times(0)).stageCompleted(any())
262+
}
263+
264+
@Test
265+
fun whenFireEducationEnabledAndCtaDismissedAndAllDaxOnboardingCtasShownThenStageNotCompleted() = coroutineRule.runBlocking {
266+
givenFireButtonEducationActive()
267+
givenOnboardingActive()
268+
givenShownDaxOnboardingCtas(requiredDaxOnboardingCtas)
269+
testee.onUserDismissedCta(DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore))
270+
verify(mockUserStageStore, times(0)).stageCompleted(any())
271+
}
272+
273+
@Test
274+
fun whenFireEducationEnabledAndCtaDismissedAndAllFireEducationDaxOnboardingCtasShownThenStageCompleted() = coroutineRule.runBlocking {
275+
givenFireButtonEducationActive()
276+
givenOnboardingActive()
277+
givenShownDaxOnboardingCtas(requiredFireEducationDaxOnboardingCtas)
278+
testee.onUserDismissedCta(DaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore))
279+
verify(mockUserStageStore).stageCompleted(AppStage.DAX_ONBOARDING)
280+
}
281+
234282
@Test
235283
fun whenHideTipsForeverThenPixelIsFired() = coroutineRule.runBlocking {
236284
testee.hideTipsForever(HomePanelCta.AddWidgetAuto)
@@ -513,6 +561,125 @@ class CtaViewModelTest {
513561
verify(mockUserStageStore).stageCompleted(AppStage.USE_OUR_APP_ONBOARDING)
514562
}
515563

564+
@Test
565+
fun whenUserHidAllTipsThenFireButtonAnimationShouldNotShow() = coroutineRule.runBlocking {
566+
givenFireButtonEducationActive()
567+
whenever(mockSettingsDataStore.hideTips).thenReturn(true)
568+
launch {
569+
dismissedCtaDaoChannel.send(emptyList())
570+
}
571+
572+
assertFalse(testee.showFireButtonPulseAnimation.first())
573+
}
574+
575+
@Test
576+
fun whenUserHasAlreadySeenFireButtonCtaThenFireButtonAnimationShouldNotShow() = coroutineRule.runBlocking {
577+
givenFireButtonEducationActive()
578+
whenever(mockDismissedCtaDao.exists(CtaId.DAX_FIRE_BUTTON)).thenReturn(true)
579+
launch {
580+
dismissedCtaDaoChannel.send(emptyList())
581+
}
582+
583+
assertFalse(testee.showFireButtonPulseAnimation.first())
584+
}
585+
586+
@Test
587+
fun whenTipsAndFireOnboardingActiveAndUserSeesAnyTriggerFirePulseAnimationCtaThenFireButtonAnimationShouldShow() = coroutineRule.runBlocking {
588+
givenFireButtonEducationActive()
589+
givenOnboardingActive()
590+
val willTriggerFirePulseAnimationCtas = listOf(CtaId.DAX_DIALOG_TRACKERS_FOUND, CtaId.DAX_DIALOG_NETWORK, CtaId.DAX_DIALOG_OTHER)
591+
592+
val launch = launch {
593+
testee.showFireButtonPulseAnimation.collect {
594+
assertTrue(it)
595+
}
596+
}
597+
willTriggerFirePulseAnimationCtas.forEach {
598+
dismissedCtaDaoChannel.send(listOf(DismissedCta(it)))
599+
}
600+
601+
launch.cancel()
602+
}
603+
604+
@Test
605+
fun whenTipsAndFireOnboardingActiveAndUserSeesAnyNonTriggerFirePulseAnimationCtaThenFireButtonAnimationShouldNotShow() = coroutineRule.runBlocking {
606+
givenFireButtonEducationActive()
607+
givenOnboardingActive()
608+
val willTriggerFirePulseAnimationCtas = listOf(CtaId.DAX_DIALOG_TRACKERS_FOUND, CtaId.DAX_DIALOG_NETWORK, CtaId.DAX_DIALOG_OTHER)
609+
val willNotTriggerFirePulseAnimationCtas = CtaId.values().toList() - willTriggerFirePulseAnimationCtas
610+
611+
val launch = launch {
612+
testee.showFireButtonPulseAnimation.collect {
613+
assertFalse(it)
614+
}
615+
}
616+
willNotTriggerFirePulseAnimationCtas.forEach {
617+
dismissedCtaDaoChannel.send(listOf(DismissedCta(it)))
618+
}
619+
620+
launch.cancel()
621+
}
622+
623+
@Test
624+
fun whenFireEducationDisabledAndUserSeesAnyCtaThenFireButtonAnimationShouldNotShow() = coroutineRule.runBlocking {
625+
givenControlGroup()
626+
givenOnboardingActive()
627+
val allCtas = CtaId.values().toList()
628+
629+
val launch = launch {
630+
testee.showFireButtonPulseAnimation.collect {
631+
assertFalse(it)
632+
}
633+
}
634+
allCtas.forEach {
635+
dismissedCtaDaoChannel.send(listOf(DismissedCta(it)))
636+
}
637+
638+
launch.cancel()
639+
}
640+
641+
@Test
642+
fun whenFirstTimeUserClicksOnFireButtonThenFireDialogCtaReturned() = coroutineRule.runBlocking {
643+
givenFireButtonEducationActive()
644+
givenOnboardingActive()
645+
646+
val fireDialogCta = testee.getFireDialogCta()
647+
648+
assertTrue(fireDialogCta is DaxFireDialogCta.TryClearDataCta)
649+
}
650+
651+
@Test
652+
fun whenFirstTimeUserClicksOnFireButtonButUserHidAllTipsThenFireDialogCtaIsNull() = coroutineRule.runBlocking {
653+
givenFireButtonEducationActive()
654+
givenOnboardingActive()
655+
whenever(mockSettingsDataStore.hideTips).thenReturn(true)
656+
657+
val fireDialogCta = testee.getFireDialogCta()
658+
659+
assertNull(fireDialogCta)
660+
}
661+
662+
@Test
663+
fun whenFireCtaDismissedThenFireDialogCtaIsNull() = coroutineRule.runBlocking {
664+
givenFireButtonEducationActive()
665+
givenOnboardingActive()
666+
whenever(mockDismissedCtaDao.exists(CtaId.DAX_FIRE_BUTTON)).thenReturn(true)
667+
668+
val fireDialogCta = testee.getFireDialogCta()
669+
670+
assertNull(fireDialogCta)
671+
}
672+
673+
@Test
674+
fun whenFireEducationDisabledThenFireDialogCtaIsNull() = coroutineRule.runBlocking {
675+
givenControlGroup()
676+
givenOnboardingActive()
677+
678+
val fireDialogCta = testee.getFireDialogCta()
679+
680+
assertNull(fireDialogCta)
681+
}
682+
516683
private suspend fun givenDaxOnboardingActive() {
517684
whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
518685
}
@@ -539,6 +706,21 @@ class CtaViewModelTest {
539706
whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.USE_OUR_APP_ONBOARDING)
540707
}
541708

709+
private fun givenFireButtonEducationActive() {
710+
whenever(mockVariantManager.getVariant()).thenReturn(
711+
Variant(
712+
"test",
713+
features = listOf(
714+
VariantManager.VariantFeature.FireButtonEducation
715+
),
716+
filterBy = { true })
717+
)
718+
}
719+
720+
private fun givenControlGroup() {
721+
whenever(mockVariantManager.getVariant()).thenReturn(DEFAULT_VARIANT)
722+
}
723+
542724
private fun site(
543725
url: String = "http://www.test.com",
544726
uri: Uri? = Uri.parse(url),

app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,21 @@ class VariantManagerTest {
183183
assertTrue(variant.hasFeature(InAppUsage))
184184
}
185185

186+
// Fire button education
187+
@Test
188+
fun fireButtonEducationControlGroupVariantIsActive() {
189+
val variant = variants.first { it.key == "zm" }
190+
assertEqualsDouble(1.0, variant.weight)
191+
}
192+
193+
@Test
194+
fun fireButtonEducationVariantHasExpectedWeightAndFeatures() {
195+
val variant = variants.first { it.key == "zr" }
196+
assertEqualsDouble(1.0, variant.weight)
197+
assertEquals(1, variant.features.size)
198+
assertTrue(variant.hasFeature(FireButtonEducation))
199+
}
200+
186201
@Test
187202
fun verifyNoDuplicateVariantNames() {
188203
val existingNames = mutableSetOf<String>()

0 commit comments

Comments
 (0)
0