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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,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
Expand Down Expand Up @@ -115,7 +115,7 @@ class StubStatisticsModule {
@AppCoroutineScope appCoroutineScope: CoroutineScope,
statisticsDataStore: StatisticsDataStore,
statisticsUpdater: StatisticsUpdater,
listeners: DaggerSet<AtbInitializerListener>,
listeners: PluginPoint<AtbInitializerListener>,
dispatcherProvider: DispatcherProvider,
): MainProcessLifecycleObserver {
return AtbInitializer(appCoroutineScope, statisticsDataStore, statisticsUpdater, listeners, dispatcherProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,34 @@
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<VariantManager>, // 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
Expand Down Expand Up @@ -65,6 +78,59 @@ class RealAppBuildConfig @Inject constructor(
override val variantName: String?
get() = variantManager.get().getVariantKey()

override suspend fun isAppReinstall(): Boolean = withContext(dispatcherProvider.io()) {
return@withContext kotlin.runCatching {
if (sdkInt < 30) {
return@withContext false
}

if (preferences.contains(APP_REINSTALLED_KEY)) {
return@withContext preferences.getBoolean(APP_REINSTALLED_KEY, false)
}

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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> = Pair(
"isReinstall",
"${statisticsDataStore.variant == "ru"}",
"${appBuildConfig.isAppReinstall()}",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ 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
Expand All @@ -31,9 +36,27 @@ interface AppReferrerDataStore {
var utmOriginAttributeCampaign: String?
}

@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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -57,8 +55,8 @@ class AppReferrerInstallPixelSender @Inject constructor(
}
}

private fun sendOriginAttribute(originAttribute: String?) {
val returningUser = statisticsDataStore.variant == RETURNING_USER_VARIANT
private suspend fun sendOriginAttribute(originAttribute: String?) {
val returningUser = appBuildConfig.isAppReinstall()

val params = mutableMapOf(
PIXEL_PARAM_LOCALE to appBuildConfig.deviceLocale.toLanguageTag(),
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@

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
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())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,14 +34,14 @@ 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<Map<String, String>>()

@Before
fun setup() {
fun setup() = runTest {
whenever(appBuildConfig.deviceLocale).thenReturn(Locale.US)
whenever(playStoreInstallChecker.installedFromPlayStore()).thenReturn(true)
configureAsNewUser()
}

private val testee = AppReferrerInstallPixelSender(
Expand All @@ -51,7 +50,6 @@ class AppReferrerInstallPixelSenderTest {
appCoroutineScope = coroutineTestRule.testScope,
dispatchers = coroutineTestRule.testDispatcherProvider,
appBuildConfig = appBuildConfig,
statisticsDataStore = statisticsDataStore,
)

@Test
Expand Down Expand Up @@ -85,12 +83,12 @@ class AppReferrerInstallPixelSenderTest {
verifyNoMoreInteractions(pixel)
}

private fun configureAsReturningUser() {
whenever(statisticsDataStore.variant).thenReturn("ru")
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?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.browser.api.referrer

/** Public interface for app referral parameters */
interface AppReferrer {

/**
* Sets the attribute campaign origin.
*/
fun setOriginAttributeCampaign(origin: String?)
}

This file was deleted.

Loading
Loading