Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.work.WorkManager
import androidx.work.impl.utils.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import com.duckduckgo.app.CoroutineTestRule
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker
import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker
import com.duckduckgo.app.notification.model.SchedulableNotification
Expand All @@ -36,10 +37,12 @@ import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.TimeUnit
import kotlin.reflect.jvm.jvmName

class AndroidNotificationSchedulerTest {
Expand All @@ -52,6 +55,7 @@ class AndroidNotificationSchedulerTest {
private val privacyNotification: SchedulableNotification = mock()
private val useOurAppNotification: SchedulableNotification = mock()
private val variantManager: VariantManager = mock()
private val appInstallStore: AppInstallStore = mock()

private val context = InstrumentationRegistry.getInstrumentation().targetContext
private lateinit var workManager: WorkManager
Expand All @@ -66,7 +70,8 @@ class AndroidNotificationSchedulerTest {
clearNotification,
privacyNotification,
useOurAppNotification,
variantManager
variantManager,
appInstallStore
)
}

Expand Down Expand Up @@ -204,6 +209,67 @@ class AndroidNotificationSchedulerTest {
assertNoNotificationScheduled()
}

@Test
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled0DaysThenReturn1() {
setInAppUsageVariant()
givenAppHasBeenInstalledForDays(days = 0)

assertEquals(1, testee.getDurationForInactiveNotification(1))
}

@Test
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled1DayThenReturn1() {
setInAppUsageVariant()
givenAppHasBeenInstalledForDays(days = 1)

assertEquals(1, testee.getDurationForInactiveNotification(1))
}

@Test
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled2DaysThenReturn2() {
setInAppUsageVariant()
givenAppHasBeenInstalledForDays(days = 2)

assertEquals(2, testee.getDurationForInactiveNotification(1))
}

@Test
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay3AndAppHasBeenInstalled0DaysThenReturn4() {
setInAppUsageVariant()
givenAppHasBeenInstalledForDays(days = 0)

assertEquals(4, testee.getDurationForInactiveNotification(3))
}

@Test
fun whenInAppVariantAndGetDurationForInactiveNotificationForDay3AndAppHasBeenInstalled1DayThenReturn3() {
setInAppUsageVariant()
givenAppHasBeenInstalledForDays(days = 1)

assertEquals(3, testee.getDurationForInactiveNotification(3))
}

@Test
fun whenDefaultVariantAndGetDurationForInactiveNotificationForDay1AndAppHasBeenInstalled2DaysThenReturn1() {
setDefaultVariant()
givenAppHasBeenInstalledForDays(days = 2)

assertEquals(1, testee.getDurationForInactiveNotification(1))
}

@Test
fun whenDefaultVariantAndGetDurationForInactiveNotificationForDay3AndAppHasBeenInstalled0DaysThenReturn3() {
setDefaultVariant()
givenAppHasBeenInstalledForDays(days = 0)

assertEquals(3, testee.getDurationForInactiveNotification(3))
}

private fun givenAppHasBeenInstalledForDays(days: Long) {
val timeSinceInstallation = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days)
whenever(appInstallStore.installTimestamp).thenReturn(timeSinceInstallation)
}

