Source code
Revision control
Copy as Markdown
Other Tools
/* 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
package org.mozilla.fenix.historymetadata
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.storage.DocumentType
import mozilla.components.concept.storage.HistoryMetadataKey
import mozilla.components.concept.storage.HistoryMetadataObservation
import mozilla.components.concept.storage.HistoryMetadataStorage
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.utils.NamedThreadFactory
/** Service for managing (creating, updating, deleting) history metadata. */
interface HistoryMetadataService {
/**
* Creates a history metadata record for the provided tab.
*
* @param tab the [TabSessionState] to record metadata for.
* @param searchTerms Search terms associated with this metadata.
* @param referrerUrl Referrer url associated with this metadata.
*/
fun createMetadata(
tab: TabSessionState,
searchTerms: String? = null,
referrerUrl: String? = null,
): HistoryMetadataKey
/**
* Updates the history metadata corresponding to the provided tab.
*
* @param key the [HistoryMetadataKey] identifying history metadata.
* @param tab the [TabSessionState] to update history metadata for.
*/
fun updateMetadata(key: HistoryMetadataKey, tab: TabSessionState)
/**
* Deletes history metadata records that haven't been updated since the specified timestamp.
*
* @param olderThan timestamp indicating which records to delete.
*/
fun cleanup(olderThan: Long)
}
class DefaultHistoryMetadataService(
private val storage: HistoryMetadataStorage,
private val scope: CoroutineScope =
CoroutineScope(
Executors.newSingleThreadExecutor(NamedThreadFactory("HistoryMetadataService")).asCoroutineDispatcher()
),
private val currentTimeMillis: () -> Long = { System.currentTimeMillis() },
) : HistoryMetadataService {
private val logger = Logger("DefaultHistoryMetadataService")
// NB: this map is only accessed from a single-thread executor (dispatcher of `scope`).
private val tabsLastUpdated = mutableMapOf<String, Long>()
override fun createMetadata(tab: TabSessionState, searchTerms: String?, referrerUrl: String?): HistoryMetadataKey {
logger.debug("Creating metadata for tab ${tab.id}")
val existingMetadata = tab.historyMetadata
val metadataKey =
if (existingMetadata != null && existingMetadata.url == tab.content.url) {
existingMetadata
} else {
tab.toHistoryMetadataKey(searchTerms, referrerUrl)
}
val documentTypeObservation =
HistoryMetadataObservation.DocumentTypeObservation(
documentType =
when (tab.mediaSessionState) {
null -> DocumentType.Regular
else -> DocumentType.Media
}
)
scope.launch {
storage.noteHistoryMetadataObservation(metadataKey, documentTypeObservation)
}
return metadataKey
}
override fun updateMetadata(key: HistoryMetadataKey, tab: TabSessionState) {
val now = currentTimeMillis()
val lastAccess = tab.lastAccess
if (lastAccess == 0L) {
logger.debug("Not updating metadata for tab $tab - lastAccess=0")
return
} else {
logger.debug("Updating metadata for tab $tab")
}
// If it's possible that multiple threads overlap and run this block simultaneously, we
// may over-observe, and record when we didn't intend to.
// To make these cases easier to reason through (and likely correct),
// `scope` is a single-threaded dispatcher. Execution of these blocks is thus serialized.
scope.launch {
val lastUpdated = tabsLastUpdated[tab.id] ?: 0
if (lastUpdated > lastAccess) {
logger.debug("Failed to update metadata because it was already recorded or lastAccess is incorrect")
return@launch
}
val viewTimeObservation =
HistoryMetadataObservation.ViewTimeObservation(viewTime = (now - lastAccess).toInt())
storage.noteHistoryMetadataObservation(key, viewTimeObservation)
tabsLastUpdated[tab.id] = now
}
}
override fun cleanup(olderThan: Long) {
logger.debug("Deleting metadata last updated before $olderThan")
scope.launch {
storage.deleteHistoryMetadataOlderThan(olderThan)
}
}
}
fun TabSessionState.toHistoryMetadataKey(searchTerms: String?, referrerUrl: String?): HistoryMetadataKey =
HistoryMetadataKey(
url = content.url,
referrerUrl = referrerUrl,
searchTerm = searchTerms,
)