diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 8fe838836..5a3f2ec22 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -109,4 +109,10 @@ object FeatureFlags { * Enables the Unified Search feature. */ val unifiedSearchFeature = Config.channel.isNightlyOrDebug + + /** + * Enables receiving from the messaging framework. + */ + @Suppress("MayBeConst") + val messagingFeature = false } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 41c492648..e785fa2ec 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -65,6 +65,7 @@ import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Core +import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.components.metrics.MozillaProductDetector @@ -159,6 +160,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider { GlobalScope.launch(Dispatchers.IO) { setStartupMetrics(store, settings()) } + if (FeatureFlags.messagingFeature && settings().isExperimentationEnabled) { + components.appStore.dispatch(AppAction.MessagingAction.Restore) + } } @CallSuper diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index f347e282c..d6195c246 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -1016,18 +1016,22 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } + fun processIntent(intent: Intent): Boolean { + return externalSourceIntentProcessors.any { + it.process( + intent, + navHost.navController, + this.intent + ) + } + } + @VisibleForTesting internal fun getSettings(): Settings = settings() private fun shouldNavigateToBrowserOnColdStart(savedInstanceState: Bundle?): Boolean { return isActivityColdStarted(intent, savedInstanceState) && - !externalSourceIntentProcessors.any { - it.process( - intent, - navHost.navController, - this.intent - ) - } + !processIntent(intent) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt index 21e0d873f..e6cda2bd3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt @@ -25,6 +25,8 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.experiments.createNimbus import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.gleanplumb.KeyPairMessageMetadataStorage +import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID @@ -116,6 +118,15 @@ class Analytics( FxNimbus.api = api } } + + val messagingStorage by lazyMonitored { + NimbusMessagingStorage( + context = context, + metadataStorage = KeyPairMessageMetadataStorage(), + gleanPlumb = experiments, + messagingFeature = FxNimbus.features.messaging, + ) + } } fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty() diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index f5dc3cd5f..c52c25427 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -33,6 +33,7 @@ import org.mozilla.fenix.ext.asRecentTabs import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.filterState import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.gleanplumb.state.MessagingMiddleware import org.mozilla.fenix.ext.sort import org.mozilla.fenix.home.PocketUpdatesMiddleware import org.mozilla.fenix.home.blocklist.BlocklistHandler @@ -196,6 +197,7 @@ class Components(private val context: Context) { val appStartReasonProvider by lazyMonitored { AppStartReasonProvider() } val startupActivityLog by lazyMonitored { StartupActivityLog() } val startupStateProvider by lazyMonitored { StartupStateProvider(startupActivityLog, appStartReasonProvider) } + val appStore by lazyMonitored { val blocklistHandler = BlocklistHandler(settings) @@ -206,7 +208,6 @@ class Components(private val context: Context) { topSites = core.topSitesStorage.cachedTopSites.sort(), recentBookmarks = emptyList(), showCollectionPlaceholder = settings.showCollectionsPlaceholderOnHome, - showSetAsDefaultBrowserCard = settings.shouldShowSetAsDefaultBrowserCard(), // Provide an initial state for recent tabs to prevent re-rendering on the home screen. // This will otherwise cause a visual jump as the section gets rendered from no state // to some state. @@ -222,7 +223,8 @@ class Components(private val context: Context) { PocketUpdatesMiddleware( core.pocketStoriesService, context.pocketStoriesSelectedCategoriesDataStore - ) + ), + MessagingMiddleware(messagingStorage = analytics.messagingStorage) ) ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt index 7ff9ff931..1af3c4c14 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt @@ -16,6 +16,8 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem +import org.mozilla.fenix.gleanplumb.Message +import org.mozilla.fenix.gleanplumb.MessagingState /** * [Action] implementation related to [AppStore]. @@ -61,5 +63,48 @@ sealed class AppAction : Action { val categoriesSelected: List ) : AppAction() object RemoveCollectionsPlaceholder : AppAction() - object RemoveSetDefaultBrowserCard : AppAction() + /** + * [Action]s related to interactions with the Messaging Framework. + */ + sealed class MessagingAction : AppAction() { + /** + * Restores the [Message] state from the storage. + */ + object Restore : MessagingAction() + + /** + * Evaluates if a new messages should be shown to users. + */ + object Evaluate : MessagingAction() + + /** + * Updates [MessagingState.messageToShow] with the given [message]. + */ + data class UpdateMessageToShow(val message: Message) : MessagingAction() + + /** + * Updates [MessagingState.messageToShow] with the given [message]. + */ + object ConsumeMessageToShow : MessagingAction() + + /** + * Updates [MessagingState.messages] with the given [messages]. + */ + data class UpdateMessages(val messages: List) : MessagingAction() + + /** + * Indicates the given [message] was clicked. + */ + data class MessageClicked(val message: Message) : MessagingAction() + + /** + * Indicates the given [message] was shown. + */ + data class MessageDisplayed(val message: Message) : MessagingAction() + + /** + * Indicates the given [message] was dismissed. + */ + data class MessageDismissed(val message: Message) : MessagingAction() + } } diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt index 0f39e2b28..5c226b14e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt @@ -17,6 +17,7 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem +import org.mozilla.fenix.gleanplumb.MessagingState /** * Value type that represents the state of the tabs tray. @@ -30,12 +31,12 @@ import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem * @property mode The state of the [HomeFragment] UI. * @property topSites The list of [TopSite] in the [HomeFragment]. * @property showCollectionPlaceholder If true, shows a placeholder when there are no collections. - * @property showSetAsDefaultBrowserCard If true, shows the default browser card * @property recentTabs The list of recent [RecentTab] in the [HomeFragment]. * @property recentBookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment]. * @property recentHistory The list of [RecentlyVisitedItem]s. * @property pocketStories The list of currently shown [PocketRecommendedStory]s. * @property pocketStoriesCategories All [PocketRecommendedStory] categories. + * @property messaging State related messages. * Also serves as an in memory cache of all stories mapped by category allowing for quick stories filtering. */ data class AppState( @@ -46,11 +47,11 @@ data class AppState( val mode: Mode = Mode.Normal, val topSites: List = emptyList(), val showCollectionPlaceholder: Boolean = false, - val showSetAsDefaultBrowserCard: Boolean = false, val recentTabs: List = emptyList(), val recentBookmarks: List = emptyList(), val recentHistory: List = emptyList(), val pocketStories: List = emptyList(), val pocketStoriesCategories: List = emptyList(), - val pocketStoriesCategoriesSelections: List = emptyList() + val pocketStoriesCategoriesSelections: List = emptyList(), + val messaging: MessagingState = MessagingState(), ) : State diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt index bcb0c3f96..c716d4b80 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt @@ -14,6 +14,7 @@ import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup +import org.mozilla.fenix.gleanplumb.state.MessagingReducer /** * Reducer for [AppStore]. @@ -30,6 +31,8 @@ internal object AppStoreReducer { is AppAction.RemoveAllNonFatalCrashes -> state.copy(nonFatalCrashes = emptyList()) + is AppAction.MessagingAction -> MessagingReducer.reduce(state, action) + is AppAction.Change -> state.copy( collections = action.collections, mode = action.mode, @@ -60,7 +63,6 @@ internal object AppStoreReducer { is AppAction.RemoveCollectionsPlaceholder -> { state.copy(showCollectionPlaceholder = false) } - is AppAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false) is AppAction.RecentTabsChange -> { val recentSearchGroup = action.recentTabs.find { it is RecentTab.SearchGroup } as RecentTab.SearchGroup? state.copy( diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt new file mode 100644 index 000000000..c555fe6eb --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt @@ -0,0 +1,60 @@ +/* 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.gleanplumb + +import android.content.Intent +import android.net.Uri +import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri +import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed + +/** + * Handles default interactions with the ui of GleanPlumb messages. + */ +class DefaultMessageController( + private val appStore: AppStore, + private val messagingStorage: NimbusMessagingStorage, + private val homeActivity: HomeActivity +) : MessageController { + + override fun onMessagePressed(message: Message) { + // Report telemetry event + // This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224 + val action = messagingStorage.getMessageAction(message) + handleAction(action) + appStore.dispatch(MessageClicked(message)) + } + + override fun onMessageDismissed(message: Message) { + // Report telemetry event + // This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224 + appStore.dispatch(MessageDismissed(message)) + } + + override fun onMessageDisplayed(message: Message) { + // Report telemetry event + // This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224 + appStore.dispatch(MessageDisplayed(message)) + } + + @VisibleForTesting + internal fun handleAction(action: String): Intent { + val partialAction = if (action.startsWith("http", ignoreCase = true)) { + "://open?url=${Uri.encode(action)}" + } else { + action + } + val intent = + Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEP_LINK_SCHEME}$partialAction".toUri()) + homeActivity.processIntent(intent) + + return intent + } +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/KeyPairMessageMetadataStorage.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/KeyPairMessageMetadataStorage.kt new file mode 100644 index 000000000..3c6942117 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/KeyPairMessageMetadataStorage.kt @@ -0,0 +1,29 @@ +/* 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.gleanplumb + +/* Dummy implementation until we provide full implementation. +* This will covered on https://github.com/mozilla-mobile/fenix/issues/24222 +* */ +class KeyPairMessageMetadataStorage : MessageMetadataStorage { + override fun getMetadata(): List { + return listOf( + Message.Metadata( + id = "eu-tracking-protection-for-ireland", + displayCount = 0, + pressed = false, + dismissed = false + ) + ) + } + + override fun addMetadata(metadata: Message.Metadata): Message.Metadata { + return metadata + } + + @SuppressWarnings("EmptyFunctionBlock") + override fun updateMetadata(metadata: Message.Metadata) { + } +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt new file mode 100644 index 000000000..6d17f9054 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt @@ -0,0 +1,44 @@ +/* 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.gleanplumb + +import org.mozilla.fenix.nimbus.MessageData +import org.mozilla.fenix.nimbus.StyleData + +/** + * A data class that holds a representation of GleanPlum message from Nimbus. + * + * @param id identifies a message as unique. + * @param data Data information provided from Nimbus. + * @param action A strings that represents which action should be performed + * after a message is clicked. + * @param style Indicates how a message should be styled. + * @param triggers A list of strings corresponding to targeting expressions. The message + * will be shown if all expressions `true`. + * @param metadata Metadata that help to identify if a message should shown. + */ +data class Message( + val id: String, + val data: MessageData, + val action: String, + val style: StyleData, + val triggers: List, + val metadata: Metadata +) { + /** + * A data class that holds metadata that help to identify if a message should shown. + * + * @param id identifies a message as unique. + * @param displayCount Indicates how many times a message is displayed. + * @param pressed Indicates if a message has been clicked. + * @param dismissed Indicates if a message has been closed. + */ + data class Metadata( + val id: String, + val displayCount: Int = 0, + val pressed: Boolean = false, + val dismissed: Boolean = false + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageController.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageController.kt new file mode 100644 index 000000000..63a03aaa9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageController.kt @@ -0,0 +1,25 @@ +/* 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.gleanplumb + +/** + * Controls all the interactions with a [Message]. + */ +interface MessageController { + /** + * Indicates the provided [message] was pressed by a user. + */ + fun onMessagePressed(message: Message) + + /** + * Indicates the provided [message] was dismissed by a user. + */ + fun onMessageDismissed(message: Message) + + /** + * Indicates the provided [message] was displayed to a user. + */ + fun onMessageDisplayed(message: Message) +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageMetadataStorage.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageMetadataStorage.kt new file mode 100644 index 000000000..a9ca921d0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageMetadataStorage.kt @@ -0,0 +1,23 @@ +/* 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.gleanplumb + +interface MessageMetadataStorage { + /** + * Provide all the message metadata saved in the storage. + */ + fun getMetadata(): List + + /** + * Given a [metadata] add the message metadata on the storage. + * @return the added message on the [MessageMetadataStorage] + */ + fun addMetadata(metadata: Message.Metadata): Message.Metadata + + /** + * Given a [metadata] update the message metadata on the storage. + */ + fun updateMetadata(metadata: Message.Metadata) +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt new file mode 100644 index 000000000..f7df88b76 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt @@ -0,0 +1,24 @@ +/* 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.gleanplumb + +import mozilla.components.support.base.feature.LifecycleAwareFeature +import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction + +/** + * A message observer that updates the provided. + */ +class MessagingFeature(val store: AppStore) : LifecycleAwareFeature { + + override fun start() { + if (FeatureFlags.messagingFeature) { + store.dispatch(MessagingAction.Evaluate) + } + } + + override fun stop() = Unit +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingState.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingState.kt new file mode 100644 index 000000000..c3234a5d2 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingState.kt @@ -0,0 +1,16 @@ +/* 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.gleanplumb + +/** + * Represent all the state related to the Messaging framework. + * @param messages Indicates all the available messages. + * @param messageToShow Indicates the message that should be shown to users, + * if it is null means there is not message that is eligible to be shown to users. + */ +data class MessagingState( + val messages: List = emptyList(), + val messageToShow: Message? = null +) diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt new file mode 100644 index 000000000..9fe06ad72 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt @@ -0,0 +1,174 @@ +/* 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.gleanplumb + +import android.content.Context +import androidx.annotation.VisibleForTesting +import mozilla.components.support.base.log.logger.Logger +import org.json.JSONObject +import org.mozilla.experiments.nimbus.GleanPlumbInterface +import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper +import org.mozilla.experiments.nimbus.internal.FeatureHolder +import org.mozilla.experiments.nimbus.internal.NimbusException + +import org.mozilla.fenix.nimbus.Messaging +import org.mozilla.fenix.nimbus.StyleData + +/** + * Provides messages from [messagingFeature] and combine with the metadata store on [metadataStorage]. + */ +class NimbusMessagingStorage( + private val context: Context, + private val metadataStorage: MessageMetadataStorage, + private val gleanPlumb: GleanPlumbInterface, + private val messagingFeature: FeatureHolder +) { + private val logger = Logger("MessagingStorage") + private val nimbusFeature = messagingFeature.value() + private val customAttributes: JSONObject + get() = JSONObject() + + /** + * Returns a list of available messages descending sorted by their priority. + */ + fun getMessages(): List { + val nimbusTriggers = nimbusFeature.triggers + val nimbusStyles = nimbusFeature.styles + val nimbusActions = nimbusFeature.actions + + val nimbusMessages = nimbusFeature.messages + val defaultStyle = StyleData(context) + val storageMetadata = metadataStorage.getMetadata().associateBy { + it.id + } + + return nimbusMessages.mapNotNull { (key, value) -> + val action = sanitizeAction(value.action, nimbusActions) ?: return@mapNotNull null + Message( + id = key, + data = value, + action = action, + style = nimbusStyles[value.style] ?: defaultStyle, + metadata = storageMetadata[key] ?: addMetadata(key), + triggers = sanitizeTriggers(value.trigger, nimbusTriggers) ?: return@mapNotNull null + ) + }.filter { + it.data.maxDisplayCount >= it.metadata.displayCount && + !it.metadata.dismissed && + !it.metadata.pressed + }.sortedByDescending { + it.style.priority + } + } + + /** + * Returns the next higher priority message which all their triggers are true. + */ + fun getNextMessage(availableMessages: List): Message? { + val helper = gleanPlumb.createMessageHelper(customAttributes) + var message = availableMessages.firstOrNull { + isMessageEligible(it, helper) + } ?: return null + + if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) { + messagingFeature.recordExposure() + + if (message.data.isControl) { + message = availableMessages.firstOrNull { + !it.data.isControl && isMessageEligible(it, helper) + } ?: return null + } + } + return message + } + + /** + * Returns a valid action for the provided [message]. + */ + fun getMessageAction(message: Message): String { + val helper = gleanPlumb.createMessageHelper(customAttributes) + val uuid = helper.getUuid(message.action) + + return helper.stringFormat(message.action, uuid) + } + + /** + * Updated the provided [metadata] in the storage. + */ + fun updateMetadata(metadata: Message.Metadata) { + metadataStorage.updateMetadata(metadata) + } + + @VisibleForTesting + internal fun sanitizeAction( + unsafeAction: String, + nimbusActions: Map + ): String? { + return if (unsafeAction.startsWith("http")) { + unsafeAction + } else { + val safeAction = nimbusActions[unsafeAction] + if (safeAction.isNullOrBlank() || safeAction.isEmpty()) { + return null + } + safeAction + } + } + + @VisibleForTesting + internal fun sanitizeTriggers( + unsafeTriggers: List, + nimbusTriggers: Map + ): List? { + return unsafeTriggers.map { + val safeTrigger = nimbusTriggers[it] + if (safeTrigger.isNullOrBlank() || safeTrigger.isEmpty()) { + return null + } + safeTrigger + } + } + + @VisibleForTesting + internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean { + return when { + expression.isNullOrBlank() -> { + false + } + expression.endsWith("-") -> { + message.id.startsWith(expression) + } + else -> { + message.id == expression + } + } + } + + @VisibleForTesting + internal fun isMessageEligible( + message: Message, + helper: GleanPlumbMessageHelper + ): Boolean { + return message.triggers.all { condition -> + try { + helper.evalJexl(condition) + } catch (e: NimbusException.EvaluationException) { + // Report to glean as malformed message + // Will be addressed on https://github.com/mozilla-mobile/fenix/issues/24224 + logger.info("Unable to evaluate $condition") + false + } + } + } + + private fun addMetadata(id: String): Message.Metadata { + // This will be improve on https://github.com/mozilla-mobile/fenix/issues/24222 + return metadataStorage.addMetadata( + Message.Metadata( + id = id, + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt new file mode 100644 index 000000000..5ccfccb81 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt @@ -0,0 +1,139 @@ +/* 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.gleanplumb.state + +import androidx.annotation.VisibleForTesting +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages +import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.gleanplumb.Message +import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage + +typealias AppStoreMiddlewareContext = MiddlewareContext + +class MessagingMiddleware( + private val messagingStorage: NimbusMessagingStorage +) : Middleware { + + override fun invoke( + context: AppStoreMiddlewareContext, + next: (AppAction) -> Unit, + action: AppAction + ) { + when (action) { + is Restore -> { + val messages = messagingStorage.getMessages() + + context.dispatch(UpdateMessages(messages)) + } + + is Evaluate -> { + val message = messagingStorage.getNextMessage(context.state.messaging.messages) + if (message != null) { + context.dispatch(UpdateMessageToShow(message)) + } else { + context.dispatch(ConsumeMessageToShow) + } + } + + is MessageClicked -> onMessageClicked(action.message, context) + + is MessageDismissed -> onMessageDismissed(context, action.message) + + is MessageDisplayed -> onMessagedDisplayed(action.message, context) + } + next(action) + } + + @VisibleForTesting + internal fun onMessagedDisplayed( + oldMessage: Message, + context: AppStoreMiddlewareContext + ) { + val newMetadata = oldMessage.metadata.copy( + displayCount = oldMessage.metadata.displayCount + 1 + ) + val newMessage = oldMessage.copy( + metadata = newMetadata + ) + val newMessages = if (newMetadata.displayCount < oldMessage.data.maxDisplayCount) { + updateMessage(context, oldMessage, newMessage) + } else { + consumeMessageToShowIfNeeded(context, oldMessage) + removeMessage(context, oldMessage) + } + context.dispatch(UpdateMessages(newMessages)) + messagingStorage.updateMetadata(newMetadata) + } + + @VisibleForTesting + internal fun onMessageDismissed( + context: AppStoreMiddlewareContext, + message: Message + ) { + val newMessages = removeMessage(context, message) + val updatedMetadata = message.metadata.copy(dismissed = true) + + messagingStorage.updateMetadata(updatedMetadata) + context.dispatch(UpdateMessages(newMessages)) + consumeMessageToShowIfNeeded(context, message) + } + + @VisibleForTesting + internal fun onMessageClicked( + message: Message, + context: AppStoreMiddlewareContext + ) { + // Update Nimbus storage. + val updatedMetadata = message.metadata.copy(pressed = true) + messagingStorage.updateMetadata(updatedMetadata) + + // Update app state. + val newMessages = removeMessage(context, message) + context.dispatch(UpdateMessages(newMessages)) + consumeMessageToShowIfNeeded(context, message) + } + + @VisibleForTesting + internal fun consumeMessageToShowIfNeeded( + context: AppStoreMiddlewareContext, + message: Message + ) { + if (context.state.messaging.messageToShow?.id == message.id) { + context.dispatch(ConsumeMessageToShow) + } + } + + @VisibleForTesting + internal fun removeMessage( + context: AppStoreMiddlewareContext, + message: Message + ): List { + return context.state.messaging.messages.filter { it.id != message.id } + } + + @VisibleForTesting + internal fun updateMessage( + context: AppStoreMiddlewareContext, + oldMessage: Message, + updatedMessage: Message + ): List { + val actualMessageToShow = context.state.messaging.messageToShow + + if (actualMessageToShow?.id == oldMessage.id) { + context.dispatch(UpdateMessageToShow(updatedMessage)) + } + return removeMessage(context, oldMessage) + updatedMessage + } +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingReducer.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingReducer.kt new file mode 100644 index 000000000..209dd1d42 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingReducer.kt @@ -0,0 +1,42 @@ +/* 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.gleanplumb.state + +import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages +import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.gleanplumb.MessagingState + +/** + * Reducer for [MessagingState]. + */ +internal object MessagingReducer { + fun reduce(state: AppState, action: AppAction.MessagingAction): AppState = when (action) { + is UpdateMessageToShow -> { + state.copy( + messaging = state.messaging.copy( + messageToShow = action.message + ) + ) + } + is UpdateMessages -> { + state.copy( + messaging = state.messaging.copy( + messages = action.messages + ) + ) + } + is ConsumeMessageToShow -> { + state.copy( + messaging = state.messaging.copy( + messageToShow = null + ) + ) + } + else -> state + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 6ed1c592c..e18259dd4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -97,6 +97,8 @@ import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.gleanplumb.DefaultMessageController +import org.mozilla.fenix.gleanplumb.MessagingFeature import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory @@ -172,6 +174,7 @@ class HomeFragment : Fragment() { private lateinit var currentMode: CurrentMode private val topSitesFeature = ViewBoundFeatureWrapper() + private val messagingFeature = ViewBoundFeatureWrapper() private val recentTabsListFeature = ViewBoundFeatureWrapper() private val recentBookmarksFeature = ViewBoundFeatureWrapper() private val historyMetadataFeature = ViewBoundFeatureWrapper() @@ -239,6 +242,16 @@ class HomeFragment : Fragment() { } } + if (requireContext().settings().isExperimentationEnabled) { + messagingFeature.set( + feature = MessagingFeature( + store = requireComponents.appStore, + ), + owner = viewLifecycleOwner, + view = binding.root + ) + } + if (requireContext().settings().showTopSitesFeature) { topSitesFeature.set( feature = TopSitesFeature( @@ -298,6 +311,11 @@ class HomeFragment : Fragment() { settings = components.settings, engine = components.core.engine, metrics = components.analytics.metrics, + messageController = DefaultMessageController( + appStore = components.appStore, + messagingStorage = components.analytics.messagingStorage, + homeActivity = activity + ), store = store, tabCollectionStorage = components.core.tabCollectionStorage, addTabUseCase = components.useCases.tabsUseCases.addTab, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 016284c78..8740fa007 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -18,6 +18,7 @@ import mozilla.components.feature.top.sites.TopSite import mozilla.components.ui.widgets.WidgetSiteItemView import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.Components +import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.home.BottomSpacerViewHolder import org.mozilla.fenix.home.TopPlaceholderViewHolder import org.mozilla.fenix.home.pocket.PocketCategoriesViewHolder @@ -35,7 +36,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CustomizeHomeButtonView import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.ExperimentDefaultBrowserCardViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.MessageCardViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingManualSignInViewHolder @@ -142,7 +143,12 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.LAYOUT_ID) - object ExperimentDefaultBrowserCard : AdapterItem(ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID) + data class NimbusMessageCard( + val message: Message + ) : AdapterItem(MessageCardViewHolder.LAYOUT_ID) { + override fun sameAs(other: AdapterItem) = + other is NimbusMessageCard && message.id == other.message.id + } object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID) object OnboardingTrackingProtection : @@ -283,7 +289,7 @@ class SessionControlAdapter( OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder( view ) - ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor) + MessageCardViewHolder.LAYOUT_ID -> MessageCardViewHolder(view, interactor) RecentTabsHeaderViewHolder.LAYOUT_ID -> RecentTabsHeaderViewHolder(view, interactor) RecentBookmarksHeaderViewHolder.LAYOUT_ID -> RecentBookmarksHeaderViewHolder(view, interactor) RecentVisitsHeaderViewHolder.LAYOUT_ID -> RecentVisitsHeaderViewHolder( @@ -345,6 +351,9 @@ class SessionControlAdapter( is TopSitePagerViewHolder -> { holder.bind((item as AdapterItem.TopSitePager).topSites) } + is MessageCardViewHolder -> { + holder.bind((item as AdapterItem.NimbusMessageCard).message) + } is CollectionViewHolder -> { val (collection, expanded) = item as AdapterItem.CollectionItem holder.bindSession(collection, expanded) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index 6b586b4f2..95eacb015 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -46,8 +46,9 @@ import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav -import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.gleanplumb.Message +import org.mozilla.fenix.gleanplumb.MessageController import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.Mode @@ -173,14 +174,19 @@ interface SessionControlController { fun handleMenuOpened() /** - * @see [ExperimentCardInteractor.onSetDefaultBrowserClicked] + * @see [MessageCardInteractor.onMessageClicked] */ - fun handleSetDefaultBrowser() + fun handleMessageClicked(message: Message) /** - * @see [ExperimentCardInteractor.onCloseExperimentCardClicked] + * @see [MessageCardInteractor.onMessageClosedClicked] */ - fun handleCloseExperimentCard() + fun handleMessageClosed(message: Message) + + /** + * @see [MessageCardInteractor.onMessageDisplayed] + */ + fun handleMessageDisplayed(message: Message) /** * @see [TabSessionInteractor.onPrivateModeButtonClicked] @@ -209,6 +215,7 @@ class DefaultSessionControlController( private val settings: Settings, private val engine: Engine, private val metrics: MetricController, + private val messageController: MessageController, private val store: BrowserStore, private val tabCollectionStorage: TabCollectionStorage, private val addTabUseCase: TabsUseCases.AddNewTabUseCase, @@ -606,14 +613,16 @@ class DefaultSessionControlController( navController.nav(R.id.homeFragment, directions) } - override fun handleSetDefaultBrowser() { - settings.userDismissedExperimentCard = true - activity.openSetDefaultBrowserOption() + override fun handleMessageClicked(message: Message) { + messageController.onMessagePressed(message) + } + + override fun handleMessageClosed(message: Message) { + messageController.onMessageDismissed(message) } - override fun handleCloseExperimentCard() { - settings.userDismissedExperimentCard = true - appStore.dispatch(AppAction.RemoveSetDefaultBrowserCard) + override fun handleMessageDisplayed(message: Message) { + messageController.onMessageDisplayed(message) } override fun handlePrivateModeButtonClicked( diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index d67583d01..bc86cd9c7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -10,6 +10,7 @@ import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.pocket.PocketRecommendedStory import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketStoriesController import org.mozilla.fenix.home.pocket.PocketStoriesInteractor @@ -225,16 +226,21 @@ interface TopSiteInteractor { fun onTopSiteMenuOpened() } -interface ExperimentCardInteractor { +interface MessageCardInteractor { /** - * Called when set default browser button is clicked + * Called when a [Message]'s button is clicked */ - fun onSetDefaultBrowserClicked() + fun onMessageClicked(message: Message) /** - * Called when close button on experiment card + * Called when close button on a [Message] card. */ - fun onCloseExperimentCardClicked() + fun onMessageClosedClicked(message: Message) + + /** + * Called when close button on a [Message] card. + */ + fun onMessageDisplayed(message: Message) } /** @@ -255,7 +261,7 @@ class SessionControlInteractor( TopSiteInteractor, TabSessionInteractor, ToolbarInteractor, - ExperimentCardInteractor, + MessageCardInteractor, RecentTabInteractor, RecentBookmarksInteractor, RecentVisitsInteractor, @@ -362,14 +368,6 @@ class SessionControlInteractor( controller.handleMenuOpened() } - override fun onSetDefaultBrowserClicked() { - controller.handleSetDefaultBrowser() - } - - override fun onCloseExperimentCardClicked() { - controller.handleCloseExperimentCard() - } - override fun onRecentTabClicked(tabId: String) { recentTabController.handleRecentTabClicked(tabId) } @@ -447,4 +445,16 @@ class SessionControlInteractor( override fun reportSessionMetrics(state: AppState) { controller.handleReportSessionMetrics(state) } + + override fun onMessageClicked(message: Message) { + controller.handleMessageClicked(message) + } + + override fun onMessageClosedClicked(message: Message) { + controller.handleMessageClosed(message) + } + + override fun onMessageDisplayed(message: Message) { + controller.handleMessageDisplayed(message) + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 55b5216cf..89a70bf54 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -15,6 +15,7 @@ import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.pocket.PocketRecommendedStory import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.Mode @@ -36,7 +37,7 @@ internal fun normalModeAdapterItems( expandedCollections: Set, recentBookmarks: List, showCollectionsPlaceholder: Boolean, - showSetAsDefaultBrowserCard: Boolean, + nimbusMessageCard: Message? = null, recentTabs: List, recentVisits: List, pocketStories: List @@ -47,8 +48,8 @@ internal fun normalModeAdapterItems( // Add a synchronous, unconditional and invisible placeholder so home is anchored to the top when created. items.add(AdapterItem.TopPlaceholderItem) - if (showSetAsDefaultBrowserCard) { - items.add(AdapterItem.ExperimentDefaultBrowserCard) + nimbusMessageCard?.let { + items.add(AdapterItem.NimbusMessageCard(it)) } if (settings.showTopSitesFeature && topSites.isNotEmpty()) { @@ -157,7 +158,7 @@ private fun AppState.toAdapterList(settings: Settings): List = when expandedCollections, recentBookmarks, showCollectionPlaceholder, - showSetAsDefaultBrowserCard, + messaging.messageToShow, recentTabs, recentHistory, pocketStories diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/ExperimentDefaultBrowserCardViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/ExperimentDefaultBrowserCardViewHolder.kt deleted file mode 100644 index a68a6f023..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/ExperimentDefaultBrowserCardViewHolder.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* 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.onboarding - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import org.mozilla.fenix.R -import org.mozilla.fenix.databinding.ExperimentDefaultBrowserBinding -import org.mozilla.fenix.ext.increaseTapArea -import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor - -class ExperimentDefaultBrowserCardViewHolder( - view: View, - private val interactor: SessionControlInteractor -) : RecyclerView.ViewHolder(view) { - - init { - val binding = ExperimentDefaultBrowserBinding.bind(view) - binding.setDefaultBrowser.setOnClickListener { - interactor.onSetDefaultBrowserClicked() - } - - binding.close.apply { - increaseTapArea(CLOSE_BUTTON_EXTRA_DPS) - setOnClickListener { - interactor.onCloseExperimentCardClicked() - } - } - } - - companion object { - internal const val LAYOUT_ID = R.layout.experiment_default_browser - private const val CLOSE_BUTTON_EXTRA_DPS = 38 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt new file mode 100644 index 000000000..8af3c3b24 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt @@ -0,0 +1,57 @@ +/* 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.onboarding + +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.NimbusMessageCardBinding +import org.mozilla.fenix.ext.increaseTapArea +import org.mozilla.fenix.gleanplumb.Message +import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor + +class MessageCardViewHolder( + view: View, + private val interactor: SessionControlInteractor +) : RecyclerView.ViewHolder(view) { + + fun bind(message: Message) { + val binding = NimbusMessageCardBinding.bind(itemView) + + if (message.data.title.isNullOrBlank()) { + binding.titleText.isVisible = false + } else { + binding.titleText.text = message.data.title + } + + binding.descriptionText.text = message.data.text + + if (message.data.buttonLabel.isNullOrBlank()) { + binding.messageButton.isVisible = false + binding.experimentCard.setOnClickListener { + interactor.onMessageClicked(message) + } + } else { + binding.messageButton.text = message.data.buttonLabel + binding.messageButton.setOnClickListener { + interactor.onMessageClicked(message) + } + } + + binding.close.apply { + increaseTapArea(CLOSE_BUTTON_EXTRA_DPS) + setOnClickListener { + interactor.onMessageClosedClicked(message) + } + } + interactor.onMessageDisplayed(message) + } + + companion object { + internal const val LAYOUT_ID = R.layout.nimbus_message_card + private const val CLOSE_BUTTON_EXTRA_DPS = 38 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 8315e7f81..2c1b81475 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -311,24 +311,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) - /** - * Shows if the user has chosen to close the set default browser experiment card - * on home screen or has clicked the set as default browser button. - */ - var userDismissedExperimentCard by booleanPreference( - appContext.getPreferenceKey(R.string.pref_key_experiment_card_home), - default = false - ) - - /** - * Shows if the set default browser experiment card should be shown on home screen. - */ - fun shouldShowSetAsDefaultBrowserCard(): Boolean { - return isDefaultBrowserMessageLocation(MessageSurfaceId.HOMESCREEN_BANNER) && - !userDismissedExperimentCard && - numberOfAppLaunches > APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD - } - private val defaultBrowserFeature: DefaultBrowserMessage by lazy { FxNimbus.features.defaultBrowserMessage.value() } @@ -1212,7 +1194,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { ) private val homescreenSections: Map by lazy { - FxNimbus.features.homescreen.value().sectionsEnabled + FxNimbus.features.homescreen.value(appContext).sectionsEnabled } var historyMetadataUIFeature by lazyFeatureFlagPreference( diff --git a/app/src/main/res/layout/experiment_default_browser.xml b/app/src/main/res/layout/nimbus_message_card.xml similarity index 78% rename from app/src/main/res/layout/experiment_default_browser.xml rename to app/src/main/res/layout/nimbus_message_card.xml index 83dbd9d07..ea312f331 100644 --- a/app/src/main/res/layout/experiment_default_browser.xml +++ b/app/src/main/res/layout/nimbus_message_card.xml @@ -10,6 +10,18 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/home_item_horizontal_margin"> + +