Revert "For #24220 and #24223: Connect GleanPlumb messages with the new tab ui card. Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>"

This reverts commit f953c5ec
This commit is contained in:
Arturo Mejia 2022-04-06 19:12:34 -04:00
parent febff55459
commit cc9e91809b
38 changed files with 151 additions and 2208 deletions

View File

@ -109,10 +109,4 @@ object FeatureFlags {
* Enables the Unified Search feature.
*/
val unifiedSearchFeature = Config.channel.isNightlyOrDebug
/**
* Enables receiving from the messaging framework.
*/
@Suppress("MayBeConst")
val messagingFeature = false
}

View File

@ -66,7 +66,6 @@ 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.MetricServiceType
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.toolbar.ToolbarPosition
@ -156,9 +155,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
GlobalScope.launch(Dispatchers.IO) {
setStartupMetrics(store, settings())
}
if (FeatureFlags.messagingFeature && settings().isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore)
}
}
@CallSuper

View File

@ -1019,22 +1019,18 @@ 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) &&
!processIntent(intent)
!externalSourceIntentProcessors.any {
it.process(
intent,
navHost.navController,
this.intent
)
}
}
companion object {

View File

@ -25,8 +25,6 @@ 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
@ -126,15 +124,6 @@ class Analytics(
FxNimbus.api = api
}
}
val messagingStorage by lazyMonitored {
NimbusMessagingStorage(
context = context,
metadataStorage = KeyPairMessageMetadataStorage(),
gleanPlumb = experiments,
messagingFeature = FxNimbus.features.messaging,
)
}
}
private fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty()

View File

@ -32,7 +32,6 @@ 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
@ -194,7 +193,6 @@ 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)
@ -205,6 +203,7 @@ 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.
@ -220,8 +219,7 @@ class Components(private val context: Context) {
PocketUpdatesMiddleware(
core.pocketStoriesService,
context.pocketStoriesSelectedCategoriesDataStore
),
MessagingMiddleware(messagingStorage = analytics.messagingStorage)
)
)
)
}

View File

@ -16,8 +16,6 @@ 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].
@ -64,47 +62,7 @@ sealed class AppAction : Action {
) : AppAction()
object RemoveCollectionsPlaceholder : AppAction()
/**
* [Action]s related to interactions with the Messaging Framework.
* [Action] implementation related to remove the DefaultBrowserCard.
*/
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<Message>) : 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()
}
object RemoveSetDefaultBrowserCard : AppAction()
}

View File

@ -17,7 +17,6 @@ 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.
@ -31,12 +30,12 @@ import org.mozilla.fenix.gleanplumb.MessagingState
* @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(
@ -47,11 +46,11 @@ data class AppState(
val mode: Mode = Mode.Normal,
val topSites: List<TopSite> = emptyList(),
val showCollectionPlaceholder: Boolean = false,
val showSetAsDefaultBrowserCard: Boolean = false,
val recentTabs: List<RecentTab> = emptyList(),
val recentBookmarks: List<RecentBookmark> = emptyList(),
val recentHistory: List<RecentlyVisitedItem> = emptyList(),
val pocketStories: List<PocketRecommendedStory> = emptyList(),
val pocketStoriesCategories: List<PocketRecommendedStoriesCategory> = emptyList(),
val pocketStoriesCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList(),
val messaging: MessagingState = MessagingState(),
val pocketStoriesCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList()
) : State

View File

@ -14,7 +14,6 @@ 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].
@ -31,8 +30,6 @@ 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,
@ -63,6 +60,7 @@ 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(

View File

@ -1,34 +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.gleanplumb
import android.content.Context
import org.json.JSONObject
import org.mozilla.fenix.utils.BrowsersCache
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Calendar
/**
* Custom attributes that the messaging framework will use to evaluate if message is eligible
* to be shown.
*/
object CustomAttributeProvider {
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
/**
* Returns a [JSONObject] that contains all the custom attributes, evaluated when the function
* was called.
*/
fun getCustomAttributes(context: Context): JSONObject {
val now = Calendar.getInstance()
return JSONObject(
mapOf(
"is_default_browser" to BrowsersCache.all(context).isDefaultBrowser,
"date_string" to formatter.format(now.time)
)
)
}
}

View File

@ -1,60 +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.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
}
}

View File

@ -1,29 +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.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<Message.Metadata> {
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) {
}
}

View File

