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

Issue #11698: Add an updater to periodically fetch the Contile Top Sites #11721

Merged
merged 2 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
gabrielluong marked this conversation as resolved.
Show resolved Hide resolved
context: Context,
private val client: Client,
private val endPointURL: String = CONTILE_ENDPOINT_URL,
private val maxCacheAgeInMinutes: Long = -1
) : TopSitesProvider {

private val applicationContext = context.applicationContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks good to me now with the context things figure out! Did you want to address / handle mozilla-mobile/fenix#23781 (comment) here as well or in a separate PR. I think the Fenix integration is dependent on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am gonna do that separately.

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(
gabrielluong marked this conversation as resolved.
Show resolved Hide resolved
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() {
Amejia481 marked this conversation as resolved.
Show resolved Hide resolved
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