[fenix] For https://github.com/mozilla-mobile/fenix/issues/20586 - Basic Jetpack Compose implementation.

This is just the basic skeleton for the feature.
pull/600/head
Mugurell 3 years ago committed by mergify[bot]
parent 96042fa50a
commit b1cc91a9ac

@ -367,6 +367,26 @@ android.applicationVariants.all { variant ->
println("--")
}
// -------------------------------------------------------------------------------------------------
// Pocket recommendations: Read token from local file if it exists (Only debug builds)
// -------------------------------------------------------------------------------------------------
print("Pocket recommendations token: ")
if (isDebug) {
if (gradle.hasProperty("localProperties.pocketConsumerKey")) {
def token = gradle.getProperty("localProperties.pocketConsumerKey")
buildConfigField 'String', 'POCKET_TOKEN', '"' + token + '"'
println "Added from local.properties file"
} else {
buildConfigField 'String', 'POCKET_TOKEN', '""'
println "Not found in local.properties file"
}
} else {
buildConfigField 'String', 'POCKET_TOKEN', '""'
println "Is to be only used in debug"
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set flag for official builds; similar to MOZILLA_OFFICIAL in mozilla-central.
// -------------------------------------------------------------------------------------------------
@ -475,6 +495,7 @@ dependencies {
implementation Deps.mozilla_feature_webcompat
implementation Deps.mozilla_feature_webnotifications
implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_pocket
implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_sync_autofill

@ -259,6 +259,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.requestInterceptor.setNavigationController(navHost.navController)
if (settings().pocketRecommendations) {
components.core.pocketStoriesService.startPeriodicStoriesRefresh()
}
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
@ -394,6 +398,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
)
)
components.core.pocketStoriesService.stopPeriodicStoriesRefresh()
privateNotificationObserver?.stop()
}

