Skip to content

Commit 6afd992

Browse files
authored
Add mechanism to remove deprecated WorkManager jobs (#805)
* we want to make sure that old work tags are cancelled * this could be private * better naming for this method * we want to separate the responsibilities of clearing old jobs and scheduling new ones * we want to separate the responsibilities of clearing old jobs and scheduling new ones * ensure we are scheduling some jobs before removing them * adding work managet test library * properly testing that the jobs are scheduled or not * initialising workmanager as documented * better testing strategy * add filter back * formatting and comment cleanup * clean up duplicate scheduler code * added new test to ensure non deprecated work is still enqueued
1 parent 4fc5b1f commit 6afd992

File tree

12 files changed

+330
-44
lines changed

12 files changed

+330
-44
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ dependencies {
187187

188188
// WorkManager
189189
implementation "androidx.work:work-runtime-ktx:$workManager"
190+
androidTestImplementation "androidx.work:work-testing:$workManager"
190191

191192
// Dagger
192193
kapt "com.google.dagger:dagger-android-processor:$dagger"

app/src/androidTest/java/com/duckduckgo/app/di/StubJobSchedulerModule.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ package com.duckduckgo.app.di
1919
import android.app.job.JobInfo
2020
import android.app.job.JobScheduler
2121
import android.app.job.JobWorkItem
22+
import androidx.work.WorkManager
23+
import com.duckduckgo.app.job.AndroidJobCleaner
24+
import com.duckduckgo.app.job.AndroidWorkScheduler
25+
import com.duckduckgo.app.job.JobCleaner
26+
import com.duckduckgo.app.job.WorkScheduler
27+
import com.duckduckgo.app.notification.AndroidNotificationScheduler
2228
import dagger.Module
2329
import dagger.Provides
2430
import javax.inject.Singleton
@@ -44,4 +50,16 @@ class StubJobSchedulerModule {
4450

4551
}
4652
}
53+
54+
@Singleton
55+
@Provides
56+
fun providesJobCleaner(workManager: WorkManager): JobCleaner {
57+
return AndroidJobCleaner(workManager)
58+
}
59+
60+
@Singleton
61+
@Provides
62+
fun providesWorkScheduler(notificationScheduler: AndroidNotificationScheduler, jobCleaner: JobCleaner): WorkScheduler {
63+
return AndroidWorkScheduler(notificationScheduler, jobCleaner)
64+
}
4765
}

app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import retrofit2.Retrofit
3434
import javax.inject.Named
3535
import javax.inject.Singleton
3636

