Skip to content

Commit d2faadf

Browse files
committed
persist client state in datastore
1 parent 82c7dfe commit d2faadf

File tree

6 files changed

+228
-9
lines changed

6 files changed

+228
-9
lines changed

attributed-metrics/attributed-metrics-impl/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ dependencies {
4646

4747
implementation Google.dagger
4848

49+
// DataStore
50+
api AndroidX.dataStore.preferences
51+
4952
// Room
5053
implementation AndroidX.room.ktx
5154
ksp AndroidX.room.compiler
@@ -97,4 +100,4 @@ android {
97100
buildFeatures {
98101
buildConfig = true
99102
}
100-
}
103+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.attributed.metrics
18+
19+
import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.duckduckgo.feature.toggles.api.Toggle
22+
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
23+
24+
@ContributesRemoteFeature(
25+
scope = AppScope::class,
26+
featureName = "attributedMetrics",
27+
)
28+
interface AttributedMetricsConfigFeature {
29+
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
30+
fun self(): Toggle
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.attributed.metrics
18+
19+
internal interface AttributedMetricsState {
20+
fun isEnabled(): Boolean
21+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.attributed.metrics.di
18+
19+
import android.content.Context
20+
import androidx.datastore.core.DataStore
21+
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
22+
import androidx.datastore.preferences.core.Preferences
23+
import androidx.datastore.preferences.preferencesDataStoreFile
24+
import com.duckduckgo.di.scopes.AppScope
25+
import com.squareup.anvil.annotations.ContributesTo
26+
import dagger.Module
27+
import dagger.Provides
28+
import dagger.SingleInstanceIn
29+
import javax.inject.Qualifier
30+
31+
@Module
32+
@ContributesTo(AppScope::class)
33+
object AttributedMetricsDataStoreModule {
34+
@Provides
35+
@SingleInstanceIn(AppScope::class)
36+
@AttributedMetrics
37+
fun provideDataStore(context: Context): DataStore<Preferences> =
38+
PreferenceDataStoreFactory.create(
39+
produceFile = { context.preferencesDataStoreFile("attributed_metrics_v1") },
40+
)
41+
}
42+
43+
@Qualifier
44+
@Retention(AnnotationRetention.BINARY)
45+
annotation class AttributedMetrics

attributed-metrics/attributed-metrics-impl/src/main/java/com/duckduckgo/app/attributed/metrics/impl/RealAttributedMetricClient.kt

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@
1616

1717
package com.duckduckgo.app.attributed.metrics.impl
1818

19+
import com.duckduckgo.app.attributed.metrics.AttributedMetricsConfigFeature
1920
import com.duckduckgo.app.attributed.metrics.api.AttributedMetric
2021
import com.duckduckgo.app.attributed.metrics.api.AttributedMetricClient
2122
import com.duckduckgo.app.attributed.metrics.api.EventStats
23+
import com.duckduckgo.app.attributed.metrics.store.AttributedMetricsDataStore
24+
import com.duckduckgo.app.attributed.metrics.store.DateProvider
2225
import com.duckduckgo.app.attributed.metrics.store.EventRepository
2326
import com.duckduckgo.app.di.AppCoroutineScope
2427
import com.duckduckgo.app.statistics.api.AtbLifecyclePlugin
2528
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2629
import com.duckduckgo.common.utils.DispatcherProvider
2730
import com.duckduckgo.di.scopes.AppScope
31+
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
2832
import com.squareup.anvil.annotations.ContributesBinding
2933
import com.squareup.anvil.annotations.ContributesMultibinding
3034
import dagger.SingleInstanceIn
3135
import kotlinx.coroutines.CoroutineScope
36+
import kotlinx.coroutines.Job
3237
import kotlinx.coroutines.launch
3338
import kotlinx.coroutines.withContext
3439
import javax.inject.Inject
@@ -41,21 +46,36 @@ class RealAttributedMetricClient @Inject constructor(
4146
private val dispatcherProvider: DispatcherProvider,
4247
private val appBuildConfig: AppBuildConfig,
4348
private val eventRepository: EventRepository,
49+
private val dataStore: AttributedMetricsDataStore,
50+
private val dateProvider: DateProvider,
51+
private val attributedMetricsConfigFeature: AttributedMetricsConfigFeature,
4452
) : AttributedMetricClient,
45-
AtbLifecyclePlugin {
53+
AtbLifecyclePlugin,
54+
PrivacyConfigCallbackPlugin {
55+
56+
// We only want to enable Attributed Metrics for new installations
4657
override fun onAppAtbInitialized() {
47-
appCoroutineScope.launch {
48-
if (appBuildConfig.isAppReinstall()) {
49-
// Do not start metrics for returning users
50-
return@launch
51-
} else {
52-
// enable collecting events and emitting metrics
58+
appCoroutineScope.launch(dispatcherProvider.io()) {
59+
// atb happens after remote config is downloaded, here we should have the latest value
60+
if (attributedMetricsConfigFeature.self().isEnabled().not()) return@launch
61+
62+
val initDate = dataStore.getInitializationDate()
63+
if (initDate == null) {
64+
val currentDate = dateProvider.getCurrentDate()
65+
dataStore.setInitializationDate(currentDate)
66+
if (appBuildConfig.isAppReinstall()) {
67+
// Do not start metrics for returning users
68+
return@launch
69+
} else {
70+
dataStore.setEnabled(true)
71+
}
5372
}
5473
}
5574
}
5675

5776
override fun collectEvent(eventName: String) {
5877
appCoroutineScope.launch(dispatcherProvider.io()) {
78+
if (!isEnabled()) return@launch
5979
eventRepository.collectEvent(eventName)
6080
}
6181
}
@@ -65,10 +85,26 @@ class RealAttributedMetricClient @Inject constructor(
6585
days: Int,
6686
): EventStats =
6787
withContext(dispatcherProvider.io()) {
88+
if (!isEnabled()) {
89+
return@withContext EventStats(daysWithEvents = 0, rollingAverage = 0.0, totalEvents = 0)
90+
}
6891
eventRepository.getEventStats(eventName, days)
6992
}
7093

7194
override fun emitMetric(metric: AttributedMetric) {
72-
TODO("Not yet implemented")
95+
appCoroutineScope.launch(dispatcherProvider.io()) {
96+
if (!isEnabled()) return@launch
97+
val parameters = metric.getMetricParameters()
98+
// Implement metric emission logic
99+
}
100+
}
101+
102+
// Check if Attributed Metrics is enabled (RemoteConfig) and initialization date is set
103+
private suspend fun isEnabled(): Boolean = dataStore.isEnabled() && dataStore.getInitializationDate() != null
104+
105+
override fun onPrivacyConfigDownloaded() {
106+
appCoroutineScope.launch(dispatcherProvider.io()) {
107+
dataStore.setEnabled(attributedMetricsConfigFeature.self().isEnabled())
108+
}
73109
}
74110
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) 2024 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.attributed.metrics.store
18+
19+
import androidx.datastore.core.DataStore
20+
import androidx.datastore.preferences.core.Preferences
21+
import androidx.datastore.preferences.core.booleanPreferencesKey
22+
import androidx.datastore.preferences.core.edit
23+
import androidx.datastore.preferences.core.stringPreferencesKey
24+
import com.duckduckgo.app.attributed.metrics.di.AttributedMetrics
25+
import com.duckduckgo.app.di.AppCoroutineScope
26+
import com.duckduckgo.di.scopes.AppScope
27+
import com.squareup.anvil.annotations.ContributesBinding
28+
import dagger.SingleInstanceIn
29+
import kotlinx.coroutines.CoroutineScope
30+
import kotlinx.coroutines.flow.*
31+
import javax.inject.Inject
32+
33+
interface AttributedMetricsDataStore {
34+
suspend fun isEnabled(): Boolean
35+
36+
suspend fun setEnabled(enabled: Boolean)
37+
38+
suspend fun getInitializationDate(): String?
39+
40+
suspend fun setInitializationDate(date: String?)
41+
42+
fun observeEnabled(): Flow<Boolean>
43+
}
44+
45+
@ContributesBinding(AppScope::class)
46+
@SingleInstanceIn(AppScope::class)
47+
class RealAttributedMetricsDataStore @Inject constructor(
48+
@AttributedMetrics private val store: DataStore<Preferences>,
49+
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
50+
) : AttributedMetricsDataStore {
51+
private object Keys {
52+
val IS_ENABLED = booleanPreferencesKey("is_enabled")
53+
val INIT_DATE = stringPreferencesKey("client_init_date")
54+
}
55+
56+
private val enabledState: StateFlow<Boolean> =
57+
store.data
58+
.map { prefs -> prefs[Keys.IS_ENABLED] ?: false }
59+
.distinctUntilChanged()
60+
.stateIn(appCoroutineScope, SharingStarted.Eagerly, false)
61+
62+
override suspend fun isEnabled(): Boolean = store.data.firstOrNull()?.get(Keys.IS_ENABLED) ?: false
63+
64+
override suspend fun setEnabled(enabled: Boolean) {
65+
store.edit { preferences ->
66+
preferences[Keys.IS_ENABLED] = enabled
67+
}
68+
}
69+
70+
override suspend fun getInitializationDate(): String? = store.data.firstOrNull()?.get(Keys.INIT_DATE)
71+
72+
override suspend fun setInitializationDate(date: String?) {
73+
store.edit { preferences ->
74+
if (date != null) {
75+
preferences[Keys.INIT_DATE] = date
76+
} else {
77+
preferences.remove(Keys.INIT_DATE)
78+
}
79+
}
80+
}
81+
82+
override fun observeEnabled(): Flow<Boolean> = enabledState
83+
}

0 commit comments

Comments
 (0)