@ -59,6 +59,9 @@ import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.location.LocationService
import mozilla.components.service.location.MozillaLocationService
import mozilla.components.service.pocket.Frequency
import mozilla.components.service.pocket.PocketStoriesConfig
import mozilla.components.service.pocket.PocketStoriesService
import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.locale.LocaleManager
@ -84,6 +87,8 @@ import org.mozilla.fenix.telemetry.TelemetryMiddleware
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay
import org.mozilla.geckoview.GeckoRuntime
import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit
/**
* Component group for all core browser functionality.
@ -319,6 +324,14 @@ class Core(
val pinnedSiteStorage by lazyMonitored { PinnedSiteStorage(context) }
@Suppress("MagicNumber")
val pocketStoriesConfig by lazyMonitored {
PocketStoriesConfig(
BuildConfig.POCKET_TOKEN, client, Frequency(4, TimeUnit.HOURS), 7
)
}
val pocketStoriesService by lazyMonitored { PocketStoriesService(context, pocketStoriesConfig) }
val topSitesStorage by lazyMonitored {
val defaultTopSites = mutableListOf<Pair<String, String>>()

@ -47,6 +47,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
@ -68,6 +69,7 @@ import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.pocket.stories.PocketStoriesUseCases
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
@ -239,6 +241,13 @@ class HomeFragment : Fragment() {
)
}
if (requireContext().settings().pocketRecommendations) {
lifecycleScope.async(IO) {
val stories = PocketStoriesUseCases().GetPocketStories(requireContext()).invoke()
homeFragmentStore.dispatch(HomeFragmentAction.PocketArticlesChange(stories))
}
}
topSitesFeature.set(
feature = TopSitesFeature(
view = DefaultTopSitesView(homeFragmentStore),
@ -328,6 +337,7 @@ class HomeFragment : Fragment() {
updateLayout(binding.root)
sessionControlView = SessionControlView(
homeFragmentStore,
binding.sessionControlRecyclerView,
viewLifecycleOwner,
sessionControlInteractor,

@ -13,6 +13,7 @@ import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
@ -60,7 +61,8 @@ data class HomeFragmentState(
val showSetAsDefaultBrowserCard: Boolean = false,
val recentTabs: List<TabSessionState> = emptyList(),
val recentBookmarks: List<BookmarkNode> = emptyList(),
val historyMetadata: List<HistoryMetadataGroup> = emptyList()
val historyMetadata: List<HistoryMetadataGroup> = emptyList(),
val pocketArticles: List<PocketRecommendedStory> = emptyList()
) : State
sealed class HomeFragmentAction : Action {
@ -87,6 +89,7 @@ sealed class HomeFragmentAction : Action {
data class RecentBookmarksChange(val recentBookmarks: List<BookmarkNode>) : HomeFragmentAction()
data class HistoryMetadataChange(val historyMetadata: List<HistoryMetadataGroup>) : HomeFragmentAction()
data class HistoryMetadataExpanded(val historyMetadataGroup: HistoryMetadataGroup) : HomeFragmentAction()
data class PocketArticlesChange(val pocketArticles: List<PocketRecommendedStory>) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction()
object RemoveSetDefaultBrowserCard : HomeFragmentAction()
}
@ -141,5 +144,6 @@ private fun homeFragmentStateReducer(
}
)
}
is HomeFragmentAction.PocketArticlesChange -> state.copy(pocketArticles = action.pocketArticles)
}
}

@ -8,6 +8,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -24,6 +25,7 @@ import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.historymetadata.view.HistoryMetadataGroupViewHolder
import org.mozilla.fenix.historymetadata.view.HistoryMetadataHeaderViewHolder
import org.mozilla.fenix.historymetadata.view.HistoryMetadataViewHolder
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.OnboardingState
import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarksViewHolder
import org.mozilla.fenix.home.recenttabs.view.RecentTabViewDecorator
@ -33,6 +35,7 @@ import org.mozilla.fenix.home.recenttabs.view.RecentTabsItemPosition
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketStoriesViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder
@ -227,6 +230,9 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
other is HistoryMetadataItem && historyMetadata.key.url == other.historyMetadata.key.url
}
object PocketStoriesItem :
AdapterItem(PocketStoriesViewHolder.LAYOUT_ID)
/**
* True if this item represents the same value as other. Used by [AdapterItemDiffCallback].
*/
@ -254,6 +260,7 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
}
class SessionControlAdapter(
private val store: HomeFragmentStore,
private val interactor: SessionControlInteractor,
private val viewLifecycleOwner: LifecycleOwner,
private val components: Components
@ -262,6 +269,13 @@ class SessionControlAdapter(
// This method triggers the ComplexMethod lint error when in fact it's quite simple.
@SuppressWarnings("ComplexMethod")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
PocketStoriesViewHolder.LAYOUT_ID -> return PocketStoriesViewHolder(
ComposeView(parent.context),
store
)
}
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor)
@ -325,6 +339,13 @@ class SessionControlAdapter(
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
when (holder) {
is PocketStoriesViewHolder -> holder.composeView.disposeComposition()
else -> super.onViewRecycled(holder)
}
}
override fun getItemViewType(position: Int) = getItem(position).viewType
override fun onBindViewHolder(
@ -389,6 +410,10 @@ class SessionControlAdapter(
is HistoryMetadataGroupViewHolder -> {
holder.bind((item as AdapterItem.HistoryMetadataGroup).historyMetadataGroup)
}
is PocketStoriesViewHolder -> {
// no-op. This ViewHolder receives the HomeStore as argument and will observe that
// without the need for us to manually update from here the data to be displayed.
}
}
}
}

