diff --git a/app/build.gradle b/app/build.gradle index df5a19712903..de6aec87f9e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,6 +187,7 @@ dependencies { // WorkManager implementation "androidx.work:work-runtime-ktx:$workManager" + androidTestImplementation "androidx.work:work-testing:$workManager" // Dagger kapt "com.google.dagger:dagger-android-processor:$dagger" diff --git a/app/src/androidTest/java/com/duckduckgo/app/di/StubJobSchedulerModule.kt b/app/src/androidTest/java/com/duckduckgo/app/di/StubJobSchedulerModule.kt index 0e54b9d3da37..b3f1b2654367 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/di/StubJobSchedulerModule.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/di/StubJobSchedulerModule.kt @@ -19,6 +19,12 @@ package com.duckduckgo.app.di import android.app.job.JobInfo import android.app.job.JobScheduler import android.app.job.JobWorkItem +import androidx.work.WorkManager +import com.duckduckgo.app.job.AndroidJobCleaner +import com.duckduckgo.app.job.AndroidWorkScheduler +import com.duckduckgo.app.job.JobCleaner +import com.duckduckgo.app.job.WorkScheduler +import com.duckduckgo.app.notification.AndroidNotificationScheduler import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -44,4 +50,16 @@ class StubJobSchedulerModule { } } + + @Singleton + @Provides + fun providesJobCleaner(workManager: WorkManager): JobCleaner { + return AndroidJobCleaner(workManager) + } + + @Singleton + @Provides + fun providesWorkScheduler(notificationScheduler: AndroidNotificationScheduler, jobCleaner: JobCleaner): WorkScheduler { + return AndroidWorkScheduler(notificationScheduler, jobCleaner) + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt b/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt index 3c3151c54476..f9f9d7f57fca 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/di/TestAppComponent.kt @@ -34,7 +34,6 @@ import retrofit2.Retrofit import javax.inject.Named import javax.inject.Singleton - @Singleton @Component( modules = [ diff --git a/app/src/androidTest/java/com/duckduckgo/app/job/JobCleanerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/job/JobCleanerTest.kt new file mode 100644 index 000000000000..62b81800a00c --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/job/JobCleanerTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.job + +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.Configuration +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.impl.utils.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import com.duckduckgo.app.job.JobCleaner.Companion.allDeprecatedNotificationWorkTags +import com.duckduckgo.app.notification.NotificationScheduler +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.concurrent.TimeUnit + +class JobCleanerTest { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var workManager: WorkManager + private lateinit var testee: JobCleaner + + @Before + fun before() { + initializeWorkManager() + testee = AndroidJobCleaner(workManager) + } + + // https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing + private fun initializeWorkManager() { + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setExecutor(SynchronousExecutor()) + .build() + + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + workManager = WorkManager.getInstance(context) + } + + @Test + fun whenStartedThenAllDeprecatedWorkIsCancelled() { + enqueueDeprecatedWorkers() + assertDeprecatedWorkersAreEnqueued() + testee.cleanDeprecatedJobs() + assertDeprecatedWorkersAreNotEnqueued() + } + + @Test + fun whenStartedAndNoDeprecatedJobsAreScheduledThenNothingIsRemoved() { + enqueueNonDeprecatedWorkers() + assertNonDeprecatedWorkersAreEnqueued() + testee.cleanDeprecatedJobs() + assertNonDeprecatedWorkersAreEnqueued() + } + + private fun enqueueDeprecatedWorkers() { + allDeprecatedNotificationWorkTags().forEach { + val requestBuilder = OneTimeWorkRequestBuilder() + val request = requestBuilder + .addTag(it) + .setInitialDelay(10, TimeUnit.SECONDS) + .build() + workManager.enqueue(request) + } + } + + private fun enqueueNonDeprecatedWorkers() { + allDeprecatedNotificationWorkTags().forEach { + val requestBuilder = OneTimeWorkRequestBuilder() + val request = requestBuilder + .addTag(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG) + .setInitialDelay(10, TimeUnit.SECONDS) + .build() + workManager.enqueue(request) + } + } + + private fun assertDeprecatedWorkersAreEnqueued() { + allDeprecatedNotificationWorkTags().forEach { + val scheduledWorkers = getScheduledWorkers(it) + assertFalse(scheduledWorkers.isEmpty()) + } + } + + private fun assertDeprecatedWorkersAreNotEnqueued() { + allDeprecatedNotificationWorkTags().forEach { + val scheduledWorkers = getScheduledWorkers(it) + assertTrue(scheduledWorkers.isEmpty()) + } + } + + private fun assertNonDeprecatedWorkersAreEnqueued() { + val scheduledWorkers = getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG) + assertFalse(scheduledWorkers.isEmpty()) + } + + private fun assertNonDeprecatedWorkersAreNotEnqueued() { + val scheduledWorkers = getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG) + assertTrue(scheduledWorkers.isEmpty()) + } + + + private fun getScheduledWorkers(tag: String): List { + return workManager + .getWorkInfosByTag(tag) + .get() + .filter { it.state == WorkInfo.State.ENQUEUED } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/job/TestWorker.kt b/app/src/androidTest/java/com/duckduckgo/app/job/TestWorker.kt new file mode 100644 index 000000000000..6787656cc76a --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/job/TestWorker.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.job + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters + +class TestWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) { + override fun doWork(): Result { + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/job/WorkSchedulerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/job/WorkSchedulerTest.kt new file mode 100644 index 000000000000..77ac299c56fe --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/job/WorkSchedulerTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.job + +import com.duckduckgo.app.notification.AndroidNotificationScheduler +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test + +class WorkSchedulerTest { + + private val notificationScheduler: AndroidNotificationScheduler = mock() + private val jobCleaner: JobCleaner = mock() + + private lateinit var testee: WorkScheduler + + @Before + fun before() { + testee = AndroidWorkScheduler( + notificationScheduler, + jobCleaner + ) + } + + @Test + fun schedulesNextNotificationAndCleansDeprecatedJobs() = runBlocking { + testee.scheduleWork() + + verify(notificationScheduler).scheduleNextNotification() + verify(jobCleaner).cleanDeprecatedJobs() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt index 544a48b13e4f..e3489ee94a66 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/notification/AndroidNotificationSchedulerTest.kt @@ -18,10 +18,14 @@ package com.duckduckgo.app.notification +import android.util.Log import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.Configuration import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkManager +import androidx.work.impl.utils.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.notification.NotificationScheduler.ClearDataNotificationWorker import com.duckduckgo.app.notification.NotificationScheduler.PrivacyNotificationWorker @@ -50,11 +54,12 @@ class AndroidNotificationSchedulerTest { private val privacyNotification: SchedulableNotification = mock() private val context = InstrumentationRegistry.getInstrumentation().targetContext - private var workManager = WorkManager.getInstance(context) + private lateinit var workManager: WorkManager private lateinit var testee: NotificationScheduler @Before fun before() { + initializeWorkManager() whenever(variantManager.getVariant(any())).thenReturn(DEFAULT_VARIANT) testee = NotificationScheduler( workManager, @@ -63,6 +68,17 @@ class AndroidNotificationSchedulerTest { ) } + // https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing + private fun initializeWorkManager() { + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setExecutor(SynchronousExecutor()) + .build() + + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + workManager = WorkManager.getInstance(context) + } + @Test fun whenPrivacyNotificationClearDataCanShowThenPrivacyNotificationIsScheduled() = runBlocking { whenever(privacyNotification.canShow()).thenReturn(true) @@ -99,30 +115,6 @@ class AndroidNotificationSchedulerTest { assertNoUnusedAppNotificationScheduled() } - @Test - fun whenNotificationIsScheduledOldJobsAreCancelled() = runBlocking { - whenever(privacyNotification.canShow()).thenReturn(false) - whenever(clearNotification.canShow()).thenReturn(false) - - enqueueDeprecatedJobs() - - testee.scheduleNextNotification() - - NotificationScheduler.allDeprecatedNotificationWorkTags().forEach { - assertTrue(getScheduledWorkers(it).isEmpty()) - } - } - - private fun enqueueDeprecatedJobs() { - NotificationScheduler.allDeprecatedNotificationWorkTags().forEach { - val request = OneTimeWorkRequestBuilder() - .addTag(it) - .build() - - workManager.enqueue(request) - } - } - private fun assertUnusedAppNotificationScheduled(workerName: String) { assertTrue(getScheduledWorkers(NotificationScheduler.UNUSED_APP_WORK_REQUEST_TAG).any { it.tags.contains(workerName) }) } diff --git a/app/src/main/java/com/duckduckgo/app/di/JobsModule.kt b/app/src/main/java/com/duckduckgo/app/di/JobsModule.kt index dec7f870b8f6..867a8628e823 100644 --- a/app/src/main/java/com/duckduckgo/app/di/JobsModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/JobsModule.kt @@ -18,11 +18,16 @@ package com.duckduckgo.app.di import android.app.job.JobScheduler import android.content.Context +import androidx.work.WorkManager +import com.duckduckgo.app.job.AndroidJobCleaner +import com.duckduckgo.app.job.AndroidWorkScheduler +import com.duckduckgo.app.job.JobCleaner +import com.duckduckgo.app.job.WorkScheduler +import com.duckduckgo.app.notification.AndroidNotificationScheduler import dagger.Module import dagger.Provides import javax.inject.Singleton - @Module class JobsModule { @@ -32,4 +37,15 @@ class JobsModule { return context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler } + @Singleton + @Provides + fun providesJobCleaner(workManager: WorkManager): JobCleaner { + return AndroidJobCleaner(workManager) + } + + @Singleton + @Provides + fun providesWorkScheduler(notificationScheduler: AndroidNotificationScheduler, jobCleaner: JobCleaner): WorkScheduler { + return AndroidWorkScheduler(notificationScheduler, jobCleaner) + } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt index 83097f16a618..b02aa7ee20f1 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoApplication.kt @@ -37,7 +37,7 @@ import com.duckduckgo.app.global.rating.AppEnjoymentLifecycleObserver import com.duckduckgo.app.global.shortcut.AppShortcutCreator import com.duckduckgo.app.httpsupgrade.HttpsUpgrader import com.duckduckgo.app.job.AppConfigurationSyncer -import com.duckduckgo.app.notification.AndroidNotificationScheduler +import com.duckduckgo.app.job.WorkScheduler import com.duckduckgo.app.notification.NotificationRegistrar import com.duckduckgo.app.referral.AppInstallationReferrerStateListener import com.duckduckgo.app.settings.db.SettingsDataStore @@ -120,7 +120,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO lateinit var dataClearer: DataClearer @Inject - lateinit var notificationScheduler: AndroidNotificationScheduler + lateinit var workScheduler: WorkScheduler @Inject lateinit var workerFactory: WorkerFactory @@ -293,7 +293,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO fun onAppResumed() { notificationRegistrar.updateStatus() GlobalScope.launch { - notificationScheduler.scheduleNextNotification() + workScheduler.scheduleWork() atbInitializer.initializeAfterReferrerAvailable() } } diff --git a/app/src/main/java/com/duckduckgo/app/job/JobCleaner.kt b/app/src/main/java/com/duckduckgo/app/job/JobCleaner.kt new file mode 100644 index 000000000000..8d6b9cfd7f58 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/job/JobCleaner.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.job + +import androidx.work.WorkManager +import com.duckduckgo.app.job.JobCleaner.Companion.allDeprecatedNotificationWorkTags + +interface JobCleaner { + fun cleanDeprecatedJobs() + + companion object { + private const val STICKY_SEARCH_CONTINUOUS_APP_USE_REQUEST_TAG = "com.duckduckgo.notification.schedule.continuous" + + fun allDeprecatedNotificationWorkTags() = listOf(STICKY_SEARCH_CONTINUOUS_APP_USE_REQUEST_TAG) + } +} + +class AndroidJobCleaner(private val workManager: WorkManager) : JobCleaner { + + override fun cleanDeprecatedJobs() { + allDeprecatedNotificationWorkTags().forEach { tag -> + workManager.cancelAllWorkByTag(tag) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/job/WorkScheduler.kt b/app/src/main/java/com/duckduckgo/app/job/WorkScheduler.kt new file mode 100644 index 000000000000..354cc7f9168c --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/job/WorkScheduler.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.job + +import com.duckduckgo.app.notification.AndroidNotificationScheduler + +interface WorkScheduler { + suspend fun scheduleWork() +} + +class AndroidWorkScheduler( + private val notificationScheduler: AndroidNotificationScheduler, + private val jobCleaner: JobCleaner +) : WorkScheduler { + override suspend fun scheduleWork() { + jobCleaner.cleanDeprecatedJobs() + notificationScheduler.scheduleNextNotification() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt index 099bc0f8660e..d4e4053d526c 100644 --- a/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt +++ b/app/src/main/java/com/duckduckgo/app/notification/AndroidNotificationScheduler.kt @@ -46,17 +46,9 @@ class NotificationScheduler( ) : AndroidNotificationScheduler { override suspend fun scheduleNextNotification() { - cancelAllUnnecessaryWork() scheduleInactiveUserNotifications() } - private fun cancelAllUnnecessaryWork(){ - allDeprecatedNotificationWorkTags().forEach { tag -> - workManager.cancelAllWorkByTag(tag) - } - - } - private suspend fun scheduleInactiveUserNotifications() { workManager.cancelAllWorkByTag(UNUSED_APP_WORK_REQUEST_TAG) @@ -117,11 +109,5 @@ class NotificationScheduler( companion object { const val UNUSED_APP_WORK_REQUEST_TAG = "com.duckduckgo.notification.schedule" - - // below there is a list of TAGs that were used at some point but that are no longer active - // we want to make sure that this TAGs are cancelled to avoid inconsistencies - private const val CONTINUOUS_APP_USE_REQUEST_TAG = "com.duckduckgo.notification.schedule.continuous" // Sticky Search Experiment - - fun allDeprecatedNotificationWorkTags() = listOf(CONTINUOUS_APP_USE_REQUEST_TAG) } } \ No newline at end of file