@ -1,50 +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.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<String>,
val metadata: Metadata
) {
val maxDisplayCount: Int
get() = style.maxDisplayCount
val priority: Int
get() = style.priority
/**
* 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
)
}

View File

@ -1,25 +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.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)
}

View File

@ -1,23 +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.gleanplumb
interface MessageMetadataStorage {
/**
* Provide all the message metadata saved in the storage.
*/
fun getMetadata(): List<Message.Metadata>
/**
* 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)
}

View File

@ -1,24 +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.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
}

View File

@ -1,16 +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.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<Message> = emptyList(),
val messageToShow: Message? = null
)

View File

@ -1,174 +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.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<Messaging>
) {
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<Message> {
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>): 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, String>
): 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<String>,
nimbusTriggers: Map<String, String>
): List<String>? {
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,
)
)
}
}

View File

@ -1,139 +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.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<AppState, AppAction>
class MessagingMiddleware(
private val messagingStorage: NimbusMessagingStorage
) : Middleware<AppState, AppAction> {
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.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<Message> {
return context.state.messaging.messages.filter { it.id != message.id }
}
@VisibleForTesting
internal fun updateMessage(
context: AppStoreMiddlewareContext,
oldMessage: Message,
updatedMessage: Message
): List<Message> {
val actualMessageToShow = context.state.messaging.messageToShow
if (actualMessageToShow?.id == oldMessage.id) {
context.dispatch(UpdateMessageToShow(updatedMessage))
}
return removeMessage(context, oldMessage) + updatedMessage
}
}

View File

@ -1,42 +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.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
}
}

View File

@ -97,8 +97,6 @@ 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
@ -174,7 +172,6 @@ class HomeFragment : Fragment() {
private lateinit var currentMode: CurrentMode
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>()
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()
private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>()
private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>()
@ -242,16 +239,6 @@ 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(
@ -311,11 +298,6 @@ 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,

View File

@ -18,7 +18,6 @@ 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
@ -36,7 +35,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.MessageCardViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.ExperimentDefaultBrowserCardViewHolder
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
@ -143,12 +142,10 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
object OnboardingManualSignIn : AdapterItem(OnboardingManualSignInViewHolder.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
}
/**
* AdapterItem for the default browser card.
*/
object ExperimentDefaultBrowserCard : AdapterItem(ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID)
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID)
object OnboardingTrackingProtection :
@ -308,7 +305,7 @@ class SessionControlAdapter(
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
view
)
MessageCardViewHolder.LAYOUT_ID -> MessageCardViewHolder(view, interactor)
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
BottomSpacerViewHolder.LAYOUT_ID -> BottomSpacerViewHolder(view)
else -> throw IllegalStateException()
}
@ -369,9 +366,6 @@ 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)

View File

@ -48,9 +48,8 @@ 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
@ -176,19 +175,14 @@ interface SessionControlController {
fun handleMenuOpened()
/**
* @see [MessageCardInteractor.onMessageClicked]
* @see [ExperimentCardInteractor.onSetDefaultBrowserClicked]
*/
fun handleMessageClicked(message: Message)
fun handleSetDefaultBrowser()
/**
* @see [MessageCardInteractor.onMessageClosedClicked]
* @see [ExperimentCardInteractor.onCloseExperimentCardClicked]
*/
fun handleMessageClosed(message: Message)
/**
* @see [MessageCardInteractor.onMessageDisplayed]
*/
fun handleMessageDisplayed(message: Message)
fun handleCloseExperimentCard()
/**
* @see [TabSessionInteractor.onPrivateModeButtonClicked]
@ -211,13 +205,12 @@ interface SessionControlController {
fun handleReportSessionMetrics(state: AppState)
}
@Suppress("TooManyFunctions", "LargeClass")
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
class DefaultSessionControlController(
private val activity: HomeActivity,
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,
@ -615,16 +608,14 @@ class DefaultSessionControlController(
navController.nav(R.id.homeFragment, directions)
}
override fun handleMessageClicked(message: Message) {
messageController.onMessagePressed(message)
override fun handleSetDefaultBrowser() {
settings.userDismissedExperimentCard = true
activity.openSetDefaultBrowserOption()
}
override fun handleMessageClosed(message: Message) {
messageController.onMessageDismissed(message)
}
override fun handleMessageDisplayed(message: Message) {
messageController.onMessageDisplayed(message)
override fun handleCloseExperimentCard() {
settings.userDismissedExperimentCard = true
appStore.dispatch(AppAction.RemoveSetDefaultBrowserCard)
}
override fun handlePrivateModeButtonClicked(

View File

@ -10,7 +10,6 @@ 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
@ -226,21 +225,19 @@ interface TopSiteInteractor {
fun onTopSiteMenuOpened()
}
interface MessageCardInteractor {
/**
* Interface for interactions with the default browser card.
*/
interface ExperimentCardInteractor {
/**
* Called when a [Message]'s button is clicked
* Called when set default browser button is clicked
*/
fun onMessageClicked(message: Message)
fun onSetDefaultBrowserClicked()
/**
* Called when close button on a [Message] card.
* Called when close button on experiment card
*/
fun onMessageClosedClicked(message: Message)
/**
* Called when close button on a [Message] card.
*/
fun onMessageDisplayed(message: Message)
fun onCloseExperimentCardClicked()
}
/**
@ -261,7 +258,7 @@ class SessionControlInteractor(
TopSiteInteractor,
TabSessionInteractor,
ToolbarInteractor,
MessageCardInteractor,
ExperimentCardInteractor,
RecentTabInteractor,
RecentBookmarksInteractor,
RecentVisitsInteractor,
@ -368,6 +365,14 @@ class SessionControlInteractor(
controller.handleMenuOpened()
}
override fun onSetDefaultBrowserClicked() {
controller.handleSetDefaultBrowser()
}
override fun onCloseExperimentCardClicked() {
controller.handleCloseExperimentCard()
}
override fun onRecentTabClicked(tabId: String) {
recentTabController.handleRecentTabClicked(tabId)
}
@ -445,16 +450,4 @@ 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)
}
}

View File

@ -15,7 +15,6 @@ 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
@ -37,7 +36,7 @@ internal fun normalModeAdapterItems(
expandedCollections: Set<Long>,
recentBookmarks: List<RecentBookmark>,
showCollectionsPlaceholder: Boolean,
nimbusMessageCard: Message? = null,
showSetAsDefaultBrowserCard: Boolean,
recentTabs: List<RecentTab>,
recentVisits: List<RecentlyVisitedItem>,
pocketStories: List<PocketRecommendedStory>
@ -48,8 +47,8 @@ internal fun normalModeAdapterItems(
// Add a synchronous, unconditional and invisible placeholder so home is anchored to the top when created.
items.add(AdapterItem.TopPlaceholderItem)
nimbusMessageCard?.let {
items.add(AdapterItem.NimbusMessageCard(it))
if (showSetAsDefaultBrowserCard) {
items.add(AdapterItem.ExperimentDefaultBrowserCard)
}
if (settings.showTopSitesFeature && topSites.isNotEmpty()) {
@ -158,7 +157,7 @@ private fun AppState.toAdapterList(settings: Settings): List<AdapterItem> = when
expandedCollections,
recentBookmarks,
showCollectionPlaceholder,
messaging.messageToShow,
showSetAsDefaultBrowserCard,
recentTabs,
recentHistory,
pocketStories

View File

@ -0,0 +1,40 @@
/* 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
/**
* View holder for the default browser card.
*/
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
}
}