37-
3837
@Singleton
3938
@Component(
4039
modules = [
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.util.Log
20+
import androidx.test.platform.app.InstrumentationRegistry
21+
import androidx.work.Configuration
22+
import androidx.work.OneTimeWorkRequestBuilder
23+
import androidx.work.WorkInfo
24+
import androidx.work.WorkManager
25+
import androidx.work.impl.utils.SynchronousExecutor
26+
import androidx.work.testing.WorkManagerTestInitHelper
27+
import com.duckduckgo.app.job.JobCleaner.Companion.allDeprecatedNotificationWorkTags
28+
import com.duckduckgo.app.notification.NotificationScheduler
29+
import org.junit.Assert.assertFalse
30+
import org.junit.Assert.assertTrue
31+
import org.junit.Before
32+
import org.junit.Test
33+
import java.util.concurrent.TimeUnit
34+
35+
class JobCleanerTest {
36+
37+
private val context = InstrumentationRegistry.getInstrumentation().targetContext
38+
private lateinit var workManager: WorkManager
39+
private lateinit var testee: JobCleaner
40+
41+
@Before
42+
fun before() {
43+
initializeWorkManager()
44+
testee = AndroidJobCleaner(workManager)
45+
}
46+
47+
// https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing
48+
private fun initializeWorkManager() {
49+
val config = Configuration.Builder()
50+
.setMinimumLoggingLevel(Log.DEBUG)
51+
.setExecutor(SynchronousExecutor())
52+
.build()
53+
54+
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
55+
workManager = WorkManager.getInstance(context)
56+
}
57+
58+
@Test
59+
fun whenStartedThenAllDeprecatedWorkIsCancelled() {
60+
enqueueDeprecatedWorkers()
61+
assertDeprecatedWorkersAreEnqueued()
62+
testee.cleanDeprecatedJobs()
63+
assertDeprecatedWorkersAreNotEnqueued()
64+
}
65+
66+
@Test
67+
fun whenStartedAndNoDeprecatedJobsAreScheduledThenNothingIsRemoved() {
68+
enqueueNonDeprecatedWorkers()
69+
assertNonDeprecatedWorkersAreEnqueued()
70+
testee.cleanDeprecatedJobs()
71+
assertNonDeprecatedWorkersAreEnqueued()
72+
}
73+
74+
private fun enqueueDeprecatedWorkers() {
75+
allDeprecatedNotificationWorkTags().forEach {
76+
val requestBuilder = OneTimeWorkRequestBuilder<TestWorker>()
77+
val request = requestBuilder
78+
.addTag(it)
79+
.setInitialDelay(10, TimeUnit.SECONDS)
80+
.build()
81+
workManager.enqueue(request)
82+
}
83+
}
84+
85+
private fun enqueueNonDeprecatedWorkers() {
86+
allDeprecatedNotificationWorkTags().forEach {
87+
val requestBuilder = OneTimeWorkRequestBuilder<TestWorker>()
88+
val request = requestBuilder
89+
.addTag(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG)
90+
.setInitialDelay(10, TimeUnit.SECONDS)
91+
.build()
92+
workManager.enqueue(request)
93+
}
94+
}
95+
96+
private fun assertDeprecatedWorkersAreEnqueued() {
97+
allDeprecatedNotificationWorkTags().forEach {
98+
val scheduledWorkers = getScheduledWorkers(it)
99+
assertFalse(scheduledWorkers.isEmpty())
100+
}
101+
}
102+
103+
private fun assertDeprecatedWorkersAreNotEnqueued() {
104+
allDeprecatedNotificationWorkTags().forEach {
105+
val scheduledWorkers = getScheduledWorkers(it)
106+
assertTrue(scheduledWorkers.isEmpty())
107+
}
108+
}
109+
110+
private fun assertNonDeprecatedWorkersAreEnqueued() {
111+
val scheduledWorkers = getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG)
112+
assertFalse(scheduledWorkers.isEmpty())
113+
}
114+
115+
private fun assertNonDeprecatedWorkersAreNotEnqueued() {
116+
val scheduledWorkers = getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG)
117+
assertTrue(scheduledWorkers.isEmpty())
118+
}
119+
120+
121+
private fun getScheduledWorkers(tag: String): List<WorkInfo> {
122+
return workManager
123+
.getWorkInfosByTag(tag)
124+
.get()
125+
.filter { it.state == WorkInfo.State.ENQUEUED }
126+
}
127+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 androidx.work.Worker
21+
import androidx.work.WorkerParameters
22+
23+
class TestWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
24+
override fun doWork(): Result {
25+
return Result.success()
26+
}
27+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 com.duckduckgo.app.notification.AndroidNotificationScheduler
20+
import com.nhaarman.mockitokotlin2.mock
21+
import com.nhaarman.mockitokotlin2.verify
22+
import kotlinx.coroutines.runBlocking
23+
import org.junit.Before
24+
import org.junit.Test
25+
26+
class WorkSchedulerTest {
27+
28+
private val notificationScheduler: AndroidNotificationScheduler = mock()
29+
private val jobCleaner: JobCleaner = mock()
30+
31+
private lateinit var testee: WorkScheduler
32+
33+
@Before
34+
fun before() {
35+
testee = AndroidWorkScheduler(
36+
notificationScheduler,
37+
jobCleaner
38+
)
39+
}
40+
41+
@Test
42+
fun schedulesNextNotificationAndCleansDeprecatedJobs() = runBlocking<Unit> {
43+
testee.scheduleWork()
44+
45+
verify(notificationScheduler).scheduleNextNotification()
46+
verify(jobCleaner).cleanDeprecatedJobs()
47+
}
48+
}

app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818

1919
package com.duckduckgo.app.notification
2020

21+
import android.util.Log
2122
import androidx.test.platform.app.InstrumentationRegistry
23+
import androidx.work.Configuration
2224
import androidx.work.OneTimeWorkRequestBuilder
2325
import androidx.work.WorkInfo
2426
import androidx.work.WorkManager
27+
import androidx.work.impl.utils.SynchronousExecutor
28+
import androidx.work.testing.WorkManagerTestInitHelper
2529
import com.duckduckgo.app.CoroutineTestRule
2630
import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker
2731
import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker
@@ -50,11 +54,12 @@ class AndroidNotificationSchedulerTest {
5054
private val privacyNotification: SchedulableNotification = mock()
5155

5256
private val context = InstrumentationRegistry.getInstrumentation().targetContext
53-
private var workManager = WorkManager.getInstance(context)
57+
private lateinit var workManager: WorkManager
5458
private lateinit var testee: NotificationScheduler
5559

5660
@Before
5761
fun before() {
62+
initializeWorkManager()
5863
whenever(variantManager.getVariant(any())).thenReturn(DEFAULT_VARIANT)
5964
testee = NotificationScheduler(
6065
workManager,
@@ -63,6 +68,17 @@ class AndroidNotificationSchedulerTest {
6368
)
6469
}
6570

71+
// https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing
72+
private fun initializeWorkManager() {
73+
val config = Configuration.Builder()
74+
.setMinimumLoggingLevel(Log.DEBUG)
75+
.setExecutor(SynchronousExecutor())
76+
.build()
77+
78+
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
79+
workManager = WorkManager.getInstance(context)
80+
}
81+
6682
@Test
6783
fun whenPrivacyNotificationClearDataCanShowThenPrivacyNotificationIsScheduled() = runBlocking<Unit> {
6884
whenever(privacyNotification.canShow()).thenReturn(true)
@@ -99,30 +115,6 @@ class AndroidNotificationSchedulerTest {
99115
assertNoUnusedAppNotificationScheduled()
100116
}
101117

102-
@Test
103-
fun whenNotificationIsScheduledOldJobsAreCancelled() = runBlocking<Unit> {
104-
whenever(privacyNotification.canShow()).thenReturn(false)
105-
whenever(clearNotification.canShow()).thenReturn(false)
106-
107-
enqueueDeprecatedJobs()
108-
109-
testee.scheduleNextNotification()
110-
111-
NotificationScheduler.allDeprecatedNotificationWorkTags().forEach {
112-
assertTrue(getScheduledWorkers(it).isEmpty())
113-
}
114-
}
115-
116-
private fun enqueueDeprecatedJobs() {
117-
NotificationScheduler.allDeprecatedNotificationWorkTags().forEach {
118-
val request = OneTimeWorkRequestBuilder<PrivacyNotificationWorker>()
119-
.addTag(it)
120-
.build()
121-
122-
workManager.enqueue(request)
123-
}
124-
}
125-
126118
private fun assertUnusedAppNotificationScheduled(workerName: String) {
127119
assertTrue(getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG).any { it.tags.contains(workerName) })
128120
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ package com.duckduckgo.app.di
1818

1919
import android.app.job.JobScheduler
2020
import android.content.Context
21+
import androidx.work.WorkManager
22+
import com.duckduckgo.app.job.AndroidJobCleaner
23+
import com.duckduckgo.app.job.AndroidWorkScheduler
24+
import com.duckduckgo.app.job.JobCleaner
25+
import com.duckduckgo.app.job.WorkScheduler
26+
import com.duckduckgo.app.notification.AndroidNotificationScheduler
2127
import dagger.Module
2228
import dagger.Provides
2329
import javax.inject.Singleton
2430

25-
2631
@Module
2732
class JobsModule {
2833

@@ -32,4 +37,15 @@ class JobsModule {
3237
return context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
3338
}
3439

40+
@Singleton
41+
@Provides
42+
fun providesJobCleaner(workManager: WorkManager): JobCleaner {
43+
return AndroidJobCleaner(workManager)
44+
}
45+
46+
@Singleton
47+
@Provides
48+
fun providesWorkScheduler(notificationScheduler: AndroidNotificationScheduler, jobCleaner: JobCleaner): WorkScheduler {
49+
return AndroidWorkScheduler(notificationScheduler, jobCleaner)
50+
}
3551
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import com.duckduckgo.app.global.rating.AppEnjoymentLifecycleObserver
3737
import com.duckduckgo.app.global.shortcut.AppShortcutCreator
3838
import com.duckduckgo.app.httpsupgrade.HttpsUpgrader
3939
import com.duckduckgo.app.job.AppConfigurationSyncer
40-
import com.duckduckgo.app.notification.AndroidNotificationScheduler
40+
import com.duckduckgo.app.job.WorkScheduler
4141
import com.duckduckgo.app.notification.NotificationRegistrar
4242
import com.duckduckgo.app.referral.AppInstallationReferrerStateListener
4343
import com.duckduckgo.app.settings.db.SettingsDataStore
@@ -120,7 +120,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO
120120
lateinit var dataClearer: DataClearer
121121

122122
@Inject
123-
lateinit var notificationScheduler: AndroidNotificationScheduler
123+
lateinit var workScheduler: WorkScheduler
124124

125125
@Inject
126126
lateinit var workerFactory: WorkerFactory
@@ -293,7 +293,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO
293293
fun onAppResumed() {
294294
notificationRegistrar.updateStatus()
295295
GlobalScope.launch {
296-
notificationScheduler.scheduleNextNotification()
296+
workScheduler.scheduleWork()
297297
atbInitializer.initializeAfterReferrerAvailable()
298298
}
299299
}

0 commit comments

Comments
 (0)