Skip to content

Commit 084e859

Browse files
CDRussellmalmstein
andauthored
Feature/craig/migrate sync to work manager (#824)
* Add WorkManager RxJava support * Migrate app config sync from JobScheduler to WorkManager * Code tidy * Add sync configuration scheduling tests * Code tidy Co-authored-by: David Gonzalez <[email protected]>
1 parent 6afd992 commit 084e859

File tree

10 files changed

+212
-108
lines changed

10 files changed

+212
-108
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ dependencies {
188188
// WorkManager
189189
implementation "androidx.work:work-runtime-ktx:$workManager"
190190
androidTestImplementation "androidx.work:work-testing:$workManager"
191+
implementation "androidx.work:work-rxjava2:$workManager"
191192

192193
// Dagger
193194
kapt "com.google.dagger:dagger-android-processor:$dagger"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright (c) 2020 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.job
18+
19+
import android.content.Context
20+
import android.util.Log
21+
import androidx.test.platform.app.InstrumentationRegistry
22+
import androidx.work.*
23+
import androidx.work.impl.utils.SynchronousExecutor
24+
import androidx.work.testing.WorkManagerTestInitHelper
25+
import com.duckduckgo.app.global.job.AppConfigurationSyncWorkRequestBuilder
26+
import com.duckduckgo.app.global.job.AppConfigurationSyncWorkRequestBuilder.Companion.APP_CONFIG_SYNC_WORK_TAG
27+
import com.duckduckgo.app.global.job.AppConfigurationWorker
28+
import com.nhaarman.mockitokotlin2.mock
29+
import com.nhaarman.mockitokotlin2.whenever
30+
import io.reactivex.Completable
31+
import org.junit.Assert.assertEquals
32+
import org.junit.Assert.assertTrue
33+
import org.junit.Before
34+
import org.junit.Test
35+
36+
class AppConfigurationSyncerTest {
37+
38+
private lateinit var testee: AppConfigurationSyncer
39+
private val context = InstrumentationRegistry.getInstrumentation().targetContext
40+
private lateinit var workManager: WorkManager
41+
private val mockDownloader: ConfigurationDownloader = mock()
42+
43+
@Before
44+
fun setup() {
45+
initializeWorkManager()
46+
whenever(mockDownloader.downloadTask()).thenReturn(Completable.complete())
47+
testee = AppConfigurationSyncer(AppConfigurationSyncWorkRequestBuilder(), workManager, mockDownloader)
48+
}
49+
50+
@Test
51+
fun whenInitializedThenNoWorkScheduled() {
52+
assertTrue(workManager.getSyncWork().isEmpty())
53+
}
54+
55+
@Test
56+
fun whenSyncScheduledButNotYetRunThenWorkEnqueued() {
57+
testee.scheduleRegularSync()
58+
59+
val workInfos = workManager.getSyncWork()
60+
assertWorkIsEnqueuedStatus(workInfos)
61+
}
62+
63+
@Test
64+
fun whenSyncFinishedThenWorkStillScheduled() {
65+
testee.scheduleRegularSync()
66+
67+
executeWorker()
68+
69+
val workInfos = workManager.getSyncWork()
70+
assertWorkIsEnqueuedStatus(workInfos)
71+
}
72+
73+
private fun executeWorker() {
74+
WorkManagerTestInitHelper.getTestDriver(context)?.setAllConstraintsMet(workManager.getSyncWork().first().id)
75+
}
76+
77+
private fun assertWorkIsEnqueuedStatus(workInfos: List<WorkInfo>) {
78+
assertSingleInstanceOfWork(workInfos)
79+
assertEquals(WorkInfo.State.ENQUEUED, workInfos.first().state)
80+
}
81+
82+
private fun assertSingleInstanceOfWork(workInfos: List<WorkInfo>) {
83+
assertEquals(1, workInfos.size)
84+
}
85+
86+
private fun initializeWorkManager() {
87+
val config = Configuration.Builder()
88+
.setMinimumLoggingLevel(Log.DEBUG)
89+
.setExecutor(SynchronousExecutor())
90+
.setWorkerFactory(testWorkerFactory())
91+
.build()
92+
93+
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
94+
workManager = WorkManager.getInstance(context)
95+
}
96+
97+
private fun testWorkerFactory(): WorkerFactory {
98+
return object : WorkerFactory() {
99+
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? {
100+
return AppConfigurationWorker(appContext, workerParameters).also {
101+
it.appConfigurationDownloader = mockDownloader
102+
}
103+
}
104+
}
105+
}
106+
107+
private fun WorkManager.getSyncWork(): List<WorkInfo> {
108+
return getWorkInfosByTag(APP_CONFIG_SYNC_WORK_TAG).get()
109+
}
110+
}

app/src/main/java/com/duckduckgo/app/di/DaggerWorkerFactory.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import androidx.work.ListenableWorker
2222
import androidx.work.WorkerFactory
2323
import androidx.work.WorkerParameters
2424
import com.duckduckgo.app.fire.DataClearingWorker
25+
import com.duckduckgo.app.global.job.AppConfigurationWorker
2526
import com.duckduckgo.app.global.view.ClearDataAction
27+
import com.duckduckgo.app.job.AppConfigurationDownloader
28+
import com.duckduckgo.app.job.ConfigurationDownloader
2629
import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker
2730
import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker
2831
import com.duckduckgo.app.notification.NotificationFactory
@@ -44,6 +47,7 @@ class DaggerWorkerFactory(
4447
private val notificationFactory: NotificationFactory,
4548
private val clearDataNotification: ClearDataNotification,
4649
private val privacyProtectionNotification: PrivacyProtectionNotification,
50+
private val configurationDownloader: ConfigurationDownloader,
4751
private val pixel: Pixel
4852
) : WorkerFactory() {
4953

@@ -59,6 +63,7 @@ class DaggerWorkerFactory(
5963
is DataClearingWorker -> injectDataClearWorker(instance)
6064
is ClearDataNotificationWorker -> injectClearDataNotificationWorker(instance)
6165
is PrivacyNotificationWorker -> injectPrivacyNotificationWorker(instance)
66+
is AppConfigurationWorker -> injectAppConfigurationWorker(instance)
6267
else -> Timber.i("No injection required for worker $workerClassName")
6368
}
6469

@@ -70,6 +75,10 @@ class DaggerWorkerFactory(
7075

7176
}
7277

78+
private fun injectAppConfigurationWorker(worker: AppConfigurationWorker) {
79+
worker.appConfigurationDownloader = configurationDownloader
80+
}
81+
7382
private fun injectOfflinePixelWorker(worker: OfflinePixelScheduler.OfflinePixelWorker) {
7483
worker.offlinePixelSender = offlinePixelSender
7584
}

app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt

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

1717
package com.duckduckgo.app.di
1818

19-
import android.app.job.JobScheduler
2019
import android.content.Context
20+
import androidx.work.WorkManager
2121
import com.duckduckgo.app.autocomplete.api.AutoCompleteService
2222
import com.duckduckgo.app.brokensite.api.BrokenSiteSender
2323
import com.duckduckgo.app.brokensite.api.BrokenSiteSubmitter
@@ -28,7 +28,7 @@ import com.duckduckgo.app.feedback.api.SubReasonApiMapper
2828
import com.duckduckgo.app.global.AppUrl.Url
2929
import com.duckduckgo.app.global.api.ApiRequestInterceptor
3030
import com.duckduckgo.app.global.api.NetworkApiCache
31-
import com.duckduckgo.app.global.job.JobBuilder
31+
import com.duckduckgo.app.global.job.AppConfigurationSyncWorkRequestBuilder
3232
import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeService
3333
import com.duckduckgo.app.job.AppConfigurationSyncer
3434
import com.duckduckgo.app.job.ConfigurationDownloader
@@ -155,11 +155,11 @@ class NetworkModule {
155155
@Provides
156156
@Singleton
157157
fun appConfigurationSyncer(
158-
jobBuilder: JobBuilder,
159-
jobScheduler: JobScheduler,
158+
appConfigurationSyncWorkRequestBuilder: AppConfigurationSyncWorkRequestBuilder,
159+
workManager: WorkManager,
160160
appConfigurationDownloader: ConfigurationDownloader
161161
): AppConfigurationSyncer {
162-
return AppConfigurationSyncer(jobBuilder, jobScheduler, appConfigurationDownloader)
162+
return AppConfigurationSyncer(appConfigurationSyncWorkRequestBuilder, workManager, appConfigurationDownloader)
163163
}
164164

165165
companion object {

app/src/main/java/com/duckduckgo/app/di/WorkerModule.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import androidx.work.Configuration
2222
import androidx.work.WorkManager
2323
import androidx.work.WorkerFactory
2424
import com.duckduckgo.app.global.view.ClearDataAction
25+
import com.duckduckgo.app.job.AppConfigurationDownloader
26+
import com.duckduckgo.app.job.ConfigurationDownloader
2527
import com.duckduckgo.app.notification.NotificationFactory
2628
import com.duckduckgo.app.notification.db.NotificationDao
2729
import com.duckduckgo.app.notification.model.ClearDataNotification
@@ -52,22 +54,24 @@ class WorkerModule {
5254
offlinePixelSender: OfflinePixelSender,
5355
settingsDataStore: SettingsDataStore,
5456
clearDataAction: ClearDataAction,
55-
notficationManager: NotificationManagerCompat,
57+
notificationManager: NotificationManagerCompat,
5658
notificationDao: NotificationDao,
5759
notificationFactory: NotificationFactory,
5860
clearDataNotification: ClearDataNotification,
5961
privacyProtectionNotification: PrivacyProtectionNotification,
62+
configurationDownloader: ConfigurationDownloader,
6063
pixel: Pixel
6164
): WorkerFactory {
6265
return DaggerWorkerFactory(
6366
offlinePixelSender,
6467
settingsDataStore,
6568
clearDataAction,
66-
notficationManager,
69+
notificationManager,
6770
notificationDao,
6871
notificationFactory,
6972
clearDataNotification,
7073
privacyProtectionNotification,
74+
configurationDownloader,
7175
pixel
7276
)
7377
}

app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO
266266
appConfigurationSyncer.scheduleImmediateSync()
267267
.subscribeOn(Schedulers.io())
268268
.doAfterTerminate {
269-
appConfigurationSyncer.scheduleRegularSync(this)
269+
appConfigurationSyncer.scheduleRegularSync()
270270
}
271271
.subscribe({}, { Timber.w("Failed to download initial app configuration ${it.localizedMessage}") })
272272
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2017 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.global.job
18+
19+
import android.content.Context
20+
import androidx.work.*
21+
import com.duckduckgo.app.job.ConfigurationDownloader
22+
import io.reactivex.Single
23+
import timber.log.Timber
24+
import java.util.concurrent.TimeUnit
25+
import javax.inject.Inject
26+
27+
class AppConfigurationSyncWorkRequestBuilder @Inject constructor() {
28+
29+
fun appConfigurationWork(): PeriodicWorkRequest {
30+
return PeriodicWorkRequestBuilder<AppConfigurationWorker>(12, TimeUnit.HOURS)
31+
.setConstraints(networkAvailable())
32+
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 60, TimeUnit.MINUTES)
33+
.addTag(APP_CONFIG_SYNC_WORK_TAG)
34+
.build()
35+
}
36+
37+
private fun networkAvailable() = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
38+
39+
companion object {
40+
const val APP_CONFIG_SYNC_WORK_TAG = "AppConfigurationWorker"
41+
}
42+
}
43+
44+
class AppConfigurationWorker(context: Context, workerParams: WorkerParameters) : RxWorker(context, workerParams) {
45+
46+
@Inject
47+
lateinit var appConfigurationDownloader: ConfigurationDownloader
48+
49+
override fun createWork(): Single<Result> {
50+
Timber.i("Running app config sync")
51+
return appConfigurationDownloader.downloadTask()
52+
.toSingle {
53+
Timber.i("App configuration sync was successful")
54+
Result.success()
55+
}
56+
.onErrorReturn {
57+
Timber.w(it, "App configuration sync work failed")
58+
Result.retry()
59+
}
60+
}
61+
}

app/src/main/java/com/duckduckgo/app/global/job/JobBuilder.kt

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

0 commit comments

Comments
 (0)