Skip to content

Commit 17d84c1

Browse files
UOA experiment 2nd iteration (#1205)
1 parent 75606bc commit 17d84c1

File tree

5 files changed

+123
-15
lines changed

5 files changed

+123
-15
lines changed

app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.work.WorkManager
2626
import androidx.work.impl.utils.SynchronousExecutor
2727
import androidx.work.testing.WorkManagerTestInitHelper
2828
import com.duckduckgo.app.CoroutineTestRule
29+
import com.duckduckgo.app.global.install.AppInstallStore
2930
import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker
3031
import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker
3132
import com.duckduckgo.app.notification.model.SchedulableNotification
@@ -36,10 +37,12 @@ import com.nhaarman.mockitokotlin2.mock
3637
import com.nhaarman.mockitokotlin2.whenever
3738
import kotlinx.coroutines.ExperimentalCoroutinesApi
3839
import kotlinx.coroutines.runBlocking
40+
import org.junit.Assert.assertEquals
3941
import org.junit.Assert.assertTrue
4042
import org.junit.Before
4143
import org.junit.Rule
4244
import org.junit.Test
45+
import java.util.concurrent.TimeUnit
4346
import kotlin.reflect.jvm.jvmName
4447

4548
class AndroidNotificationSchedulerTest {
@@ -52,6 +55,7 @@ class AndroidNotificationSchedulerTest {
5255
private val privacyNotification: SchedulableNotification = mock()
5356
private val useOurAppNotification: SchedulableNotification = mock()
5457
private val variantManager: VariantManager = mock()
58+
private val appInstallStore: AppInstallStore = mock()
5559

5660
private val context = InstrumentationRegistry.getInstrumentation().targetContext
5761
private lateinit var workManager: WorkManager
@@ -66,7 +70,8 @@ class AndroidNotificationSchedulerTest {
6670
clearNotification,
6771
privacyNotification,
6872
useOurAppNotification,
69-
variantManager
73+
variantManager,
74+
appInstallStore
7075
)
7176
}
7277

@@ -204,6 +209,67 @@ class AndroidNotificationSchedulerTest {
204209
assertNoNotificationScheduled()
205210
}
206211

212+
@Test
213+
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled0DaysThenReturn1() {
214+
setInAppUsageVariant()
215+
givenAppHasBeenInstalledForDays(days = 0)
216+
217+
assertEquals(1, testee.getDurationForInactiveNotification(1))
218+
}
219+
220+
@Test
221+
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled1DayThenReturn1() {
222+
setInAppUsageVariant()
223+
givenAppHasBeenInstalledForDays(days = 1)
224+
225+
assertEquals(1, testee.getDurationForInactiveNotification(1))
226+
}
227+
228+
@Test
229+
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled2DaysThenReturn2() {
230+
setInAppUsageVariant()
231+
givenAppHasBeenInstalledForDays(days = 2)
232+
233+
assertEquals(2, testee.getDurationForInactiveNotification(1))
234+
}
235+
236+
@Test
237+
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay3AndAppHasBeenInstalled0DaysThenReturn4() {
238+
setInAppUsageVariant()
239+
givenAppHasBeenInstalledForDays(days = 0)
240+
241+
assertEquals(4, testee.getDurationForInactiveNotification(3))
242+
}
243+
244+
@Test
245+
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay3AndAppHasBeenInstalled1DayThenReturn3() {
246+
setInAppUsageVariant()
247+
givenAppHasBeenInstalledForDays(days = 1)
248+
249+
assertEquals(3, testee.getDurationForInactiveNotification(3))
250+
}
251+
252+
@Test
253+
fun whenDefaultVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled2DaysThenReturn1() {
254+
setDefaultVariant()
255+
givenAppHasBeenInstalledForDays(days = 2)
256+
257+
assertEquals(1, testee.getDurationForInactiveNotification(1))
258+
}
259+
260+
@Test
261+
fun whenDefaultVariantAndGetDurationForInactiveNotificationForDay3AndAppHasBeenInstalled0DaysThenReturn3() {
262+
setDefaultVariant()
263+
givenAppHasBeenInstalledForDays(days = 0)
264+
265+
assertEquals(3, testee.getDurationForInactiveNotification(3))
266+
}
267+
268+
private fun givenAppHasBeenInstalledForDays(days: Long) {
269+
val timeSinceInstallation = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days)
270+
whenever(appInstallStore.installTimestamp).thenReturn(timeSinceInstallation)
271+
}
272+
207273
private suspend fun givenNoInactiveUserNotifications() {
208274
whenever(privacyNotification.canShow()).thenReturn(false)
209275
whenever(clearNotification.canShow()).thenReturn(false)
@@ -215,7 +281,6 @@ class AndroidNotificationSchedulerTest {
215281
"test",
216282
features = listOf(
217283
VariantManager.VariantFeature.InAppUsage,
218-
VariantManager.VariantFeature.RemoveDay1AndDay3Notifications,
219284
VariantManager.VariantFeature.KillOnboarding
220285
),
221286
filterBy = { true }

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ class VariantManagerTest {
4747
@Test
4848
fun inBrowserControlVariantHasExpectedWeightAndNoFeatures() {
4949
val variant = variants.first { it.key == "ma" }
50-
assertEqualsDouble(1.0, variant.weight)
50+
assertEqualsDouble(0.0, variant.weight)
5151
assertEquals(0, variant.features.size)
5252
}
5353

5454
@Test
5555
fun inBrowserSecondControlVariantHasExpectedWeightAndRemoveDay1And3NotificationsAndKillOnboardingFeatures() {
5656
val variant = variants.first { it.key == "mb" }
57-
assertEqualsDouble(1.0, variant.weight)
57+
assertEqualsDouble(0.0, variant.weight)
5858
assertEquals(2, variant.features.size)
5959
assertTrue(variant.hasFeature(KillOnboarding))
6060
assertTrue(variant.hasFeature(RemoveDay1AndDay3Notifications))
@@ -63,13 +63,29 @@ class VariantManagerTest {
6363
@Test
6464
fun inBrowserInAppUsageVariantHasExpectedWeightAndRemoveDay1And3NotificationsAndKillOnboardingAndInAppUsageFeatures() {
6565
val variant = variants.first { it.key == "mc" }
66-
assertEqualsDouble(1.0, variant.weight)
66+
assertEqualsDouble(0.0, variant.weight)
6767
assertEquals(3, variant.features.size)
6868
assertTrue(variant.hasFeature(KillOnboarding))
6969
assertTrue(variant.hasFeature(RemoveDay1AndDay3Notifications))
7070
assertTrue(variant.hasFeature(InAppUsage))
7171
}
7272

73+
@Test
74+
fun newInBrowserControlVariantHasExpectedWeightAndNoFeatures() {
75+
val variant = variants.first { it.key == "zx" }
76+
assertEqualsDouble(1.0, variant.weight)
77+
assertEquals(0, variant.features.size)
78+
}
79+
80+
@Test
81+
fun newInBrowserInAppUsageVariantHasExpectedWeightAndKillOnboardingAndInAppUsageFeatures() {
82+
val variant = variants.first { it.key == "zy" }
83+
assertEqualsDouble(1.0, variant.weight)
84+
assertEquals(2, variant.features.size)
85+
assertTrue(variant.hasFeature(KillOnboarding))
86+
assertTrue(variant.hasFeature(InAppUsage))
87+
}
88+
7389
@Test
7490
fun verifyNoDuplicateVariantNames() {
7591
val existingNames = mutableSetOf<String>()

app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.core.app.NotificationManagerCompat
2222
import androidx.localbroadcastmanager.content.LocalBroadcastManager
2323
import androidx.work.WorkManager
2424
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
25+
import com.duckduckgo.app.global.install.AppInstallStore
2526
import com.duckduckgo.app.notification.AndroidNotificationScheduler
2627
import com.duckduckgo.app.notification.NotificationFactory
2728
import com.duckduckgo.app.notification.NotificationScheduler
@@ -92,14 +93,16 @@ class NotificationModule {
9293
clearDataNotification: ClearDataNotification,
9394
privacyProtectionNotification: PrivacyProtectionNotification,
9495
useOurAppNotification: UseOurAppNotification,
95-
variantManager: VariantManager
96+
variantManager: VariantManager,
97+
appInstallStore: AppInstallStore
9698
): AndroidNotificationScheduler {
9799
return NotificationScheduler(
98100
workManager,
99101
clearDataNotification,
100102
privacyProtectionNotification,
101103
useOurAppNotification,
102-
variantManager
104+
variantManager,
105+
appInstallStore
103106
)
104107
}
105108

app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
package com.duckduckgo.app.notification
1818

1919
import android.content.Context
20+
import androidx.annotation.VisibleForTesting
2021
import androidx.annotation.WorkerThread
2122
import androidx.core.app.NotificationManagerCompat
2223
import androidx.work.*
24+
import com.duckduckgo.app.global.install.AppInstallStore
25+
import com.duckduckgo.app.global.install.daysInstalled
2326
import com.duckduckgo.app.global.plugins.worker.WorkerInjectorPlugin
2427
import com.duckduckgo.app.notification.db.NotificationDao
2528
import com.duckduckgo.app.notification.model.ClearDataNotification
@@ -48,7 +51,8 @@ class NotificationScheduler(
4851
private val clearDataNotification: SchedulableNotification,
4952
private val privacyNotification: SchedulableNotification,
5053
private val useOurAppNotification: SchedulableNotification,
51-
private val variantManager: VariantManager
54+
private val variantManager: VariantManager,
55+
private val appInstallStore: AppInstallStore
5256
) : AndroidNotificationScheduler {
5357

5458
override suspend fun scheduleNextNotification() {
@@ -60,7 +64,7 @@ class NotificationScheduler(
6064
if (variant().hasFeature(VariantManager.VariantFeature.InAppUsage) && useOurAppNotification.canShow()) {
6165
val operation = scheduleUniqueNotification(
6266
OneTimeWorkRequestBuilder<UseOurAppNotificationWorker>(),
63-
3,
67+
UOA_DELAY_DURATION_IN_DAYS,
6468
TimeUnit.DAYS,
6569
USE_OUR_APP_WORK_REQUEST_TAG
6670
)
@@ -77,15 +81,29 @@ class NotificationScheduler(
7781

7882
when {
7983
(!variant().hasFeature(VariantManager.VariantFeature.RemoveDay1AndDay3Notifications) && privacyNotification.canShow()) -> {
80-
scheduleNotification(OneTimeWorkRequestBuilder<PrivacyNotificationWorker>(), 1, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
84+
val duration = getDurationForInactiveNotification(PRIVACY_DELAY_DURATION_IN_DAYS)
85+
scheduleNotification(OneTimeWorkRequestBuilder<PrivacyNotificationWorker>(), duration, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
8186
}
8287
(!variant().hasFeature(VariantManager.VariantFeature.RemoveDay1AndDay3Notifications) && clearDataNotification.canShow()) -> {
83-
scheduleNotification(OneTimeWorkRequestBuilder<ClearDataNotificationWorker>(), 3, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
88+
val duration = getDurationForInactiveNotification(CLEAR_DATA_DELAY_DURATION_IN_DAYS)
89+
scheduleNotification(OneTimeWorkRequestBuilder<ClearDataNotificationWorker>(), duration, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
8490
}
8591
else -> Timber.v("Notifications not enabled for this variant")
8692
}
8793
}
8894

95+
@VisibleForTesting
96+
fun getDurationForInactiveNotification(day: Long): Long {
97+
Timber.d("Inactive notification days installed is ${appInstallStore.daysInstalled()} day is $day")
98+
var duration = day
99+
if (variantHasInAppUsage() && (appInstallStore.daysInstalled() + day) == UOA_DELAY_DURATION_IN_DAYS) {
100+
duration += 1
101+
}
102+
return duration
103+
}
104+
105+
private fun variantHasInAppUsage() = variant().hasFeature(VariantManager.VariantFeature.InAppUsage)
106+
89107
private fun variant() = variantManager.getVariant()
90108

91109
private fun scheduleUniqueNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String): Operation {
@@ -99,7 +117,7 @@ class NotificationScheduler(
99117
}
100118

101119
private fun scheduleNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String) {
102-
Timber.v("Scheduling notification")
120+
Timber.v("Scheduling notification for $duration")
103121
val request = builder
104122
.addTag(tag)
105123
.setInitialDelay(duration, unit)
@@ -146,6 +164,9 @@ class NotificationScheduler(
146164
companion object {
147165
const val UNUSED_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.schedule"
148166
const val USE_OUR_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.useOurApp"
167+
const val UOA_DELAY_DURATION_IN_DAYS = 3L
168+
const val CLEAR_DATA_DELAY_DURATION_IN_DAYS = 3L
169+
const val PRIVACY_DELAY_DURATION_IN_DAYS = 1L
149170
}
150171
}
151172

statistics/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@ interface VariantManager {
4747
Variant(key = "se", weight = 0.0, features = emptyList(), filterBy = { isSerpRegionToggleCountry() }),
4848

4949
// InAppUsage Experiments
50-
Variant(key = "ma", weight = 1.0, features = emptyList(), filterBy = { isEnglishLocale() }),
51-
Variant(key = "mb", weight = 1.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() }),
52-
Variant(key = "mc", weight = 1.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.InAppUsage, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() })
50+
Variant(key = "ma", weight = 0.0, features = emptyList(), filterBy = { isEnglishLocale() }),
51+
Variant(key = "mb", weight = 0.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() }),
52+
Variant(key = "mc", weight = 0.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.InAppUsage, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() }),
53+
54+
Variant(key = "zx", weight = 1.0, features = emptyList(), filterBy = { isEnglishLocale() }),
55+
Variant(key = "zy", weight = 1.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.InAppUsage), filterBy = { isEnglishLocale() })
5356
)
5457

5558
val REFERRER_VARIANTS = listOf(

0 commit comments

Comments
 (0)