Skip to content

Commit 8749207

Browse files
authored
Conditionally send installer package pixel if allowed for package (#5076)
Task/Issue URL: https://app.asana.com/0/608920331025315/1208315780180369/f ### Description ### Steps to test this PR **Testing when remote config is not enabled** Remote config isn't yet configured, it is effectively remotely disabled at the moment already. - [x] Fresh install from this branch, launch the app and wait for privacy config to download. - [x] Verify you do **not** see `m_installation_installer` in the logs **Testing when package list is wildcarded** - [x] Apply [Wildcard patch](https://app.asana.com/0/1208420780019923/1208420780019923/f) - [x] Fresh install from this branch, launch the app and wait for privacy config to download. - [x] Verify `m_installation_installer` in the logs (it's ok if `package=null`, which it likely will be if installed from the IDE) - [x] Kill the app and re-launch. Verify you do **not** see `m_installation_installer` in the logs again **Testing when package list isn't wildcarded, but is a match** - [x] Apply [Matching package patch](https://app.asana.com/0/1208420780019924/1208420780019924/f), which fakes the extracted installer package to be `a.b.c` and points to a remote config where `a.b.c` is in the list. - [x] Fresh install, launch the app and wait for privacy config to download. - [x] Verify `m_installation_installer` in the logs - [x] Verify`package=a.b.c` is pixel param
1 parent 949899d commit 8749207

File tree

4 files changed

+108
-5
lines changed

4 files changed

+108
-5
lines changed

installation/installation-impl/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies {
4343
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
4444
testImplementation Testing.robolectric
4545
testImplementation project(path: ':common-test')
46+
testImplementation project(':feature-toggles-test')
4647

4748
coreLibraryDesugaring Android.tools.desugarJdkLibs
4849
}

installation/installation-impl/src/main/java/com/duckduckgo/installation/impl/installer/InstallSourcePrivacyConfigObserver.kt

+45-3
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ import androidx.core.content.edit
2424
import com.duckduckgo.app.di.AppCoroutineScope
2525
import com.duckduckgo.app.statistics.pixels.Pixel
2626
import com.duckduckgo.common.utils.DispatcherProvider
27+
import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin
28+
import com.duckduckgo.common.utils.plugins.pixel.PixelParamRemovalPlugin.PixelParameter
2729
import com.duckduckgo.di.scopes.AppScope
30+
import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_FULL_PACKAGE_NAME
2831
import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_PACKAGE_NAME
32+
import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore
33+
import com.duckduckgo.installation.impl.installer.fullpackage.feature.InstallSourceFullPackageFeature
2934
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
3035
import com.squareup.anvil.annotations.ContributesMultibinding
3136
import dagger.SingleInstanceIn
@@ -45,6 +50,8 @@ class InstallSourcePrivacyConfigObserver @Inject constructor(
4550
private val context: Context,
4651
private val pixel: Pixel,
4752
private val dispatchers: DispatcherProvider,
53+
private val installSourceFullPackageFeature: InstallSourceFullPackageFeature,
54+
private val store: InstallSourceFullPackageStore,
4855
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
4956
) : PrivacyConfigCallbackPlugin {
5057

@@ -58,9 +65,8 @@ class InstallSourcePrivacyConfigObserver @Inject constructor(
5865
val installationSource = installSourceExtractor.extract()
5966
Timber.i("Installation source extracted: $installationSource")
6067

61-
val isFromPlayStoreParam = if (installationSource == PLAY_STORE_PACKAGE_NAME) "1" else "0"
62-
val params = mapOf(PIXEL_PARAMETER_INSTALLED_THROUGH_PLAY_STORE to isFromPlayStoreParam)
63-
pixel.fire(APP_INSTALLER_PACKAGE_NAME, params)
68+
sendPixelIndicatingIfPlayStoreInstall(installationSource)
69+
conditionallySendFullInstallerPackage(installationSource)
6470

6571
recordInstallSourceProcessed()
6672
} else {
@@ -69,6 +75,28 @@ class InstallSourcePrivacyConfigObserver @Inject constructor(
6975
}
7076
}
7177

78+
private fun sendPixelIndicatingIfPlayStoreInstall(installationSource: String?) {
79+
val isFromPlayStoreParam = if (installationSource == PLAY_STORE_PACKAGE_NAME) "1" else "0"
80+
val params = mapOf(PIXEL_PARAMETER_INSTALLED_THROUGH_PLAY_STORE to isFromPlayStoreParam)
81+
pixel.fire(APP_INSTALLER_PACKAGE_NAME, params)
82+
}
83+
84+
private suspend fun conditionallySendFullInstallerPackage(installationSource: String?) {
85+
if (installationSource.shouldSendFullInstallerPackage()) {
86+
val params = mapOf(PIXEL_PARAMETER_FULL_INSTALLER_SOURCE to installationSource.toString())
87+
pixel.fire(APP_INSTALLER_FULL_PACKAGE_NAME, params)
88+
}
89+
}
90+
91+
private suspend fun String?.shouldSendFullInstallerPackage(): Boolean {
92+
if (!installSourceFullPackageFeature.self().isEnabled()) {
93+
return false
94+
}
95+
96+
val packages = store.getInstallSourceFullPackages()
97+
return packages.hasWildcard() || packages.list.contains(this)
98+
}
99+
72100
@VisibleForTesting
73101
fun recordInstallSourceProcessed() {
74102
sharedPreferences.edit {
@@ -85,9 +113,23 @@ class InstallSourcePrivacyConfigObserver @Inject constructor(
85113
private const val SHARED_PREFERENCES_FILENAME = "com.duckduckgo.app.installer.InstallSource"
86114
private const val SHARED_PREFERENCES_PROCESSED_KEY = "processed"
87115
private const val PIXEL_PARAMETER_INSTALLED_THROUGH_PLAY_STORE = "installedThroughPlayStore"
116+
private const val PIXEL_PARAMETER_FULL_INSTALLER_SOURCE = "package"
88117
}
89118
}
90119

91120
enum class InstallationPixelName(override val pixelName: String) : Pixel.PixelName {
92121
APP_INSTALLER_PACKAGE_NAME("m_installation_source"),
122+
APP_INSTALLER_FULL_PACKAGE_NAME("m_installation_installer"),
123+
}
124+
125+
@ContributesMultibinding(
126+
scope = AppScope::class,
127+
boundType = PixelParamRemovalPlugin::class,
128+
)
129+
object InstallerPixelsRequiringDataCleaning : PixelParamRemovalPlugin {
130+
override fun names(): List<Pair<String, Set<PixelParameter>>> {
131+
return listOf(
132+
APP_INSTALLER_FULL_PACKAGE_NAME.pixelName to PixelParameter.removeAtb(),
133+
)
134+
}
93135
}

installation/installation-impl/src/test/java/InstallSourcePrivacyConfigObserverTest.kt

+60
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@
1616

1717
package com.duckduckgo.installation.impl.installer
1818

19+
import android.annotation.SuppressLint
1920
import androidx.test.ext.junit.runners.AndroidJUnit4
2021
import com.duckduckgo.app.statistics.pixels.Pixel
2122
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count
2223
import com.duckduckgo.common.test.CoroutineTestRule
24+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
25+
import com.duckduckgo.feature.toggles.api.Toggle.State
26+
import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_FULL_PACKAGE_NAME
2327
import com.duckduckgo.installation.impl.installer.InstallationPixelName.APP_INSTALLER_PACKAGE_NAME
28+
import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore
29+
import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore.IncludedPackages
30+
import com.duckduckgo.installation.impl.installer.fullpackage.feature.InstallSourceFullPackageFeature
2431
import kotlinx.coroutines.test.runTest
32+
import org.junit.Before
2533
import org.junit.Rule
2634
import org.junit.Test
2735
import org.junit.runner.RunWith
@@ -30,6 +38,7 @@ import org.mockito.kotlin.eq
3038
import org.mockito.kotlin.mock
3139
import org.mockito.kotlin.never
3240
import org.mockito.kotlin.verify
41+
import org.mockito.kotlin.whenever
3342
import org.robolectric.RuntimeEnvironment
3443

3544
@RunWith(AndroidJUnit4::class)
@@ -41,15 +50,26 @@ class InstallSourcePrivacyConfigObserverTest {
4150
private val mockPixel = mock<Pixel>()
4251
private val context = RuntimeEnvironment.getApplication()
4352
private val mockInstallSourceExtractor = mock<InstallSourceExtractor>()
53+
private val mockFullPackageFeatureStore: InstallSourceFullPackageStore = mock()
54+
private val fakeFeature = FakeFeatureToggleFactory.create(InstallSourceFullPackageFeature::class.java)
4455

4556
private val testee = InstallSourcePrivacyConfigObserver(
4657
context = context,
4758
pixel = mockPixel,
4859
dispatchers = coroutineTestRule.testDispatcherProvider,
4960
appCoroutineScope = coroutineTestRule.testScope,
5061
installSourceExtractor = mockInstallSourceExtractor,
62+
store = mockFullPackageFeatureStore,
63+
installSourceFullPackageFeature = fakeFeature,
5164
)
5265

66+
@Before
67+
@SuppressLint("DenyListedApi")
68+
fun setup() {
69+
fakeFeature.self().setEnabled(State(enable = true))
70+
whenever(mockInstallSourceExtractor.extract()).thenReturn("app.installer.package")
71+
}
72+
5373
@Test
5474
fun whenNotPreviouslyProcessedThenPixelSent() = runTest {
5575
testee.onPrivacyConfigDownloaded()
@@ -62,4 +82,44 @@ class InstallSourcePrivacyConfigObserverTest {
6282
testee.onPrivacyConfigDownloaded()
6383
verify(mockPixel, never()).fire(eq(APP_INSTALLER_PACKAGE_NAME), any(), any(), eq(Count))
6484
}
85+
86+
@Test
87+
fun whenInstallerPackageIsInIncludedListThenFiresInstallerPackagePixel() = runTest {
88+
configurePackageIsMatching()
89+
testee.onPrivacyConfigDownloaded()
90+
verify(mockPixel).fire(eq(APP_INSTALLER_FULL_PACKAGE_NAME), any(), any(), eq(Count))
91+
}
92+
93+
@Test
94+
fun whenInstallerPackageIsNotInIncludedListDoesNotFirePixel() = runTest {
95+
configurePackageNotMatching()
96+
testee.onPrivacyConfigDownloaded()
97+
verify(mockPixel, never()).fire(eq(APP_INSTALLER_FULL_PACKAGE_NAME), any(), any(), eq(Count))
98+
}
99+
100+
@Test
101+
fun whenInstallerPackageIsNotInIncludedListButListContainsWildcardThenDoesFirePixel() = runTest {
102+
configureListHasWildcard()
103+
testee.onPrivacyConfigDownloaded()
104+
verify(mockPixel).fire(eq(APP_INSTALLER_FULL_PACKAGE_NAME), any(), any(), eq(Count))
105+
}
106+
107+
private suspend fun configurePackageIsMatching() {
108+
whenever(mockFullPackageFeatureStore.getInstallSourceFullPackages()).thenReturn(IncludedPackages(listOf("app.installer.package")))
109+
}
110+
111+
private suspend fun configureListHasWildcard() {
112+
whenever(mockFullPackageFeatureStore.getInstallSourceFullPackages()).thenReturn(IncludedPackages(listOf("*")))
113+
}
114+
115+
private suspend fun configurePackageNotMatching() {
116+
whenever(mockFullPackageFeatureStore.getInstallSourceFullPackages()).thenReturn(
117+
IncludedPackages(
118+
listOf(
119+
"this.will.not.match",
120+
"nor.will.this",
121+
),
122+
),
123+
)
124+
}
65125
}

installation/installation-impl/src/test/java/com/duckduckgo/installation/impl/installer/fullpackage/InstallSourceFullPackageStoreImplTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package com.duckduckgo.installation.impl.installer.fullpackage
22

33
import androidx.datastore.core.DataStore
4-
import androidx.datastore.dataStore
54
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
65
import androidx.datastore.preferences.core.Preferences
76
import androidx.datastore.preferences.core.edit
87
import com.duckduckgo.common.test.CoroutineTestRule
98
import com.duckduckgo.installation.impl.installer.fullpackage.InstallSourceFullPackageStore.IncludedPackages
109
import com.duckduckgo.installation.impl.installer.fullpackage.feature.InstallSourceFullPackageListJsonParser
1110
import kotlinx.coroutines.test.runTest
12-
import org.junit.Assert.*
11+
import org.junit.Assert.assertEquals
12+
import org.junit.Assert.assertTrue
1313
import org.junit.Rule
1414
import org.junit.Test
1515
import org.junit.rules.TemporaryFolder

0 commit comments

Comments
 (0)