private suspend fun givenNoInactiveUserNotifications() {
whenever(privacyNotification.canShow()).thenReturn(false)
whenever(clearNotification.canShow()).thenReturn(false)
Expand All @@ -215,7 +281,6 @@ class AndroidNotificationSchedulerTest {
"test",
features = listOf(
VariantManager.VariantFeature.InAppUsage,
VariantManager.VariantFeature.RemoveDay1AndDay3Notifications,
VariantManager.VariantFeature.KillOnboarding
),
filterBy = { true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ class VariantManagerTest {
@Test
fun inBrowserControlVariantHasExpectedWeightAndNoFeatures() {
val variant = variants.first { it.key == "ma" }
assertEqualsDouble(1.0, variant.weight)
assertEqualsDouble(0.0, variant.weight)
assertEquals(0, variant.features.size)
}

@Test
fun inBrowserSecondControlVariantHasExpectedWeightAndRemoveDay1And3NotificationsAndKillOnboardingFeatures() {
val variant = variants.first { it.key == "mb" }
assertEqualsDouble(1.0, variant.weight)
assertEqualsDouble(0.0, variant.weight)
assertEquals(2, variant.features.size)
assertTrue(variant.hasFeature(KillOnboarding))
assertTrue(variant.hasFeature(RemoveDay1AndDay3Notifications))
Expand All @@ -63,13 +63,29 @@ class VariantManagerTest {
@Test
fun inBrowserInAppUsageVariantHasExpectedWeightAndRemoveDay1And3NotificationsAndKillOnboardingAndInAppUsageFeatures() {
val variant = variants.first { it.key == "mc" }
assertEqualsDouble(1.0, variant.weight)
assertEqualsDouble(0.0, variant.weight)
assertEquals(3, variant.features.size)
assertTrue(variant.hasFeature(KillOnboarding))
assertTrue(variant.hasFeature(RemoveDay1AndDay3Notifications))
assertTrue(variant.hasFeature(InAppUsage))
}

@Test
fun newInBrowserControlVariantHasExpectedWeightAndNoFeatures() {
val variant = variants.first { it.key == "zx" }
assertEqualsDouble(1.0, variant.weight)
assertEquals(0, variant.features.size)
}

@Test
fun newInBrowserInAppUsageVariantHasExpectedWeightAndKillOnboardingAndInAppUsageFeatures() {
val variant = variants.first { it.key == "zy" }
assertEqualsDouble(1.0, variant.weight)
assertEquals(2, variant.features.size)
assertTrue(variant.hasFeature(KillOnboarding))
assertTrue(variant.hasFeature(InAppUsage))
}

@Test
fun verifyNoDuplicateVariantNames() {
val existingNames = mutableSetOf<String>()
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/java/com/duckduckgo/app/di/NotificationModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.core.app.NotificationManagerCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.WorkManager
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.notification.AndroidNotificationScheduler
import com.duckduckgo.app.notification.NotificationFactory
import com.duckduckgo.app.notification.NotificationScheduler
Expand Down Expand Up @@ -92,14 +93,16 @@ class NotificationModule {
clearDataNotification: ClearDataNotification,
privacyProtectionNotification: PrivacyProtectionNotification,
useOurAppNotification: UseOurAppNotification,
variantManager: VariantManager
variantManager: VariantManager,
appInstallStore: AppInstallStore
): AndroidNotificationScheduler {
return NotificationScheduler(
workManager,
clearDataNotification,
privacyProtectionNotification,
useOurAppNotification,
variantManager
variantManager,
appInstallStore
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
package com.duckduckgo.app.notification

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationManagerCompat
import androidx.work.*
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.global.install.daysInstalled
import com.duckduckgo.app.global.plugins.worker.WorkerInjectorPlugin
import com.duckduckgo.app.notification.db.NotificationDao
import com.duckduckgo.app.notification.model.ClearDataNotification
Expand Down Expand Up @@ -48,7 +51,8 @@ class NotificationScheduler(
private val clearDataNotification: SchedulableNotification,
private val privacyNotification: SchedulableNotification,
private val useOurAppNotification: SchedulableNotification,
private val variantManager: VariantManager
private val variantManager: VariantManager,
private val appInstallStore: AppInstallStore
) : AndroidNotificationScheduler {

override suspend fun scheduleNextNotification() {
Expand All @@ -60,7 +64,7 @@ class NotificationScheduler(
if (variant().hasFeature(VariantManager.VariantFeature.InAppUsage) && useOurAppNotification.canShow()) {
val operation = scheduleUniqueNotification(
OneTimeWorkRequestBuilder<UseOurAppNotificationWorker>(),
3,
UOA_DELAY_DURATION_IN_DAYS,
TimeUnit.DAYS,
USE_OUR_APP_WORK_REQUEST_TAG
)
Expand All @@ -77,15 +81,29 @@ class NotificationScheduler(

when {
(!variant().hasFeature(VariantManager.VariantFeature.RemoveDay1AndDay3Notifications) && privacyNotification.canShow()) -> {
scheduleNotification(OneTimeWorkRequestBuilder<PrivacyNotificationWorker>(), 1, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
val duration = getDurationForInactiveNotification(PRIVACY_DELAY_DURATION_IN_DAYS)
scheduleNotification(OneTimeWorkRequestBuilder<PrivacyNotificationWorker>(), duration, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
}
(!variant().hasFeature(VariantManager.VariantFeature.RemoveDay1AndDay3Notifications) && clearDataNotification.canShow()) -> {
scheduleNotification(OneTimeWorkRequestBuilder<ClearDataNotificationWorker>(), 3, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
val duration = getDurationForInactiveNotification(CLEAR_DATA_DELAY_DURATION_IN_DAYS)
scheduleNotification(OneTimeWorkRequestBuilder<ClearDataNotificationWorker>(), duration, TimeUnit.DAYS, UNUSED_APP_WORK_REQUEST_TAG)
}
else -> Timber.v("Notifications not enabled for this variant")
}
}

@VisibleForTesting
fun getDurationForInactiveNotification(day: Long): Long {
Timber.d("Inactive notification days installed is ${appInstallStore.daysInstalled()} day is $day")
var duration = day
if (variantHasInAppUsage() && (appInstallStore.daysInstalled() + day) == UOA_DELAY_DURATION_IN_DAYS) {
duration += 1
}
return duration
}

private fun variantHasInAppUsage() = variant().hasFeature(VariantManager.VariantFeature.InAppUsage)

private fun variant() = variantManager.getVariant()

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

private fun scheduleNotification(builder: OneTimeWorkRequest.Builder, duration: Long, unit: TimeUnit, tag: String) {
Timber.v("Scheduling notification")
Timber.v("Scheduling notification for $duration")
val request = builder
.addTag(tag)
.setInitialDelay(duration, unit)
Expand Down Expand Up @@ -146,6 +164,9 @@ class NotificationScheduler(
companion object {
const val UNUSED_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.schedule"
const val USE_OUR_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.useOurApp"
const val UOA_DELAY_DURATION_IN_DAYS = 3L
const val CLEAR_DATA_DELAY_DURATION_IN_DAYS = 3L
const val PRIVACY_DELAY_DURATION_IN_DAYS = 1L
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ interface VariantManager {
Variant(key = "se", weight = 0.0, features = emptyList(), filterBy = { isSerpRegionToggleCountry() }),

// InAppUsage Experiments
Variant(key = "ma", weight = 1.0, features = emptyList(), filterBy = { isEnglishLocale() }),
Variant(key = "mb", weight = 1.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() }),
Variant(key = "mc", weight = 1.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.InAppUsage, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() })
Variant(key = "ma", weight = 0.0, features = emptyList(), filterBy = { isEnglishLocale() }),
Variant(key = "mb", weight = 0.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() }),
Variant(key = "mc", weight = 0.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.InAppUsage, VariantFeature.RemoveDay1AndDay3Notifications), filterBy = { isEnglishLocale() }),

Variant(key = "zx", weight = 1.0, features = emptyList(), filterBy = { isEnglishLocale() }),
Variant(key = "zy", weight = 1.0, features = listOf(VariantFeature.KillOnboarding, VariantFeature.InAppUsage), filterBy = { isEnglishLocale() })
)

val REFERRER_VARIANTS = listOf(
Expand Down