Skip to content

Commit 7ef8adc

Browse files
Wait for local config before proceeding (#6554)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1210999612419280?focus=true ### Description This PR adds a 1.5s wait to allow for the local config to be persisted. This can be disabled remotely using `waitForLocalPrivacyConfig` ### Steps to test this PR In the `LaunchViewModel` inside `determineViewToShow()` change the new code with: ``` if (onboardingDesignExperimentManager.isWaitForLocalPrivacyConfigEnabled()) { val startTime = System.currentTimeMillis() withTimeoutOrNull(MAX_REFERRER_WAIT_TIME_MS) { val referrerJob = async { waitForReferrerData() } val configJob = async { onboardingDesignExperimentManager.waitForPrivacyConfig() } awaitAll(referrerJob, configJob) } logcat { "Onboarding waited ${System.currentTimeMillis() - startTime}ms" } } else { waitForReferrerData() } ``` - [ ] Delete the DuckDuckGO folder from Downloads and install the app - [ ] Wait time should be lower than 1.5s and you should always land in a variant from the experiment.
1 parent 540ddc3 commit 7ef8adc

File tree

16 files changed

+222
-42
lines changed

16 files changed

+222
-42
lines changed

app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel
2121
import com.duckduckgo.app.global.install.AppInstallStore
2222
import com.duckduckgo.app.onboarding.store.UserStageStore
2323
import com.duckduckgo.app.onboarding.store.isNewUser
24+
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager
2425
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener
2526
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener.Companion.MAX_REFERRER_WAIT_TIME_MS
2627
import com.duckduckgo.common.utils.SingleLiveEvent
@@ -31,6 +32,8 @@ import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_VARIANT_BROWSER_
3132
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_VARIANT_DUCKPLAYER
3233
import com.duckduckgo.di.scopes.ActivityScope
3334
import javax.inject.Inject
35+
import kotlinx.coroutines.async
36+
import kotlinx.coroutines.awaitAll
3437
import kotlinx.coroutines.withTimeoutOrNull
3538
import logcat.logcat
3639

@@ -40,6 +43,7 @@ class LaunchViewModel @Inject constructor(
4043
private val appReferrerStateListener: AppInstallationReferrerStateListener,
4144
private val daxPrompts: DaxPrompts,
4245
private val appInstallStore: AppInstallStore,
46+
private val onboardingDesignExperimentManager: OnboardingDesignExperimentManager,
4347
) :
4448
ViewModel() {
4549

@@ -55,7 +59,19 @@ class LaunchViewModel @Inject constructor(
5559
}
5660

5761
suspend fun determineViewToShow() {
58-
waitForReferrerData()
62+
if (onboardingDesignExperimentManager.isWaitForLocalPrivacyConfigEnabled()) {
63+
withTimeoutOrNull(MAX_REFERRER_WAIT_TIME_MS) {
64+
val referrerJob = async {
65+
waitForReferrerData()
66+
}
67+
val configJob = async {
68+
onboardingDesignExperimentManager.waitForPrivacyConfig()
69+
}
70+
awaitAll(referrerJob, configJob)
71+
}
72+
} else {
73+
waitForReferrerData()
74+
}
5975

6076
when (daxPrompts.evaluate()) {
6177
SHOW_CONTROL, NONE -> {

app/src/main/java/com/duckduckgo/app/onboardingdesignexperiment/OnboardingDesignExperimentManager.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616

1717
package com.duckduckgo.app.onboardingdesignexperiment
1818

19-
import androidx.lifecycle.LifecycleOwner
2019
import com.duckduckgo.app.browser.DuckDuckGoUrlDetector
2120
import com.duckduckgo.app.cta.ui.Cta
2221
import com.duckduckgo.app.cta.ui.DaxBubbleCta
2322
import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta
2423
import com.duckduckgo.app.di.AppCoroutineScope
25-
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
2624
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentToggles.OnboardingDesignExperimentCohort
2725
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentToggles.OnboardingDesignExperimentCohort.BB
2826
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentToggles.OnboardingDesignExperimentCohort.BUCK
@@ -40,6 +38,7 @@ import com.squareup.anvil.annotations.ContributesMultibinding
4038
import dagger.SingleInstanceIn
4139
import javax.inject.Inject
4240
import kotlinx.coroutines.CoroutineScope
41+
import kotlinx.coroutines.delay
4342
import kotlinx.coroutines.launch
4443
import kotlinx.coroutines.withContext
4544

@@ -64,12 +63,10 @@ interface OnboardingDesignExperimentManager {
6463
suspend fun fireSiteSuggestionOptionSelectedPixel(index: Int)
6564
suspend fun onWebPageFinishedLoading(url: String?)
6665
fun getCohort(): String?
66+
suspend fun waitForPrivacyConfig(): Boolean
67+
suspend fun isWaitForLocalPrivacyConfigEnabled(): Boolean
6768
}
6869

69-
@ContributesMultibinding(
70-
scope = AppScope::class,
71-
boundType = MainProcessLifecycleObserver::class,
72-
)
7370
@ContributesMultibinding(
7471
scope = AppScope::class,
7572
boundType = PrivacyConfigCallbackPlugin::class,
@@ -89,12 +86,26 @@ class RealOnboardingDesignExperimentManager @Inject constructor(
8986
private val appBuildConfig: AppBuildConfig,
9087
private val deviceInfo: DeviceInfo,
9188
private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector,
92-
) : OnboardingDesignExperimentManager, MainProcessLifecycleObserver, PrivacyConfigCallbackPlugin {
89+
) : OnboardingDesignExperimentManager, PrivacyConfigCallbackPlugin {
9390

9491
private var isExperimentEnabled: Boolean? = null
9592
private var onboardingDesignExperimentCohort: OnboardingDesignExperimentCohort? = null
9693

97-
override fun onCreate(owner: LifecycleOwner) {
94+
private var privacyPersisted: Boolean = false
95+
96+
override suspend fun waitForPrivacyConfig(): Boolean {
97+
while (!privacyPersisted) {
98+
delay(10)
99+
}
100+
return true
101+
}
102+
103+
override suspend fun isWaitForLocalPrivacyConfigEnabled(): Boolean {
104+
return onboardingDesignExperimentToggles.waitForLocalPrivacyConfig().isEnabled()
105+
}
106+
107+
override fun onPrivacyConfigPersisted() {
108+
privacyPersisted = true
98109
coroutineScope.launch {
99110
setCachedProperties()
100111
}

app/src/main/java/com/duckduckgo/app/onboardingdesignexperiment/OnboardingDesignExperimentToggles.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ interface OnboardingDesignExperimentToggles {
4747
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
4848
fun onboardingDesignExperimentAug25(): Toggle
4949

50+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
51+
fun waitForLocalPrivacyConfig(): Toggle
52+
5053
enum class OnboardingDesignExperimentCohort(override val cohortName: String) : Toggle.State.CohortName {
5154
MODIFIED_CONTROL("modifiedControl"),
5255
BUCK("buck"),

app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@ import com.duckduckgo.app.launch.LaunchViewModel.Command.Home
2525
import com.duckduckgo.app.launch.LaunchViewModel.Command.Onboarding
2626
import com.duckduckgo.app.onboarding.store.AppStage
2727
import com.duckduckgo.app.onboarding.store.UserStageStore
28+
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager
2829
import com.duckduckgo.app.referral.StubAppReferrerFoundStateListener
2930
import com.duckduckgo.common.test.CoroutineTestRule
3031
import com.duckduckgo.daxprompts.api.DaxPrompts
3132
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType
33+
import kotlinx.coroutines.CompletableDeferred
3234
import kotlinx.coroutines.test.runTest
3335
import org.junit.After
36+
import org.junit.Before
3437
import org.junit.Rule
3538
import org.junit.Test
3639
import org.mockito.kotlin.any
40+
import org.mockito.kotlin.doSuspendableAnswer
3741
import org.mockito.kotlin.mock
3842
import org.mockito.kotlin.verify
3943
import org.mockito.kotlin.whenever
@@ -51,9 +55,15 @@ class LaunchViewModelTest {
5155
private val mockCommandObserver: Observer<LaunchViewModel.Command> = mock()
5256
private val mockDaxPrompts: DaxPrompts = mock()
5357
private val mockAppInstallStore: AppInstallStore = mock()
58+
private val mockOnboardingExperiment: OnboardingDesignExperimentManager = mock()
5459

5560
private lateinit var testee: LaunchViewModel
5661

62+
@Before
63+
fun before() = runTest {
64+
whenever(mockOnboardingExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(false)
65+
}
66+
5767
@After
5868
fun after() {
5969
testee.command.removeObserver(mockCommandObserver)
@@ -66,6 +76,7 @@ class LaunchViewModelTest {
6676
StubAppReferrerFoundStateListener("xx"),
6777
mockDaxPrompts,
6878
mockAppInstallStore,
79+
mockOnboardingExperiment,
6980
)
7081
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
7182
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
@@ -83,6 +94,7 @@ class LaunchViewModelTest {
8394
StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000),
8495
mockDaxPrompts,
8596
mockAppInstallStore,
97+
mockOnboardingExperiment,
8698
)
8799
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
88100
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
@@ -100,6 +112,7 @@ class LaunchViewModelTest {
100112
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
101113
mockDaxPrompts,
102114
mockAppInstallStore,
115+
mockOnboardingExperiment,
103116
)
104117
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
105118
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
@@ -117,6 +130,7 @@ class LaunchViewModelTest {
117130
StubAppReferrerFoundStateListener("xx"),
118131
mockDaxPrompts,
119132
mockAppInstallStore,
133+
mockOnboardingExperiment,
120134
)
121135
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
122136
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
@@ -132,6 +146,7 @@ class LaunchViewModelTest {
132146
StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000),
133147
mockDaxPrompts,
134148
mockAppInstallStore,
149+
mockOnboardingExperiment,
135150
)
136151
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
137152
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
@@ -147,6 +162,7 @@ class LaunchViewModelTest {
147162
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
148163
mockDaxPrompts,
149164
mockAppInstallStore,
165+
mockOnboardingExperiment,
150166
)
151167
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
152168
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
@@ -162,6 +178,7 @@ class LaunchViewModelTest {
162178
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
163179
mockDaxPrompts,
164180
mockAppInstallStore,
181+
mockOnboardingExperiment,
165182
)
166183
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_VARIANT_DUCKPLAYER)
167184
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
@@ -177,11 +194,96 @@ class LaunchViewModelTest {
177194
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
178195
mockDaxPrompts,
179196
mockAppInstallStore,
197+
mockOnboardingExperiment,
180198
)
181199
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_VARIANT_BROWSER_COMPARISON)
182200
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
183201
testee.command.observeForever(mockCommandObserver)
184202
testee.determineViewToShow()
185203
verify(mockCommandObserver).onChanged(any<DaxPromptBrowserComparison>())
186204
}
205+
206+
@Test
207+
fun whenOnboardingShouldShowAndPrivacyConfigIsEnabledThenCommandIsOnboarding() = runTest {
208+
whenever(mockOnboardingExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(true)
209+
testee = LaunchViewModel(
210+
userStageStore,
211+
StubAppReferrerFoundStateListener("xx"),
212+
mockDaxPrompts,
213+
mockAppInstallStore,
214+
mockOnboardingExperiment,
215+
)
216+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
217+
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
218+
testee.command.observeForever(mockCommandObserver)
219+
220+
testee.determineViewToShow()
221+
222+
verify(mockOnboardingExperiment).waitForPrivacyConfig()
223+
verify(mockCommandObserver).onChanged(any<Onboarding>())
224+
}
225+
226+
@Test
227+
fun whenOnboardingShouldNotShowAndPrivacyConfigIsEnabledThenCommandIsHome() = runTest {
228+
whenever(mockOnboardingExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(true)
229+
testee = LaunchViewModel(
230+
userStageStore,
231+
StubAppReferrerFoundStateListener("xx"),
232+
mockDaxPrompts,
233+
mockAppInstallStore,
234+
mockOnboardingExperiment,
235+
)
236+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
237+
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
238+
testee.command.observeForever(mockCommandObserver)
239+
240+
testee.determineViewToShow()
241+
242+
verify(mockOnboardingExperiment).waitForPrivacyConfig()
243+
verify(mockCommandObserver).onChanged(any<Home>())
244+
}
245+
246+
@Test
247+
fun whenOnboardingExperimentIsEnabledAndOnboardingShouldShowAndReferrerTimesOutThenCommandIsOnboarding() = runTest {
248+
whenever(mockOnboardingExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(true)
249+
250+
testee = LaunchViewModel(
251+
userStageStore,
252+
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
253+
mockDaxPrompts,
254+
mockAppInstallStore,
255+
mockOnboardingExperiment,
256+
)
257+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
258+
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
259+
testee.command.observeForever(mockCommandObserver)
260+
261+
testee.determineViewToShow()
262+
263+
verify(mockOnboardingExperiment).waitForPrivacyConfig()
264+
verify(mockCommandObserver).onChanged(any<Onboarding>())
265+
}
266+
267+
@Test
268+
fun whenOnboardingExperimentIsEnabledAndOnboardingShouldShowAndWaitForPrivacyConfigTimesOutThenCommandIsOnboarding() = runTest {
269+
whenever(mockOnboardingExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(true)
270+
whenever(mockOnboardingExperiment.waitForPrivacyConfig()).doSuspendableAnswer {
271+
CompletableDeferred<Boolean>().await()
272+
}
273+
274+
testee = LaunchViewModel(
275+
userStageStore,
276+
StubAppReferrerFoundStateListener("xx"),
277+
mockDaxPrompts,
278+
mockAppInstallStore,
279+
mockOnboardingExperiment,
280+
)
281+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
282+
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
283+
testee.command.observeForever(mockCommandObserver)
284+
285+
testee.determineViewToShow()
286+
287+
verify(mockCommandObserver).onChanged(any<Onboarding>())
288+
}
187289
}

app/src/test/java/com/duckduckgo/app/onboarding/onboardingdesignexperiment/OnboardingDesignExperimentManagerTest.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import com.duckduckgo.app.browser.DuckDuckGoUrlDetector
2121
import com.duckduckgo.app.cta.ui.DaxBubbleCta
2222
import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta
2323
import com.duckduckgo.app.global.install.AppInstallStore
24-
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
2524
import com.duckduckgo.app.onboarding.store.OnboardingStore
2625
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentCountDataStore
2726
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager
@@ -85,7 +84,7 @@ class OnboardingDesignExperimentManagerTest {
8584
}
8685

8786
@Test
88-
fun whenLifecycleOwnerCreatedThenCachedPropertiesAreSet() = runTest {
87+
fun whenPrivacyConfigPersistedCreatedThenCachedPropertiesAreSet() = runTest {
8988
val mockToggle = mock<Toggle>()
9089
val mockCohort = mock<Toggle.State.Cohort>()
9190

@@ -94,8 +93,8 @@ class OnboardingDesignExperimentManagerTest {
9493
whenever(mockToggle.getCohort()).thenReturn(mockCohort)
9594
whenever(mockToggle.getCohort()!!.name).thenReturn(OnboardingDesignExperimentToggles.OnboardingDesignExperimentCohort.BUCK.cohortName)
9695

97-
val lifecycleObserver = testee as MainProcessLifecycleObserver
98-
lifecycleObserver.onCreate(mock())
96+
val lifecycleObserver = testee as PrivacyConfigCallbackPlugin
97+
lifecycleObserver.onPrivacyConfigPersisted()
9998

10099
coroutineRule.testScope.testScheduler.advanceUntilIdle()
101100

@@ -104,14 +103,14 @@ class OnboardingDesignExperimentManagerTest {
104103
}
105104

106105
@Test
107-
fun whenLifecycleOwnerCreatedWithDisabledExperimentThenCachedPropertiesReflectDisabledState() = runTest {
106+
fun whenPrivacyConfigPersistedWithDisabledExperimentThenCachedPropertiesReflectDisabledState() = runTest {
108107
val mockToggle = mock<Toggle>()
109108

110109
whenever(onboardingDesignExperimentToggles.onboardingDesignExperimentAug25()).thenReturn(mockToggle)
111110
whenever(mockToggle.isEnabled()).thenReturn(false)
112111

113-
val lifecycleObserver = testee as MainProcessLifecycleObserver
114-
lifecycleObserver.onCreate(mock())
112+
val lifecycleObserver = testee as PrivacyConfigCallbackPlugin
113+
lifecycleObserver.onPrivacyConfigPersisted()
115114

116115
coroutineRule.testScope.testScheduler.advanceUntilIdle()
117116

app/src/test/java/com/duckduckgo/app/settings/clear/OnboardingExperimentFireAnimationHelperTest.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,6 @@ class OnboardingExperimentFireAnimationHelperTest {
7272
var buckEnabled = false
7373
var modifiedControlEnabled = false
7474

75-
override fun getCohort(): String? {
76-
TODO("Not yet implemented")
77-
}
78-
7975
override suspend fun enroll() {
8076
TODO("Not yet implemented")
8177
}
@@ -148,5 +144,17 @@ class OnboardingExperimentFireAnimationHelperTest {
148144
override suspend fun onWebPageFinishedLoading(url: String?) {
149145
TODO("Not yet implemented")
150146
}
147+
148+
override fun getCohort(): String? {
149+
TODO("Not yet implemented")
150+
}
151+
152+
override suspend fun isWaitForLocalPrivacyConfigEnabled(): Boolean {
153+
TODO("Not yet implemented")
154+
}
155+
156+
override suspend fun waitForPrivacyConfig(): Boolean {
157+
TODO("Not yet implemented")
158+
}
151159
}
152160
}

privacy-config/privacy-config-api/src/main/java/com/duckduckgo/privacy/config/api/PrivacyConfigCallbackPlugin.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ interface PrivacyConfigCallbackPlugin {
2424
* This method will be called every time it downloads a new version of the privacy config.
2525
*/
2626
fun onPrivacyConfigDownloaded()
27+
28+
/**
29+
* Notifies that onPrivacyConfigPersisted event occurred.
30+
* This method will be called every time it persists a new version of the privacy config.
31+
*/
32+
fun onPrivacyConfigPersisted() {
33+
// Default NO-OP
34+
}
2735
}

0 commit comments

Comments
 (0)