@ -7,6 +7,7 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context
import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.core.content.ContextCompat.getColor
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
@ -21,6 +22,7 @@ import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuImageTextCheckboxButton
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
import mozilla.components.browser.state.selector.findTab
@ -33,13 +35,19 @@ import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.FeatureFlags.tabsTrayRewrite
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments
import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
/ * *
* Builds the toolbar object used with the 3 - dot menu in the browser fragment .
@ -50,13 +58,12 @@ import org.mozilla.fenix.theme.ThemeManager
* @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs .
* @param bookmarksStorage Used to check if a page is bookmarked .
* /
@Suppress ( " LargeClass " , " LongParameterList " )
@Suppress ( " LargeClass " , " LongParameterList " , " TooManyFunctions " )
@ExperimentalCoroutinesApi
class DefaultToolbarMenu (
open class DefaultToolbarMenu (
private val context : Context ,
private val store : BrowserStore ,
hasAccountProblem : Boolean = false ,
shouldReverseItems : Boolean ,
private val onItemTapped : ( ToolbarMenu . Item ) -> Unit = { } ,
private val lifecycleOwner : LifecycleOwner ,
private val bookmarksStorage : BookmarksStorage ,
@ -65,7 +72,11 @@ class DefaultToolbarMenu(
private var isCurrentUrlBookmarked = false
private var isBookmarkedJob : Job ? = null
private val isTopToolbarSelected = shouldReverseItems
private val shouldDeleteDataOnQuit = context . settings ( ) . shouldDeleteBrowsingDataOnQuit
private val shouldUseBottomToolbar = context . settings ( ) . shouldUseBottomToolbar
private val accountManager = FenixAccountManager ( context )
private val selectedSession : TabSessionState ?
get ( ) = store . state . selectedTab
@ -77,13 +88,16 @@ class DefaultToolbarMenu(
} else {
oldCoreMenuItems
} ,
endOfMenuAlwaysVisible = ! shouldReverseItems ,
endOfMenuAlwaysVisible = shouldUseBottomToolbar ,
store = store ,
webExtIconTintColorResource = primaryTextColor ( ) ,
style = WebExtensionBrowserMenuBuilder . Style (
webExtIconTintColorResource = primaryTextColor ( ) ,
addonsManagerMenuItemDrawableRes = R . drawable . ic _addons _extensions
) ,
onAddonsManagerTapped = {
onItemTapped . invoke ( ToolbarMenu . Item . AddonsManager )
} ,
appendExtensionSubMenuAtStart = ! shouldReverseItems
appendExtensionSubMenuAtStart = shouldUseBottomToolbar
)
}
@ -137,7 +151,7 @@ class DefaultToolbarMenu(
}
val share = BrowserMenuItemToolbar . Button (
imageResource = R . drawable . ic _share _filled ,
imageResource = R . drawable . ic _share ,
contentDescription = context . getString ( R . string . browser _menu _share ) ,
iconTintColorResource = primaryTextColor ( ) ,
listener = {
@ -148,7 +162,7 @@ class DefaultToolbarMenu(
registerForIsBookmarkedUpdates ( )
if ( FeatureFlags . toolbarMenuFeature ) {
BrowserMenuItemToolbar ( listOf ( back , forward , share , refresh ) )
BrowserMenuItemToolbar ( listOf ( back , forward , share , refresh ) , isSticky = true )
} else {
val bookmark = BrowserMenuItemToolbar . TwoStateButton (
primaryImageResource = R . drawable . ic _bookmark _filled ,
@ -163,8 +177,7 @@ class DefaultToolbarMenu(
secondaryImageTintResource = primaryTextColor ( ) ,
disableInSecondaryState = false
) {
if ( !is CurrentUrlBookmarked ) isCurrentUrlBookmarked = true
onItemTapped . invoke ( ToolbarMenu . Item . Bookmark )
handleBookmarkItemTapped ( )
}
BrowserMenuItemToolbar ( listOf ( back , forward , bookmark , share , refresh ) )
@ -172,24 +185,43 @@ class DefaultToolbarMenu(
}
// Predicates that need to be repeatedly called as the session changes
private fun canAddToHomescreen ( ) : Boolean =
@VisibleForTesting ( otherwise = PRIVATE )
fun canAddToHomescreen ( ) : Boolean =
selectedSession != null && isPinningSupported &&
! context . components . useCases . webAppUseCases . isInstallable ( )
private fun canInstall ( ) : Boolean =
@VisibleForTesting ( otherwise = PRIVATE )
fun canInstall ( ) : Boolean =
selectedSession != null && isPinningSupported &&
context . components . useCases . webAppUseCases . isInstallable ( )
private fun shouldShowOpenInApp ( ) : Boolean = selectedSession ?. let { session ->
@VisibleForTesting ( otherwise = PRIVATE )
fun shouldShowOpenInApp ( ) : Boolean = selectedSession ?. let { session ->
val appLink = context . components . useCases . appLinksUseCases . appLinkRedirect
appLink ( session . content . url ) . hasExternalApp ( )
} ?: false
private fun shouldShowReaderViewCustomization ( ) : Boolean = selectedSession ?. let {
@VisibleForTesting ( otherwise = PRIVATE )
fun shouldShowReaderViewCustomization ( ) : Boolean = selectedSession ?. let {
store . state . findTab ( it . id ) ?. readerState ?. active
} ?: false
// End of predicates //
val installToHomescreen = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _install _on _homescreen ) ,
startImageResource = R . drawable . ic _add _to _homescreen ,
iconTintColorResource = primaryTextColor ( ) ,
highlight = BrowserMenuHighlight . LowPriority (
label = context . getString ( R . string . browser _menu _install _on _homescreen ) ,
notificationTint = getColor ( context , R . color . whats _new _notification _color )
) ,
isHighlighted = {
! context . settings ( ) . installPwaOpened
}
) {
onItemTapped . invoke ( ToolbarMenu . Item . InstallPwaToHomeScreen )
}
private val oldCoreMenuItems by lazy {
val settings = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _settings ) ,
@ -244,21 +276,6 @@ class DefaultToolbarMenu(
onItemTapped . invoke ( ToolbarMenu . Item . SyncedTabs )
}
val installToHomescreen = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _install _on _homescreen ) ,
startImageResource = R . drawable . ic _add _to _homescreen ,
iconTintColorResource = primaryTextColor ( ) ,
highlight = BrowserMenuHighlight . LowPriority (
label = context . getString ( R . string . browser _menu _install _on _homescreen ) ,
notificationTint = getColor ( context , R . color . whats _new _notification _color )
) ,
isHighlighted = {
! context . settings ( ) . installPwaOpened
}
) {
onItemTapped . invoke ( ToolbarMenu . Item . InstallToHomeScreen )
}
val findInPage = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _find _in _page ) ,
imageResource = R . drawable . mozac _ic _search ,
@ -348,6 +365,9 @@ class DefaultToolbarMenu(
BrowserMenuDivider ( ) ,
reportSiteIssuePlaceholder ,
findInPage ,
getSetDefaultBrowserItem ( ) ?. let { BrowserMenuDivider ( ) } ,
getSetDefaultBrowserItem ( ) ,
getSetDefaultBrowserItem ( ) ?. let { BrowserMenuDivider ( ) } ,
addToTopSites ,
addToHomescreen . apply { visible = :: canAddToHomescreen } ,
installToHomescreen . apply { visible = :: canInstall } ,
@ -359,157 +379,195 @@ class DefaultToolbarMenu(
menuToolbar
)
if ( shouldReverseItems ) {
menuItems . reversed ( )
} else {
if ( shouldUseBottomToolbar ) {
menuItems
} else {
menuItems . reversed ( )
}
}
private val newCoreMenuItems by lazy {
val newTabItem = BrowserMenuImageText (
context . getString ( R . string . library _new _tab ) ,
R . drawable . ic _new ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . NewTab )
}
val newTabItem = BrowserMenuImageText (
context . getString ( R . string . library _new _tab ) ,
R . drawable . ic _new ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . NewTab )
}
val bookmarks Item = BrowserMenuImageText (
context . getString ( R . string . library _ bookmarks ) ,
R . drawable . ic _ bookmark_filled ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . Bookmarks )
}
val history Item = BrowserMenuImageText (
context . getString ( R . string . library _ history ) ,
R . drawable . ic _ history ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . History )
}
val history Item = BrowserMenuImageText (
context . getString ( R . string . library _ history ) ,
R . drawable . ic _ history ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . History )
}
val downloads Item = BrowserMenuImageText (
context . getString ( R . string . library _ downloads ) ,
R . drawable . ic _ download ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . Downloads )
}
val downloadsItem = BrowserMenuImageText (
context . getString ( R . string . library _downloads ) ,
R . drawable . ic _download ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . Downloads )
}
val extensionsItem = WebExtensionPlaceholderMenuItem (
id = WebExtensionPlaceholderMenuItem . MAIN _EXTENSIONS _MENU _ID
)
val extensions Item = BrowserMenuImageText (
context . getString ( R . string . browser _menu _ extensions ) ,
R . drawable . ic _addons _extensions ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . AddonsManager )
}
val findInPageItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _find _in _page ) ,
imageResource = R . drawable . mozac _ic _search ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . FindInPage )
}
val syncedTabs = BrowserMenuImageText (
label = context . getString ( R . string . synced _tabs ) ,
imageResource = R . drawable . ic _synced _tabs ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . SyncedTabs )
val desktopSiteItem = BrowserMenuImageSwitch (
imageResource = R . drawable . ic _desktop ,
label = context . getString ( R . string . browser _menu _desktop _site ) ,
initialState = {
selectedSession ?. content ?. desktopMode ?: false
}
) { checked ->
onItemTapped . invoke ( ToolbarMenu . Item . RequestDesktop ( checked ) )
}
val findInPageItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _find _in _page ) ,
imageResource = R . drawable . mozac_ic _search ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . FindInPage )
}
val customizeReaderView = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _ customize_reader _view ) ,
imageResource = R . drawable . ic_readermode _appearance ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . CustomizeReaderView )
}
val desktopSiteItem = BrowserMenuImageSwitch (
imageResource = R . drawable . ic _desktop ,
label = context . getString ( R . string . browser _menu _desktop _site ) ,
initialState = {
selectedSession ?. content ?. desktopMode ?: false
}
) { checked ->
onItemTapped . invoke ( ToolbarMenu . Item . RequestDesktop ( checked ) )
}
val openInApp = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _open _app _link ) ,
startImageResource = R . drawable . ic _open _in _app ,
iconTintColorResource = primaryTextColor ( ) ,
highlight = BrowserMenuHighlight . LowPriority (
label = context . getString ( R . string . browser _menu _open _app _link ) ,
notificationTint = getColor ( context , R . color . whats _new _notification _color )
) ,
isHighlighted = { ! context . settings ( ) . openInAppOpened }
) {
onItemTapped . invoke ( ToolbarMenu . Item . OpenInApp )
}
val customizeReaderView = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _customize _reader _view ) ,
imageResource = R . drawable . ic _readermode _appearance ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . CustomizeReaderView )
}
val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem (
id = WebCompatReporterFeature . WEBCOMPAT _REPORTER _EXTENSION _ID
)
val addToHomeScreenItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _add _to _homescreen ) ,
imageResource = R . drawable . ic _add _to _homescreen ,
iconTintColorResource = primaryTextColor ( ) ,
isCollapsingMenuLimit = true
) {
onItemTapped . invoke ( ToolbarMenu . Item . AddToHomeScreen )
}
val openInApp = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _open _app _link ) ,
startImageResource = R . drawable . ic _open _in _app ,
iconTintColorResource = primaryTextColor ( ) ,
highlight = BrowserMenuHighlight . LowPriority (
label = context . getString ( R . string . browser _menu _open _app _link ) ,
notificationTint = getColor ( context , R . color . whats _new _notification _color )
) ,
isHighlighted = { ! context . settings ( ) . openInAppOpened }
) {
onItemTapped . invoke ( ToolbarMenu . Item . OpenInApp )
}
val addToTopSitesItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _add _to _top _sites ) ,
imageResource = R . drawable . ic _top _sites ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . AddToTopSites )
}
val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem (
id = WebCompatReporterFeature . WEBCOMPAT _REPORTER _EXTENSION _ID
)
val saveToCollectionItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _save _to _collection _2 ) ,
imageResource = R . drawable . ic _tab _collection ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . SaveToCollection )
}
val addToHomeScreenItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _add _to _homescreen ) ,
imageResource = R . drawable . ic _add _to _homescreen ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . AddToHomeScreen )
}
val settingsItem = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _settings ) ,
startImageResource = R . drawable . ic _settings ,
iconTintColorResource = if ( hasAccountProblem )
ThemeManager . resolveAttribute ( R . attr . syncDisconnected , context ) else
primaryTextColor ( ) ,
textColorResource = if ( hasAccountProblem )
ThemeManager . resolveAttribute ( R . attr . primaryText , context ) else
primaryTextColor ( ) ,
highlight = BrowserMenuHighlight . HighPriority (
endImageResource = R . drawable . ic _sync _disconnected ,
backgroundTint = context . getColorFromAttr ( R . attr . syncDisconnectedBackground ) ,
canPropagate = false
) ,
isHighlighted = { hasAccountProblem }
) {
onItemTapped . invoke ( ToolbarMenu . Item . Settings )
}
val addToTopSitesItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _add _to _top _sites ) ,
imageResource = R . drawable . ic _top _sites ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . AddToTopSites )
}
val bookmarksItem = BrowserMenuImageTextCheckboxButton (
imageResource = R . drawable . ic _bookmarks _menu ,
iconTintColorResource = primaryTextColor ( ) ,
label = context . getString ( R . string . library _bookmarks ) ,
labelListener = {
onItemTapped . invoke ( ToolbarMenu . Item . Bookmarks )
} ,
primaryStateIconResource = R . drawable . ic _bookmark _outline ,
secondaryStateIconResource = R . drawable . ic _bookmark _filled ,
tintColorResource = menuItemButtonTintColor ( ) ,
primaryLabel = context . getString ( R . string . browser _menu _add ) ,
secondaryLabel = context . getString ( R . string . browser _menu _edit ) ,
isInPrimaryState = { !is CurrentUrlBookmarked }
) {
handleBookmarkItemTapped ( )
}
val saveToCollectionItem = BrowserMenuImageText (
label = context . getString ( R . string . browser _menu _save _to _collection _2 ) ,
imageResource = R . drawable . ic _tab _collection ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . SaveToCollection )
}
val deleteDataOnQuit = BrowserMenuImageText (
label = context . getString ( R . string . delete_browsing _data _on _quit _action ) ,
imageResource = R . drawable . ic _ exit ,
iconTintColorResource = primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . Quit )
}
val settingsItem = BrowserMenuHighlightableItem (
label = context . getString ( R . string . browser _menu _settings ) ,
startImageResource = R . drawable . ic _settings ,
iconTintColorResource = primaryTextColor ( ) ,
textColorResource = if ( hasAccountProblem )
ThemeManager . resolveAttribute ( R . attr . primaryText , context ) else
primaryTextColor ( ) ,
highlight = BrowserMenuHighlight . HighPriority (
endImageResource = R . drawable . ic _sync _disconnected ,
backgroundTint = context . getColorFromAttr ( R . attr . syncDisconnectedBackground ) ,
canPropagate = false
) ,
isHighlighted = { hasAccountProblem }
) {
onItemTapped . invoke ( ToolbarMenu . Item . Settings )
val syncedTabsItem = BrowserMenuImageText (
context . getString ( R . string . synced _tabs ) ,
R . drawable . ic _synced _tabs ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . SyncedTabs )
}
private fun getSyncItemTitle ( ) : String {
val authenticatedAccount = accountManager . authenticatedAccount
val email = accountManager . accountProfileEmail
return if ( authenticatedAccount && ! email . isNullOrEmpty ( ) ) {
email
} else {
context . getString ( R . string . sync _menu _sign _in )
}
}
val syncMenuItem = BrowserMenuImageText (
getSyncItemTitle ( ) ,
R . drawable . ic _signed _out ,
primaryTextColor ( )
) {
onItemTapped . invoke ( ToolbarMenu . Item . SyncAccount ( accountManager . signedInToFxa ( ) ) )
}
@VisibleForTesting ( otherwise = PRIVATE )
val newCoreMenuItems by lazy {
val menuItems =
listOfNotNull (
if ( isTopToolbarSelected ) menuToolbar else null ,
if ( shouldUseBottomToolbar) null else menuToolbar ,
newTabItem ,
BrowserMenuDivider ( ) ,
bookmarksItem ,
historyItem ,
downloadsItem ,
extensionsItem ,
syncedTabs ,
if ( tabsTrayRewrite ) syncMenuItem else syncedTabs Item ,
BrowserMenuDivider ( ) ,
getSetDefaultBrowserItem ( ) ,
getSetDefaultBrowserItem ( ) ?. let { BrowserMenuDivider ( ) } ,
findInPageItem ,
desktopSiteItem ,
customizeReaderView . apply { visible = :: shouldShowReaderViewCustomization } ,
@ -517,21 +575,32 @@ class DefaultToolbarMenu(
reportSiteIssuePlaceholder ,
BrowserMenuDivider ( ) ,
addToHomeScreenItem . apply { visible = :: canAddToHomescreen } ,
installToHomescreen . apply { visible = :: canInstall } ,
addToTopSitesItem ,
saveToCollectionItem ,
BrowserMenuDivider ( ) ,
settingsItem ,
if ( isTopToolbarSelected ) null else BrowserMenuDivider ( ) ,
if ( isTopToolbarSelected ) null else menuToolbar
if ( shouldDeleteDataOnQuit ) deleteDataOnQuit else null ,
if ( shouldUseBottomToolbar ) BrowserMenuDivider ( ) else null ,
if ( shouldUseBottomToolbar ) menuToolbar else null
)
menuItems
}
private fun handleBookmarkItemTapped ( ) {
if ( !is CurrentUrlBookmarked ) isCurrentUrlBookmarked = true
onItemTapped . invoke ( ToolbarMenu . Item . Bookmark )
}
@ColorRes
@VisibleForTesting
internal fun primaryTextColor ( ) = ThemeManager . resolveAttribute ( R . attr . primaryText , context )
@ColorRes
@VisibleForTesting
internal fun menuItemButtonTintColor ( ) = ThemeManager . resolveAttribute ( R . attr . menuItemButtonTintColor , context )
@VisibleForTesting
internal fun registerForIsBookmarkedUpdates ( ) {
store . flowScoped ( lifecycleOwner ) { flow ->
@ -558,4 +627,24 @@ class DefaultToolbarMenu(
. any { it . url == newUrl }
}
}
private fun getSetDefaultBrowserItem ( ) : BrowserMenuImageText ? {
val experiments = context . components . analytics . experiments
val browsers = BrowsersCache . all ( context )
return experiments . withExperiment ( Experiments . DEFAULT _BROWSER ) { experimentBranch ->
if ( experimentBranch == ExperimentBranch . DEFAULT _BROWSER _TOOLBAR _MENU &&
! browsers . isFirefoxDefaultBrowser
) {
return @withExperiment BrowserMenuImageText (
label = context . getString ( R . string . preferences _set _as _default _browser ) ,
imageResource = R . mipmap . ic _launcher
) {
onItemTapped . invoke ( ToolbarMenu . Item . SetDefaultBrowser )
}
} else {
null
}
}
}
}