From 7c3df854dc85dc1fcd6dad9732a0097da854f949 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 10:18:09 +0000 Subject: [PATCH 01/16] Add Aura experiment --- app/build.gradle | 1 + .../app/browser/aura/AuraExperimentFeature.kt | 31 +++++++ .../aura/AuraExperimentListJsonParser.kt | 58 ++++++++++++ .../browser/aura/AuraExperimentManagerImpl.kt | 59 ++++++++++++ .../app/referral/AppReferrerDataStore.kt | 6 ++ .../referral/AppReferrerInstallPixelSender.kt | 2 +- .../AuraExperimentListJsonParserImplTest.kt | 52 +++++++++++ .../aura/AuraExperimentManagerImplTest.kt | 91 +++++++++++++++++++ .../json/auraExperiment_emptyList.json | 3 + .../auraExperiment_multipleEntryList.json | 7 ++ .../json/auraExperiment_singleEntryList.json | 3 + .../AppReferrerInstallPixelSenderTest.kt | 12 +++ .../app/aura/AuraExperimentManager.kt | 21 +++++ installation/installation-api/.gitignore | 0 installation/installation-api/build.gradle | 35 +++++++ .../impl/installer/InstallSourceExtractor.kt | 21 +++++ installation/installation-impl/build.gradle | 1 + ...actor.kt => RealInstallSourceExtractor.kt} | 4 - .../app/statistics/AtbInitializer.kt | 9 +- .../app/statistics/AtbInitializerTest.kt | 14 ++- 20 files changed, 421 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt create mode 100644 app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt create mode 100644 app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt create mode 100644 app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt create mode 100644 app/src/test/resources/json/auraExperiment_emptyList.json create mode 100644 app/src/test/resources/json/auraExperiment_multipleEntryList.json create mode 100644 app/src/test/resources/json/auraExperiment_singleEntryList.json create mode 100644 browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt create mode 100644 installation/installation-api/.gitignore create mode 100644 installation/installation-api/build.gradle create mode 100644 installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt rename installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/{InstallSourceExtractor.kt => RealInstallSourceExtractor.kt} (96%) diff --git a/app/build.gradle b/app/build.gradle index 6dfdeba02996..10c27ab67b38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -358,6 +358,7 @@ dependencies { implementation project(':runtime-checks-impl') implementation project(':runtime-checks-store') + implementation project(':installation-api') implementation project(':installation-impl') implementation project(':internal-features-api') diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt new file mode 100644 index 000000000000..2924c03d6d8d --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.aura + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "auraExperiment", +) +interface AuraExperimentFeature { + + @Toggle.DefaultValue(false) + fun self(): Toggle +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt new file mode 100644 index 000000000000..0a7a7d01813f --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.aura + +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import javax.inject.Inject + +data class Packages(val list: List = emptyList()) + +interface AuraExperimentListJsonParser { + suspend fun parseJson(json: String?): Packages +} + +@ContributesBinding(AppScope::class) +class AuraExperimentListJsonParserImpl @Inject constructor() : AuraExperimentListJsonParser { + + private val jsonAdapter by lazy { buildJsonAdapter() } + + private fun buildJsonAdapter(): JsonAdapter { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + return moshi.adapter(SettingsJson::class.java) + } + + override suspend fun parseJson(json: String?): Packages { + if (json == null) return Packages() + + return kotlin.runCatching { + val parsed = jsonAdapter.fromJson(json) + return parsed?.asPackages() ?: Packages() + }.getOrDefault(Packages()) + } + + private fun SettingsJson.asPackages(): Packages { + return Packages(packages.map { it }) + } + + private data class SettingsJson( + val packages: List, + ) +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt new file mode 100644 index 000000000000..1d1bb03b98fa --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.aura + +import com.duckduckgo.app.aura.AuraExperimentManager +import com.duckduckgo.app.referral.AppReferrerDataStore +import com.duckduckgo.app.statistics.store.StatisticsDataStore +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.installation.impl.installer.InstallSourceExtractor +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class AuraExperimentManagerImpl @Inject constructor( + private val auraExperimentFeature: AuraExperimentFeature, + private val auraExperimentListJsonParser: AuraExperimentListJsonParser, + private val installSourceExtractor: InstallSourceExtractor, + private val statisticsDataStore: StatisticsDataStore, + private val appReferrerDataStore: AppReferrerDataStore, +) : AuraExperimentManager { + + override suspend fun initialize() { + if (auraExperimentFeature.self().isEnabled()) { + installSourceExtractor.extract()?.let { source -> + val settings = auraExperimentFeature.self().getSettings() + val packages = auraExperimentListJsonParser.parseJson(settings).list + if (packages.contains(source)) { + if (statisticsDataStore.variant == RETURNING_USER) { + appReferrerDataStore.returningUser = true + } + statisticsDataStore.variant = VARIANT + appReferrerDataStore.utmOriginAttributeCampaign = ORIGIN + } + } + } + } + + companion object { + const val VARIANT = "mq" + const val RETURNING_USER = "ru" + const val ORIGIN = "funnel_app_aurapaid_android" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt b/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt index 9c4f3873da8d..d3280710d5ff 100644 --- a/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt @@ -29,6 +29,7 @@ interface AppReferrerDataStore { var campaignSuffix: String? var installedFromEuAuction: Boolean var utmOriginAttributeCampaign: String? + var returningUser: Boolean } @ContributesBinding(AppScope::class) @@ -50,6 +51,10 @@ class AppReferenceSharePreferences @Inject constructor(private val context: Cont get() = preferences.getBoolean(KEY_INSTALLED_FROM_EU_AUCTION, false) set(value) = preferences.edit(true) { putBoolean(KEY_INSTALLED_FROM_EU_AUCTION, value) } + override var returningUser: Boolean + get() = preferences.getBoolean(KEY_RETURNING_USER, false) + set(value) = preferences.edit(true) { putBoolean(KEY_RETURNING_USER, value) } + private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } companion object { @@ -58,5 +63,6 @@ class AppReferenceSharePreferences @Inject constructor(private val context: Cont private const val KEY_ORIGIN_ATTRIBUTE_CAMPAIGN = "KEY_ORIGIN_ATTRIBUTE_CAMPAIGN" private const val KEY_CHECKED_PREVIOUSLY = "KEY_CHECKED_PREVIOUSLY" private const val KEY_INSTALLED_FROM_EU_AUCTION = "KEY_INSTALLED_FROM_EU_AUCTION" + private const val KEY_RETURNING_USER = "KEY_RETURNING_USER" } } diff --git a/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt b/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt index 2f8604593a0f..1f520ab0ae70 100644 --- a/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt +++ b/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt @@ -58,7 +58,7 @@ class AppReferrerInstallPixelSender @Inject constructor( } private fun sendOriginAttribute(originAttribute: String?) { - val returningUser = statisticsDataStore.variant == RETURNING_USER_VARIANT + val returningUser = statisticsDataStore.variant == RETURNING_USER_VARIANT || appReferrerDataStore.returningUser val params = mutableMapOf( PIXEL_PARAM_LOCALE to appBuildConfig.deviceLocale.toLanguageTag(), diff --git a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt new file mode 100644 index 000000000000..5be925a5ba59 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt @@ -0,0 +1,52 @@ +package com.duckduckgo.app.browser.aura + +import com.duckduckgo.common.test.FileUtilities +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Test + +class AuraExperimentListJsonParserImplTest { + + private val testee = AuraExperimentListJsonParserImpl() + + @Test + fun whenGibberishInputThenReturnsReturnsEmptyPackages() = runTest { + val result = testee.parseJson("invalid json") + assertTrue(result.list.isEmpty()) + } + + @Test + fun whenInstallerListIsMissingFieldThenReturnsEmptyPackages() = runTest { + val result = testee.parseJson("{}") + assertTrue(result.list.isEmpty()) + } + + @Test + fun whenInstallerListIsEmptyThenReturnsEmptyPackages() = runTest { + val result = testee.parseJson("auraExperiment_emptyList".loadJsonFile()) + assertTrue(result.list.isEmpty()) + } + + @Test + fun whenInstallerListHasSingleEntryThenReturnsSinglePackage() = runTest { + val result = testee.parseJson("auraExperiment_singleEntryList".loadJsonFile()) + assertEquals(1, result.list.size) + assertEquals("a.b.c", result.list[0]) + } + + @Test + fun whenInstallerListHasMultipleEntriesThenReturnsMultiplePackages() = runTest { + val result = testee.parseJson("auraExperiment_multipleEntryList".loadJsonFile()) + assertEquals(3, result.list.size) + assertEquals("a.b.c", result.list[0]) + assertEquals("d.e.f", result.list[1]) + assertEquals("g.h.i", result.list[2]) + } + + private fun String.loadJsonFile(): String { + return FileUtilities.loadText( + AuraExperimentListJsonParserImplTest::class.java.classLoader!!, + "json/$this.json", + ) + } +} diff --git a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt new file mode 100644 index 000000000000..77d2f7089ace --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt @@ -0,0 +1,91 @@ +package com.duckduckgo.app.browser.aura + +import com.duckduckgo.app.referral.AppReferrerDataStore +import com.duckduckgo.app.statistics.store.StatisticsDataStore +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.installation.impl.installer.InstallSourceExtractor +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.* + +class AuraExperimentManagerImplTest { + + private val auraExperimentFeature: AuraExperimentFeature = mock() + private val auraExperimentListJsonParser: AuraExperimentListJsonParser = mock() + private val installSourceExtractor: InstallSourceExtractor = mock() + private val statisticsDataStore: StatisticsDataStore = mock() + private val appReferrerDataStore: AppReferrerDataStore = mock() + private val toggle: Toggle = mock() + + private lateinit var testee: AuraExperimentManagerImpl + + @Before + fun setup() { + testee = AuraExperimentManagerImpl( + auraExperimentFeature, + auraExperimentListJsonParser, + installSourceExtractor, + statisticsDataStore, + appReferrerDataStore, + ) + whenever(auraExperimentFeature.self()).thenReturn(toggle) + } + + @Test + fun whenFeatureIsDisabledThenInitializeDoesNothing() = runTest { + whenever(toggle.isEnabled()).thenReturn(false) + + testee.initialize() + + verifyNoInteractions(auraExperimentListJsonParser, installSourceExtractor, statisticsDataStore, appReferrerDataStore) + } + + @Test + fun whenFeatureIsEnabledAndInstallSourceIsNullThenInitializeDoesNothing() = runTest { + whenever(toggle.isEnabled()).thenReturn(true) + whenever(installSourceExtractor.extract()).thenReturn(null) + + testee.initialize() + + verifyNoInteractions(auraExperimentListJsonParser, statisticsDataStore, appReferrerDataStore) + } + + @Test + fun whenFeatureIsEnabledAndInstallSourceNotInPackagesThenInitializeDoesNothing() = runTest { + whenever(toggle.isEnabled()).thenReturn(true) + whenever(installSourceExtractor.extract()).thenReturn("x.y.z") + whenever(toggle.getSettings()).thenReturn("json") + whenever(auraExperimentListJsonParser.parseJson("json")).thenReturn(Packages(list = listOf("a.b.c"))) + + testee.initialize() + + verifyNoInteractions(statisticsDataStore, appReferrerDataStore) + } + + @Test + fun whenFeatureIsEnabledAndInstallSourceInPackagesThenSetVariantAndOrigin() = runTest { + whenever(toggle.isEnabled()).thenReturn(true) + whenever(installSourceExtractor.extract()).thenReturn("a.b.c") + whenever(toggle.getSettings()).thenReturn("json") + whenever(auraExperimentListJsonParser.parseJson("json")).thenReturn(Packages(list = listOf("a.b.c"))) + + testee.initialize() + + verify(statisticsDataStore).variant = AuraExperimentManagerImpl.VARIANT + verify(appReferrerDataStore).utmOriginAttributeCampaign = AuraExperimentManagerImpl.ORIGIN + } + + @Test + fun whenReturningUserThenSetsReturningUserFlag() = runTest { + whenever(toggle.isEnabled()).thenReturn(true) + whenever(installSourceExtractor.extract()).thenReturn("a.b.c") + whenever(toggle.getSettings()).thenReturn("valid_json") + whenever(auraExperimentListJsonParser.parseJson("valid_json")).thenReturn(Packages(list = listOf("a.b.c"))) + whenever(statisticsDataStore.variant).thenReturn(AuraExperimentManagerImpl.RETURNING_USER) + + testee.initialize() + + verify(appReferrerDataStore).returningUser = true + } +} diff --git a/app/src/test/resources/json/auraExperiment_emptyList.json b/app/src/test/resources/json/auraExperiment_emptyList.json new file mode 100644 index 000000000000..897f681263f5 --- /dev/null +++ b/app/src/test/resources/json/auraExperiment_emptyList.json @@ -0,0 +1,3 @@ +{ + "packages": [] +} \ No newline at end of file diff --git a/app/src/test/resources/json/auraExperiment_multipleEntryList.json b/app/src/test/resources/json/auraExperiment_multipleEntryList.json new file mode 100644 index 000000000000..86824478ca39 --- /dev/null +++ b/app/src/test/resources/json/auraExperiment_multipleEntryList.json @@ -0,0 +1,7 @@ +{ + "packages": [ + "a.b.c", + "d.e.f", + "g.h.i" + ] +} \ No newline at end of file diff --git a/app/src/test/resources/json/auraExperiment_singleEntryList.json b/app/src/test/resources/json/auraExperiment_singleEntryList.json new file mode 100644 index 000000000000..d2e903b75995 --- /dev/null +++ b/app/src/test/resources/json/auraExperiment_singleEntryList.json @@ -0,0 +1,3 @@ +{ + "packages": ["a.b.c"] +} \ No newline at end of file diff --git a/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt b/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt index 6746f2eb13bd..9f436a6b969d 100644 --- a/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt +++ b/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt @@ -67,6 +67,14 @@ class AppReferrerInstallPixelSenderTest { verifyCorrectPixelSent("foo", returningUser = true) } + @Test + fun whenBothUserCheckAndReferrerExtractionFinishedForAuraReturningUserThenPixelSent() = runTest { + configureAsAuraReturningUser() + configureReferrerCampaign("foo") + testee.onAppAtbInitialized() + verifyCorrectPixelSent("foo", returningUser = true) + } + @Test fun whenBothUserCheckAndReferrerExtractionFinishedForNewUserThenPixelSent() = runTest { configureAsNewUser() @@ -89,6 +97,10 @@ class AppReferrerInstallPixelSenderTest { whenever(statisticsDataStore.variant).thenReturn("ru") } + private fun configureAsAuraReturningUser() { + whenever(appReferrerDataStore.returningUser).thenReturn(true) + } + private fun configureAsNewUser() { whenever(statisticsDataStore.variant).thenReturn("") } diff --git a/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt b/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt new file mode 100644 index 000000000000..d386474a56fa --- /dev/null +++ b/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.aura + +interface AuraExperimentManager { + suspend fun initialize() +} diff --git a/installation/installation-api/.gitignore b/installation/installation-api/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/installation/installation-api/build.gradle b/installation/installation-api/build.gradle new file mode 100644 index 000000000000..3f7a685dccde --- /dev/null +++ b/installation/installation-api/build.gradle @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'java-library' + id 'kotlin' +} + +apply from: "$rootProject.projectDir/code-formatting.gradle" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + implementation Kotlin.stdlib.jdk7 +} diff --git a/installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt b/installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt new file mode 100644 index 000000000000..cd853486527d --- /dev/null +++ b/installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.installation.impl.installer + +interface InstallSourceExtractor { + fun extract(): String? +} diff --git a/installation/installation-impl/build.gradle b/installation/installation-impl/build.gradle index 533afed819ef..f21477dd2925 100644 --- a/installation/installation-impl/build.gradle +++ b/installation/installation-impl/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation project(path: ':di') implementation project(path: ':common-utils') implementation project(path: ':statistics-api') + implementation project(path: ':installation-api') implementation AndroidX.core.ktx implementation KotlinX.coroutines.core implementation Google.dagger diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt similarity index 96% rename from installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt rename to installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt index 7c0ba0a27756..b88e9e424789 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt @@ -25,10 +25,6 @@ import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject -interface InstallSourceExtractor { - fun extract(): String? -} - @ContributesBinding(AppScope::class) class RealInstallSourceExtractor @Inject constructor( private val context: Context, diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt index 700533d76f1d..1b7ab01b956d 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt @@ -18,6 +18,7 @@ package com.duckduckgo.app.statistics import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner +import com.duckduckgo.app.aura.AuraExperimentManager import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.statistics.api.StatisticsUpdater @@ -49,6 +50,7 @@ class AtbInitializer @Inject constructor( private val statisticsUpdater: StatisticsUpdater, private val listeners: DaggerSet, private val dispatcherProvider: DispatcherProvider, + private val auraExperimentManager: AuraExperimentManager, ) : MainProcessLifecycleObserver, PrivacyConfigCallbackPlugin { override fun onResume(owner: LifecycleOwner) { @@ -73,8 +75,11 @@ class AtbInitializer @Inject constructor( override fun onPrivacyConfigDownloaded() { if (!statisticsDataStore.hasInstallationStatistics) { - // First time we initializeAtb - statisticsUpdater.initializeAtb() + appCoroutineScope.launch(dispatcherProvider.io()) { + auraExperimentManager.initialize() + // First time we initializeAtb + statisticsUpdater.initializeAtb() + } } } } diff --git a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt index a346fcb8f985..195f88b0622f 100644 --- a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt +++ b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.statistics +import com.duckduckgo.app.aura.AuraExperimentManager import com.duckduckgo.app.statistics.api.StatisticsUpdater import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.common.test.CoroutineTestRule @@ -25,6 +26,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -38,6 +40,7 @@ class AtbInitializerTest { private val statisticsDataStore: StatisticsDataStore = mock() private val statisticsUpdater: StatisticsUpdater = mock() + private val auraExperimentManager: AuraExperimentManager = mock() private var atbInitializerListener = FakeAtbInitializerListener() @Test @@ -59,6 +62,7 @@ class AtbInitializerTest { statisticsUpdater, setOf(atbInitializerListener), coroutineRule.testDispatcherProvider, + auraExperimentManager, ) testee.onPrivacyConfigDownloaded() @@ -76,6 +80,7 @@ class AtbInitializerTest { statisticsUpdater, setOf(atbInitializerListener), coroutineRule.testDispatcherProvider, + auraExperimentManager, ) testee.initialize() @@ -102,12 +107,15 @@ class AtbInitializerTest { } @Test - fun givenNeverInstallationStatisticsWhenOnPrivacyConfigDownloadedThenAtbInitialized() = runTest { + fun givenNeverInstallationStatisticsWhenOnPrivacyConfigDownloadedThenAuraExperimentAndAtbInitialized() = runTest { configureNeverInitialized() testee.onPrivacyConfigDownloaded() - verify(statisticsUpdater).initializeAtb() + inOrder(auraExperimentManager, statisticsUpdater) { + verify(auraExperimentManager).initialize() + verify(statisticsUpdater).initializeAtb() + } } private fun configureNeverInitialized() { @@ -118,6 +126,7 @@ class AtbInitializerTest { statisticsUpdater, setOf(atbInitializerListener), coroutineRule.testDispatcherProvider, + auraExperimentManager, ) } @@ -129,6 +138,7 @@ class AtbInitializerTest { statisticsUpdater, setOf(atbInitializerListener), coroutineRule.testDispatcherProvider, + auraExperimentManager, ) } } From 605ab51bcb68ab255d50a52d758faf4ec5996f22 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 10:23:46 +0000 Subject: [PATCH 02/16] Update string --- .../app/browser/aura/AuraExperimentManagerImplTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt index 77d2f7089ace..1cdb81a6c2fb 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt @@ -80,8 +80,8 @@ class AuraExperimentManagerImplTest { fun whenReturningUserThenSetsReturningUserFlag() = runTest { whenever(toggle.isEnabled()).thenReturn(true) whenever(installSourceExtractor.extract()).thenReturn("a.b.c") - whenever(toggle.getSettings()).thenReturn("valid_json") - whenever(auraExperimentListJsonParser.parseJson("valid_json")).thenReturn(Packages(list = listOf("a.b.c"))) + whenever(toggle.getSettings()).thenReturn("json") + whenever(auraExperimentListJsonParser.parseJson("json")).thenReturn(Packages(list = listOf("a.b.c"))) whenever(statisticsDataStore.variant).thenReturn(AuraExperimentManagerImpl.RETURNING_USER) testee.initialize() From 695306add419a8bde9b3228e0ed901ae70e73ada Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 10:41:56 +0000 Subject: [PATCH 03/16] Fix issues --- .../java/com/duckduckgo/app/di/StubStatisticsModule.kt | 4 +++- .../installation/impl/installer/RealInstallSourceExtractor.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt b/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt index e86800fba459..f5a67a9dc89e 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt @@ -17,6 +17,7 @@ package com.duckduckgo.app.di import android.content.Context +import com.duckduckgo.app.aura.AuraExperimentManager import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.statistics.AtbInitializer import com.duckduckgo.app.statistics.AtbInitializerListener @@ -117,8 +118,9 @@ class StubStatisticsModule { statisticsUpdater: StatisticsUpdater, listeners: DaggerSet, dispatcherProvider: DispatcherProvider, + auraExperimentManager: AuraExperimentManager, ): MainProcessLifecycleObserver { - return AtbInitializer(appCoroutineScope, statisticsDataStore, statisticsUpdater, listeners, dispatcherProvider) + return AtbInitializer(appCoroutineScope, statisticsDataStore, statisticsUpdater, listeners, dispatcherProvider, auraExperimentManager) } @Provides diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt index b88e9e424789..2ab54973e30f 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt @@ -33,7 +33,7 @@ class RealInstallSourceExtractor @Inject constructor( @SuppressLint("NewApi") override fun extract(): String? { - return if (appBuildConfig.sdkInt >= VERSION_CODES.R) { + return if (appBuildConfig.sdkInt >= 30) { installationSourceModern(context.packageName) } else { installationSourceLegacy(context.packageName) From 86ace433c6e6913f3d0d2a1d49acaa4ef96ff4b1 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 11:22:34 +0000 Subject: [PATCH 04/16] Fix lint issue --- .../installation/impl/installer/RealInstallSourceExtractor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt index 2ab54973e30f..0db6b9b9aed4 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt @@ -45,7 +45,7 @@ class RealInstallSourceExtractor @Inject constructor( return context.packageManager.getInstallerPackageName(packageName) } - @RequiresApi(VERSION_CODES.R) + @RequiresApi(30) private fun installationSourceModern(packageName: String): String? { return context.packageManager.getInstallSourceInfo(packageName).installingPackageName } From 70c4f2fec759f9029341aaa0a3c4c21e8cc68c36 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 11:24:12 +0000 Subject: [PATCH 05/16] Remove unused import --- .../installation/impl/installer/RealInstallSourceExtractor.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt index 0db6b9b9aed4..8c0f070a0ce0 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt @@ -18,7 +18,6 @@ package com.duckduckgo.installation.impl.installer import android.annotation.SuppressLint import android.content.Context -import android.os.Build.VERSION_CODES import androidx.annotation.RequiresApi import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope From ae8658c2e1df715644228543073917b589a6df71 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 11:54:06 +0000 Subject: [PATCH 06/16] Comment public interfaces --- .../app/browser/aura/AuraExperimentManagerImpl.kt | 2 +- .../app/browser/aura/AuraExperimentManagerImplTest.kt | 2 +- .../java/com/duckduckgo/app/aura/AuraExperimentManager.kt | 6 ++++++ .../{impl => api}/installer/InstallSourceExtractor.kt | 7 ++++++- .../impl/installer/InstallSourcePrivacyConfigObserver.kt | 1 + .../impl/installer/RealInstallSourceExtractor.kt | 1 + 6 files changed, 16 insertions(+), 3 deletions(-) rename installation/installation-api/src/main/java/com/duckduckgo/installation/{impl => api}/installer/InstallSourceExtractor.kt (77%) diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt index 1d1bb03b98fa..c48282976a66 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt @@ -20,7 +20,7 @@ import com.duckduckgo.app.aura.AuraExperimentManager import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.installation.impl.installer.InstallSourceExtractor +import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn import javax.inject.Inject diff --git a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt index 1cdb81a6c2fb..4b46ceea9359 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt @@ -3,7 +3,7 @@ package com.duckduckgo.app.browser.aura import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.feature.toggles.api.Toggle -import com.duckduckgo.installation.impl.installer.InstallSourceExtractor +import com.duckduckgo.installation.api.installer.InstallSourceExtractor import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test diff --git a/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt b/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt index d386474a56fa..471af70c1148 100644 --- a/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt +++ b/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt @@ -16,6 +16,12 @@ package com.duckduckgo.app.aura +/** Public interface for AuraExperimentManagerImpl */ interface AuraExperimentManager { + + /** + * Initializes the AuraExperimentManager. + * This checks if the install source is an Aura package, setting the ATB variant and origin accordingly. + */ suspend fun initialize() } diff --git a/installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt b/installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt similarity index 77% rename from installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt rename to installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt index cd853486527d..f1a92c8c7908 100644 --- a/installation/installation-api/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourceExtractor.kt +++ b/installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt @@ -14,8 +14,13 @@ * limitations under the License. */ -package com.duckduckgo.installation.impl.installer +package com.duckduckgo.installation.api.installer +/** Public interface for RealInstallSourceExtractor */ interface InstallSourceExtractor { + + /** + * Extracts the installer package name from the PackageManager. + */ fun extract(): String? } diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt index 2e35c732ffb7..b71f897b9259 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt @@ -27,6 +27,7 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_FULL_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt index 8c0f070a0ce0..5d2234a60a88 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt @@ -21,6 +21,7 @@ import android.content.Context import androidx.annotation.RequiresApi import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject From 68ddd17386801459807101bccef7605ee4fad5d5 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Wed, 4 Dec 2024 12:18:45 +0000 Subject: [PATCH 07/16] Fix test --- .../src/test/java/InstallSourcePrivacyConfigObserverTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt b/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt index d7a0f3046b29..607acf99eeae 100644 --- a/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt +++ b/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt @@ -23,6 +23,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_FULL_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore From b911d7c1161376fdaa4cd07503b83deeb003dfd1 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 14:50:46 +0000 Subject: [PATCH 08/16] Adjust to use AppBuildConfig.isAppReinstall and AtbInitializer --- .../appbuildconfig/api/AppBuildConfig.kt | 5 ++ app/build.gradle | 1 - .../app/buildconfig/RealAppBuildConfig.kt | 11 ++++ .../params/StatisticsAdditionalPixelParams.kt | 6 +- .../app/referral/AppReferrerDataStore.kt | 33 +++++++--- .../referral/AppReferrerInstallPixelSender.kt | 8 +-- ...tatisticsAdditionalPixelParamPluginTest.kt | 14 ++-- .../AppReferrerInstallPixelSenderTest.kt | 27 +++----- .../app/aura/AuraExperimentManager.kt | 27 -------- .../browser/api/referrer/AppReferrer.kt | 10 +-- .../impl/reinstalls/ReinstallAtbListener.kt | 2 + installation/installation-api/.gitignore | 0 installation/installation-api/build.gradle | 35 ---------- installation/installation-impl/build.gradle | 2 +- .../InstallSourcePrivacyConfigObserver.kt | 1 - .../installer/RealInstallSourceExtractor.kt | 9 ++- .../installer}/aura/AuraExperimentFeature.kt | 2 +- .../aura/AuraExperimentListJsonParser.kt | 2 +- .../aura/AuraExperimentManagerImpl.kt | 37 ++++++----- .../InstallSourcePrivacyConfigObserverTest.kt | 1 - .../AuraExperimentListJsonParserImplTest.kt | 19 +++++- .../aura/AuraExperimentManagerImplTest.kt | 65 +++++++++++-------- .../json/auraExperiment_emptyList.json | 0 .../auraExperiment_multipleEntryList.json | 0 .../json/auraExperiment_singleEntryList.json | 0 .../app/statistics/AtbInitializerListener.kt | 5 ++ statistics/statistics-impl/build.gradle | 1 + .../app/statistics/AtbInitializer.kt | 33 +++++----- .../user_segments/SegmentCalculation.kt | 8 +-- .../app/statistics/AtbInitializerTest.kt | 37 ++++++----- .../user_segments/SegmentCalculationTest.kt | 13 ++-- 31 files changed, 209 insertions(+), 205 deletions(-) delete mode 100644 browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt rename installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt => browser-api/src/main/java/com/duckduckgo/browser/api/referrer/AppReferrer.kt (72%) delete mode 100644 installation/installation-api/.gitignore delete mode 100644 installation/installation-api/build.gradle rename {app/src/main/java/com/duckduckgo/app/browser => installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer}/aura/AuraExperimentFeature.kt (94%) rename {app/src/main/java/com/duckduckgo/app/browser => installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer}/aura/AuraExperimentListJsonParser.kt (97%) rename {app/src/main/java/com/duckduckgo/app/browser => installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer}/aura/AuraExperimentManagerImpl.kt (62%) rename {app/src/test/java/com/duckduckgo/app/browser => installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer}/aura/AuraExperimentListJsonParserImplTest.kt (68%) rename {app/src/test/java/com/duckduckgo/app/browser => installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer}/aura/AuraExperimentManagerImplTest.kt (57%) rename {app => installation/installation-impl}/src/test/resources/json/auraExperiment_emptyList.json (100%) rename {app => installation/installation-impl}/src/test/resources/json/auraExperiment_multipleEntryList.json (100%) rename {app => installation/installation-impl}/src/test/resources/json/auraExperiment_singleEntryList.json (100%) diff --git a/app-build-config/app-build-config-api/src/main/java/com/duckduckgo/appbuildconfig/api/AppBuildConfig.kt b/app-build-config/app-build-config-api/src/main/java/com/duckduckgo/appbuildconfig/api/AppBuildConfig.kt index 4d6f63edf828..b7ac9272df66 100644 --- a/app-build-config/app-build-config-api/src/main/java/com/duckduckgo/appbuildconfig/api/AppBuildConfig.kt +++ b/app-build-config/app-build-config-api/src/main/java/com/duckduckgo/appbuildconfig/api/AppBuildConfig.kt @@ -38,6 +38,11 @@ interface AppBuildConfig { * You should call [variantName] in a background thread */ val variantName: String? + + /** + * @return `true` if the user re-installed the app, `false` otherwise + */ + suspend fun isAppReinstall(): Boolean } enum class BuildFlavor { diff --git a/app/build.gradle b/app/build.gradle index 10c27ab67b38..6dfdeba02996 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -358,7 +358,6 @@ dependencies { implementation project(':runtime-checks-impl') implementation project(':runtime-checks-store') - implementation project(':installation-api') implementation project(':installation-impl') implementation project(':internal-features-api') diff --git a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt index 9c95226d5bb5..daf69f09c0b8 100644 --- a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt +++ b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt @@ -17,9 +17,11 @@ package com.duckduckgo.app.buildconfig import android.os.Build +import android.os.Environment import com.duckduckgo.app.browser.BuildConfig import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.appbuildconfig.api.BuildFlavor +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.experiments.api.VariantManager import com.squareup.anvil.annotations.ContributesBinding @@ -27,10 +29,12 @@ import dagger.Lazy import java.lang.IllegalStateException import java.util.* import javax.inject.Inject +import kotlinx.coroutines.withContext @ContributesBinding(AppScope::class) class RealAppBuildConfig @Inject constructor( private val variantManager: Lazy, // break any possible DI dependency cycle + private val dispatcherProvider: DispatcherProvider, ) : AppBuildConfig { override val isDebug: Boolean = BuildConfig.DEBUG override val applicationId: String = BuildConfig.APPLICATION_ID @@ -65,6 +69,13 @@ class RealAppBuildConfig @Inject constructor( override val variantName: String? get() = variantManager.get().getVariantKey() + override suspend fun isAppReinstall(): Boolean = withContext(dispatcherProvider.io()) { + return@withContext kotlin.runCatching { + val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + downloadDirectory.exists() + }.getOrDefault(false) + } + override val buildDateTimeMillis: Long get() = BuildConfig.BUILD_DATE_MILLIS } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParams.kt b/app/src/main/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParams.kt index 00102805b37f..fe042e20bef0 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParams.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParams.kt @@ -16,17 +16,17 @@ package com.duckduckgo.app.pixels.campaign.params -import com.duckduckgo.app.statistics.store.StatisticsDataStore +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject @ContributesMultibinding(AppScope::class) class ReinstallAdditionalPixelParamPlugin @Inject constructor( - private val statisticsDataStore: StatisticsDataStore, + private val appBuildConfig: AppBuildConfig, ) : AdditionalPixelParamPlugin { override suspend fun params(): Pair = Pair( "isReinstall", - "${statisticsDataStore.variant == "ru"}", + "${appBuildConfig.isAppReinstall()}", ) } diff --git a/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt b/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt index d3280710d5ff..5810c911f75b 100644 --- a/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/referral/AppReferrerDataStore.kt @@ -19,22 +19,44 @@ package com.duckduckgo.app.referral import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.browser.api.referrer.AppReferrer +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch interface AppReferrerDataStore { var referrerCheckedPreviously: Boolean var campaignSuffix: String? var installedFromEuAuction: Boolean var utmOriginAttributeCampaign: String? - var returningUser: Boolean } -@ContributesBinding(AppScope::class) +@ContributesBinding( + scope = AppScope::class, + boundType = AppReferrerDataStore::class, +) +@ContributesBinding( + scope = AppScope::class, + boundType = AppReferrer::class, +) @SingleInstanceIn(AppScope::class) -class AppReferenceSharePreferences @Inject constructor(private val context: Context) : AppReferrerDataStore { +class AppReferenceSharePreferences @Inject constructor( + private val context: Context, + @AppCoroutineScope private val coroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, +) : AppReferrerDataStore, AppReferrer { + + override fun setOriginAttributeCampaign(origin: String?) { + coroutineScope.launch(dispatcherProvider.io()) { + utmOriginAttributeCampaign = origin + } + } + override var campaignSuffix: String? get() = preferences.getString(KEY_CAMPAIGN_SUFFIX, null) set(value) = preferences.edit(true) { putString(KEY_CAMPAIGN_SUFFIX, value) } @@ -51,10 +73,6 @@ class AppReferenceSharePreferences @Inject constructor(private val context: Cont get() = preferences.getBoolean(KEY_INSTALLED_FROM_EU_AUCTION, false) set(value) = preferences.edit(true) { putBoolean(KEY_INSTALLED_FROM_EU_AUCTION, value) } - override var returningUser: Boolean - get() = preferences.getBoolean(KEY_RETURNING_USER, false) - set(value) = preferences.edit(true) { putBoolean(KEY_RETURNING_USER, value) } - private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } companion object { @@ -63,6 +81,5 @@ class AppReferenceSharePreferences @Inject constructor(private val context: Cont private const val KEY_ORIGIN_ATTRIBUTE_CAMPAIGN = "KEY_ORIGIN_ATTRIBUTE_CAMPAIGN" private const val KEY_CHECKED_PREVIOUSLY = "KEY_CHECKED_PREVIOUSLY" private const val KEY_INSTALLED_FROM_EU_AUCTION = "KEY_INSTALLED_FROM_EU_AUCTION" - private const val KEY_RETURNING_USER = "KEY_RETURNING_USER" } } diff --git a/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt b/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt index 1f520ab0ae70..0a5da439feab 100644 --- a/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt +++ b/app/src/play/java/com/duckduckgo/referral/AppReferrerInstallPixelSender.kt @@ -21,7 +21,6 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope @@ -39,7 +38,6 @@ class AppReferrerInstallPixelSender @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, private val appBuildConfig: AppBuildConfig, - private val statisticsDataStore: StatisticsDataStore, ) : AtbLifecyclePlugin { private val pixelSent = AtomicBoolean(false) @@ -57,8 +55,8 @@ class AppReferrerInstallPixelSender @Inject constructor( } } - private fun sendOriginAttribute(originAttribute: String?) { - val returningUser = statisticsDataStore.variant == RETURNING_USER_VARIANT || appReferrerDataStore.returningUser + private suspend fun sendOriginAttribute(originAttribute: String?) { + val returningUser = appBuildConfig.isAppReinstall() val params = mutableMapOf( PIXEL_PARAM_LOCALE to appBuildConfig.deviceLocale.toLanguageTag(), @@ -74,8 +72,6 @@ class AppReferrerInstallPixelSender @Inject constructor( } companion object { - private const val RETURNING_USER_VARIANT = "ru" - const val PIXEL_PARAM_ORIGIN = "origin" const val PIXEL_PARAM_LOCALE = "locale" const val PIXEL_PARAM_RETURNING_USER = "reinstall" diff --git a/app/src/test/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParamPluginTest.kt b/app/src/test/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParamPluginTest.kt index 401dfc2cd61d..9b2d59879feb 100644 --- a/app/src/test/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParamPluginTest.kt +++ b/app/src/test/java/com/duckduckgo/app/pixels/campaign/params/StatisticsAdditionalPixelParamPluginTest.kt @@ -16,7 +16,7 @@ package com.duckduckgo.app.pixels.campaign.params -import com.duckduckgo.app.statistics.store.StatisticsDataStore +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Test @@ -24,20 +24,20 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever class StatisticsAdditionalPixelParamPluginTest { + private val appBuildConfig: AppBuildConfig = mock() + @Test fun whenRuVariantSetThenPluginShouldReturnParamTrue() = runTest { - val statisticsDataStore: StatisticsDataStore = mock() - whenever(statisticsDataStore.variant).thenReturn("ru") - val plugin = ReinstallAdditionalPixelParamPlugin(statisticsDataStore) + whenever(appBuildConfig.isAppReinstall()).thenReturn(true) + val plugin = ReinstallAdditionalPixelParamPlugin(appBuildConfig) Assert.assertEquals("isReinstall" to "true", plugin.params()) } @Test fun whenVariantIsNotRuThenPluginShouldReturnParamFalse() = runTest { - val statisticsDataStore: StatisticsDataStore = mock() - whenever(statisticsDataStore.variant).thenReturn("atb-1234") - val plugin = ReinstallAdditionalPixelParamPlugin(statisticsDataStore) + whenever(appBuildConfig.isAppReinstall()).thenReturn(false) + val plugin = ReinstallAdditionalPixelParamPlugin(appBuildConfig) Assert.assertEquals("isReinstall" to "false", plugin.params()) } diff --git a/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt b/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt index 9f436a6b969d..f3fe24e47264 100644 --- a/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt +++ b/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt @@ -4,7 +4,6 @@ import com.duckduckgo.app.pixels.AppPixelName.REFERRAL_INSTALL_UTM_CAMPAIGN import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Unique -import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.referral.AppReferrerInstallPixelSender @@ -13,6 +12,7 @@ import com.duckduckgo.referral.AppReferrerInstallPixelSender.Companion.PIXEL_PAR import com.duckduckgo.referral.AppReferrerInstallPixelSender.Companion.PIXEL_PARAM_RETURNING_USER import com.duckduckgo.verifiedinstallation.installsource.VerificationCheckPlayStoreInstall import java.util.* +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before @@ -35,7 +35,6 @@ class AppReferrerInstallPixelSenderTest { private val pixel: Pixel = mock() private val appBuildConfig: AppBuildConfig = mock() private val appReferrerDataStore: AppReferrerDataStore = mock() - private val statisticsDataStore: StatisticsDataStore = mock() private val playStoreInstallChecker: VerificationCheckPlayStoreInstall = mock() private val captor = argumentCaptor>() @@ -43,6 +42,9 @@ class AppReferrerInstallPixelSenderTest { fun setup() { whenever(appBuildConfig.deviceLocale).thenReturn(Locale.US) whenever(playStoreInstallChecker.installedFromPlayStore()).thenReturn(true) + runBlocking { + configureAsNewUser() + } } private val testee = AppReferrerInstallPixelSender( @@ -51,7 +53,6 @@ class AppReferrerInstallPixelSenderTest { appCoroutineScope = coroutineTestRule.testScope, dispatchers = coroutineTestRule.testDispatcherProvider, appBuildConfig = appBuildConfig, - statisticsDataStore = statisticsDataStore, ) @Test @@ -67,14 +68,6 @@ class AppReferrerInstallPixelSenderTest { verifyCorrectPixelSent("foo", returningUser = true) } - @Test - fun whenBothUserCheckAndReferrerExtractionFinishedForAuraReturningUserThenPixelSent() = runTest { - configureAsAuraReturningUser() - configureReferrerCampaign("foo") - testee.onAppAtbInitialized() - verifyCorrectPixelSent("foo", returningUser = true) - } - @Test fun whenBothUserCheckAndReferrerExtractionFinishedForNewUserThenPixelSent() = runTest { configureAsNewUser() @@ -93,16 +86,12 @@ class AppReferrerInstallPixelSenderTest { verifyNoMoreInteractions(pixel) } - private fun configureAsReturningUser() { - whenever(statisticsDataStore.variant).thenReturn("ru") - } - - private fun configureAsAuraReturningUser() { - whenever(appReferrerDataStore.returningUser).thenReturn(true) + private suspend fun configureAsReturningUser() { + whenever(appBuildConfig.isAppReinstall()).thenReturn(true) } - private fun configureAsNewUser() { - whenever(statisticsDataStore.variant).thenReturn("") + private suspend fun configureAsNewUser() { + whenever(appBuildConfig.isAppReinstall()).thenReturn(false) } private fun configureReferrerCampaign(campaign: String?) { diff --git a/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt b/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt deleted file mode 100644 index 471af70c1148..000000000000 --- a/browser-api/src/main/java/com/duckduckgo/app/aura/AuraExperimentManager.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.aura - -/** Public interface for AuraExperimentManagerImpl */ -interface AuraExperimentManager { - - /** - * Initializes the AuraExperimentManager. - * This checks if the install source is an Aura package, setting the ATB variant and origin accordingly. - */ - suspend fun initialize() -} diff --git a/installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/referrer/AppReferrer.kt similarity index 72% rename from installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt rename to browser-api/src/main/java/com/duckduckgo/browser/api/referrer/AppReferrer.kt index f1a92c8c7908..5af1d10b24fb 100644 --- a/installation/installation-api/src/main/java/com/duckduckgo/installation/api/installer/InstallSourceExtractor.kt +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/referrer/AppReferrer.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.duckduckgo.installation.api.installer +package com.duckduckgo.browser.api.referrer -/** Public interface for RealInstallSourceExtractor */ -interface InstallSourceExtractor { +/** Public interface for app referral parameters */ +interface AppReferrer { /** - * Extracts the installer package name from the PackageManager. + * Sets the attribute campaign origin. */ - fun extract(): String? + fun setOriginAttributeCampaign(origin: String?) } diff --git a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt index 508b6f9c203a..fc24341ad9d5 100644 --- a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt +++ b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.SharedPreferences import android.os.Build import androidx.core.content.edit +import com.duckduckgo.anvil.annotations.PriorityKey import com.duckduckgo.app.statistics.AtbInitializerListener import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.appbuildconfig.api.AppBuildConfig @@ -38,6 +39,7 @@ import timber.log.Timber @SingleInstanceIn(AppScope::class) @ContributesMultibinding(AppScope::class) +@PriorityKey(AtbInitializerListener.REINSTALL_LISTENER) class ReinstallAtbListener @Inject constructor( private val backupDataStore: BackupServiceDataStore, private val statisticsDataStore: StatisticsDataStore, diff --git a/installation/installation-api/.gitignore b/installation/installation-api/.gitignore deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/installation/installation-api/build.gradle b/installation/installation-api/build.gradle deleted file mode 100644 index 3f7a685dccde..000000000000 --- a/installation/installation-api/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -plugins { - id 'java-library' - id 'kotlin' -} - -apply from: "$rootProject.projectDir/code-formatting.gradle" - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -kotlin { - jvmToolchain(17) -} - -dependencies { - implementation Kotlin.stdlib.jdk7 -} diff --git a/installation/installation-impl/build.gradle b/installation/installation-impl/build.gradle index f21477dd2925..731d5f64dc22 100644 --- a/installation/installation-impl/build.gradle +++ b/installation/installation-impl/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation project(path: ':di') implementation project(path: ':common-utils') implementation project(path: ':statistics-api') - implementation project(path: ':installation-api') + implementation project(path: ':browser-api') implementation AndroidX.core.ktx implementation KotlinX.coroutines.core implementation Google.dagger diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt index b71f897b9259..2e35c732ffb7 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt @@ -27,7 +27,6 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_FULL_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt index 5d2234a60a88..e35c2f04bb23 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/RealInstallSourceExtractor.kt @@ -21,10 +21,17 @@ import android.content.Context import androidx.annotation.RequiresApi import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject +interface InstallSourceExtractor { + + /** + * Extracts the installer package name from the PackageManager. + */ + fun extract(): String? +} + @ContributesBinding(AppScope::class) class RealInstallSourceExtractor @Inject constructor( private val context: Context, diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentFeature.kt similarity index 94% rename from app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt rename to installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentFeature.kt index 2924c03d6d8d..27c43d7f2d3a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentFeature.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentFeature.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.browser.aura +package com.duckduckgo.installation.impl.installer.aura import com.duckduckgo.anvil.annotations.ContributesRemoteFeature import com.duckduckgo.di.scopes.AppScope diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt similarity index 97% rename from app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt rename to installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt index 0a7a7d01813f..37ee2110f3d8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParser.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.browser.aura +package com.duckduckgo.installation.impl.installer.aura import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding diff --git a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImpl.kt similarity index 62% rename from app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt rename to installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImpl.kt index c48282976a66..7204ca199b0f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImpl.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImpl.kt @@ -14,46 +14,53 @@ * limitations under the License. */ -package com.duckduckgo.app.browser.aura +package com.duckduckgo.installation.impl.installer.aura -import com.duckduckgo.app.aura.AuraExperimentManager -import com.duckduckgo.app.referral.AppReferrerDataStore +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.app.statistics.AtbInitializerListener import com.duckduckgo.app.statistics.store.StatisticsDataStore +import com.duckduckgo.browser.api.referrer.AppReferrer +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.installation.api.installer.InstallSourceExtractor -import com.squareup.anvil.annotations.ContributesBinding +import com.duckduckgo.installation.impl.installer.InstallSourceExtractor +import com.squareup.anvil.annotations.ContributesMultibinding import dagger.SingleInstanceIn import javax.inject.Inject +import kotlinx.coroutines.withContext -@ContributesBinding(AppScope::class) +@ContributesMultibinding(AppScope::class) +@PriorityKey(AtbInitializerListener.AURA_EXPERIMENT_MANAGER) @SingleInstanceIn(AppScope::class) class AuraExperimentManagerImpl @Inject constructor( private val auraExperimentFeature: AuraExperimentFeature, private val auraExperimentListJsonParser: AuraExperimentListJsonParser, private val installSourceExtractor: InstallSourceExtractor, private val statisticsDataStore: StatisticsDataStore, - private val appReferrerDataStore: AppReferrerDataStore, -) : AuraExperimentManager { + private val appReferrer: AppReferrer, + private val dispatcherProvider: DispatcherProvider, +) : AtbInitializerListener { - override suspend fun initialize() { + override suspend fun beforeAtbInit() { + initialize() + } + + override fun beforeAtbInitTimeoutMillis(): Long = MAX_WAIT_TIME_MS + + private suspend fun initialize() = withContext(dispatcherProvider.io()) { if (auraExperimentFeature.self().isEnabled()) { installSourceExtractor.extract()?.let { source -> val settings = auraExperimentFeature.self().getSettings() val packages = auraExperimentListJsonParser.parseJson(settings).list if (packages.contains(source)) { - if (statisticsDataStore.variant == RETURNING_USER) { - appReferrerDataStore.returningUser = true - } statisticsDataStore.variant = VARIANT - appReferrerDataStore.utmOriginAttributeCampaign = ORIGIN + appReferrer.setOriginAttributeCampaign(ORIGIN) } } } } - companion object { const val VARIANT = "mq" - const val RETURNING_USER = "ru" const val ORIGIN = "funnel_app_aurapaid_android" + const val MAX_WAIT_TIME_MS = 1_500L } } diff --git a/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt b/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt index 607acf99eeae..d7a0f3046b29 100644 --- a/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt +++ b/installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt @@ -23,7 +23,6 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State -import com.duckduckgo.installation.api.installer.InstallSourceExtractor import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_FULL_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_PACKAGE_NAME import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore diff --git a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt similarity index 68% rename from app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt rename to installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt index 5be925a5ba59..74128c5bdbd1 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentListJsonParserImplTest.kt +++ b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt @@ -1,6 +1,23 @@ -package com.duckduckgo.app.browser.aura +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.installation.impl.installer.com.duckduckgo.installation.impl.installer.aura import com.duckduckgo.common.test.FileUtilities +import com.duckduckgo.installation.impl.installer.aura.AuraExperimentListJsonParserImpl import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Test diff --git a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImplTest.kt similarity index 57% rename from app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt rename to installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImplTest.kt index 4b46ceea9359..9487a2331e23 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/aura/AuraExperimentManagerImplTest.kt +++ b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImplTest.kt @@ -1,21 +1,46 @@ -package com.duckduckgo.app.browser.aura +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.installation.impl.installer.com.duckduckgo.installation.impl.installer.aura -import com.duckduckgo.app.referral.AppReferrerDataStore import com.duckduckgo.app.statistics.store.StatisticsDataStore +import com.duckduckgo.browser.api.referrer.AppReferrer +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.Toggle -import com.duckduckgo.installation.api.installer.InstallSourceExtractor +import com.duckduckgo.installation.impl.installer.InstallSourceExtractor +import com.duckduckgo.installation.impl.installer.aura.AuraExperimentFeature +import com.duckduckgo.installation.impl.installer.aura.AuraExperimentListJsonParser +import com.duckduckgo.installation.impl.installer.aura.AuraExperimentManagerImpl +import com.duckduckgo.installation.impl.installer.aura.Packages import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.* class AuraExperimentManagerImplTest { + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + private val auraExperimentFeature: AuraExperimentFeature = mock() private val auraExperimentListJsonParser: AuraExperimentListJsonParser = mock() private val installSourceExtractor: InstallSourceExtractor = mock() private val statisticsDataStore: StatisticsDataStore = mock() - private val appReferrerDataStore: AppReferrerDataStore = mock() + private val appReferrer: AppReferrer = mock() private val toggle: Toggle = mock() private lateinit var testee: AuraExperimentManagerImpl @@ -27,7 +52,8 @@ class AuraExperimentManagerImplTest { auraExperimentListJsonParser, installSourceExtractor, statisticsDataStore, - appReferrerDataStore, + appReferrer, + coroutinesTestRule.testDispatcherProvider, ) whenever(auraExperimentFeature.self()).thenReturn(toggle) } @@ -36,9 +62,9 @@ class AuraExperimentManagerImplTest { fun whenFeatureIsDisabledThenInitializeDoesNothing() = runTest { whenever(toggle.isEnabled()).thenReturn(false) - testee.initialize() + testee.beforeAtbInit() - verifyNoInteractions(auraExperimentListJsonParser, installSourceExtractor, statisticsDataStore, appReferrerDataStore) + verifyNoInteractions(auraExperimentListJsonParser, installSourceExtractor, statisticsDataStore, appReferrer) } @Test @@ -46,9 +72,9 @@ class AuraExperimentManagerImplTest { whenever(toggle.isEnabled()).thenReturn(true) whenever(installSourceExtractor.extract()).thenReturn(null) - testee.initialize() + testee.beforeAtbInit() - verifyNoInteractions(auraExperimentListJsonParser, statisticsDataStore, appReferrerDataStore) + verifyNoInteractions(auraExperimentListJsonParser, statisticsDataStore, appReferrer) } @Test @@ -58,9 +84,9 @@ class AuraExperimentManagerImplTest { whenever(toggle.getSettings()).thenReturn("json") whenever(auraExperimentListJsonParser.parseJson("json")).thenReturn(Packages(list = listOf("a.b.c"))) - testee.initialize() + testee.beforeAtbInit() - verifyNoInteractions(statisticsDataStore, appReferrerDataStore) + verifyNoInteractions(statisticsDataStore, appReferrer) } @Test @@ -70,22 +96,9 @@ class AuraExperimentManagerImplTest { whenever(toggle.getSettings()).thenReturn("json") whenever(auraExperimentListJsonParser.parseJson("json")).thenReturn(Packages(list = listOf("a.b.c"))) - testee.initialize() + testee.beforeAtbInit() verify(statisticsDataStore).variant = AuraExperimentManagerImpl.VARIANT - verify(appReferrerDataStore).utmOriginAttributeCampaign = AuraExperimentManagerImpl.ORIGIN - } - - @Test - fun whenReturningUserThenSetsReturningUserFlag() = runTest { - whenever(toggle.isEnabled()).thenReturn(true) - whenever(installSourceExtractor.extract()).thenReturn("a.b.c") - whenever(toggle.getSettings()).thenReturn("json") - whenever(auraExperimentListJsonParser.parseJson("json")).thenReturn(Packages(list = listOf("a.b.c"))) - whenever(statisticsDataStore.variant).thenReturn(AuraExperimentManagerImpl.RETURNING_USER) - - testee.initialize() - - verify(appReferrerDataStore).returningUser = true + verify(appReferrer).setOriginAttributeCampaign(AuraExperimentManagerImpl.ORIGIN) } } diff --git a/app/src/test/resources/json/auraExperiment_emptyList.json b/installation/installation-impl/src/test/resources/json/auraExperiment_emptyList.json similarity index 100% rename from app/src/test/resources/json/auraExperiment_emptyList.json rename to installation/installation-impl/src/test/resources/json/auraExperiment_emptyList.json diff --git a/app/src/test/resources/json/auraExperiment_multipleEntryList.json b/installation/installation-impl/src/test/resources/json/auraExperiment_multipleEntryList.json similarity index 100% rename from app/src/test/resources/json/auraExperiment_multipleEntryList.json rename to installation/installation-impl/src/test/resources/json/auraExperiment_multipleEntryList.json diff --git a/app/src/test/resources/json/auraExperiment_singleEntryList.json b/installation/installation-impl/src/test/resources/json/auraExperiment_singleEntryList.json similarity index 100% rename from app/src/test/resources/json/auraExperiment_singleEntryList.json rename to installation/installation-impl/src/test/resources/json/auraExperiment_singleEntryList.json diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt index 42fa9ca95933..389fc485ed2d 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt @@ -23,4 +23,9 @@ interface AtbInitializerListener { /** @return the timeout in milliseconds after which [beforeAtbInit] will be stopped */ fun beforeAtbInitTimeoutMillis(): Long + + companion object { + const val REINSTALL_LISTENER = 10 + const val AURA_EXPERIMENT_MANAGER = 20 + } } diff --git a/statistics/statistics-impl/build.gradle b/statistics/statistics-impl/build.gradle index 48f83290a0ef..df3c7b874624 100644 --- a/statistics/statistics-impl/build.gradle +++ b/statistics/statistics-impl/build.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation Testing.junit4 testImplementation "org.mockito.kotlin:mockito-kotlin:_" + testImplementation "androidx.lifecycle:lifecycle-runtime-testing:_" testImplementation project(path: ':common-test') testImplementation project(':data-store-test') testImplementation CashApp.turbine diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt index 1b7ab01b956d..b3eaa93c34d9 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt @@ -16,15 +16,14 @@ package com.duckduckgo.app.statistics -import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner -import com.duckduckgo.app.aura.AuraExperimentManager +import com.duckduckgo.anvil.annotations.ContributesPluginPoint import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.statistics.api.StatisticsUpdater import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.DaggerSet +import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin import com.squareup.anvil.annotations.ContributesMultibinding @@ -48,26 +47,15 @@ class AtbInitializer @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val statisticsDataStore: StatisticsDataStore, private val statisticsUpdater: StatisticsUpdater, - private val listeners: DaggerSet, + private val listeners: PluginPoint, private val dispatcherProvider: DispatcherProvider, - private val auraExperimentManager: AuraExperimentManager, ) : MainProcessLifecycleObserver, PrivacyConfigCallbackPlugin { override fun onResume(owner: LifecycleOwner) { - appCoroutineScope.launch(dispatcherProvider.io()) { initialize() } + appCoroutineScope.launch(dispatcherProvider.io()) { refreshAppRetentionAtb() } } - @VisibleForTesting - suspend fun initialize() { - Timber.v("Initialize ATB") - listeners.forEach { - withTimeoutOrNull(it.beforeAtbInitTimeoutMillis()) { it.beforeAtbInit() } - } - - initializeAtb() - } - - private fun initializeAtb() { + private fun refreshAppRetentionAtb() { if (statisticsDataStore.hasInstallationStatistics) { statisticsUpdater.refreshAppRetentionAtb() } @@ -76,10 +64,19 @@ class AtbInitializer @Inject constructor( override fun onPrivacyConfigDownloaded() { if (!statisticsDataStore.hasInstallationStatistics) { appCoroutineScope.launch(dispatcherProvider.io()) { - auraExperimentManager.initialize() + Timber.v("Initialize ATB") + listeners.getPlugins().forEach { + withTimeoutOrNull(it.beforeAtbInitTimeoutMillis()) { it.beforeAtbInit() } + } // First time we initializeAtb statisticsUpdater.initializeAtb() } } } } + +@ContributesPluginPoint( + scope = AppScope::class, + boundType = AtbInitializerListener::class, +) +private interface AtbInitializerListenerTrigger diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculation.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculation.kt index c79a948c15d4..3efe38ac2ebc 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculation.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculation.kt @@ -19,9 +19,9 @@ package com.duckduckgo.app.statistics.user_segments import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.app.statistics.user_segments.SegmentCalculation.ActivityType import com.duckduckgo.app.statistics.user_segments.SegmentCalculation.UserSegment +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.experiments.api.VariantManager import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import kotlinx.coroutines.withContext @@ -90,11 +90,9 @@ interface SegmentCalculation { class RealSegmentCalculation @Inject constructor( private val dispatcherProvider: DispatcherProvider, private val store: StatisticsDataStore, - private val variantManager: VariantManager, + private val appBuildConfig: AppBuildConfig, ) : SegmentCalculation { - private val returningAtb: Boolean - get() = variantManager.getVariantKey() == "ru" private val cohortAtb: String by lazy { store.atb?.version!! } @@ -270,7 +268,7 @@ class RealSegmentCalculation @Inject constructor( } } - if (this.returningAtb && newSetAtb.asNumber() <= cohortAtb.asNumber() + 28) { + if (appBuildConfig.isAppReinstall() && newSetAtb.asNumber() <= cohortAtb.asNumber() + 28) { segments.add("reinstaller") } diff --git a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt index 195f88b0622f..9d8841b39578 100644 --- a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt +++ b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/AtbInitializerTest.kt @@ -16,17 +16,17 @@ package com.duckduckgo.app.statistics -import com.duckduckgo.app.aura.AuraExperimentManager +import androidx.lifecycle.testing.TestLifecycleOwner import com.duckduckgo.app.statistics.api.StatisticsUpdater import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.plugins.PluginPoint import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -40,8 +40,18 @@ class AtbInitializerTest { private val statisticsDataStore: StatisticsDataStore = mock() private val statisticsUpdater: StatisticsUpdater = mock() - private val auraExperimentManager: AuraExperimentManager = mock() private var atbInitializerListener = FakeAtbInitializerListener() + private val lifecycleOwner = TestLifecycleOwner() + private val listeners = object : PluginPoint { + override fun getPlugins(): Collection { + return setOf(atbInitializerListener) + } + } + private val emptyListeners = object : PluginPoint { + override fun getPlugins(): Collection { + return emptyList() + } + } @Test fun whenReferrerInformationInstantlyAvailableThenAtbInitialized() = runTest { @@ -60,9 +70,8 @@ class AtbInitializerTest { coroutineRule.testScope, statisticsDataStore, statisticsUpdater, - setOf(atbInitializerListener), + emptyListeners, coroutineRule.testDispatcherProvider, - auraExperimentManager, ) testee.onPrivacyConfigDownloaded() @@ -78,12 +87,11 @@ class AtbInitializerTest { coroutineRule.testScope, statisticsDataStore, statisticsUpdater, - setOf(atbInitializerListener), + listeners, coroutineRule.testDispatcherProvider, - auraExperimentManager, ) - testee.initialize() + testee.onResume(lifecycleOwner) verify(statisticsUpdater, never()).initializeAtb() } @@ -92,7 +100,7 @@ class AtbInitializerTest { fun whenAlreadyInitializedThenRefreshCalled() = runTest { configureAlreadyInitialized() - testee.initialize() + testee.onResume(lifecycleOwner) verify(statisticsUpdater).refreshAppRetentionAtb() } @@ -112,10 +120,7 @@ class AtbInitializerTest { testee.onPrivacyConfigDownloaded() - inOrder(auraExperimentManager, statisticsUpdater) { - verify(auraExperimentManager).initialize() - verify(statisticsUpdater).initializeAtb() - } + verify(statisticsUpdater).initializeAtb() } private fun configureNeverInitialized() { @@ -124,9 +129,8 @@ class AtbInitializerTest { coroutineRule.testScope, statisticsDataStore, statisticsUpdater, - setOf(atbInitializerListener), + listeners, coroutineRule.testDispatcherProvider, - auraExperimentManager, ) } @@ -136,9 +140,8 @@ class AtbInitializerTest { coroutineRule.testScope, statisticsDataStore, statisticsUpdater, - setOf(atbInitializerListener), + listeners, coroutineRule.testDispatcherProvider, - auraExperimentManager, ) } } diff --git a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt index 77d8be4c4f0b..5486dd16beff 100644 --- a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt +++ b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt @@ -6,13 +6,14 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.app.statistics.user_segments.SegmentCalculation.ActivityType.APP_USE import com.duckduckgo.app.statistics.user_segments.SegmentCalculation.ActivityType.SEARCH +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities.loadText import com.duckduckgo.data.store.api.FakeSharedPreferencesProvider -import com.duckduckgo.experiments.api.VariantManager import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.Assert.assertEquals @@ -53,7 +54,7 @@ class SegmentCalculationTest(private val input: TestInput) { private lateinit var usageHistory: UsageHistory private lateinit var atbStore: StatisticsDataStore - private val mockVariantManager = mock() + private val appBuildConfig = mock() private val mockPixel: Pixel = mock() private val crashLogger: CrashLogger = org.mockito.kotlin.mock() @@ -66,7 +67,7 @@ class SegmentCalculationTest(private val input: TestInput) { @Before fun setup() { atbStore = FakeStatisticsDataStore() - whenever(mockVariantManager.getVariantKey()).thenReturn(null) + runBlocking { whenever(appBuildConfig.isAppReinstall()).thenReturn(false) } usageHistory = SegmentStoreModule().provideSegmentStore( FakeSharedPreferencesProvider(), @@ -76,7 +77,7 @@ class SegmentCalculationTest(private val input: TestInput) { segmentCalculation = RealSegmentCalculation( coroutineTestRule.testDispatcherProvider, atbStore, - mockVariantManager, + appBuildConfig, ) userSegmentsPixelSender = UserSegmentsPixelSender( usageHistory, @@ -93,7 +94,7 @@ class SegmentCalculationTest(private val input: TestInput) { // prepping test atbStore.atb = Atb(input.client.atb.removeSuffix("ru")) if (input.client.atb.contains("ru")) { - whenever(mockVariantManager.getVariantKey()).thenReturn("ru") + whenever(appBuildConfig.isAppReinstall()).thenReturn(true) } input.client.usage.forEachIndexed { index, usage -> @@ -115,7 +116,7 @@ class SegmentCalculationTest(private val input: TestInput) { // prepping test atbStore.atb = Atb(input.client.atb.removeSuffix("ru")) if (input.client.atb.contains("ru")) { - whenever(mockVariantManager.getVariantKey()).thenReturn("ru") + whenever(appBuildConfig.isAppReinstall()).thenReturn(true) } var oldAtb: String = atbStore.atb!!.version input.client.usage.forEachIndexed { index, usage -> From 2f9446a5fa613f98ff81411e7c6f3c478844a568 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 15:14:41 +0000 Subject: [PATCH 09/16] Add withContext to parseJson --- .../installer/aura/AuraExperimentListJsonParser.kt | 14 +++++++++----- .../aura/AuraExperimentListJsonParserImplTest.kt | 7 ++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt index 37ee2110f3d8..d7e4670ef778 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParser.kt @@ -16,12 +16,14 @@ package com.duckduckgo.installation.impl.installer.aura +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import javax.inject.Inject +import kotlinx.coroutines.withContext data class Packages(val list: List = emptyList()) @@ -30,7 +32,9 @@ interface AuraExperimentListJsonParser { } @ContributesBinding(AppScope::class) -class AuraExperimentListJsonParserImpl @Inject constructor() : AuraExperimentListJsonParser { +class AuraExperimentListJsonParserImpl @Inject constructor( + private val dispatcherProvider: DispatcherProvider, +) : AuraExperimentListJsonParser { private val jsonAdapter by lazy { buildJsonAdapter() } @@ -39,12 +43,12 @@ class AuraExperimentListJsonParserImpl @Inject constructor() : AuraExperimentLis return moshi.adapter(SettingsJson::class.java) } - override suspend fun parseJson(json: String?): Packages { - if (json == null) return Packages() + override suspend fun parseJson(json: String?): Packages = withContext(dispatcherProvider.io()) { + if (json == null) return@withContext Packages() - return kotlin.runCatching { + kotlin.runCatching { val parsed = jsonAdapter.fromJson(json) - return parsed?.asPackages() ?: Packages() + parsed?.asPackages() ?: Packages() }.getOrDefault(Packages()) } diff --git a/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt index 74128c5bdbd1..29764e9e8f64 100644 --- a/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt +++ b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentListJsonParserImplTest.kt @@ -16,15 +16,20 @@ package com.duckduckgo.installation.impl.installer.com.duckduckgo.installation.impl.installer.aura +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.test.FileUtilities import com.duckduckgo.installation.impl.installer.aura.AuraExperimentListJsonParserImpl import kotlinx.coroutines.test.runTest import org.junit.Assert.* +import org.junit.Rule import org.junit.Test class AuraExperimentListJsonParserImplTest { - private val testee = AuraExperimentListJsonParserImpl() + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val testee = AuraExperimentListJsonParserImpl(coroutineTestRule.testDispatcherProvider) @Test fun whenGibberishInputThenReturnsReturnsEmptyPackages() = runTest { From fb938df1313706aea0bc112b44eb1ec450ac8034 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 15:25:15 +0000 Subject: [PATCH 10/16] Fix StubStatisticsModule --- .../java/com/duckduckgo/app/di/StubStatisticsModule.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt b/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt index f5a67a9dc89e..fd5ff2405b0f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt @@ -17,7 +17,6 @@ package com.duckduckgo.app.di import android.content.Context -import com.duckduckgo.app.aura.AuraExperimentManager import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver import com.duckduckgo.app.statistics.AtbInitializer import com.duckduckgo.app.statistics.AtbInitializerListener @@ -32,7 +31,7 @@ import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.device.ContextDeviceInfo import com.duckduckgo.common.utils.device.DeviceInfo -import com.duckduckgo.di.DaggerSet +import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesTo import dagger.Module @@ -116,11 +115,10 @@ class StubStatisticsModule { @AppCoroutineScope appCoroutineScope: CoroutineScope, statisticsDataStore: StatisticsDataStore, statisticsUpdater: StatisticsUpdater, - listeners: DaggerSet, + listeners: PluginPoint, dispatcherProvider: DispatcherProvider, - auraExperimentManager: AuraExperimentManager, ): MainProcessLifecycleObserver { - return AtbInitializer(appCoroutineScope, statisticsDataStore, statisticsUpdater, listeners, dispatcherProvider, auraExperimentManager) + return AtbInitializer(appCoroutineScope, statisticsDataStore, statisticsUpdater, listeners, dispatcherProvider) } @Provides From 73aeb7a6bd389922a7ae4622e0d7f5b39b59b41e Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 17:03:14 +0000 Subject: [PATCH 11/16] Update reinstall checking logic --- .../app/buildconfig/RealAppBuildConfig.kt | 65 ++++++++++++++++- .../reinstalls/DownloadsDirectoryManager.kt | 58 ---------------- .../impl/reinstalls/ReinstallAtbListener.kt | 51 +------------- .../reinstalls/ReinstallAtbListenerTest.kt | 69 ++----------------- 4 files changed, 70 insertions(+), 173 deletions(-) delete mode 100644 experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/DownloadsDirectoryManager.kt diff --git a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt index daf69f09c0b8..6fddda56a6ce 100644 --- a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt +++ b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt @@ -18,24 +18,33 @@ package com.duckduckgo.app.buildconfig import android.os.Build import android.os.Environment +import androidx.core.content.edit import com.duckduckgo.app.browser.BuildConfig import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.appbuildconfig.api.BuildFlavor import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.data.store.api.SharedPreferencesProvider import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.experiments.api.VariantManager import com.squareup.anvil.annotations.ContributesBinding import dagger.Lazy +import java.io.File import java.lang.IllegalStateException import java.util.* import javax.inject.Inject import kotlinx.coroutines.withContext +import timber.log.Timber @ContributesBinding(AppScope::class) class RealAppBuildConfig @Inject constructor( private val variantManager: Lazy, // break any possible DI dependency cycle private val dispatcherProvider: DispatcherProvider, + private val sharedPreferencesProvider: SharedPreferencesProvider, ) : AppBuildConfig { + private val preferences by lazy { + sharedPreferencesProvider.getSharedPreferences("com.duckduckgo.app.buildconfig.cache", false, false) + } + override val isDebug: Boolean = BuildConfig.DEBUG override val applicationId: String = BuildConfig.APPLICATION_ID override val buildType: String = BuildConfig.BUILD_TYPE @@ -71,11 +80,63 @@ class RealAppBuildConfig @Inject constructor( override suspend fun isAppReinstall(): Boolean = withContext(dispatcherProvider.io()) { return@withContext kotlin.runCatching { - val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - downloadDirectory.exists() + if (sdkInt < Build.VERSION_CODES.R) { + return@withContext false + } + + if (preferences.contains(APP_REINSTALLED_KEY)) { + return@withContext preferences.getBoolean(APP_REINSTALLED_KEY, false) + } + + val isAppReinstalled = preferences.getBoolean(APP_REINSTALLED_KEY, false) + + if (isAppReinstalled) { + return@withContext true + } else { + val downloadDirectory = getDownloadsDirectory() + val ddgDirectoryExists = (downloadDirectory.list()?.asList() ?: emptyList()).contains(DDG_DOWNLOADS_DIRECTORY) + val appReinstallValue = if (!ddgDirectoryExists) { + createNewDirectory(DDG_DOWNLOADS_DIRECTORY) + // this is a new install + false + } else { + true + } + preferences.edit(commit = true) { putBoolean(APP_REINSTALLED_KEY, appReinstallValue) } + return@withContext appReinstallValue + } }.getOrDefault(false) } override val buildDateTimeMillis: Long get() = BuildConfig.BUILD_DATE_MILLIS + + private fun getDownloadsDirectory(): File { + val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + if (!downloadDirectory.exists()) { + Timber.i("Download directory doesn't exist; trying to create it. %s", downloadDirectory.absolutePath) + downloadDirectory.mkdirs() + } + return downloadDirectory + } + + private fun createNewDirectory(directoryName: String) { + val directory = File(getDownloadsDirectory(), directoryName) + val success = directory.mkdirs() + Timber.i("Directory creation success: %s", success) + if (!success) { + Timber.e("Directory creation failed") + kotlin.runCatching { + val directoryCreationSuccess = directory.createNewFile() + Timber.i("File creation success: %s", directoryCreationSuccess) + }.onFailure { + Timber.w("Failed to create file: %s", it.message) + } + } + } + + companion object { + private const val APP_REINSTALLED_KEY = "appReinstalled" + private const val DDG_DOWNLOADS_DIRECTORY = "DuckDuckGo" + } } diff --git a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/DownloadsDirectoryManager.kt b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/DownloadsDirectoryManager.kt deleted file mode 100644 index 158051de1d23..000000000000 --- a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/DownloadsDirectoryManager.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.experiments.impl.reinstalls - -import android.os.Environment -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import java.io.File -import javax.inject.Inject -import timber.log.Timber - -interface DownloadsDirectoryManager { - - fun getDownloadsDirectory(): File - fun createNewDirectory(directoryName: String) -} - -@ContributesBinding(AppScope::class) -class DownloadsDirectoryManagerImpl @Inject constructor() : DownloadsDirectoryManager { - - override fun getDownloadsDirectory(): File { - val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - if (!downloadDirectory.exists()) { - Timber.i("Download directory doesn't exist; trying to create it. %s", downloadDirectory.absolutePath) - downloadDirectory.mkdirs() - } - return downloadDirectory - } - - override fun createNewDirectory(directoryName: String) { - val directory = File(getDownloadsDirectory(), directoryName) - val success = directory.mkdirs() - Timber.i("Directory creation success: %s", success) - if (!success) { - Timber.e("Directory creation failed") - kotlin.runCatching { - val directoryCreationSuccess = directory.createNewFile() - Timber.i("File creation success: %s", directoryCreationSuccess) - }.onFailure { - Timber.w("Failed to create file: %s", it.message) - } - } - } -} diff --git a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt index fc24341ad9d5..3aff253513b2 100644 --- a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt +++ b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt @@ -16,10 +16,6 @@ package com.duckduckgo.experiments.impl.reinstalls -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import androidx.core.content.edit import com.duckduckgo.anvil.annotations.PriorityKey import com.duckduckgo.app.statistics.AtbInitializerListener import com.duckduckgo.app.statistics.store.StatisticsDataStore @@ -27,13 +23,8 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding -import com.squareup.anvil.annotations.ContributesTo -import dagger.Lazy -import dagger.Module -import dagger.Provides import dagger.SingleInstanceIn import javax.inject.Inject -import javax.inject.Qualifier import kotlinx.coroutines.withContext import timber.log.Timber @@ -44,59 +35,23 @@ class ReinstallAtbListener @Inject constructor( private val backupDataStore: BackupServiceDataStore, private val statisticsDataStore: StatisticsDataStore, private val appBuildConfig: AppBuildConfig, - private val downloadsDirectoryManager: DownloadsDirectoryManager, - @ReinstallSharedPrefs private val reinstallSharedPrefs: Lazy, private val dispatcherProvider: DispatcherProvider, ) : AtbInitializerListener { override suspend fun beforeAtbInit() = withContext(dispatcherProvider.io()) { backupDataStore.clearBackupPreferences() - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.R && !reinstallSharedPrefs.get().isReturningUserChecked()) { - val downloadDirectory = downloadsDirectoryManager.getDownloadsDirectory() - - val downloadFiles = downloadDirectory.list()?.asList() ?: emptyList() - val ddgDirectoryExists = downloadFiles.contains(DDG_DOWNLOADS_DIRECTORY) - if (ddgDirectoryExists) { - statisticsDataStore.variant = REINSTALL_VARIANT - Timber.i("Variant update for returning user") - } else { - downloadsDirectoryManager.createNewDirectory(DDG_DOWNLOADS_DIRECTORY) - } - reinstallSharedPrefs.get().setReturningUserChecked() + if (appBuildConfig.isAppReinstall()) { + statisticsDataStore.variant = REINSTALL_VARIANT + Timber.i("Variant update for returning user") } } override fun beforeAtbInitTimeoutMillis(): Long = MAX_REINSTALL_WAIT_TIME_MS - private fun SharedPreferences.isReturningUserChecked(): Boolean { - return getBoolean(RETURNING_USER_CHECKED_TAG, false) - } - - private fun SharedPreferences.setReturningUserChecked() { - this.edit(commit = true) { putBoolean(RETURNING_USER_CHECKED_TAG, true) } - } - companion object { private const val MAX_REINSTALL_WAIT_TIME_MS = 1_500L - private const val DDG_DOWNLOADS_DIRECTORY = "DuckDuckGo" - private const val RETURNING_USER_CHECKED_TAG = "RETURNING_USER_CHECKED_TAG" } } internal const val REINSTALL_VARIANT = "ru" - -@Retention(AnnotationRetention.BINARY) -@Qualifier -private annotation class ReinstallSharedPrefs - -@Module -@ContributesTo(AppScope::class) -class ReinstallAtbListenerModule { - @Provides - @ReinstallSharedPrefs - fun provideReinstallSharedPrefs(context: Context): SharedPreferences { - val filename = "com.duckduckgo.experiments.impl.reinstalls.store.v1" - return context.getSharedPreferences(filename, Context.MODE_PRIVATE) - } -} diff --git a/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt b/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt index 18dd3ecdbb8a..0915c8c88769 100644 --- a/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt +++ b/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt @@ -16,19 +16,13 @@ package com.duckduckgo.experiments.impl.reinstalls -import android.os.Build -import androidx.core.content.edit import com.duckduckgo.app.statistics.store.StatisticsDataStore import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.common.test.api.InMemorySharedPreferences -import java.io.File import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -41,8 +35,6 @@ class ReinstallAtbListenerTest { private val mockBackupDataStore: BackupServiceDataStore = mock() private val mockStatisticsDataStore: StatisticsDataStore = mock() private val mockAppBuildConfig: AppBuildConfig = mock() - private val mockDownloadsDirectoryManager: DownloadsDirectoryManager = mock() - private val preferences = InMemorySharedPreferences() @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() @@ -53,8 +45,6 @@ class ReinstallAtbListenerTest { mockBackupDataStore, mockStatisticsDataStore, mockAppBuildConfig, - mockDownloadsDirectoryManager, - { preferences }, coroutineTestRule.testDispatcherProvider, ) } @@ -67,71 +57,20 @@ class ReinstallAtbListenerTest { } @Test - fun whenAndroidVersionIs10OrLowerThenDontCheckForDownloadsDirectory() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.Q) - - testee.beforeAtbInit() - - verify(mockDownloadsDirectoryManager, never()).getDownloadsDirectory() - } - - @Test - fun whenReturningUserHasBeenAlreadyCheckedThenDontCheckForDownloadsDirectory() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.R) - setReturningUserChecked() - - testee.beforeAtbInit() - - verify(mockDownloadsDirectoryManager, never()).getDownloadsDirectory() - } - - @Test - fun whenDDGDirectoryIsFoundThenUpdateVariantForReturningUser() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.R) - val mockDownloadsDirectory: File = mock { - on { list() } doReturn arrayOf("DuckDuckGo") - } - whenever(mockDownloadsDirectoryManager.getDownloadsDirectory()).thenReturn(mockDownloadsDirectory) + fun whenIsAppReinstallThenUpdateVariantForReturningUser() = runTest { + whenever(mockAppBuildConfig.isAppReinstall()).thenReturn(true) testee.beforeAtbInit() verify(mockStatisticsDataStore).variant = REINSTALL_VARIANT - assertTrue(isReturningUserChecked()) } @Test - fun whenDDGDirectoryIsNotFoundThenVariantForReturningUserIsNotSet() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.R) - val mockDownloadsDirectory: File = mock { - on { list() } doReturn emptyArray() - } - whenever(mockDownloadsDirectoryManager.getDownloadsDirectory()).thenReturn(mockDownloadsDirectory) + fun whenIsNotAppReinstallThenVariantForReturningUserIsNotSet() = runTest { + whenever(mockAppBuildConfig.isAppReinstall()).thenReturn(false) testee.beforeAtbInit() verify(mockStatisticsDataStore, never()).variant = REINSTALL_VARIANT - assertTrue(isReturningUserChecked()) - } - - @Test - fun whenDDGDirectoryIsNotFoundThenCreateIt() = runTest { - whenever(mockAppBuildConfig.sdkInt).thenReturn(Build.VERSION_CODES.R) - val mockDownloadsDirectory: File = mock { - on { list() } doReturn emptyArray() - } - whenever(mockDownloadsDirectoryManager.getDownloadsDirectory()).thenReturn(mockDownloadsDirectory) - - testee.beforeAtbInit() - - verify(mockDownloadsDirectoryManager).createNewDirectory("DuckDuckGo") - assertTrue(isReturningUserChecked()) - } - - private fun isReturningUserChecked(): Boolean { - return preferences.getBoolean("RETURNING_USER_CHECKED_TAG", false) - } - - private fun setReturningUserChecked() { - preferences.edit(commit = true) { putBoolean("RETURNING_USER_CHECKED_TAG", true) } } } From 7fed0f23861cbe95674da343202d88988758dc37 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 17:15:58 +0000 Subject: [PATCH 12/16] Fix ReinstallAtbListenerTest --- .../experiments/impl/reinstalls/ReinstallAtbListenerTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt b/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt index 0915c8c88769..3a3e1e449fcc 100644 --- a/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt +++ b/experiments/experiments-impl/src/test/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListenerTest.kt @@ -51,6 +51,8 @@ class ReinstallAtbListenerTest { @Test fun whenBeforeAtbInitIsCalledThenClearBackupServiceSharedPreferences() = runTest { + whenever(mockAppBuildConfig.isAppReinstall()).thenReturn(false) + testee.beforeAtbInit() verify(mockBackupDataStore).clearBackupPreferences() From fa2b93d9a0082fd3d0c46dd6dbaebbb4a29afedf Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 17:17:06 +0000 Subject: [PATCH 13/16] Fix lint issue --- .../java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt index 6fddda56a6ce..e3a8a3fdbfef 100644 --- a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt +++ b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt @@ -80,7 +80,7 @@ class RealAppBuildConfig @Inject constructor( override suspend fun isAppReinstall(): Boolean = withContext(dispatcherProvider.io()) { return@withContext kotlin.runCatching { - if (sdkInt < Build.VERSION_CODES.R) { + if (sdkInt < 30) { return@withContext false } From 7e434feb241cc3b38c26c355b044b3c5e9416c28 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Fri, 6 Dec 2024 17:32:01 +0000 Subject: [PATCH 14/16] Rename AuraExperimentManagerImpl to AuraExperimentManager --- ...rimentManagerImpl.kt => AuraExperimentManager.kt} | 2 +- ...nagerImplTest.kt => AuraExperimentManagerTest.kt} | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) rename installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/{AuraExperimentManagerImpl.kt => AuraExperimentManager.kt} (98%) rename installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/{AuraExperimentManagerImplTest.kt => AuraExperimentManagerTest.kt} (95%) diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImpl.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt similarity index 98% rename from installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImpl.kt rename to installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt index 7204ca199b0f..5b4979c187be 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImpl.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.withContext @ContributesMultibinding(AppScope::class) @PriorityKey(AtbInitializerListener.AURA_EXPERIMENT_MANAGER) @SingleInstanceIn(AppScope::class) -class AuraExperimentManagerImpl @Inject constructor( +class AuraExperimentManager @Inject constructor( private val auraExperimentFeature: AuraExperimentFeature, private val auraExperimentListJsonParser: AuraExperimentListJsonParser, private val installSourceExtractor: InstallSourceExtractor, diff --git a/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImplTest.kt b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerTest.kt similarity index 95% rename from installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImplTest.kt rename to installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerTest.kt index 9487a2331e23..dd09e6cee6bf 100644 --- a/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerImplTest.kt +++ b/installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManagerTest.kt @@ -23,7 +23,7 @@ import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.installation.impl.installer.InstallSourceExtractor import com.duckduckgo.installation.impl.installer.aura.AuraExperimentFeature import com.duckduckgo.installation.impl.installer.aura.AuraExperimentListJsonParser -import com.duckduckgo.installation.impl.installer.aura.AuraExperimentManagerImpl +import com.duckduckgo.installation.impl.installer.aura.AuraExperimentManager import com.duckduckgo.installation.impl.installer.aura.Packages import kotlinx.coroutines.test.runTest import org.junit.Before @@ -31,7 +31,7 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.* -class AuraExperimentManagerImplTest { +class AuraExperimentManagerTest { @get:Rule var coroutinesTestRule = CoroutineTestRule() @@ -43,11 +43,11 @@ class AuraExperimentManagerImplTest { private val appReferrer: AppReferrer = mock() private val toggle: Toggle = mock() - private lateinit var testee: AuraExperimentManagerImpl + private lateinit var testee: AuraExperimentManager @Before fun setup() { - testee = AuraExperimentManagerImpl( + testee = AuraExperimentManager( auraExperimentFeature, auraExperimentListJsonParser, installSourceExtractor, @@ -98,7 +98,7 @@ class AuraExperimentManagerImplTest { testee.beforeAtbInit() - verify(statisticsDataStore).variant = AuraExperimentManagerImpl.VARIANT - verify(appReferrer).setOriginAttributeCampaign(AuraExperimentManagerImpl.ORIGIN) + verify(statisticsDataStore).variant = AuraExperimentManager.VARIANT + verify(appReferrer).setOriginAttributeCampaign(AuraExperimentManager.ORIGIN) } } From bcffa4c170db63e02658f112feb3d8323212d80c Mon Sep 17 00:00:00 2001 From: joshliebe Date: Mon, 9 Dec 2024 10:58:10 +0000 Subject: [PATCH 15/16] Address PR comments --- .../app/referrer/AppReferrerInstallPixelSenderTest.kt | 7 ++----- .../experiments/impl/reinstalls/ReinstallAtbListener.kt | 2 +- .../impl/installer/aura/AuraExperimentManager.kt | 2 +- .../duckduckgo/app/statistics/AtbInitializerListener.kt | 4 ++-- .../app/statistics/user_segments/SegmentCalculationTest.kt | 5 ++--- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt b/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt index f3fe24e47264..d6ab133a47af 100644 --- a/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt +++ b/app/src/testPlay/java/com/duckduckgo/app/referrer/AppReferrerInstallPixelSenderTest.kt @@ -12,7 +12,6 @@ import com.duckduckgo.referral.AppReferrerInstallPixelSender.Companion.PIXEL_PAR import com.duckduckgo.referral.AppReferrerInstallPixelSender.Companion.PIXEL_PARAM_RETURNING_USER import com.duckduckgo.verifiedinstallation.installsource.VerificationCheckPlayStoreInstall import java.util.* -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before @@ -39,12 +38,10 @@ class AppReferrerInstallPixelSenderTest { private val captor = argumentCaptor>() @Before - fun setup() { + fun setup() = runTest { whenever(appBuildConfig.deviceLocale).thenReturn(Locale.US) whenever(playStoreInstallChecker.installedFromPlayStore()).thenReturn(true) - runBlocking { - configureAsNewUser() - } + configureAsNewUser() } private val testee = AppReferrerInstallPixelSender( diff --git a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt index 3aff253513b2..0491692f36a3 100644 --- a/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt +++ b/experiments/experiments-impl/src/main/java/com/duckduckgo/experiments/impl/reinstalls/ReinstallAtbListener.kt @@ -30,7 +30,7 @@ import timber.log.Timber @SingleInstanceIn(AppScope::class) @ContributesMultibinding(AppScope::class) -@PriorityKey(AtbInitializerListener.REINSTALL_LISTENER) +@PriorityKey(AtbInitializerListener.PRIORITY_REINSTALL_LISTENER) class ReinstallAtbListener @Inject constructor( private val backupDataStore: BackupServiceDataStore, private val statisticsDataStore: StatisticsDataStore, diff --git a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt index 5b4979c187be..7191b1674e7b 100644 --- a/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt +++ b/installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/aura/AuraExperimentManager.kt @@ -29,7 +29,7 @@ import javax.inject.Inject import kotlinx.coroutines.withContext @ContributesMultibinding(AppScope::class) -@PriorityKey(AtbInitializerListener.AURA_EXPERIMENT_MANAGER) +@PriorityKey(AtbInitializerListener.PRIORITY_AURA_EXPERIMENT_MANAGER) @SingleInstanceIn(AppScope::class) class AuraExperimentManager @Inject constructor( private val auraExperimentFeature: AuraExperimentFeature, diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt index 389fc485ed2d..ecef62ac98e3 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/AtbInitializerListener.kt @@ -25,7 +25,7 @@ interface AtbInitializerListener { fun beforeAtbInitTimeoutMillis(): Long companion object { - const val REINSTALL_LISTENER = 10 - const val AURA_EXPERIMENT_MANAGER = 20 + const val PRIORITY_REINSTALL_LISTENER = 10 + const val PRIORITY_AURA_EXPERIMENT_MANAGER = 20 } } diff --git a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt index 5486dd16beff..bd6bac20db44 100644 --- a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt +++ b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/user_segments/SegmentCalculationTest.kt @@ -13,7 +13,6 @@ import com.duckduckgo.data.store.api.FakeSharedPreferencesProvider import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.Assert.assertEquals @@ -65,9 +64,9 @@ class SegmentCalculationTest(private val input: TestInput) { private lateinit var userSegmentsPixelSender: UserSegmentsPixelSender @Before - fun setup() { + fun setup() = runTest { atbStore = FakeStatisticsDataStore() - runBlocking { whenever(appBuildConfig.isAppReinstall()).thenReturn(false) } + whenever(appBuildConfig.isAppReinstall()).thenReturn(false) usageHistory = SegmentStoreModule().provideSegmentStore( FakeSharedPreferencesProvider(), From ca7590423c5f3efa78b9d559685bc22d8ec149f4 Mon Sep 17 00:00:00 2001 From: joshliebe Date: Mon, 9 Dec 2024 12:09:38 +0000 Subject: [PATCH 16/16] Remove unnecessary reinstall check --- .../app/buildconfig/RealAppBuildConfig.kt | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt index e3a8a3fdbfef..d588b63c6858 100644 --- a/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt +++ b/app/src/main/java/com/duckduckgo/app/buildconfig/RealAppBuildConfig.kt @@ -88,23 +88,17 @@ class RealAppBuildConfig @Inject constructor( return@withContext preferences.getBoolean(APP_REINSTALLED_KEY, false) } - val isAppReinstalled = preferences.getBoolean(APP_REINSTALLED_KEY, false) - - if (isAppReinstalled) { - return@withContext true + val downloadDirectory = getDownloadsDirectory() + val ddgDirectoryExists = (downloadDirectory.list()?.asList() ?: emptyList()).contains(DDG_DOWNLOADS_DIRECTORY) + val appReinstallValue = if (!ddgDirectoryExists) { + createNewDirectory(DDG_DOWNLOADS_DIRECTORY) + // this is a new install + false } else { - val downloadDirectory = getDownloadsDirectory() - val ddgDirectoryExists = (downloadDirectory.list()?.asList() ?: emptyList()).contains(DDG_DOWNLOADS_DIRECTORY) - val appReinstallValue = if (!ddgDirectoryExists) { - createNewDirectory(DDG_DOWNLOADS_DIRECTORY) - // this is a new install - false - } else { - true - } - preferences.edit(commit = true) { putBoolean(APP_REINSTALLED_KEY, appReinstallValue) } - return@withContext appReinstallValue + true } + preferences.edit(commit = true) { putBoolean(APP_REINSTALLED_KEY, appReinstallValue) } + return@withContext appReinstallValue }.getOrDefault(false) }