@ -14,10 +14,12 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.OnboardingState
@ -35,7 +37,8 @@ private fun normalModeAdapterItems(
showCollectionsPlaceholder: Boolean,
showSetAsDefaultBrowserCard: Boolean,
recentTabs: List<TabSessionState>,
historyMetadata: List<HistoryMetadataGroup>
historyMetadata: List<HistoryMetadataGroup>,
pocketArticles: List<PocketRecommendedStory>
): List<AdapterItem> {
val items = mutableListOf<AdapterItem>()
@ -69,6 +72,10 @@ private fun normalModeAdapterItems(
showCollections(collections, expandedCollections, items)
}
if (pocketArticles.isNotEmpty()) {
items.add(AdapterItem.PocketStoriesItem)
}
return items
}
@ -194,7 +201,8 @@ private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
showCollectionPlaceholder,
showSetAsDefaultBrowserCard,
recentTabs,
historyMetadata
historyMetadata,
pocketArticles
)
is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
@ -206,6 +214,7 @@ private fun collectionTabItems(collection: TabCollection) =
}
class SessionControlView(
store: HomeFragmentStore,
val containerView: View,
viewLifecycleOwner: LifecycleOwner,
interactor: SessionControlInteractor,
@ -215,6 +224,7 @@ class SessionControlView(
val view: RecyclerView = containerView as RecyclerView
private val sessionControlAdapter = SessionControlAdapter(
store,
interactor,
viewLifecycleOwner,
containerView.context.components

@ -0,0 +1,285 @@
/* 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/. */
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
@file:Suppress("MagicNumber")
package org.mozilla.fenix.home.sessioncontrol.viewholders.pocket
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.R
import kotlin.random.Random
/**
* Displays a single [PocketRecommendedStory].
*/
@Composable
fun PocketStory(
@PreviewParameter(PocketStoryProvider::class) story: PocketRecommendedStory,
modifier: Modifier = Modifier
) {
Column(
modifier
.size(160.dp, 191.dp)
.clip(RoundedCornerShape(4.dp))
.clickable { /* no-op */ }
) {
Card(
elevation = 6.dp,
shape = RoundedCornerShape(4.dp),
modifier = Modifier
.size(160.dp, 87.dp)
.padding(bottom = 8.dp)
) {
// Don't yet have a easy way to load URLs in Images.
// Default to a solid color to make it easy to appreciate dimensions
Box(Modifier.background(Color.Blue))
// Image(
// painterResource(R.drawable.ic_pdd),
// contentDescription = "hero image",
// contentScale = ContentScale.FillHeight,
// )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
modifier = Modifier.padding(bottom = 2.dp),
text = story.domain,
style = MaterialTheme.typography.caption,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = story.title,
style = MaterialTheme.typography.subtitle1,
maxLines = 4,
overflow = TextOverflow.Ellipsis
)
}
}
/**
* Displays a list of [PocketRecommendedStory]es.
*/
@Composable
fun PocketStories(
@PreviewParameter(PocketStoryProvider::class) stories: List<PocketRecommendedStory>
) {
// Items will be shown on two rows. Ceil the divide result to show more items on the top row.
val halfStoriesIndex = (stories.size + 1) / 2
LazyRow {
itemsIndexed(stories) { index, item ->
if (index < halfStoriesIndex) {
Column(
Modifier.padding(end = if (index == halfStoriesIndex) 0.dp else 8.dp)
) {
PocketStory(item)
Spacer(modifier = Modifier.height(24.dp))
stories.getOrNull(halfStoriesIndex + index)?.let {
PocketStory(it)
}
}
}
}
}
}
/**
* Displays [content] in a layout which will have at the bottom more information about Pocket
* and also an external link for more up-to-date content.
*/
@Composable
fun PocketRecommendations(
content: @Composable (() -> Unit)
) {
val annotatedText = buildAnnotatedString {
val text = "Pocket is part of the Firefox family. "
val link = "Learn more."
val annotationStartIndex = text.length
val annotationEndIndex = annotationStartIndex + link.length
append(text + link)
addStyle(
SpanStyle(textDecoration = TextDecoration.Underline),
start = annotationStartIndex,
end = annotationEndIndex
)
addStringAnnotation(
tag = "link",
annotation = "https://www.mozilla.org/en-US/firefox/pocket/",
start = annotationStartIndex,
end = annotationEndIndex
)
}
Column(
modifier = Modifier.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
content()
// Don't yet have the bottom image from designs as a proper Android SVG.
Box(
Modifier
.background(Color.Red)
.size(64.dp, 27.dp)
.padding(top = 16.dp)
)
// Image(
// painterResource(R.drawable.ic_firefox_pocket),
// "Firefox and Pocket logos",
// Modifier
// .size(64.dp, 27.dp)
// .padding(top = 16.dp),
// contentScale = ContentScale.FillHeight
// )
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
ClickableText(
text = annotatedText,
style = MaterialTheme.typography.caption,
onClick = {
annotatedText
.getStringAnnotations("link", it, it)
.firstOrNull()?.let {
println("Learn more clicked! Should now access ${it.item}")
}
}
)
}
}
}
/**
* Displays [content] in an expandable card.
*/
@Composable
fun ExpandableCard(content: @Composable (() -> Unit)) {
var isExpanded by remember { mutableStateOf(true) }
val chevronRotationState by animateFloatAsState(targetValue = if (isExpanded) 0f else 180f)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { isExpanded = !isExpanded }
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(10f),
text = "Trending stories from Pocket",
style = MaterialTheme.typography.h6,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
IconButton(
onClick = { isExpanded = !isExpanded },
modifier = Modifier.rotate(chevronRotationState)
) {
Icon(
modifier = Modifier.weight(1f),
painter = painterResource(id = R.drawable.ic_chevron_up),
contentDescription = "Expand or collapse Pocket recommended stories",
)
}
}
AnimatedVisibility(visible = isExpanded) {
content()
}
}
}
}
@Composable
@Preview
private fun FinalDesign() {
ExpandableCard {
PocketRecommendations {
PocketStories(stories = getFakePocketStories(7))
}
}
}
private class PocketStoryProvider : PreviewParameterProvider<PocketRecommendedStory> {
override val values = getFakePocketStories(7).asSequence()
override val count = 7
}
private fun getFakePocketStories(limit: Int = 1): List<PocketRecommendedStory> {
return mutableListOf<PocketRecommendedStory>().apply {
for (index in 0 until limit) {
val randomNumber = Random.nextInt(0, 10)
add(
PocketRecommendedStory(
id = randomNumber.toLong(),
url = "https://story$randomNumber.com",
title = "This is a ${"very ".repeat(randomNumber)} long title",
domain = "Website no #$randomNumber",
excerpt = "FOO",
dedupeUrl = "BAR",
imageSrc = "",
sortId = randomNumber,
publishedTimestamp = randomNumber.toString()
)
)
}
}
}