View File

@ -1,57 +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.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
}
}

View File

@ -71,6 +71,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private const val ALLOWED_INT = 2
private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1
private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3
private const val APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD = 3
private const val INACTIVE_TAB_MINIMUM_TO_SHOW_AUTO_CLOSE_DIALOG = 20
const val FOUR_HOURS_MS = 60 * 60 * 4 * 1000L
@ -311,6 +312,24 @@ 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()
}
@ -1209,7 +1228,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
)
private val homescreenSections: Map<HomeScreenSection, Boolean> by lazy {
FxNimbus.features.homescreen.value(appContext).sectionsEnabled
FxNimbus.features.homescreen.value().sectionsEnabled
}
var historyMetadataUIFeature by lazyFeatureFlagPreference(

View File

@ -10,18 +10,6 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/home_item_horizontal_margin">
<TextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
tools:text="Title"
android:textAppearance="@style/Header16TextStyle"
app:layout_constraintBottom_toTopOf="@id/description_text"
app:layout_constraintEnd_toStartOf="@id/close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/close"
android:layout_width="10dp"
@ -38,15 +26,16 @@
android:id="@+id/description_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/default_browser_experiment_card_text"
android:textAppearance="@style/Body14TextStyle"
app:layout_constraintBottom_toTopOf="@id/message_button"
app:layout_constraintBottom_toTopOf="@id/set_default_browser"
app:layout_constraintEnd_toStartOf="@id/close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/message_button"
android:id="@+id/set_default_browser"
style="@style/PositiveButton"
android:layout_height="36dp"
android:background="@drawable/rounded_button_background"

View File

@ -276,6 +276,9 @@
<string name="pref_key_open_next_tab_desktop_mode" translatable="false">pref_key_open_next_tab_desktop_mode</string>
<!-- Set default browser experiment card-->
<string name="pref_key_experiment_card_home" translatable="false">pref_key_experiment_card_home</string>
<!-- Secret Info Setting Keys -->
<string name="pref_key_secret_debug_info" translatable="false">pref_key_secret_debug_info</string>

View File

@ -16,8 +16,6 @@ import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.pocket.PocketRecommendedStory
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
@ -25,7 +23,6 @@ import org.junit.Test
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.filterOut
import org.mozilla.fenix.ext.components
@ -74,6 +71,7 @@ class AppStoreTest {
mode = currentMode.getCurrentMode(),
topSites = emptyList(),
showCollectionPlaceholder = true,
showSetAsDefaultBrowserCard = true,
recentTabs = emptyList()
)
@ -94,16 +92,6 @@ class AppStoreTest {
assertEquals(Mode.Normal, appStore.state.mode)
}
@Test
fun `GIVEN a new value for messageToShow WHEN NimbusMessageChange is called THEN update the current value`() =
runBlocking {
assertNull(appStore.state.messaging.messageToShow)
appStore.dispatch(UpdateMessageToShow(mockk())).join()
assertNotNull(appStore.state.messaging.messageToShow)
}
@Test
fun `Test changing the collections in AppStore`() = runBlocking {
assertEquals(0, appStore.state.collections.size)

View File

@ -1,109 +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.gleanplumb
import android.net.Uri
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
@RunWith(FenixRobolectricTestRunner::class)
class DefaultMessageControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
private lateinit var controller: DefaultMessageController
private val store: AppStore = mockk(relaxed = true)
@Before
fun setup() {
controller = DefaultMessageController(
messagingStorage = storageNimbus,
appStore = store,
homeActivity = activity
)
}
@Test
fun `WHEN calling onMessagePressed THEN update the store and handle the action`() {
val customController = spyk(controller)
every { customController.handleAction(any()) } returns mockk()
val message = mockMessage()
customController.onMessagePressed(message)
verify { customController.handleAction(any()) }
verify { store.dispatch(MessageClicked(message)) }
}
@Test
fun `GIVEN an URL WHEN calling handleAction THEN process the intent with an open uri`() {
val intent = controller.handleAction("http://mozilla.org")
verify { activity.processIntent(any()) }
val encodedUrl = Uri.encode("http://mozilla.org")
assertEquals(
"${BuildConfig.DEEP_LINK_SCHEME}://open?url=$encodedUrl",
intent.data.toString()
)
}
@Test
fun `GIVEN an deeplink WHEN calling handleAction THEN process the intent with an deeplink uri`() {
val intent = controller.handleAction("://settings_privacy")
verify { activity.processIntent(any()) }
assertEquals("${BuildConfig.DEEP_LINK_SCHEME}://settings_privacy", intent.data.toString())
}
@Test
fun `WHEN calling onMessageDismissed THEN report to the messageManager`() {
val message = mockMessage()
controller.onMessageDismissed(message)
verify { store.dispatch(AppAction.MessagingAction.MessageDismissed(message)) }
}
@Test
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
val message = mockMessage()
controller.onMessageDisplayed(message)
verify { store.dispatch(MessageDisplayed(message)) }
}
private fun mockMessage() = Message(
id = "id",
data = MessageData(_context = testContext),
style = mockk(),
action = "action",
triggers = emptyList(),
metadata = Message.Metadata(
id = "id",
displayCount = 0,
pressed = false,
dismissed = false
)
)
}

