Skip to content

Commit c229f3d

Browse files
authored
Experiment set-up: [Android] Experiment: Re-activate users by prompting after absence (#6024)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1210042996818872?focus=true ### Description Set up an experiment to reactivate users by prompting them after a period of absence. ### Steps to test this PR See Asana task above and https://app.asana.com/1/137249556945/project/1142021229838617/task/1210254344043593?focus=true ### UI changes See screenshots in https://app.asana.com/1/137249556945/project/1142021229838617/task/1210254344043593?focus=true
1 parent e048c02 commit c229f3d

File tree

50 files changed

+2566
-370
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2566
-370
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ import com.duckduckgo.common.utils.DispatcherProvider
222222
import com.duckduckgo.common.utils.device.DeviceInfo
223223
import com.duckduckgo.common.utils.plugins.PluginPoint
224224
import com.duckduckgo.common.utils.plugins.headers.CustomHeadersProvider
225+
import com.duckduckgo.daxprompts.impl.ReactivateUsersExperiment
225226
import com.duckduckgo.downloads.api.DownloadStateListener
226227
import com.duckduckgo.downloads.api.FileDownloader
227228
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
@@ -553,6 +554,7 @@ class BrowserTabViewModelTest {
553554
private val mockSiteErrorHandler: StringSiteErrorHandler = mock()
554555
private val mockSiteHttpErrorHandler: HttpCodeSiteErrorHandler = mock()
555556
private val mockSubscriptionsJSHelper: SubscriptionsJSHelper = mock()
557+
private val mockReactivateUsersExperiment: ReactivateUsersExperiment = mock()
556558

557559
private val selectedTab = TabEntity("TAB_ID", "https://example.com", position = 0, sourceTabId = "TAB_ID_SOURCE")
558560

@@ -728,7 +730,14 @@ class BrowserTabViewModelTest {
728730
httpErrorPixels = { mockHttpErrorPixels },
729731
duckPlayer = mockDuckPlayer,
730732
duckChat = mockDuckChat,
731-
duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel, mockDuckDuckGoUrlDetector, mockPagesSettingPlugin),
733+
duckPlayerJSHelper = DuckPlayerJSHelper(
734+
mockDuckPlayer,
735+
mockAppBuildConfig,
736+
mockPixel,
737+
mockDuckDuckGoUrlDetector,
738+
mockPagesSettingPlugin,
739+
mockReactivateUsersExperiment,
740+
),
732741
duckChatJSHelper = mockDuckChatJSHelper,
733742
refreshPixelSender = refreshPixelSender,
734743
changeOmnibarPositionFeature = changeOmnibarPositionFeature,

app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,6 @@ import com.squareup.anvil.annotations.ContributesMultibinding
3333
import javax.inject.Inject
3434
import timber.log.Timber
3535

36-
interface DefaultBrowserDetector {
37-
fun deviceSupportsDefaultBrowserConfiguration(): Boolean
38-
fun isDefaultBrowser(): Boolean
39-
fun hasDefaultBrowser(): Boolean
40-
}
41-
4236
@ContributesMultibinding(scope = AppScope::class, boundType = BrowserFeatureStateReporterPlugin::class)
4337
@ContributesBinding(scope = AppScope::class, boundType = DefaultBrowserDetector::class)
4438
class AndroidDefaultBrowserDetector @Inject constructor(

app/src/main/java/com/duckduckgo/app/browser/duckplayer/DuckPlayerJSHelper.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel
4444
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
4545
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
4646
import com.duckduckgo.common.utils.plugins.PluginPoint
47+
import com.duckduckgo.daxprompts.impl.ReactivateUsersExperiment
4748
import com.duckduckgo.duckplayer.api.DuckPlayer
4849
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.AUTO
4950
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerOrigin.OVERLAY
@@ -74,6 +75,7 @@ class DuckPlayerJSHelper @Inject constructor(
7475
private val pixel: Pixel,
7576
private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector,
7677
private val pagesSettingPlugin: PluginPoint<DuckPlayerPageSettingsPlugin>,
78+
private val reactivateUsersExperiment: ReactivateUsersExperiment,
7779
) {
7880
private suspend fun getUserPreferences(featureName: String, method: String, id: String): JsCallbackData {
7981
val userValues = duckPlayer.getUserPreferences()
@@ -175,6 +177,9 @@ class DuckPlayerJSHelper @Inject constructor(
175177
data.getJSONObject("params").getString(it)
176178
}
177179
duckPlayer.sendDuckPlayerPixel(pixelName, paramsMap)
180+
if (pixelName == "play.use") {
181+
reactivateUsersExperiment.fireDuckPlayerUseIfInExperiment()
182+
}
178183
}
179184

180185
suspend fun processJsCallbackMessage(

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,57 @@
1717
package com.duckduckgo.app.launch
1818

1919
import android.os.Bundle
20+
import androidx.activity.result.ActivityResult
21+
import androidx.activity.result.contract.ActivityResultContracts
2022
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
2123
import androidx.lifecycle.lifecycleScope
2224
import com.duckduckgo.anvil.annotations.InjectWith
2325
import com.duckduckgo.app.browser.BrowserActivity
2426
import com.duckduckgo.app.browser.R
2527
import com.duckduckgo.app.onboarding.ui.OnboardingActivity
2628
import com.duckduckgo.common.ui.DuckDuckGoActivity
29+
import com.duckduckgo.daxprompts.api.DaxPromptBrowserComparisonNoParams
30+
import com.duckduckgo.daxprompts.api.DaxPromptDuckPlayerNoParams
31+
import com.duckduckgo.daxprompts.impl.ui.DaxPromptBrowserComparisonActivity.Companion.DAX_PROMPT_BROWSER_COMPARISON_SET_DEFAULT_EXTRA
32+
import com.duckduckgo.daxprompts.impl.ui.DaxPromptDuckPlayerActivity.Companion.DAX_PROMPT_DUCK_PLAYER_ACTIVITY_URL_EXTRA
2733
import com.duckduckgo.di.scopes.ActivityScope
34+
import com.duckduckgo.navigation.api.GlobalActivityStarter
35+
import javax.inject.Inject
2836
import kotlinx.coroutines.launch
37+
import timber.log.Timber
2938

3039
@InjectWith(ActivityScope::class)
3140
class LaunchBridgeActivity : DuckDuckGoActivity() {
3241

3342
private val viewModel: LaunchViewModel by bindViewModel()
3443

44+
private val startDaxPromptDuckPlayerActivityForResult =
45+
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
46+
if (result.resultCode == RESULT_OK) {
47+
val url = result.data?.getStringExtra(DAX_PROMPT_DUCK_PLAYER_ACTIVITY_URL_EXTRA)
48+
Timber.d("Received RESULT_OK from DaxPromptDuckPlayerActivity with extra: $url.")
49+
viewModel.onDaxPromptDuckPlayerActivityResult(url)
50+
} else {
51+
Timber.d("Received non-OK result from DaxPromptDuckPlayerActivity.")
52+
viewModel.onDaxPromptDuckPlayerActivityResult()
53+
}
54+
}
55+
56+
private val startDaxPromptBrowserComparisonActivityForResult =
57+
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
58+
if (result.resultCode == RESULT_OK) {
59+
val show = result.data?.getBooleanExtra(DAX_PROMPT_BROWSER_COMPARISON_SET_DEFAULT_EXTRA, false)
60+
Timber.d("Received RESULT_OK from DaxPromptBrowserComparisonActivity with extra: $show")
61+
viewModel.onDaxPromptBrowserComparisonActivityResult(show)
62+
} else {
63+
Timber.d("Received non-OK result from DaxPromptBrowserComparisonActivity")
64+
viewModel.onDaxPromptBrowserComparisonActivityResult()
65+
}
66+
}
67+
68+
@Inject
69+
lateinit var globalActivityStarter: GlobalActivityStarter
70+
3571
override fun onCreate(savedInstanceState: Bundle?) {
3672
val splashScreen = installSplashScreen()
3773
super.onCreate(savedInstanceState)
@@ -59,6 +95,24 @@ class LaunchBridgeActivity : DuckDuckGoActivity() {
5995
is LaunchViewModel.Command.Home -> {
6096
showHome()
6197
}
98+
99+
is LaunchViewModel.Command.DaxPromptDuckPlayer -> {
100+
showDaxPromptDuckPlayer()
101+
}
102+
103+
is LaunchViewModel.Command.CloseDaxPrompt -> {
104+
lifecycleScope.launch { viewModel.showOnboardingOrHome() }
105+
}
106+
107+
is LaunchViewModel.Command.PlayVideoInDuckPlayer -> {
108+
startActivity(BrowserActivity.intent(this, queryExtra = it.url))
109+
overridePendingTransition(0, 0)
110+
finish()
111+
}
112+
113+
is LaunchViewModel.Command.DaxPromptBrowserComparison -> {
114+
showDaxPromptBrowserComparison()
115+
}
62116
}
63117
}
64118

@@ -72,4 +126,18 @@ class LaunchBridgeActivity : DuckDuckGoActivity() {
72126
overridePendingTransition(0, 0)
73127
finish()
74128
}
129+
130+
private fun showDaxPromptDuckPlayer() {
131+
val intentDaxPromptDuckPlayer =
132+
globalActivityStarter.startIntent(this, DaxPromptDuckPlayerNoParams)
133+
intentDaxPromptDuckPlayer?.let { startDaxPromptDuckPlayerActivityForResult.launch(it) }
134+
}
135+
136+
private fun showDaxPromptBrowserComparison() {
137+
val intentDaxPromptComparisonChart =
138+
globalActivityStarter.startIntent(this, DaxPromptBrowserComparisonNoParams)
139+
intentDaxPromptComparisonChart?.let {
140+
startDaxPromptBrowserComparisonActivityForResult.launch(it)
141+
}
142+
}
75143
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ package com.duckduckgo.app.launch
1818

1919
import androidx.lifecycle.ViewModel
2020
import com.duckduckgo.anvil.annotations.ContributesViewModel
21+
import com.duckduckgo.app.global.install.AppInstallStore
2122
import com.duckduckgo.app.onboarding.store.UserStageStore
2223
import com.duckduckgo.app.onboarding.store.isNewUser
2324
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener
2425
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener.Companion.MAX_REFERRER_WAIT_TIME_MS
2526
import com.duckduckgo.common.utils.SingleLiveEvent
27+
import com.duckduckgo.daxprompts.api.DaxPrompts
28+
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.NONE
29+
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_CONTROL
30+
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_VARIANT_BROWSER_COMPARISON
31+
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType.SHOW_VARIANT_DUCKPLAYER
2632
import com.duckduckgo.di.scopes.ActivityScope
2733
import javax.inject.Inject
2834
import kotlinx.coroutines.withTimeoutOrNull
@@ -32,6 +38,8 @@ import timber.log.Timber
3238
class LaunchViewModel @Inject constructor(
3339
private val userStageStore: UserStageStore,
3440
private val appReferrerStateListener: AppInstallationReferrerStateListener,
41+
private val daxPrompts: DaxPrompts,
42+
private val appInstallStore: AppInstallStore,
3543
) :
3644
ViewModel() {
3745

@@ -40,18 +48,56 @@ class LaunchViewModel @Inject constructor(
4048
sealed class Command {
4149
data object Onboarding : Command()
4250
data class Home(val replaceExistingSearch: Boolean = false) : Command()
51+
data object DaxPromptDuckPlayer : Command()
52+
data class PlayVideoInDuckPlayer(val url: String) : Command()
53+
data object DaxPromptBrowserComparison : Command()
54+
data object CloseDaxPrompt : Command()
4355
}
4456

4557
suspend fun determineViewToShow() {
4658
waitForReferrerData()
4759

60+
when (daxPrompts.evaluate()) {
61+
SHOW_CONTROL, NONE -> {
62+
Timber.d("Control / None action")
63+
showOnboardingOrHome()
64+
}
65+
66+
SHOW_VARIANT_DUCKPLAYER -> {
67+
Timber.d("Variant Duck Player action")
68+
command.value = Command.DaxPromptDuckPlayer
69+
}
70+
71+
SHOW_VARIANT_BROWSER_COMPARISON -> {
72+
Timber.d("Variant Browser Comparison action")
73+
command.value = Command.DaxPromptBrowserComparison
74+
}
75+
}
76+
}
77+
78+
suspend fun showOnboardingOrHome() {
4879
if (userStageStore.isNewUser()) {
4980
command.value = Command.Onboarding
5081
} else {
5182
command.value = Command.Home()
5283
}
5384
}
5485

86+
fun onDaxPromptDuckPlayerActivityResult(url: String? = null) {
87+
if (url != null) {
88+
command.value = Command.PlayVideoInDuckPlayer(url)
89+
} else {
90+
command.value = Command.CloseDaxPrompt
91+
}
92+
}
93+
94+
fun onDaxPromptBrowserComparisonActivityResult(showComparisonChart: Boolean? = false) {
95+
if (showComparisonChart != null) {
96+
appInstallStore.defaultBrowser = showComparisonChart
97+
}
98+
command.value = Command.CloseDaxPrompt
99+
}
100+
55101
private suspend fun waitForReferrerData() {
56102
val startTime = System.currentTimeMillis()
57103

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ package com.duckduckgo.app.launch
1818

1919
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
2020
import androidx.lifecycle.Observer
21+
import com.duckduckgo.app.global.install.AppInstallStore
22+
import com.duckduckgo.app.launch.LaunchViewModel.Command.DaxPromptBrowserComparison
23+
import com.duckduckgo.app.launch.LaunchViewModel.Command.DaxPromptDuckPlayer
2124
import com.duckduckgo.app.launch.LaunchViewModel.Command.Home
2225
import com.duckduckgo.app.launch.LaunchViewModel.Command.Onboarding
2326
import com.duckduckgo.app.onboarding.store.AppStage
2427
import com.duckduckgo.app.onboarding.store.UserStageStore
2528
import com.duckduckgo.app.referral.StubAppReferrerFoundStateListener
2629
import com.duckduckgo.common.test.CoroutineTestRule
30+
import com.duckduckgo.daxprompts.api.DaxPrompts
31+
import com.duckduckgo.daxprompts.api.DaxPrompts.ActionType
2732
import kotlinx.coroutines.test.runTest
2833
import org.junit.After
2934
import org.junit.Rule
@@ -44,6 +49,8 @@ class LaunchViewModelTest {
4449

4550
private val userStageStore = mock<UserStageStore>()
4651
private val mockCommandObserver: Observer<LaunchViewModel.Command> = mock()
52+
private val mockDaxPrompts: DaxPrompts = mock()
53+
private val mockAppInstallStore: AppInstallStore = mock()
4754

4855
private lateinit var testee: LaunchViewModel
4956

@@ -57,7 +64,10 @@ class LaunchViewModelTest {
5764
testee = LaunchViewModel(
5865
userStageStore,
5966
StubAppReferrerFoundStateListener("xx"),
67+
mockDaxPrompts,
68+
mockAppInstallStore,
6069
)
70+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
6171
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
6272
testee.command.observeForever(mockCommandObserver)
6373

@@ -71,7 +81,10 @@ class LaunchViewModelTest {
7181
testee = LaunchViewModel(
7282
userStageStore,
7383
StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000),
84+
mockDaxPrompts,
85+
mockAppInstallStore,
7486
)
87+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
7588
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
7689
testee.command.observeForever(mockCommandObserver)
7790

@@ -85,7 +98,10 @@ class LaunchViewModelTest {
8598
testee = LaunchViewModel(
8699
userStageStore,
87100
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
101+
mockDaxPrompts,
102+
mockAppInstallStore,
88103
)
104+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
89105
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)
90106
testee.command.observeForever(mockCommandObserver)
91107

@@ -99,7 +115,10 @@ class LaunchViewModelTest {
99115
testee = LaunchViewModel(
100116
userStageStore,
101117
StubAppReferrerFoundStateListener("xx"),
118+
mockDaxPrompts,
119+
mockAppInstallStore,
102120
)
121+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
103122
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
104123
testee.command.observeForever(mockCommandObserver)
105124
testee.determineViewToShow()
@@ -111,7 +130,10 @@ class LaunchViewModelTest {
111130
testee = LaunchViewModel(
112131
userStageStore,
113132
StubAppReferrerFoundStateListener("xx", mockDelayMs = 1_000),
133+
mockDaxPrompts,
134+
mockAppInstallStore,
114135
)
136+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
115137
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
116138
testee.command.observeForever(mockCommandObserver)
117139
testee.determineViewToShow()
@@ -123,10 +145,43 @@ class LaunchViewModelTest {
123145
testee = LaunchViewModel(
124146
userStageStore,
125147
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
148+
mockDaxPrompts,
149+
mockAppInstallStore,
126150
)
151+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE)
127152
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
128153
testee.command.observeForever(mockCommandObserver)
129154
testee.determineViewToShow()
130155
verify(mockCommandObserver).onChanged(any<Home>())
131156
}
157+
158+
@Test
159+
fun whenEvaluateReturnsDuckPlayerVariantThenCommandIsDaxPromptDuckPlayer() = runTest {
160+
testee = LaunchViewModel(
161+
userStageStore,
162+
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
163+
mockDaxPrompts,
164+
mockAppInstallStore,
165+
)
166+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_VARIANT_DUCKPLAYER)
167+
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
168+
testee.command.observeForever(mockCommandObserver)
169+
testee.determineViewToShow()
170+
verify(mockCommandObserver).onChanged(any<DaxPromptDuckPlayer>())
171+
}
172+
173+
@Test
174+
fun whenEvaluateReturnsBrowserComparisonVariantThenCommandIsDaxPromptBrowserComparison() = runTest {
175+
testee = LaunchViewModel(
176+
userStageStore,
177+
StubAppReferrerFoundStateListener("xx", mockDelayMs = Long.MAX_VALUE),
178+
mockDaxPrompts,
179+
mockAppInstallStore,
180+
)
181+
whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_VARIANT_BROWSER_COMPARISON)
182+
whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
183+
testee.command.observeForever(mockCommandObserver)
184+
testee.determineViewToShow()
185+
verify(mockCommandObserver).onChanged(any<DaxPromptBrowserComparison>())
186+
}
132187
}

0 commit comments

Comments
 (0)