Handle interactions with message controller

This commit is contained in:
Arturo Mejia 2022-03-09 19:53:23 -05:00
parent 7b2c7c4cc6
commit 8c82e9f640
12 changed files with 257 additions and 85 deletions

View File

@ -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 {

View File

@ -0,0 +1,52 @@
/* 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.gleanplum
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
/**
* Handles default interactions with an ui message.
*/
class DefaultMessageController(
private val messageManager: MessagesManager,
private val homeActivity: HomeActivity
) : MessageController {
override fun onMessagePressed(message: Message) {
// TODO: report telemetry event
messageManager.onMessagePressed(message)
handleAction(message.data.action)
}
override fun onMessageDismissed(message: Message) {
// TODO: report telemetry event
messageManager.onMessageDismissed(message)
}
override fun onMessageDisplayed(message: Message) {
// TODO: report telemetry event
messageManager.onMessageDisplayed(message)
}
@VisibleForTesting
internal fun handleAction(action: String): Intent {
val url = if (action.startsWith("http", ignoreCase = true)) {
"open?url=$action"
} else {
action
}
val intent =
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEP_LINK_SCHEME}://$url".toUri())
homeActivity.processIntent(intent)
return intent
}
}

View File

@ -4,12 +4,10 @@
package org.mozilla.fenix.gleanplum
/**
* Controls all the interactions with a [Message].
*/
interface MessageController {
/**
* Finds the next message to be displayed.
*/
fun getNextMessage(): Message?
/**
* Indicates the provided [message] was pressed press by the user.
*/
@ -26,6 +24,4 @@ interface MessageController {
* to the users.
*/
fun onMessageDisplayed(message: Message)
fun initialize()
}

View File

@ -5,17 +5,22 @@
package org.mozilla.fenix.gleanplum
import android.content.Context
import androidx.core.net.toUri
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.nimbus.Messaging
import org.mozilla.fenix.nimbus.StyleData
import java.util.SortedSet
import kotlin.Comparator
/**
* Handles all interactions messages from nimbus.
*/
class MessagesManager(
private val context: Context,
private val storage: MessageStorage,
private val messagingFeature: FeatureHolder<Messaging>
) : MessageController {
) {
private val availableMessages: SortedSet<Message> = sortedSetOf(
Comparator { message1, message2 -> message2.style.priority.compareTo(message1.style.priority) }
@ -25,12 +30,12 @@ class MessagesManager(
return availableMessages.isNotEmpty()
}
override fun getNextMessage(): Message? {
fun getNextMessage(): Message? {
return availableMessages.first()
}
override fun onMessagePressed(message: Message) {
// TODO: Report telemetry event
fun onMessagePressed(message: Message) {
// Update storage
storage.updateMetadata(
message.metadata.copy(
pressed = true
@ -39,8 +44,7 @@ class MessagesManager(
availableMessages.remove(message)
}
override fun onMessageDismissed(message: Message) {
// TODO: Report telemetry event
fun onMessageDismissed(message: Message) {
storage.updateMetadata(
message.metadata.copy(
dismissed = true
@ -49,8 +53,7 @@ class MessagesManager(
availableMessages.remove(message)
}
override fun onMessageDisplayed(message: Message) {
// TODO: Report telemetry event
fun onMessageDisplayed(message: Message) {
val newMetadata = message.metadata.copy(
displayCount = message.metadata.displayCount + 1
)
@ -66,10 +69,12 @@ class MessagesManager(
}
}
override fun initialize() {
val nimbusTriggers = messagingFeature.value().triggers
val nimbusStyles = messagingFeature.value().styles
val nimbusMessages = messagingFeature.value().messages
fun initialize() {
val nimbusFeature = messagingFeature.value()
val nimbusTriggers = nimbusFeature.triggers
val nimbusStyles = nimbusFeature.styles
val nimbusMessages = nimbusFeature.messages
val defaultStyle = StyleData(context)
val storageMetadata = storage.getMetadata().associateBy {
it.id

View File

@ -105,6 +105,7 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.sort
import org.mozilla.fenix.gleanplum.DefaultMessageController
import org.mozilla.fenix.home.blocklist.BlocklistHandler
import org.mozilla.fenix.home.blocklist.BlocklistMiddleware
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
@ -345,6 +346,10 @@ class HomeFragment : Fragment() {
settings = components.settings,
engine = components.core.engine,
metrics = components.analytics.metrics,
messageController = DefaultMessageController(
messageManager = components.messagesManager,
homeActivity = activity
),
store = store,
tabCollectionStorage = components.core.tabCollectionStorage,
addTabUseCase = components.useCases.tabsUseCases.addTab,

View File

@ -44,6 +44,9 @@ 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.gleanplum.Message
import org.mozilla.fenix.gleanplum.MessageController
import org.mozilla.fenix.gleanplum.MessagesManager
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
@ -177,14 +180,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]
@ -213,6 +221,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,
@ -597,14 +606,17 @@ 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 handleCloseExperimentCard() {
settings.userDismissedExperimentCard = true
fragmentStore.dispatch(HomeFragmentAction.RemoveSetDefaultBrowserCard)
override fun handleMessageClosed(message: Message) {
messageController.onMessageDismissed(message)
}
override fun handleMessageDisplayed(message: Message) {
messageController.onMessageDismissed(message)
// fragmentStore.dispatch(HomeFragmentAction.RemoveSetDefaultBrowserCard)
}
override fun handlePrivateModeButtonClicked(

View File

@ -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.tips.Tip
import org.mozilla.fenix.gleanplum.Message
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketStoriesController
@ -232,16 +233,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)
}
/**
@ -263,7 +269,7 @@ class SessionControlInteractor(
TipInteractor,
TabSessionInteractor,
ToolbarInteractor,
ExperimentCardInteractor,
MessageCardInteractor,
RecentTabInteractor,
RecentBookmarksInteractor,
RecentVisitsInteractor,
@ -374,14 +380,6 @@ class SessionControlInteractor(
controller.handleMenuOpened()
}
override fun onSetDefaultBrowserClicked() {
controller.handleSetDefaultBrowser()
}
override fun onCloseExperimentCardClicked() {
controller.handleCloseExperimentCard()
}
override fun onRecentTabClicked(tabId: String) {
recentTabController.handleRecentTabClicked(tabId)
}
@ -459,4 +457,16 @@ class SessionControlInteractor(
override fun reportSessionMetrics(state: HomeFragmentState) {
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

@ -18,41 +18,36 @@ class ExperimentDefaultBrowserCardViewHolder(
view: View,
private val interactor: SessionControlInteractor
) : RecyclerView.ViewHolder(view) {
// TODO: Address !!
private val messagesManager: MessagesManager = view.context.components.messagesManager
private val message: Message = messagesManager.getNextMessage()!!
private val message: Message? = messagesManager.getNextMessage()
init {
initialize(view)
}
private fun initialize(view: View) {
val binding = ExperimentDefaultBrowserBinding.bind(view)
binding.setDefaultBrowser.setOnClickListener {
// TODO: use interactor
messagesManager.onMessagePressed(message)
val safeMessage = message ?: return
binding.setDefaultBrowser.setOnClickListener {
interactor.onMessageClicked(safeMessage)
interactor.onSetDefaultBrowserClicked()
}
binding.descriptionText.text = message.data.text
binding.descriptionText.text = safeMessage.data.text
//TODO: Bind button text
binding.close.apply {
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
setOnClickListener {
// TODO: use interactor
messagesManager.onMessageDismissed(message)
interactor.onCloseExperimentCardClicked()
interactor.onMessageClosedClicked(safeMessage)
}
}
binding.close.apply {
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
setOnClickListener {
// TODO: use interactor
messagesManager.onMessagePressed(message)
interactor.onCloseExperimentCardClicked()
interactor.onMessageClosedClicked(safeMessage)
}
}
}

View File

@ -323,24 +323,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()
}

View File

@ -6,6 +6,9 @@
<string name="app_name_private_5">Private %s</string>
<!-- App name for private browsing mode. The first parameter is the name of the app defined in app_name (for example: Fenix)-->
<string name="app_name_private_4">%s (Private)</string>
<string name="gdpr_has_you_covered_firefox_has_gdpr_covered">%s (Private)</string>
<string name="sharing_your_phone_autoclosing_private_tabs_is_for_you">%s (Private)</string>
<string name="love_firefox_fill_in_our_survey">%s (Private)</string>
<!-- Home Fragment -->
<!-- Content description (not visible, for screen readers etc.): "Three dot" menu button. -->

View File

@ -0,0 +1,105 @@
/* 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.gleanplum
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.MessageData
@RunWith(FenixRobolectricTestRunner::class)
class DefaultMessageControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val manager: MessagesManager = mockk(relaxed = true)
private lateinit var controller: DefaultMessageController
@Before
fun setup() {
controller = DefaultMessageController(
messageManager = manager,
homeActivity = activity
)
}
@Test
fun `WHEN calling onMessagePressed THEN report to the messageManager 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 { customController.onMessagePressed(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()) }
assertEquals(
"${BuildConfig.DEEP_LINK_SCHEME}://open?url=http://mozilla.org",
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 { controller.onMessageDismissed(message) }
}
@Test
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
val message = mockMessage()
controller.onMessageDisplayed(message)
verify { controller.onMessageDisplayed(message) }
}
private fun mockMessage() = Message(
id = "id",
data = MessageData(_context = testContext),
style = mockk(),
triggers = emptyList(),
metadata = MessageMetadata(
id = "id",
displayCount = 0,
pressed = false,
dismissed = false
)
)
}

View File

@ -55,6 +55,7 @@ import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.gleanplum.MessageController
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
@ -72,6 +73,7 @@ class DefaultSessionControlControllerTest {
private val fragmentStore: HomeFragmentStore = 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)
@ -1058,6 +1060,7 @@ class DefaultSessionControlControllerTest {
engine = engine,
metrics = metrics,
store = store,
messageController = messageController,
tabCollectionStorage = tabCollectionStorage,
addTabUseCase = tabsUseCases.addTab,
restoreUseCase = mockk(relaxed = true),