@ -0,0 +1,51 @@
/* 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 org.mozilla.fenix.home.sessioncontrol.viewholders.pocket
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.lib.state.ext.observeAsComposableState
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.home.HomeFragmentStore
/**
* [RecyclerView.ViewHolder] that will display a list of [PocketRecommendedStory]es
* which is to be provided in the [bind] method.
*
* @param composeView [ComposeView] which will be populated with Jetpack Compose UI content.
* @param store [HomeFragmentStore] containing the list of Pocket stories to be displayed.
*/
class PocketStoriesViewHolder(
val composeView: ComposeView,
val store: HomeFragmentStore
) : RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
composeView.setContent {
PocketStories(store)
}
}
companion object {
val LAYOUT_ID = View.generateViewId()
}
}
@Composable
fun PocketStories(store: HomeFragmentStore) {
val stories = store.observeAsComposableState { state -> state.pocketArticles }
ExpandableCard {
PocketRecommendations {
PocketStories(stories.value ?: emptyList())
}
}
}

@ -11,6 +11,7 @@ import android.widget.Toast
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
@ -76,6 +77,24 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
isChecked = context.settings().nimbusUsePreview
onPreferenceChangeListener = SharedPreferenceUpdater()
}
requirePreference<SwitchPreference>(R.string.pref_key_pocket_homescreen_recommendations).apply {
isVisible = Config.channel.isDebug
isChecked = context.settings().pocketRecommendations
onPreferenceChangeListener = object : SharedPreferenceUpdater() {
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
(newValue as? Boolean)?.let {
if (it) {
context.components.core.pocketStoriesService.startPeriodicStoriesRefresh()
} else {
context.components.core.pocketStoriesService.stopPeriodicStoriesRefresh()
}
}
return super.onPreferenceChange(preference, newValue)
}
}
}
}
companion object {

@ -128,6 +128,7 @@ object Deps {
const val mozilla_feature_webcompat = "org.mozilla.components:feature-webcompat:${Versions.mozilla_android_components}"
const val mozilla_feature_webnotifications = "org.mozilla.components:feature-webnotifications:${Versions.mozilla_android_components}"
const val mozilla_feature_webcompat_reporter = "org.mozilla.components:feature-webcompat-reporter:${Versions.mozilla_android_components}"
const val mozilla_service_pocket = "org.mozilla.components:service-pocket:${Versions.mozilla_android_components}"
const val mozilla_service_digitalassetlinks =
"org.mozilla.components:service-digitalassetlinks:${Versions.mozilla_android_components}"

Loading…
Cancel
Save