View File

@ -1,46 +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.gleanplumb
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
class MessagingFeatureTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@Test
fun `WHEN start is called THEN evaluate messages`() {
val store: AppStore = spyk(AppStore())
val binding = MessagingFeature(store)
mockkObject(FeatureFlags)
every { FeatureFlags.messagingFeature } returns true
binding.start()
store.dispatch(UpdateMessageToShow(mock()))
store.waitUntilIdle()
verify { store.dispatch(MessagingAction.Evaluate) }
unmockkObject(Config)
}
}

View File

@ -1,487 +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.gleanplumb
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
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.HomeActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
@RunWith(FenixRobolectricTestRunner::class)
class NimbusMessagingStorageTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
private lateinit var storage: NimbusMessagingStorage
private lateinit var metadataStorage: MessageMetadataStorage
private lateinit var gleanPlumb: GleanPlumbInterface
private lateinit var messagingFeature: FeatureHolder<Messaging>
private lateinit var messaging: Messaging
@Before
fun setup() {
gleanPlumb = mockk(relaxed = true)
metadataStorage = mockk(relaxed = true)
messagingFeature = createMessagingFeature()
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
storage = NimbusMessagingStorage(
testContext,
metadataStorage,
gleanPlumb,
messagingFeature
)
}
@Test
fun `WHEN calling getMessages THEN provide a list of available messages`() {
val message = storage.getMessages().first()
assertEquals("message-1", message.id)
assertEquals("message-1", message.metadata.id)
}
@Test
fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() {
val messages = mapOf(
"low-message" to createMessageData(style = "low-priority"),
"high-message" to createMessageData(style = "high-priority"),
"medium-message" to createMessageData(style = "medium-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
"medium-priority" to createStyle(priority = 50),
"low-priority" to createStyle(priority = 1)
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
assertEquals("high-message", results[0].id)
assertEquals("medium-message", results[1].id)
assertEquals("low-message", results[2].id)
}
@Test
fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() {
val metadataList = listOf(
Message.Metadata(id = "pressed-message", pressed = true),
Message.Metadata(id = "normal-message", pressed = false)
)
val messages = mapOf(
"pressed-message" to createMessageData(style = "high-priority"),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
every { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
@Test
fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() {
val metadataList = listOf(
Message.Metadata(id = "dismissed-message", dismissed = true),
Message.Metadata(id = "normal-message", dismissed = false)
)
val messages = mapOf(
"dismissed-message" to createMessageData(style = "high-priority"),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
every { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
@Test
fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() {
val metadataList = listOf(
Message.Metadata(id = "shown-many-times-message", displayCount = 10),
Message.Metadata(id = "normal-message", displayCount = 0)
)
val messages = mapOf(
"shown-many-times-message" to createMessageData(
style = "high-priority",
maxDisplayCount = 2
),
"normal-message" to createMessageData(style = "high-priority"),
)
val styles = mapOf(
"high-priority" to createStyle(priority = 100),
)
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
val messagingFeature = createMessagingFeature(
styles = styles,
messages = messages
)
every { metadataStorage.getMetadata() } returns metadataList
val storage = NimbusMessagingStorage(
testContext,
metadataStorage,
gleanPlumb,
messagingFeature
)
val results = storage.getMessages()
assertEquals(1, results.size)
assertEquals("normal-message", results[0].id)
}
@Test
fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() {
val messages = storage.getMessages()
val firstMessage = messages.first()
assertEquals("message-1", firstMessage.id)
assertEquals("message-1", firstMessage.metadata.id)
assertTrue(messages.size == 1)
}
@Test
fun `GIVEN a malformed action WHEN calling sanitizeAction THEN return null`() {
val actionsMap = mapOf("action-1" to "action-1-url")
val notFoundAction = storage.sanitizeAction("no-found-action", actionsMap)
val emptyAction = storage.sanitizeAction("", actionsMap)
val blankAction = storage.sanitizeAction(" ", actionsMap)
assertNull(notFoundAction)
assertNull(emptyAction)
assertNull(blankAction)
}
@Test
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() {
storage.updateMetadata(mockk())
verify { metadataStorage.updateMetadata(any()) }
}
@Test
fun `GIVEN a valid action WHEN calling sanitizeAction THEN return the action`() {
val actionsMap = mapOf("action-1" to "action-1-url")
val validAction = storage.sanitizeAction("action-1", actionsMap)
assertEquals("action-1-url", validAction)
}
@Test
fun `GIVEN a trigger action WHEN calling sanitizeTriggers THEN return null`() {
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
val notFoundTrigger = storage.sanitizeTriggers(listOf("no-found-trigger"), triggersMap)
val emptyTrigger = storage.sanitizeTriggers(listOf(""), triggersMap)
val blankTrigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
assertNull(notFoundTrigger)
assertNull(emptyTrigger)
assertNull(blankTrigger)
}
@Test
fun `GIVEN a valid trigger WHEN calling sanitizeAction THEN return the trigger`() {
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
val validTrigger = storage.sanitizeTriggers(listOf("trigger-1"), triggersMap)
assertEquals(listOf("trigger-1-expression"), validTrigger)
}
@Test
fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() {
val message = Message(
"id", mockk(),
action = "action",
mock(),
emptyList(),
Message.Metadata("id")
)
val result = storage.isMessageUnderExperiment(message, null)
assertFalse(result)
}
@Test
fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() {
val message = Message(
"end-", mockk(),
action = "action",
mock(),
emptyList(),
Message.Metadata("end-")
)
val result = storage.isMessageUnderExperiment(message, "end-")
assertTrue(result)
}
@Test
fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() {
val message = Message(
"same-id", mockk(),
action = "action",
mock(),
emptyList(),
Message.Metadata("same-id")
)
val result = storage.isMessageUnderExperiment(message, "same-id")
assertTrue(result)
}
@Test
fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
val message = Message(
"same-id", mockk(),
action = "action",
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
every { helper.evalJexl(any()) } returns true
val result = storage.isMessageEligible(message, helper)
assertTrue(result)
}
@Test
fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
val message = Message(
"same-id", mockk(),
action = "action",
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
every { helper.evalJexl(any()) } throws NimbusException.EvaluationException("")
val result = storage.isMessageEligible(message, helper)
assertFalse(result)
}
@Test
fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
val spiedStorage = spyk(storage)
val message = Message(
"same-id", mockk(),
action = "action",
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
every { spiedStorage.isMessageEligible(any(), any()) } returns false
val result = spiedStorage.getNextMessage(listOf(message))
assertNull(result)
}
@Test
fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
val spiedStorage = spyk(storage)
val message = Message(
"same-id", mockk(),
action = "action",
mock(),
listOf("trigger"),
Message.Metadata("same-id")
)
every { spiedStorage.isMessageEligible(any(), any()) } returns true
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns false
val result = spiedStorage.getNextMessage(listOf(message))
assertEquals(message.id, result!!.id)
}
@Test
fun `GIVEN a message under experiment WHEN calling getNextMessage THEN call recordExposure`() {
val spiedStorage = spyk(storage)
val messageData: MessageData = mockk(relaxed = true)
every { messageData.isControl } returns false
val message = Message(
"same-id",
messageData,
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
every { spiedStorage.isMessageEligible(any(), any()) } returns true
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
val result = spiedStorage.getNextMessage(listOf(message))
verify { messagingFeature.recordExposure() }
assertEquals(message.id, result!!.id)
}
@Test
fun `GIVEN a control message WHEN calling getNextMessage THEN return the next eligible message`() {
val spiedStorage = spyk(storage)
val messageData: MessageData = mockk(relaxed = true)
val controlMessageData: MessageData = mockk(relaxed = true)
every { messageData.isControl } returns false
every { controlMessageData.isControl } returns true
val message = Message(
"id",
messageData,
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val controlMessage = Message(
"control-id",
controlMessageData,
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
every { spiedStorage.isMessageEligible(any(), any()) } returns true
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
val result = spiedStorage.getNextMessage(listOf(controlMessage, message))
verify { messagingFeature.recordExposure() }
assertEquals(message.id, result!!.id)
}
private fun createMessageData(
action: String = "action-1",
style: String = "style-1",
triggers: List<String> = listOf("trigger-1"),
maxDisplayCount: Int = 5
): MessageData {
val messageData1: MessageData = mockk(relaxed = true)
every { messageData1.action } returns action
every { messageData1.style } returns style
every { messageData1.trigger } returns triggers
every { messageData1.maxDisplayCount } returns maxDisplayCount
return messageData1
}
private fun createMessagingFeature(
triggers: Map<String, String> = mapOf("trigger-1" to "trigger-1-expression"),
styles: Map<String, StyleData> = mapOf("style-1" to createStyle()),
actions: Map<String, String> = mapOf("action-1" to "action-1-url"),
messages: Map<String, MessageData> = mapOf(
"message-1" to createMessageData(),
"malformed" to mockk(relaxed = true)
),
): FeatureHolder<Messaging> {
val messagingFeature: FeatureHolder<Messaging> = mockk(relaxed = true)
messaging = mockk(relaxed = true)
every { messaging.triggers } returns triggers
every { messaging.styles } returns styles
every { messaging.actions } returns actions
every { messaging.messages } returns messages
every { messagingFeature.value() } returns messaging
return messagingFeature
}
private fun createStyle(priority: Int = 1): StyleData {
val style1: StyleData = mockk(relaxed = true)
every { style1.priority } returns priority
return style1
}
}

View File

@ -1,327 +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.gleanplumb.state
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.components.AppStore
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.MessagingState
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
@RunWith(FenixRobolectricTestRunner::class)
class MessagingMiddlewareTest {
private lateinit var store: AppStore
private lateinit var middleware: MessagingMiddleware
private lateinit var messagingStorage: NimbusMessagingStorage
private lateinit var middlewareContext: MiddlewareContext<AppState, AppAction>
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
@Before
fun setUp() {
messagingStorage = mockk(relaxed = true)
middlewareContext = mockk(relaxed = true)
middleware = MessagingMiddleware(
messagingStorage
)
}
@Test
fun `WHEN Restore THEN get messages from the storage and UpdateMessages`() {
val messages: List<Message> = emptyList()
every { messagingStorage.getMessages() } returns messages
middleware.invoke(middlewareContext, {}, Restore)
verify { middlewareContext.dispatch(UpdateMessages(messages)) }
}
@Test
fun `WHEN Restore THEN getNextMessage from the storage and UpdateMessageToShow`() {
val message: Message = mockk(relaxed = true)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
every { messagingStorage.getNextMessage(any()) } returns message
middleware.invoke(middlewareContext, {}, Evaluate)
verify { middlewareContext.dispatch(UpdateMessageToShow(message)) }
}
@Test
fun `WHEN MessageClicked THEN update storage`() {
val message = Message(
"control-id",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
middleware.invoke(middlewareContext, {}, MessageClicked(message))
verify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
}
@Test
fun `WHEN MessageDismissed THEN update storage`() {
val message = Message(
"control-id",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
middleware.invoke(
middlewareContext, {},
MessageDismissed(message)
)
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
}
@Test
fun `WHEN MessageDisplayed THEN update storage`() {
val message = Message(
"control-id",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messages } returns emptyList()
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
middleware.invoke(
middlewareContext, {},
MessageDisplayed(message)
)
verify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
}
@Test
fun `WHEN onMessageDismissed THEN updateMetadata,removeMessage , UpdateMessages and removeMessageToShowIfNeeded`() {
val message = Message(
"control-id",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val spiedMiddleware = spyk(middleware)
every { spiedMiddleware.removeMessage(middlewareContext, message) } returns emptyList()
every { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, message) } just Runs
spiedMiddleware.onMessageDismissed(middlewareContext, message)
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { spiedMiddleware.removeMessage(middlewareContext, message) }
}
@Test
fun `WHEN removeMessage THEN remove the message`() {
val message = Message(
"control-id",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val messages = listOf(message)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messages } returns messages
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
val results = middleware.removeMessage(middlewareContext, message)
assertTrue(results.isEmpty())
}
@Test
fun `WHEN consumeMessageToShowIfNeeded THEN consume the message`() {
val message = Message(
"control-id",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id")
)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messageToShow } returns message
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
middleware.consumeMessageToShowIfNeeded(middlewareContext, message)
verify { middlewareContext.dispatch(ConsumeMessageToShow) }
}
@Test
fun `WHEN updateMessage THEN update available messages`() {
val oldMessage = Message(
"oldMessage",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id", pressed = false)
)
val updatedMessage = Message(
"oldMessage",
mockk(relaxed = true),
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id", pressed = true)
)
val spiedMiddleware = spyk(middleware)
val appState: AppState = mockk(relaxed = true)
val messagingState: MessagingState = mockk(relaxed = true)
every { messagingState.messageToShow } returns oldMessage
every { appState.messaging } returns messagingState
every { middlewareContext.state } returns appState
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()
val results = spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage)
verify { middlewareContext.dispatch(UpdateMessageToShow(updatedMessage)) }
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
assertTrue(results.size == 1)
assertTrue(results.first().metadata.pressed)
}
@Test
fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() {
val oldMessageData: MessageData = mockk(relaxed = true)
val oldMessage = Message(
"oldMessage",
oldMessageData,
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id", displayCount = 0)
)
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
val spiedMiddleware = spyk(middleware)
every { oldMessageData.maxDisplayCount } returns 2
every {
spiedMiddleware.updateMessage(
middlewareContext,
oldMessage,
updatedMessage
)
} returns emptyList()
spiedMiddleware.onMessagedDisplayed(oldMessage, middlewareContext)
verify { spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
}
@Test
fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() {
val oldMessageData: MessageData = mockk(relaxed = true)
val oldMessage = Message(
"oldMessage",
oldMessageData,
action = "action",
mockk(relaxed = true),
listOf("trigger"),
Message.Metadata("same-id", displayCount = 0)
)
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
val spiedMiddleware = spyk(middleware)
every { oldMessageData.maxDisplayCount } returns 1
every {
spiedMiddleware.consumeMessageToShowIfNeeded(
middlewareContext,
oldMessage
)
} just Runs
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()
spiedMiddleware.onMessagedDisplayed(oldMessage, middlewareContext)
verify { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, oldMessage) }
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
}
}

View File

@ -1,61 +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.gleanplumb.state
import io.mockk.mockk
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
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.components.appstate.AppStoreReducer
import org.mozilla.fenix.gleanplumb.MessagingState
class MessagingReducerTest {
@Test
fun `GIVEN a new value for messageToShow WHEN UpdateMessageToShow is called THEN update the current value`() {
val initialState = AppState(
messaging = MessagingState(
messageToShow = null
)
)
var updatedState = MessagingReducer.reduce(
initialState,
UpdateMessageToShow(mockk())
)
assertNotNull(updatedState.messaging.messageToShow)
updatedState = AppStoreReducer.reduce(updatedState, ConsumeMessageToShow)
assertNull(updatedState.messaging.messageToShow)
}
@Test
fun `GIVEN a new value for messages WHEN UpdateMessages is called THEN update the current value`() {
val initialState = AppState(
messaging = MessagingState(
messages = emptyList()
)
)
var updatedState = MessagingReducer.reduce(
initialState,
UpdateMessages(listOf(mockk()))
)
assertFalse(updatedState.messaging.messages.isEmpty())
updatedState = AppStoreReducer.reduce(updatedState, UpdateMessages(emptyList()))
assertTrue(updatedState.messaging.messages.isEmpty())
}
}

View File

@ -63,8 +63,6 @@ import org.mozilla.fenix.components.metrics.Event.PerformedSearch.EngineSource
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplumb.Message
import org.mozilla.fenix.gleanplumb.MessageController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab
@ -86,7 +84,6 @@ class DefaultSessionControlControllerTest {
private val appStore: AppStore = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private val messageController: MessageController = mockk(relaxed = true)
private val engine: Engine = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
@ -140,6 +137,7 @@ class DefaultSessionControlControllerTest {
mode = Mode.Normal,
topSites = emptyList(),
showCollectionPlaceholder = true,
showSetAsDefaultBrowserCard = true,
recentTabs = emptyList(),
recentBookmarks = emptyList()
)
@ -1122,26 +1120,6 @@ class DefaultSessionControlControllerTest {
}
}
@Test
fun `WHEN handleMessageClicked,handleMessageClosed and handleMessageDisplayed are called THEN delegate to messageController`() {
val controller = createController()
val message = mockk<Message>()
controller.handleMessageClicked(message)
controller.handleMessageClosed(message)
controller.handleMessageDisplayed(message)
verify {
messageController.onMessagePressed(message)
}
verify {
messageController.onMessageDismissed(message)
}
verify {
messageController.onMessageDisplayed(message)
}
}
private fun createController(
hideOnboarding: () -> Unit = { },
registerCollectionStorageObserver: () -> Unit = { },
@ -1154,7 +1132,6 @@ class DefaultSessionControlControllerTest {
engine = engine,
metrics = metrics,
store = store,
messageController = messageController,
tabCollectionStorage = tabCollectionStorage,
addTabUseCase = tabsUseCases.addTab,
restoreUseCase = mockk(relaxed = true),

View File

@ -19,7 +19,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gleanplumb.Message
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab
@ -156,7 +155,7 @@ class SessionControlViewTest {
expandedCollections,
recentBookmarks,
false,
null,
false,
recentTabs,
historyMetadata,
pocketArticles
@ -168,40 +167,6 @@ class SessionControlViewTest {
assertTrue(results[3] is AdapterItem.CustomizeHomeButton)
}
@Test
fun `GIVEN a nimbusMessageCard WHEN normalModeAdapterItems is called THEN add a NimbusMessageCard`() {
val settings: Settings = mockk()
val topSites = emptyList<TopSite>()
val collections = emptyList<TabCollection>()
val expandedCollections = emptySet<Long>()
val recentBookmarks = listOf(RecentBookmark())
val recentTabs = emptyList<RecentTab.Tab>()
val historyMetadata = emptyList<RecentHistoryGroup>()
val pocketArticles = emptyList<PocketRecommendedStory>()
val nimbusMessageCard: Message = mockk()
every { settings.showTopSitesFeature } returns true
every { settings.showRecentTabsFeature } returns true
every { settings.showRecentBookmarksFeature } returns true
every { settings.historyMetadataUIFeature } returns true
every { settings.showPocketRecommendationsFeature } returns true
val results = normalModeAdapterItems(
settings,
topSites,
collections,
expandedCollections,
recentBookmarks,
false,
nimbusMessageCard,
recentTabs,
historyMetadata,
pocketArticles
)
assertTrue(results.contains(AdapterItem.NimbusMessageCard(nimbusMessageCard)))
}
@Test
fun `GIVEN recent tabs WHEN normalModeAdapterItems is called THEN add a customize home button`() {
val settings: Settings = mockk()
@ -226,7 +191,7 @@ class SessionControlViewTest {
expandedCollections,
recentBookmarks,
false,
null,
false,
recentTabs,
historyMetadata,
pocketArticles
@ -262,7 +227,7 @@ class SessionControlViewTest {
expandedCollections,
recentBookmarks,
false,
null,
false,
recentTabs,
historyMetadata,
pocketArticles
@ -298,7 +263,7 @@ class SessionControlViewTest {
expandedCollections,
recentBookmarks,
false,
null,
false,
recentTabs,
historyMetadata,
pocketArticles
@ -335,7 +300,7 @@ class SessionControlViewTest {
expandedCollections,
recentBookmarks,
false,
null,
false,
recentTabs,
historyMetadata,
pocketArticles
@ -371,7 +336,7 @@ class SessionControlViewTest {
expandedCollections,
recentBookmarks,
false,
null,
false,
recentTabs,
historyMetadata,
pocketArticles

View File

@ -70,253 +70,31 @@ features:
description: Where is the message to be put.
type: Option<MessageSurfaceId>
default: null
messaging:
description: |
Configuration for the messaging system.
In practice this is a set of growable lookup tables for the
message controller to piece together.
variables:
message-under-experiment:
description: Id or prefix of the message under experiment.
type: Option<String>
default: null
messages:
description: A growable collection of messages
type: Map<String, MessageData>
default: {}
triggers:
description: >
A collection of out the box trigger
expressions. Each entry maps to a
valid JEXL expression.
type: Map<String, String>
default: {}
styles:
description: >
A map of styles to configure message
appearance.
type: Map<String, StyleData>
default: {}
actions:
type: Map<String, String>
description: A growable map of action URLs.
default: {}
on-control:
type: ControlMessageBehavior
description: What should be displayed when a control message is selected.
default: show-next-message
defaults:
- value:
triggers:
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
DEVICE_IOS: os == 'iOS'
ALWAYS: "true"
NEVER: "false"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
- channel: developer
value:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
- channel: developer
value: {
"messages": {
"my-viewpoint-survey": {
"title": "Message tile",
"text": "Love Firefox? Fill in our survey!",
"action": "https://surveyprovider.com/survey-id/{uuid}",
"trigger": [ "ALWAYS" ],
"style": "DEFAULT",
"button-label": "Go to the survey"
}
}
}
- channel: developer
value: {
"messages": {
"private-tabs-auto-close": {
"action": "OPEN_SETTINGS",
"text": "Sharing your phone? Autoclosing private tabs is for you!",
"trigger": [
"USER_RECENTLY_INSTALLED"
]
}
},
"message-under-experiment": "private-tabs-auto-close"
}
- channel: developer
value: {
"triggers": {
"USER_IE_COUNTRY": "'IE' in locale"
},
"styles": {
"irish-green": {
"priority": 50
}
},
"messages": {
"eu-tracking-protection-for-ireland": {
"action": "OPEN_SETTINGS",
"text": "GDPR has you covered. Firefox has GDPR covered",
"style": "irish-green",
"trigger": [
"NEW_USER",
"USER_IE_COUNTRY"
]
}
},
"message-under-experiment": "eu-tracking-protection-for-"
}
types:
objects:
MessageData:
description: >
An object to describe a message. It uses human
readable strings to describe the triggers, action and
style of the message as well as the text of the message
and call to action.
fields:
action:
type: Text
description: >
A URL of a page or a deeplink.
This may have substitution variables in.
# This should never be defaulted.
default: empty_string
title:
type: Option<Text>
description: "The title text displayed to the user"
default: null
text:
type: Text
description: "The message text displayed to the user"
# This should never be defaulted.
default: empty_string
is-control:
type: Boolean
description: "Indicates if this message is the control message, if true shouldn't be displayed"
default: false
button-label:
type: Option<Text>
description: >
The text on the button. If no text
is present, the whole message is clickable.
default: null
style:
type: String
description: >
The style as described in a
`StyleData` from the styles table.
default: DEFAULT
trigger:
type: List<String>
description: >
A list of strings corresponding to
targeting expressions. The message will be
shown if all expressions `true`.
default: []
StyleData:
description: >
A group of properities (predominantly visual) to
describe the style of the message.
fields:
priority:
type: Int
description: >
The importance of this message.
0 is not very important, 100 is very important.
default: 50
max-display-count:
type: Int
description: >
How many sessions will this message be shown to the user
before it is expired.
default: 5
objects: {}
enums:
ControlMessageBehavior:
description: An enum to influence what should be displayed when a control message is selected.
variants:
show-next-message:
description: The next eligible message should be shown.
show-none:
description: The surface should show no message.
HomeScreenSection:
description: The identifiers for the sections of the homescreen.
variants:
top-sites:
description: The frecency and pinned sites.
recently-saved:
description: The sites the user has bookmarked recently.
jump-back-in:
description: The tabs the user was looking immediately before being interrupted.
recent-explorations:
description: The tab groups
pocket:
description: The pocket section. This should only be available in the US.
contile-top-sites:
description: The sponsored shortcuts in the homescreen.
description: The identifiers for the sections of the homescreen.
variants:
top-sites:
description: The frecency and pinned sites.
recently-saved:
description: The sites the user has bookmarked recently.
jump-back-in:
description: The tabs the user was looking immediately before being interrupted.
recent-explorations:
description: The tab groups
pocket:
description: The pocket section. This should only be available in the US.
contile-top-sites:
description: The sponsored shortcuts in the homescreen.
MessageSurfaceId:
description: The identity of a message surface, used in the default browser experiments
variants:
app-menu-item:
description: An item in the default toolbar menu.
settings:
description: A setting in the settings screen.
homescreen-banner:
description: A banner in the homescreen.
description: The identity of a message surface, used in the default browser experiments
variants:
app-menu-item:
description: An item in the default toolbar menu.
settings:
description: A setting in the settings screen.
homescreen-banner:
description: A banner in the homescreen.