Skip to content

Commit a220b84

Browse files
authored
Increase RMF update frequency while removing parallel execution (#6128)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1149059203486286/task/1210364424198509?focus=true ### Description Increase frequency of RMF worker updates. Process config on every download. FeatureFlags: - `scheduleEveryHour`: schedule 1h frequency (before: every 4h / app launch) - `alwaysProcessRemoteConfig`: Allow remote config to be processed on every download (before: when invalidated or expired) - `canScheduleOnPrivacyConfigUpdates`: Allow also scheduling after privacy config downloaded (before: only on app launch or periodic) ### Steps to test this PR #### Scenario 1: Remote messages shows and hides when feature flag is enabled/disabled 1. Clear the app's cache. 2. Open the app and verify you see the current production UI. 3. Update RemoteMessagingService with https://www.jsonblob.com/api/1369932692346560512 4. Update PRIVACY_REMOTE_CONFIG_URL with https://www.jsonblob.com/api/1370023061872631808 5. install code changes 6. ensure on app launch you might see visual updates applied and RMF, if not restart the app 7. Now update PRIVACY_REMOTE_CONFIG_URL with https://www.jsonblob.com/api/1370374056737693696 8. install code changes 6. ensure on app launch you don't see visual updates applied and no RMF, if not restart the app 10. Verify that you don't see the visual design nor the message anymore. ### UI changes | Before | After | | ------ | ----- | !(Upload before screenshot)|(Upload after screenshot)|
1 parent 06fcc84 commit a220b84

File tree

9 files changed

+250
-128
lines changed

9 files changed

+250
-128
lines changed

remote-messaging/remote-messaging-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ dependencies {
6969
implementation AndroidX.lifecycle.viewModelKtx
7070

7171
testImplementation project(path: ':common-test')
72+
testImplementation project(path: ':feature-toggles-test')
7273
testImplementation Testing.junit4
7374
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
7475
testImplementation CashApp.turbine

remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RemoteMessagingConfigDownloadScheduler.kt

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ package com.duckduckgo.remote.messaging.impl
1818

1919
import android.content.Context
2020
import androidx.lifecycle.LifecycleOwner
21-
import androidx.work.*
21+
import androidx.work.BackoffPolicy
22+
import androidx.work.CoroutineWorker
23+
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
24+
import androidx.work.PeriodicWorkRequestBuilder
25+
import androidx.work.WorkManager
26+
import androidx.work.WorkerParameters
2227
import com.duckduckgo.anvil.annotations.ContributesWorker
2328
import com.duckduckgo.app.di.AppCoroutineScope
2429
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
2530
import com.duckduckgo.common.utils.DispatcherProvider
2631
import com.duckduckgo.di.scopes.AppScope
27-
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
28-
import com.duckduckgo.remote.messaging.store.RemoteMessagingConfigRepository
2932
import com.squareup.anvil.annotations.ContributesMultibinding
3033
import java.util.concurrent.TimeUnit
3134
import javax.inject.Inject
@@ -34,6 +37,8 @@ import kotlinx.coroutines.launch
3437
import kotlinx.coroutines.withContext
3538
import timber.log.Timber
3639

40+
const val REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG = "REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG"
41+
3742
@ContributesWorker(AppScope::class)
3843
class RemoteMessagingConfigDownloadWorker(
3944
context: Context,
@@ -61,44 +66,30 @@ class RemoteMessagingConfigDownloadWorker(
6166
scope = AppScope::class,
6267
boundType = MainProcessLifecycleObserver::class,
6368
)
64-
65-
@ContributesMultibinding(
66-
AppScope::class,
67-
boundType = PrivacyConfigCallbackPlugin::class,
68-
)
6969
class RemoteMessagingConfigDownloadScheduler @Inject constructor(
7070
private val workManager: WorkManager,
71-
private val downloader: RemoteMessagingConfigDownloader,
7271
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
7372
private val dispatcherProvider: DispatcherProvider,
74-
private val remoteMessagingConfigRepository: RemoteMessagingConfigRepository,
7573
private val remoteMessagingFeatureToggles: RemoteMessagingFeatureToggles,
76-
) : MainProcessLifecycleObserver, PrivacyConfigCallbackPlugin {
74+
) : MainProcessLifecycleObserver {
7775

7876
override fun onCreate(owner: LifecycleOwner) {
79-
scheduleDownload()
80-
}
81-
82-
override fun onPrivacyConfigDownloaded() {
83-
if (remoteMessagingFeatureToggles.invalidateRMFAfterPrivacyConfigDownloaded().isEnabled()) {
84-
appCoroutineScope.launch(context = dispatcherProvider.io()) {
85-
Timber.d("RMF: onPrivacyConfigDownloaded, invalidating and re-downloading")
86-
remoteMessagingConfigRepository.invalidate()
87-
downloader.download()
88-
}
77+
appCoroutineScope.launch(dispatcherProvider.io()) {
78+
scheduleDownload()
8979
}
9080
}
9181

9282
private fun scheduleDownload() {
93-
Timber.v("RMF: Scheduling remote config worker")
94-
val workerRequest = PeriodicWorkRequestBuilder<RemoteMessagingConfigDownloadWorker>(4, TimeUnit.HOURS)
83+
val refreshInterval = if (remoteMessagingFeatureToggles.scheduleEveryHour().isEnabled()) 1L else 4L
84+
Timber.v("RMF: Scheduling remote config worker with fresh interval of $refreshInterval hours")
85+
val requestBuilder = PeriodicWorkRequestBuilder<RemoteMessagingConfigDownloadWorker>(refreshInterval, TimeUnit.HOURS)
9586
.addTag(REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG)
9687
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
97-
.build()
98-
workManager.enqueueUniquePeriodicWork(REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG, ExistingPeriodicWorkPolicy.REPLACE, workerRequest)
99-
}
100-
101-
companion object {
102-
private const val REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG = "REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG"
88+
if (remoteMessagingFeatureToggles.canScheduleOnPrivacyConfigUpdates().isEnabled()) {
89+
Timber.v("RMF: Add delay to remote config worker")
90+
requestBuilder.setInitialDelay(5L, TimeUnit.MINUTES)
91+
}
92+
val workerRequest = requestBuilder.build()
93+
workManager.enqueueUniquePeriodicWork(REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG, CANCEL_AND_REENQUEUE, workerRequest)
10394
}
10495
}

remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RemoteMessagingConfigProcessor.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,24 @@ class RealRemoteMessagingConfigProcessor(
3434
private val remoteMessagingConfigRepository: RemoteMessagingConfigRepository,
3535
private val remoteMessagingRepository: RemoteMessagingRepository,
3636
private val remoteMessagingConfigMatcher: RemoteMessagingConfigMatcher,
37+
private val remoteMessagingFeatureToggles: RemoteMessagingFeatureToggles,
3738
) : RemoteMessagingConfigProcessor {
3839

3940
override suspend fun process(jsonRemoteMessagingConfig: JsonRemoteMessagingConfig) {
4041
Timber.v("RMF: process ${jsonRemoteMessagingConfig.version}")
41-
val currentConfig = remoteMessagingConfigRepository.get()
42-
val currentVersion = currentConfig.version
43-
val newVersion = jsonRemoteMessagingConfig.version
4442

45-
val isNewVersion = currentVersion != newVersion
46-
val shouldProcess = currentConfig.invalidated() || currentConfig.expired()
43+
val shouldProcess = if (remoteMessagingFeatureToggles.alwaysProcessRemoteConfig().isEnabled()) {
44+
true
45+
} else {
46+
val currentConfig = remoteMessagingConfigRepository.get()
47+
val currentVersion = currentConfig.version
48+
val newVersion = jsonRemoteMessagingConfig.version
49+
50+
val isNewVersion = currentVersion != newVersion
51+
isNewVersion || currentConfig.invalidated() || currentConfig.expired()
52+
}
4753

48-
if (isNewVersion || shouldProcess) {
54+
if (shouldProcess) {
4955
val config = remoteMessagingConfigJsonMapper.map(jsonRemoteMessagingConfig)
5056
val message = remoteMessagingConfigMatcher.evaluate(config)
5157
remoteMessagingConfigRepository.insert(RemoteMessagingConfig(version = jsonRemoteMessagingConfig.version))

remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RemoteMessagingFeatureToggles.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ interface RemoteMessagingFeatureToggles {
3030
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
3131
fun self(): Toggle
3232

33-
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
33+
@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
3434
fun invalidateRMFAfterPrivacyConfigDownloaded(): Toggle
35+
36+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
37+
fun alwaysProcessRemoteConfig(): Toggle
38+
39+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
40+
fun scheduleEveryHour(): Toggle
41+
42+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
43+
fun canScheduleOnPrivacyConfigUpdates(): Toggle
3544
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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.remote.messaging.impl
18+
19+
import androidx.work.BackoffPolicy
20+
import androidx.work.ExistingPeriodicWorkPolicy
21+
import androidx.work.PeriodicWorkRequestBuilder
22+
import androidx.work.WorkManager
23+
import com.duckduckgo.app.di.AppCoroutineScope
24+
import com.duckduckgo.common.utils.DispatcherProvider
25+
import com.duckduckgo.di.scopes.AppScope
26+
import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin
27+
import com.duckduckgo.remote.messaging.store.RemoteMessagingConfigRepository
28+
import com.squareup.anvil.annotations.ContributesMultibinding
29+
import java.util.concurrent.TimeUnit
30+
import javax.inject.Inject
31+
import kotlinx.coroutines.CoroutineScope
32+
import kotlinx.coroutines.launch
33+
import timber.log.Timber
34+
35+
@ContributesMultibinding(
36+
AppScope::class,
37+
boundType = PrivacyConfigCallbackPlugin::class,
38+
)
39+
class RemoteMessagingPrivacyConfigObserver @Inject constructor(
40+
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
41+
private val dispatcherProvider: DispatcherProvider,
42+
private val remoteMessagingConfigRepository: RemoteMessagingConfigRepository,
43+
private val remoteMessagingFeatureToggles: RemoteMessagingFeatureToggles,
44+
private val workManager: WorkManager,
45+
) : PrivacyConfigCallbackPlugin {
46+
47+
override fun onPrivacyConfigDownloaded() {
48+
appCoroutineScope.launch(context = dispatcherProvider.io()) {
49+
val invalidateConfig = remoteMessagingFeatureToggles.invalidateRMFAfterPrivacyConfigDownloaded().isEnabled()
50+
val alwaysProcess = remoteMessagingFeatureToggles.alwaysProcessRemoteConfig().isEnabled()
51+
// we only need to invalidate, so we process config next run, if alwaysProcess is not enabled
52+
val shouldInvalidate = invalidateConfig && alwaysProcess.not()
53+
54+
if (shouldInvalidate) {
55+
Timber.d("RMF: onPrivacyConfigDownloaded, invalidate so next run we process the config")
56+
remoteMessagingConfigRepository.invalidate()
57+
}
58+
scheduleDownload()
59+
}
60+
}
61+
62+
private fun scheduleDownload() {
63+
if (remoteMessagingFeatureToggles.canScheduleOnPrivacyConfigUpdates().isEnabled()) {
64+
val refreshInterval = if (remoteMessagingFeatureToggles.scheduleEveryHour().isEnabled()) 1L else 4L
65+
66+
Timber.v("RMF: Scheduling remote config worker with fresh interval of $refreshInterval hours")
67+
val workerRequest = PeriodicWorkRequestBuilder<RemoteMessagingConfigDownloadWorker>(refreshInterval, TimeUnit.HOURS)
68+
.addTag(REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG)
69+
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
70+
.build()
71+
workManager.enqueueUniquePeriodicWork(
72+
REMOTE_MESSAGING_DOWNLOADER_WORKER_TAG,
73+
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
74+
workerRequest,
75+
)
76+
}
77+
}
78+
}

remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/di/RemoteMessagingModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,14 @@ object DataSourceModule {
7272
remoteMessagingConfigRepository: RemoteMessagingConfigRepository,
7373
remoteMessagingRepository: RemoteMessagingRepository,
7474
remoteMessagingConfigMatcher: RemoteMessagingConfigMatcher,
75+
remoteMessagingFeatureToggles: RemoteMessagingFeatureToggles,
7576
): RemoteMessagingConfigProcessor {
7677
return RealRemoteMessagingConfigProcessor(
7778
remoteMessagingConfigJsonMapper,
7879
remoteMessagingConfigRepository,
7980
remoteMessagingRepository,
8081
remoteMessagingConfigMatcher,
82+
remoteMessagingFeatureToggles,
8183
)
8284
}
8385

remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/RealRemoteMessagingConfigProcessorTest.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

1717
package com.duckduckgo.remote.messaging.impl
1818

19+
import android.annotation.SuppressLint
1920
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2021
import com.duckduckgo.common.test.CoroutineTestRule
22+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
23+
import com.duckduckgo.feature.toggles.api.Toggle.State
2124
import com.duckduckgo.remote.messaging.api.RemoteMessagingRepository
2225
import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.aJsonRemoteMessagingConfig
2326
import com.duckduckgo.remote.messaging.fixtures.RemoteMessagingConfigOM.aRemoteMessagingConfig
@@ -39,6 +42,7 @@ import org.mockito.kotlin.times
3942
import org.mockito.kotlin.verify
4043
import org.mockito.kotlin.whenever
4144

45+
@SuppressLint("DenyListedApi")
4246
class RealRemoteMessagingConfigProcessorTest {
4347

4448
@get:Rule var coroutineRule = CoroutineTestRule()
@@ -53,12 +57,16 @@ class RealRemoteMessagingConfigProcessorTest {
5357
private val remoteMessagingRepository = mock<RemoteMessagingRepository>()
5458
private val remoteMessagingCohortStore = mock<RemoteMessagingCohortStore>()
5559
private val remoteMessagingConfigMatcher = RemoteMessagingConfigMatcher(setOf(mock(), mock(), mock()), mock(), remoteMessagingCohortStore)
60+
private var remoteMessagingFeatureToggles: RemoteMessagingFeatureToggles = FakeFeatureToggleFactory.create(
61+
RemoteMessagingFeatureToggles::class.java,
62+
)
5663

5764
private val testee = RealRemoteMessagingConfigProcessor(
5865
remoteMessagingConfigJsonMapper,
5966
remoteMessagingConfigRepository,
6067
remoteMessagingRepository,
6168
remoteMessagingConfigMatcher,
69+
remoteMessagingFeatureToggles,
6270
)
6371

6472
@Before
@@ -78,7 +86,8 @@ class RealRemoteMessagingConfigProcessorTest {
7886
}
7987

8088
@Test
81-
fun whenSameVersionThenDoNothing() = runTest {
89+
fun whenSameVersionThenDoNothingIfAlwaysProcessFFDisabled() = runTest {
90+
remoteMessagingFeatureToggles.alwaysProcessRemoteConfig().setRawStoredState(State(false))
8291
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
8392
whenever(remoteMessagingConfigRepository.get()).thenReturn(
8493
aRemoteMessagingConfig(
@@ -92,6 +101,22 @@ class RealRemoteMessagingConfigProcessorTest {
92101
verify(remoteMessagingConfigRepository, times(0)).insert(any())
93102
}
94103

104+
@Test
105+
fun whenSameVersionThenProcessIfAlwaysProcessFFEnabled() = runTest {
106+
remoteMessagingFeatureToggles.alwaysProcessRemoteConfig().setRawStoredState(State(true))
107+
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
108+
whenever(remoteMessagingConfigRepository.get()).thenReturn(
109+
aRemoteMessagingConfig(
110+
version = 1L,
111+
evaluationTimestamp = dateTimeFormatter.format(LocalDateTime.now()),
112+
),
113+
)
114+
115+
testee.process(aJsonRemoteMessagingConfig(version = 1L))
116+
117+
verify(remoteMessagingConfigRepository).insert(any())
118+
}
119+
95120
@Test
96121
fun whenSameVersionButExpiredThenEvaluate() = runTest {
97122
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")

remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/RemoteMessagingConfigDownloadSchedulerTest.kt

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)