You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
381 lines
15 KiB
Kotlin
381 lines
15 KiB
Kotlin
/* 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.components.toolbar
|
|
|
|
import android.content.Context
|
|
import android.view.View
|
|
import androidx.annotation.VisibleForTesting
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.material.Text
|
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.semantics.semantics
|
|
import androidx.compose.ui.semantics.testTag
|
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
|
import androidx.compose.ui.text.style.TextDecoration
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.core.content.ContextCompat.getColor
|
|
import androidx.core.view.isVisible
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.cancel
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
import kotlinx.coroutines.flow.filter
|
|
import kotlinx.coroutines.flow.firstOrNull
|
|
import kotlinx.coroutines.flow.map
|
|
import kotlinx.coroutines.flow.mapNotNull
|
|
import kotlinx.coroutines.flow.transformWhile
|
|
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
|
|
import mozilla.components.browser.state.selector.selectedTab
|
|
import mozilla.components.browser.state.store.BrowserStore
|
|
import mozilla.components.browser.toolbar.BrowserToolbar
|
|
import mozilla.components.compose.cfr.CFRPopup
|
|
import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR
|
|
import mozilla.components.compose.cfr.CFRPopupProperties
|
|
import mozilla.components.lib.state.ext.flowScoped
|
|
import mozilla.components.service.glean.private.NoExtras
|
|
import org.mozilla.fenix.GleanMetrics.TrackingProtection
|
|
import org.mozilla.fenix.R
|
|
import org.mozilla.fenix.ext.components
|
|
import org.mozilla.fenix.ext.settings
|
|
import org.mozilla.fenix.settings.SupportUtils
|
|
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.TOTAL_COOKIE_PROTECTION
|
|
import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature
|
|
import org.mozilla.fenix.shopping.ShoppingExperienceFeature
|
|
import org.mozilla.fenix.theme.FirefoxTheme
|
|
import org.mozilla.fenix.utils.Settings
|
|
|
|
/**
|
|
* Vertical padding needed to improve the visual alignment of the popup and respect the UX design.
|
|
*/
|
|
private const val CFR_TO_ANCHOR_VERTICAL_PADDING = -6
|
|
|
|
/**
|
|
* The minimum number of opened tabs to show the Total Cookie Protection CFR.
|
|
*/
|
|
private const val CFR_MINIMUM_NUMBER_OPENED_TABS = 5
|
|
|
|
/**
|
|
* Delegate for handling all the business logic for showing CFRs in the toolbar.
|
|
*
|
|
* @property context used for various Android interactions.
|
|
* @property browserStore will be observed for tabs updates
|
|
* @property settings used to read and write persistent user settings
|
|
* @property toolbar will serve as anchor for the CFRs
|
|
* @property isPrivate Whether or not the session is private.
|
|
* @property sessionId optional custom tab id used to identify the custom tab in which to show a CFR.
|
|
* @property onShoppingCfrActionClicked Triggered when the user taps on the shopping CFR action.
|
|
* @property shoppingExperienceFeature Used to determine if [ShoppingExperienceFeature] is enabled.
|
|
*/
|
|
class BrowserToolbarCFRPresenter(
|
|
private val context: Context,
|
|
private val browserStore: BrowserStore,
|
|
private val settings: Settings,
|
|
private val toolbar: BrowserToolbar,
|
|
private val isPrivate: Boolean,
|
|
private val sessionId: String? = null,
|
|
private val onShoppingCfrActionClicked: () -> Unit,
|
|
private val shoppingExperienceFeature: ShoppingExperienceFeature = DefaultShoppingExperienceFeature(
|
|
context.settings(),
|
|
),
|
|
) {
|
|
@VisibleForTesting
|
|
internal var scope: CoroutineScope? = null
|
|
|
|
@VisibleForTesting
|
|
internal var popup: CFRPopup? = null
|
|
|
|
/**
|
|
* Start observing [browserStore] for updates which may trigger showing a CFR.
|
|
*/
|
|
@Suppress("MagicNumber")
|
|
fun start() {
|
|
when (getCFRToShow()) {
|
|
ToolbarCFR.TCP -> {
|
|
scope = browserStore.flowScoped { flow ->
|
|
flow.mapNotNull { it.findCustomTabOrSelectedTab(sessionId)?.content?.progress }
|
|
// The "transformWhile" below ensures that the 100% progress is only collected once.
|
|
.transformWhile { progress ->
|
|
emit(progress)
|
|
progress != 100
|
|
}.filter { popup == null && it == 100 }.collect {
|
|
scope?.cancel()
|
|
showTcpCfr()
|
|
}
|
|
}
|
|
}
|
|
|
|
ToolbarCFR.SHOPPING, ToolbarCFR.SHOPPING_OPTED_IN -> {
|
|
scope = browserStore.flowScoped { flow ->
|
|
val shouldShowCfr: Boolean? = flow.mapNotNull { it.selectedTab }
|
|
.filter { it.isProductUrl && it.content.progress == 100 && !it.content.loading }
|
|
.distinctUntilChanged()
|
|
.map { toolbar.findViewById<View>(R.id.mozac_browser_toolbar_page_actions).isVisible }
|
|
.filter { popup == null && it }
|
|
.firstOrNull()
|
|
|
|
if (shouldShowCfr == true) {
|
|
showShoppingCFR(getCFRToShow() == ToolbarCFR.SHOPPING_OPTED_IN)
|
|
}
|
|
|
|
scope?.cancel()
|
|
}
|
|
}
|
|
|
|
ToolbarCFR.ERASE -> {
|
|
scope = browserStore.flowScoped { flow ->
|
|
flow
|
|
.mapNotNull { it.findCustomTabOrSelectedTab(sessionId) }
|
|
.filter { it.content.private }
|
|
.map { it.content.progress }
|
|
// The "transformWhile" below ensures that the 100% progress is only collected once.
|
|
.transformWhile { progress ->
|
|
emit(progress)
|
|
progress != 100
|
|
}
|
|
.filter { popup == null && it == 100 }
|
|
.collect {
|
|
scope?.cancel()
|
|
showEraseCfr()
|
|
}
|
|
}
|
|
}
|
|
|
|
ToolbarCFR.NONE -> {
|
|
// no-op
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getCFRToShow(): ToolbarCFR = when {
|
|
settings.shouldShowEraseActionCFR && isPrivate -> {
|
|
ToolbarCFR.ERASE
|
|
}
|
|
|
|
settings.shouldShowTotalCookieProtectionCFR && (
|
|
!settings.shouldShowCookieBannerReEngagementDialog() ||
|
|
settings.openTabsCount >= CFR_MINIMUM_NUMBER_OPENED_TABS
|
|
) -> ToolbarCFR.TCP
|
|
|
|
shoppingExperienceFeature.isEnabled &&
|
|
settings.shouldShowReviewQualityCheckCFR -> {
|
|
val optInTime = settings.reviewQualityCheckOptInTimeInMillis
|
|
if (optInTime != 0L && System.currentTimeMillis() - optInTime > Settings.ONE_DAY_MS) {
|
|
ToolbarCFR.SHOPPING_OPTED_IN
|
|
} else {
|
|
ToolbarCFR.SHOPPING
|
|
}
|
|
}
|
|
|
|
else -> ToolbarCFR.NONE
|
|
}
|
|
|
|
/**
|
|
* Stop listening for [browserStore] updates.
|
|
* CFRs already shown are not automatically dismissed.
|
|
*/
|
|
fun stop() {
|
|
scope?.cancel()
|
|
}
|
|
|
|
@VisibleForTesting
|
|
internal fun showEraseCfr() {
|
|
CFRPopup(
|
|
anchor = toolbar.findViewById(
|
|
R.id.mozac_browser_toolbar_navigation_actions,
|
|
),
|
|
properties = CFRPopupProperties(
|
|
popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
|
|
popupBodyColors = listOf(
|
|
getColor(context, R.color.fx_mobile_layer_color_gradient_end),
|
|
getColor(context, R.color.fx_mobile_layer_color_gradient_start),
|
|
),
|
|
popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp,
|
|
dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor),
|
|
indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) {
|
|
CFRPopup.IndicatorDirection.UP
|
|
} else {
|
|
CFRPopup.IndicatorDirection.DOWN
|
|
},
|
|
),
|
|
onDismiss = {
|
|
when (it) {
|
|
true -> TrackingProtection.tcpCfrExplicitDismissal.record(NoExtras())
|
|
false -> TrackingProtection.tcpCfrImplicitDismissal.record(NoExtras())
|
|
}
|
|
},
|
|
text = {
|
|
FirefoxTheme {
|
|
Text(
|
|
text = context.getString(R.string.erase_action_cfr_message),
|
|
color = FirefoxTheme.colors.textOnColorPrimary,
|
|
style = FirefoxTheme.typography.body2,
|
|
)
|
|
}
|
|
},
|
|
).run {
|
|
settings.shouldShowEraseActionCFR = false
|
|
popup = this
|
|
show()
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalComposeUiApi::class)
|
|
@VisibleForTesting
|
|
@Suppress("LongMethod")
|
|
internal fun showTcpCfr() {
|
|
CFRPopup(
|
|
anchor = toolbar.findViewById(
|
|
R.id.mozac_browser_toolbar_security_indicator,
|
|
),
|
|
properties = CFRPopupProperties(
|
|
popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
|
|
popupBodyColors = listOf(
|
|
getColor(context, R.color.fx_mobile_layer_color_gradient_end),
|
|
getColor(context, R.color.fx_mobile_layer_color_gradient_start),
|
|
),
|
|
popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp,
|
|
dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor),
|
|
indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) {
|
|
CFRPopup.IndicatorDirection.UP
|
|
} else {
|
|
CFRPopup.IndicatorDirection.DOWN
|
|
},
|
|
),
|
|
onDismiss = {
|
|
when (it) {
|
|
true -> {
|
|
TrackingProtection.tcpCfrExplicitDismissal.record(NoExtras())
|
|
settings.shouldShowTotalCookieProtectionCFR = false
|
|
}
|
|
false -> TrackingProtection.tcpCfrImplicitDismissal.record(NoExtras())
|
|
}
|
|
},
|
|
text = {
|
|
FirefoxTheme {
|
|
Text(
|
|
text = context.getString(R.string.tcp_cfr_message),
|
|
color = FirefoxTheme.colors.textOnColorPrimary,
|
|
style = FirefoxTheme.typography.body2,
|
|
modifier = Modifier
|
|
.semantics {
|
|
testTagsAsResourceId = true
|
|
testTag = "tcp_cfr.message"
|
|
},
|
|
)
|
|
}
|
|
},
|
|
action = {
|
|
FirefoxTheme {
|
|
Text(
|
|
text = context.getString(R.string.tcp_cfr_learn_more),
|
|
color = FirefoxTheme.colors.textOnColorPrimary,
|
|
modifier = Modifier
|
|
.semantics {
|
|
testTagsAsResourceId = true
|
|
testTag = "tcp_cfr.action"
|
|
}
|
|
.clickable {
|
|
context.components.useCases.tabsUseCases.selectOrAddTab.invoke(
|
|
SupportUtils.getSumoURLForTopic(
|
|
context,
|
|
TOTAL_COOKIE_PROTECTION,
|
|
),
|
|
)
|
|
TrackingProtection.tcpSumoLinkClicked.record(NoExtras())
|
|
settings.shouldShowTotalCookieProtectionCFR = false
|
|
popup?.dismiss()
|
|
},
|
|
style = FirefoxTheme.typography.body2.copy(
|
|
textDecoration = TextDecoration.Underline,
|
|
),
|
|
)
|
|
}
|
|
},
|
|
).run {
|
|
popup = this
|
|
show()
|
|
TrackingProtection.tcpCfrShown.record(NoExtras())
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
internal fun showShoppingCFR(shouldShowOptedInCFR: Boolean) {
|
|
CFRPopup(
|
|
anchor = toolbar.findViewById(
|
|
R.id.mozac_browser_toolbar_page_actions,
|
|
),
|
|
properties = CFRPopupProperties(
|
|
popupWidth = 475.dp,
|
|
popupAlignment = CFRPopup.PopupAlignment.BODY_CENTERED_IN_SCREEN,
|
|
popupBodyColors = listOf(
|
|
getColor(context, R.color.fx_mobile_layer_color_gradient_start),
|
|
getColor(context, R.color.fx_mobile_layer_color_gradient_end),
|
|
),
|
|
popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp,
|
|
dismissButtonColor = getColor(context, R.color.fx_mobile_icon_color_oncolor),
|
|
indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) {
|
|
CFRPopup.IndicatorDirection.UP
|
|
} else {
|
|
CFRPopup.IndicatorDirection.DOWN
|
|
},
|
|
dismissOnBackPress = false,
|
|
dismissOnClickOutside = false,
|
|
),
|
|
onDismiss = {
|
|
when (it) {
|
|
true -> {
|
|
settings.shouldShowReviewQualityCheckCFR = false
|
|
}
|
|
false -> {}
|
|
}
|
|
},
|
|
text = {
|
|
FirefoxTheme {
|
|
Text(
|
|
text = if (shouldShowOptedInCFR) {
|
|
stringResource(id = R.string.review_quality_check_second_cfr_message)
|
|
} else {
|
|
stringResource(id = R.string.review_quality_check_first_cfr_message)
|
|
},
|
|
color = FirefoxTheme.colors.textOnColorPrimary,
|
|
style = FirefoxTheme.typography.body2,
|
|
)
|
|
}
|
|
},
|
|
action = {
|
|
FirefoxTheme {
|
|
Text(
|
|
text = if (shouldShowOptedInCFR) {
|
|
stringResource(id = R.string.review_quality_check_second_cfr_action)
|
|
} else {
|
|
stringResource(id = R.string.review_quality_check_first_cfr_action)
|
|
},
|
|
color = FirefoxTheme.colors.textOnColorPrimary,
|
|
modifier = Modifier
|
|
.clickable {
|
|
onShoppingCfrActionClicked()
|
|
popup?.dismiss()
|
|
},
|
|
style = FirefoxTheme.typography.body2.copy(
|
|
textDecoration = TextDecoration.Underline,
|
|
),
|
|
)
|
|
}
|
|
},
|
|
).run {
|
|
popup = this
|
|
show()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The CFR to be shown in the toolbar.
|
|
*/
|
|
private enum class ToolbarCFR {
|
|
TCP, SHOPPING, SHOPPING_OPTED_IN, ERASE, NONE
|
|
}
|