[fenix] For https://github.com/mozilla-mobile/fenix/issues/19876 - Part 1: Refactor...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / components / toolbar / BrowserToolbarView.kt
blob9e5e1cb5b82112943eec1ce0ea962d353b36ce6d
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
46 ) : LayoutContainer {
48     override val containerView: View?
49         get() = container
51     private val settings = container.context.settings()
53     @LayoutRes
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
57     }
59     private val layout = LayoutInflater.from(container.context)
60         .inflate(toolbarLayout, container, true)
62     @VisibleForTesting
63     internal var view: BrowserToolbar = layout
64         .findViewById(R.id.toolbar)
66     val toolbarIntegration: ToolbarIntegration
68     @VisibleForTesting
69     internal val isPwaTabOrTwaTab: Boolean
70         get() = customTabSession?.config?.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP ||
71                 customTabSession?.config?.externalAppType == ExternalAppType.TRUSTED_WEB_ACTIVITY
73     init {
74         val isCustomTabSession = customTabSession != null
76         view.display.setOnUrlLongClickListener {
77             ToolbarPopupWindow.show(
78                 WeakReference(view),
79                 customTabSession?.id,
80                 interactor::onBrowserToolbarPasteAndGo,
81                 interactor::onBrowserToolbarPaste
82             )
83             true
84         }
86         with(container.context) {
87             val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported()
89             view.apply {
90                 setToolbarBehavior()
92                 elevation = resources.getDimension(R.dimen.browser_fragment_toolbar_elevation)
94                 if (!isCustomTabSession) {
95                     display.setUrlBackground(getDrawable(R.drawable.search_url_background))
96                 }
98                 display.onUrlClicked = {
99                     interactor.onBrowserToolbarClicked()
100                     false
101                 }
103                 display.progressGravity = when (toolbarPosition) {
104                     ToolbarPosition.BOTTOM -> DisplayToolbar.Gravity.TOP
105                     ToolbarPosition.TOP -> DisplayToolbar.Gravity.BOTTOM
106                 }
108                 val primaryTextColor = ContextCompat.getColor(
109                     container.context,
110                     ThemeManager.resolveAttribute(R.attr.primaryText, container.context)
111                 )
112                 val secondaryTextColor = ContextCompat.getColor(
113                     container.context,
114                     ThemeManager.resolveAttribute(R.attr.secondaryText, container.context)
115                 )
116                 val separatorColor = ContextCompat.getColor(
117                     container.context,
118                     ThemeManager.resolveAttribute(R.attr.toolbarDivider, container.context)
119                 )
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(
132                         context,
133                         R.color.whats_new_notification_color
134                     )
135                 )
137                 display.hint = context.getString(R.string.search_hint)
138             }
140             val menuToolbar: ToolbarMenu
141             if (isCustomTabSession) {
142                 menuToolbar = CustomTabToolbarMenu(
143                     context = this,
144                     store = components.core.store,
145                     sessionId = customTabSession?.id,
146                     shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
147                     onItemTapped = {
148                         it.performHapticIfNeeded(view)
149                         interactor.onBrowserToolbarMenuItemTapped(it)
150                     }
151                 )
152             } else {
153                 menuToolbar = DefaultToolbarMenu(
154                     context = this,
155                     store = components.core.store,
156                     hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
157                     onItemTapped = {
158                         it.performHapticIfNeeded(view)
159                         interactor.onBrowserToolbarMenuItemTapped(it)
160                     },
161                     lifecycleOwner = lifecycleOwner,
162                     bookmarksStorage = bookmarkStorage,
163                     isPinningSupported = isPinningSupported
164                 )
165                 view.display.setMenuDismissAction {
166                     view.invalidateActions()
167                 }
168             }
170             toolbarIntegration = if (customTabSession != null) {
171                 CustomTabToolbarIntegration(
172                     this,
173                     view,
174                     menuToolbar,
175                     customTabSession.id,
176                     isPrivate = customTabSession.content.private
177                 )
178             } else {
179                 DefaultToolbarIntegration(
180                     this,
181                     view,
182                     menuToolbar,
183                     ShippedDomainsProvider().also { it.initialize(this) },
184                     components.core.historyStorage,
185                     lifecycleOwner,
186                     sessionId = null,
187                     isPrivate = components.core.store.state.selectedTab?.content?.private ?: false,
188                     interactor = interactor,
189                     engine = components.core.engine
190                 )
191             }
192         }
193     }
195     fun expand() {
196         // expand only for normal tabs and custom tabs not for PWA or TWA
197         if (isPwaTabOrTwaTab) {
198             return
199         }
201         (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
202             (behavior as? BrowserToolbarBehavior)?.forceExpand(view)
203         }
204     }
206     fun collapse() {
207         // collapse only for normal tabs and custom tabs not for PWA or TWA. Mirror expand()
208         if (isPwaTabOrTwaTab) {
209             return
210         }
212         (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
213             (behavior as? BrowserToolbarBehavior)?.forceCollapse(view)
214         }
215     }
217     fun dismissMenu() {
218         view.dismissMenu()
219     }
221     /**
222      * Sets whether the toolbar will have a dynamic behavior (to be scrolled) or not.
223      *
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
227      *
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.
230      *
231      * @param shouldDisableScroll force disable of the dynamic behavior irrespective of the intrinsic checks.
232      */
233     fun setToolbarBehavior(shouldDisableScroll: Boolean = false) {
234         when (settings.toolbarPosition) {
235             ToolbarPosition.BOTTOM -> {
236                 if (settings.isDynamicToolbarEnabled && !isPwaTabOrTwaTab && !settings.shouldUseFixedTopToolbar) {
237                     setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM)
238                 } else {
239                     expandToolbarAndMakeItFixed()
240                 }
241             }
242             ToolbarPosition.TOP -> {
243                 if (settings.shouldUseFixedTopToolbar ||
244                     !settings.isDynamicToolbarEnabled ||
245                     shouldDisableScroll
246                 ) {
247                     expandToolbarAndMakeItFixed()
248                 } else {
249                     setDynamicToolbarBehavior(MozacToolbarPosition.TOP)
250                 }
251             }
252         }
253     }
255     @VisibleForTesting
256     internal fun expandToolbarAndMakeItFixed() {
257         expand()
258         (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
259             behavior = null
260         }
261     }
263     @VisibleForTesting
264     internal fun setDynamicToolbarBehavior(toolbarPosition: MozacToolbarPosition) {
265         (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
266             behavior = BrowserToolbarBehavior(view.context, null, toolbarPosition)
267         }
268     }
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
275         ) {
276             view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
277         }
278     }