Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Issue #11698: Add an updater to periodically fetch the Contile Top Sites
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielluong authored and mergify[bot] committed Feb 18, 2022
1 parent c7eefe7 commit 300df48
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 4 deletions.
2 changes: 2 additions & 0 deletions components/service/contile/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ android {
dependencies {
implementation Dependencies.kotlin_stdlib
implementation Dependencies.kotlin_coroutines
implementation Dependencies.androidx_work_runtime

implementation project(':concept-fetch')
implementation project(':support-ktx')
Expand All @@ -32,6 +33,7 @@ dependencies {

testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.androidx_work_testing
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal const val CACHE_FILE_NAME = "mozilla_components_service_contile.json"
internal const val MINUTE_IN_MS = 60 * 1000

/**
* Provide access to the Contile services API.
* Provides access to the Contile services API.
*
* @property context A reference to the application context.
* @property client [Client] used for interacting with the Contile HTTP API.
Expand All @@ -36,14 +36,14 @@ internal const val MINUTE_IN_MS = 60 * 1000
* before a refresh is attempted. Defaults to -1, meaning no cache is being used by default.
*/
class ContileTopSitesProvider(
private val context: Context,
context: Context,
private val client: Client,
private val endPointURL: String = CONTILE_ENDPOINT_URL,
private val maxCacheAgeInMinutes: Long = -1
) : TopSitesProvider {

private val applicationContext = context.applicationContext
private val logger = Logger("ContileTopSitesProvider")

private val diskCacheLock = Any()

/**
Expand Down Expand Up @@ -130,7 +130,7 @@ class ContileTopSitesProvider(
private fun getCacheFile(): AtomicFile = AtomicFile(getBaseCacheFile())

@VisibleForTesting
internal fun getBaseCacheFile(): File = File(context.filesDir, CACHE_FILE_NAME)
internal fun getBaseCacheFile(): File = File(applicationContext.filesDir, CACHE_FILE_NAME)
}

internal fun JSONObject.getTopSites(): List<TopSite.Provided> =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.service.contile

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import mozilla.components.support.base.log.logger.Logger
import java.util.concurrent.TimeUnit

/**
* Provides functionality to schedule updates of Contile top sites.
*
* @property context A reference to the application context.
* @property provider An instance of [ContileTopSitesProvider] which provides access to the Contile
* services API for fetching top sites.
* @property frequency Optional [Frequency] that specifies how often the Contile top site updates
* should happen.
*/
class ContileTopSitesUpdater(
private val context: Context,
private val provider: ContileTopSitesProvider,
private val frequency: Frequency = Frequency(1, TimeUnit.DAYS)
) {

private val logger = Logger("ContileTopSitesUpdater")

/**
* Starts a work request in the background to periodically update the list of
* Contile top sites.
*/
fun startPeriodicWork() {
ContileTopSitesUseCases.initialize(provider)

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
PERIODIC_WORK_TAG,
ExistingPeriodicWorkPolicy.KEEP,
createPeriodicWorkRequest()
)

logger.info("Started periodic work to update Contile top sites")
}

/**
* Stops the work request to periodically update the list of Contile top sites.
*/
fun stopPeriodicWork() {
ContileTopSitesUseCases.destroy()

WorkManager.getInstance(context).cancelUniqueWork(PERIODIC_WORK_TAG)

logger.info("Stopped periodic work to update Contile top sites")
}

@VisibleForTesting
internal fun createPeriodicWorkRequest() =
PeriodicWorkRequestBuilder<ContileTopSitesUpdaterWorker>(
repeatInterval = frequency.repeatInterval,
repeatIntervalTimeUnit = frequency.repeatIntervalTimeUnit
).apply {
setConstraints(getWorkerConstraints())
addTag(PERIODIC_WORK_TAG)
}.build()

@VisibleForTesting
internal fun getWorkerConstraints() = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

companion object {
internal const val PERIODIC_WORK_TAG = "mozilla.components.service.contile.periodicWork"
}
}

/**
* Indicates how often Contile top sites should be updated.
*
* @property repeatInterval Long indicating how often the update should happen.
* @property repeatIntervalTimeUnit The time unit of the [repeatInterval].
*/
data class Frequency(
val repeatInterval: Long,
val repeatIntervalTimeUnit: TimeUnit
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.service.contile

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.support.base.log.logger.Logger

/**
* An implementation of [CoroutineWorker] to perform Contile top site updates.
*/
internal class ContileTopSitesUpdaterWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {

private val logger = Logger("ContileTopSitesUpdaterWorker")

@Suppress("TooGenericExceptionCaught")
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
ContileTopSitesUseCases().refreshContileTopSites.invoke()
Result.success()
} catch (e: Exception) {
logger.error("Failed to refresh Contile top sites", e)
Result.failure()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.service.contile

import androidx.annotation.VisibleForTesting

/**
* Contains use cases related to the Contlie top sites feature.
*/
internal class ContileTopSitesUseCases {

/**
* Refresh Contile top sites use case.
*/
class RefreshContileTopSitesUseCase internal constructor() {
/**
* Refreshes the Contile top sites.
*/
suspend operator fun invoke() {
requireContileTopSitesProvider().getTopSites(allowCache = false)
}
}

internal companion object {
@VisibleForTesting internal var provider: ContileTopSitesProvider? = null

/**
* Initializes the [ContileTopSitesProvider] which will providde access to the Contile
* services API.
*/
internal fun initialize(provider: ContileTopSitesProvider) {
this.provider = provider
}

/**
* Unbinds the [ContileTopSitesProvider].
*/
internal fun destroy() {
this.provider = null
}

/**
* Returns the [ContileTopSitesProvider], otherwise throw an exception if the [provider]
* has not been initialized.
*/
internal fun requireContileTopSitesProvider(): ContileTopSitesProvider {
return requireNotNull(provider) {
"initialize must be called before trying to access the ContileTopSitesProvider"
}
}
}

val refreshContileTopSites: RefreshContileTopSitesUseCase by lazy {
RefreshContileTopSitesUseCase()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.service.contile

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.Configuration
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
import androidx.work.testing.WorkManagerTestInitHelper
import kotlinx.coroutines.runBlocking
import mozilla.components.service.contile.ContileTopSitesUpdater.Companion.PERIODIC_WORK_TAG
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ContileTopSitesUpdaterTest {

@Before
fun setUp() {
WorkManagerTestInitHelper.initializeTestWorkManager(
testContext,
Configuration.Builder().build()
)
}

@After
fun tearDown() {
WorkManager.getInstance(testContext).cancelUniqueWork(PERIODIC_WORK_TAG)
}

@Test
fun `WHEN periodic work is started THEN work is queued`() = runBlocking {
val updater = ContileTopSitesUpdater(testContext, provider = mock())
val workManager = WorkManager.getInstance(testContext)
var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()

assertTrue(workInfo.isEmpty())
assertNull(ContileTopSitesUseCases.provider)

updater.startPeriodicWork()

assertNotNull(ContileTopSitesUseCases.provider)
assertNotNull(ContileTopSitesUseCases.requireContileTopSitesProvider())

workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
val work = workInfo.first()

assertEquals(1, workInfo.size)
assertEquals(WorkInfo.State.ENQUEUED, work.state)
assertTrue(work.tags.contains(PERIODIC_WORK_TAG))
}

@Test
fun `GIVEN periodic work is started WHEN period work is stopped THEN no work is queued`() = runBlocking {
val updater = ContileTopSitesUpdater(testContext, provider = mock())
val workManager = WorkManager.getInstance(testContext)
var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()

assertTrue(workInfo.isEmpty())

updater.startPeriodicWork()

workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()

assertEquals(1, workInfo.size)

updater.stopPeriodicWork()

workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
val work = workInfo.first()

assertNull(ContileTopSitesUseCases.provider)
assertEquals(WorkInfo.State.CANCELLED, work.state)
}

@Test
fun `WHEN period work request is created THEN it contains the correct constraints`() {
val updater = ContileTopSitesUpdater(testContext, provider = mock())
val workRequest = updater.createPeriodicWorkRequest()

assertTrue(workRequest.tags.contains(PERIODIC_WORK_TAG))
assertEquals(updater.getWorkerConstraints(), workRequest.workSpec.constraints)
}
}
Loading

0 comments on commit 300df48

Please sign in to comment.