1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.components.toolbar
7 import android.view.HapticFeedbackConstants
8 import android.view.LayoutInflater
9 import android.view.View
10 import android.view.ViewGroup
11 import androidx.annotation.LayoutRes
12 import androidx.annotation.VisibleForTesting
13 import androidx.coordinatorlayout.widget.CoordinatorLayout
14 import androidx.core.content.ContextCompat
15 import androidx.lifecycle.LifecycleOwner
16 import kotlinx.android.extensions.LayoutContainer
17 import kotlinx.coroutines.ExperimentalCoroutinesApi
18 import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
19 import mozilla.components.browser.state.selector.selectedTab
20 import mozilla.components.browser.state.state.CustomTabSessionState
21 import mozilla.components.browser.state.state.ExternalAppType
22 import mozilla.components.browser.toolbar.BrowserToolbar
23 import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior
24 import mozilla.components.browser.toolbar.display.DisplayToolbar
25 import mozilla.components.support.utils.URLStringUtils
26 import org.mozilla.fenix.R
27 import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
28 import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration
29 import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
30 import org.mozilla.fenix.ext.bookmarkStorage
31 import org.mozilla.fenix.ext.components
32 import org.mozilla.fenix.ext.settings
33 import org.mozilla.fenix.theme.ThemeManager
34 import org.mozilla.fenix.utils.ToolbarPopupWindow
35 import java.lang.ref.WeakReference
36 import mozilla.components.browser.toolbar.behavior.ToolbarPosition as MozacToolbarPosition
38 @ExperimentalCoroutinesApi
39 @SuppressWarnings("LargeClass")
40 class BrowserToolbarView(
41 private val container: ViewGroup,
42 private val toolbarPosition: ToolbarPosition,
43 private val interactor: BrowserToolbarInteractor,
44 private val customTabSession: CustomTabSessionState?,
45 private val lifecycleOwner: LifecycleOwner
48 override val containerView: View?
51 private val settings = container.context.settings()
54 private val toolbarLayout = when (settings.toolbarPosition) {
55 ToolbarPosition.BOTTOM -> R.layout.component_bottom_browser_toolbar
56 ToolbarPosition.TOP -> R.layout.component_browser_top_toolbar
59 private val layout = LayoutInflater.from(container.context)
60 .inflate(toolbarLayout, container, true)
63 internal var view: BrowserToolbar = layout
64 .findViewById(R.id.toolbar)
66 val toolbarIntegration: ToolbarIntegration
69 internal val isPwaTabOrTwaTab: Boolean
70 get() = customTabSession?.config?.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP ||
71 customTabSession?.config?.externalAppType == ExternalAppType.TRUSTED_WEB_ACTIVITY
74 val isCustomTabSession = customTabSession != null
76 view.display.setOnUrlLongClickListener {
77 ToolbarPopupWindow.show(
80 interactor::onBrowserToolbarPasteAndGo,
81 interactor::onBrowserToolbarPaste
86 with(container.context) {
87 val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported()
92 elevation = resources.getDimension(R.dimen.browser_fragment_toolbar_elevation)
94 if (!isCustomTabSession) {
95 display.setUrlBackground(getDrawable(R.drawable.search_url_background))
98 display.onUrlClicked = {
99 interactor.onBrowserToolbarClicked()
103 display.progressGravity = when (toolbarPosition) {
104 ToolbarPosition.BOTTOM -> DisplayToolbar.Gravity.TOP
105 ToolbarPosition.TOP -> DisplayToolbar.Gravity.BOTTOM
108 val primaryTextColor = ContextCompat.getColor(
110 ThemeManager.resolveAttribute(R.attr.primaryText, container.context)
112 val secondaryTextColor = ContextCompat.getColor(
114 ThemeManager.resolveAttribute(R.attr.secondaryText, container.context)
116 val separatorColor = ContextCompat.getColor(
118 ThemeManager.resolveAttribute(R.attr.toolbarDivider, container.context)
121 display.urlFormatter = { url -> URLStringUtils.toDisplayUrl(url) }
123 display.colors = display.colors.copy(
124 text = primaryTextColor,
125 securityIconSecure = primaryTextColor,
126 securityIconInsecure = primaryTextColor,
127 menu = primaryTextColor,
128 hint = secondaryTextColor,
129 separator = separatorColor,
130 trackingProtection = primaryTextColor,
131 highlight = ContextCompat.getColor(
133 R.color.whats_new_notification_color
137 display.hint = context.getString(R.string.search_hint)
140 val menuToolbar: ToolbarMenu
141 if (isCustomTabSession) {
142 menuToolbar = CustomTabToolbarMenu(
144 store = components.core.store,
145 sessionId = customTabSession?.id,
146 shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
148 it.performHapticIfNeeded(view)
149 interactor.onBrowserToolbarMenuItemTapped(it)
153 menuToolbar = DefaultToolbarMenu(
155 store = components.core.store,
156 hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
158 it.performHapticIfNeeded(view)
159 interactor.onBrowserToolbarMenuItemTapped(it)
161 lifecycleOwner = lifecycleOwner,
162 bookmarksStorage = bookmarkStorage,
163 isPinningSupported = isPinningSupported
165 view.display.setMenuDismissAction {
166 view.invalidateActions()
170 toolbarIntegration = if (customTabSession != null) {
171 CustomTabToolbarIntegration(
176 isPrivate = customTabSession.content.private
179 DefaultToolbarIntegration(
183 ShippedDomainsProvider().also { it.initialize(this) },
184 components.core.historyStorage,
187 isPrivate = components.core.store.state.selectedTab?.content?.private ?: false,
188 interactor = interactor,
189 engine = components.core.engine
196 // expand only for normal tabs and custom tabs not for PWA or TWA
197 if (isPwaTabOrTwaTab) {
201 (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
202 (behavior as? BrowserToolbarBehavior)?.forceExpand(view)
207 // collapse only for normal tabs and custom tabs not for PWA or TWA. Mirror expand()
208 if (isPwaTabOrTwaTab) {
212 (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
213 (behavior as? BrowserToolbarBehavior)?.forceCollapse(view)
222 * Sets whether the toolbar will have a dynamic behavior (to be scrolled) or not.
224 * This will intrinsically check and disable the dynamic behavior if
225 * - this is disabled in app settings
226 * - toolbar is placed at the bottom and tab shows a PWA or TWA
228 * Also if the user has not explicitly set a toolbar position and has a screen reader enabled
229 * the toolbar will be placed at the top and in a fixed position.
231 * @param shouldDisableScroll force disable of the dynamic behavior irrespective of the intrinsic checks.
233 fun setToolbarBehavior(shouldDisableScroll: Boolean = false) {
234 when (settings.toolbarPosition) {
235 ToolbarPosition.BOTTOM -> {
236 if (settings.isDynamicToolbarEnabled && !isPwaTabOrTwaTab && !settings.shouldUseFixedTopToolbar) {
237 setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM)
239 expandToolbarAndMakeItFixed()
242 ToolbarPosition.TOP -> {
243 if (settings.shouldUseFixedTopToolbar ||
244 !settings.isDynamicToolbarEnabled ||
247 expandToolbarAndMakeItFixed()
249 setDynamicToolbarBehavior(MozacToolbarPosition.TOP)
256 internal fun expandToolbarAndMakeItFixed() {
258 (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
264 internal fun setDynamicToolbarBehavior(toolbarPosition: MozacToolbarPosition) {
265 (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
266 behavior = BrowserToolbarBehavior(view.context, null, toolbarPosition)
270 @Suppress("ComplexCondition")
271 private fun ToolbarMenu.Item.performHapticIfNeeded(view: View) {
272 if (this is ToolbarMenu.Item.Reload && this.bypassCache ||
273 this is ToolbarMenu.Item.Back && this.viewHistory ||
274 this is ToolbarMenu.Item.Forward && this.viewHistory
276 view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)