Commit e5e62fa7 authored by ekager's avatar ekager Committed by Emily Kager
Browse files

For #6846: Added quick actions for nav bar in home

parent 1d604d32
......@@ -4,19 +4,12 @@
package org.mozilla.fenix.components.toolbar
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.appbar.AppBarLayout
......@@ -24,28 +17,25 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.support.ktx.android.util.dpToFloat
import mozilla.components.support.utils.URLStringUtils
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.ToolbarPopupWindow
import java.lang.ref.WeakReference
interface BrowserToolbarViewInteractor {
fun onBrowserToolbarPaste(text: String)
......@@ -90,56 +80,12 @@ class BrowserToolbarView(
val isCustomTabSession = customTabSession != null
view.display.setOnUrlLongClickListener {
val clipboard = view.context.components.clipboardHandler
val customView = LayoutInflater.from(view.context)
.inflate(R.layout.browser_toolbar_popup_window, null)
val popupWindow = PopupWindow(
customView,
LinearLayout.LayoutParams.WRAP_CONTENT,
view.context.resources.getDimensionPixelSize(R.dimen.context_menu_height),
true
ToolbarPopupWindow.show(
WeakReference(view),
customTabSession,
interactor::onBrowserToolbarPasteAndGo,
interactor::onBrowserToolbarPaste
)
popupWindow.elevation =
view.context.resources.getDimension(R.dimen.mozac_browser_menu_elevation)
// This is a workaround for SDK<23 to allow popup dismissal on outside or back button press
// See: https://github.com/mozilla-mobile/fenix/issues/10027
popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
customView.paste.isVisible = !clipboard.text.isNullOrEmpty() && !isCustomTabSession
customView.paste_and_go.isVisible =
!clipboard.text.isNullOrEmpty() && !isCustomTabSession
customView.copy.setOnClickListener {
popupWindow.dismiss()
clipboard.text = getUrlForClipboard(it.context.components.core.store, customTabSession)
FenixSnackbar.make(
view = view,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)
.setText(view.context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar))
.show()
}
customView.paste.setOnClickListener {
popupWindow.dismiss()
interactor.onBrowserToolbarPaste(clipboard.text!!)
}
customView.paste_and_go.setOnClickListener {
popupWindow.dismiss()
interactor.onBrowserToolbarPasteAndGo(clipboard.text!!)
}
popupWindow.showAsDropDown(
view,
view.context.resources.getDimensionPixelSize(R.dimen.context_menu_x_offset),
0,
Gravity.START
)
true
}
......@@ -286,9 +232,9 @@ class BrowserToolbarView(
0
} else {
SCROLL_FLAG_SCROLL or
SCROLL_FLAG_ENTER_ALWAYS or
SCROLL_FLAG_SNAP or
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
SCROLL_FLAG_ENTER_ALWAYS or
SCROLL_FLAG_SNAP or
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
}
}
}
......@@ -297,15 +243,5 @@ class BrowserToolbarView(
companion object {
private const val TOOLBAR_ELEVATION = 16
@VisibleForTesting
internal fun getUrlForClipboard(store: BrowserStore, customTabSession: Session? = null): String? {
return if (customTabSession != null) {
customTabSession.url
} else {
val selectedTab = store.state.selectedTab
selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
}
}
}
}
......@@ -103,6 +103,7 @@ import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.FragmentPreDrawManager
import org.mozilla.fenix.utils.ToolbarPopupWindow
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
......@@ -338,6 +339,16 @@ class HomeFragment : Fragment() {
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
}
view.toolbar_wrapper.setOnLongClickListener {
ToolbarPopupWindow.show(
WeakReference(view),
handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
handlePaste = sessionControlInteractor::onPaste,
copyVisible = false
)
true
}
view.tab_button.setOnClickListener {
openTabTray()
}
......@@ -465,7 +476,11 @@ class HomeFragment : Fragment() {
isSelected,
engineSessionState = state
)
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null))
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(
null
)
)
},
operation = { },
anchorView = snackbarAnchorView
......
......@@ -15,6 +15,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.ext.restore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
......@@ -24,9 +25,13 @@ import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.TopSiteStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
......@@ -120,8 +125,21 @@ interface SessionControlController {
*/
fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
/**
* @see [TipInteractor.onCloseTip]
*/
fun handleCloseTip(tip: Tip)
/**
* @see [ToolbarInteractor.onPasteAndGo]
*/
fun handlePasteAndGo(clipboardText: String)
/**
* @see [ToolbarInteractor.onPaste]
*/
fun handlePaste(clipboardText: String)
/**
* @see [CollectionInteractor.onAddTabsToCollectionTapped]
*/
......@@ -347,4 +365,37 @@ class DefaultSessionControlController(
)
navController.nav(R.id.homeFragment, directions)
}
override fun handlePasteAndGo(clipboardText: String) {
activity.openToBrowserAndLoad(
searchTermOrURL = clipboardText,
newTab = true,
from = BrowserDirection.FromHome,
engine = activity.components.search.provider.getDefaultEngine(activity)
)
val event = if (clipboardText.isUrl()) {
Event.EnteredUrl(false)
} else {
val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
activity.settings().incrementActiveSearchCount()
searchAccessPoint.let { sap ->
MetricsUtils.createSearchEvent(
activity.components.search.provider.getDefaultEngine(activity),
activity,
sap
)
}
}
event?.let { activity.metrics.track(it) }
}
override fun handlePaste(clipboardText: String) {
val directions = HomeFragmentDirections.actionGlobalSearch(
sessionId = null,
pastedText = clipboardText
)
navController.nav(R.id.homeFragment, directions)
}
}
......@@ -95,6 +95,18 @@ interface CollectionInteractor {
fun onAddTabsToCollectionTapped()
}
interface ToolbarInteractor {
/**
* Navigates to browser with clipboard text.
*/
fun onPasteAndGo(clipboardText: String)
/**
* Navigates to search with clipboard text.
*/
fun onPaste(clipboardText: String)
}
/**
* Interface for onboarding related actions in the [SessionControlInteractor].
*/
......@@ -163,7 +175,8 @@ interface TopSiteInteractor {
@SuppressWarnings("TooManyFunctions")
class SessionControlInteractor(
private val controller: SessionControlController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, TabSessionInteractor {
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection)
}
......@@ -235,4 +248,12 @@ class SessionControlInteractor(
override fun onPrivateBrowsingLearnMoreClicked() {
controller.handlePrivateBrowsingLearnMoreClicked()
}
override fun onPasteAndGo(clipboardText: String) {
controller.handlePasteAndGo(clipboardText)
}
override fun onPaste(clipboardText: String) {
controller.handlePaste(clipboardText)
}
}
/* 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.utils
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.*
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.components
import java.lang.ref.WeakReference
object ToolbarPopupWindow {
fun show(
view: WeakReference<View>,
customTabSession: Session? = null,
handlePasteAndGo: (String) -> Unit,
handlePaste: (String) -> Unit,
copyVisible: Boolean = true
) {
val context = view.get()?.context ?: return
val isCustomTabSession = customTabSession != null
val clipboard = context.components.clipboardHandler
val customView = LayoutInflater.from(context)
.inflate(R.layout.browser_toolbar_popup_window, null)
val popupWindow = PopupWindow(
customView,
LinearLayout.LayoutParams.WRAP_CONTENT,
context.resources.getDimensionPixelSize(R.dimen.context_menu_height),
true
)
popupWindow.elevation =
context.resources.getDimension(R.dimen.mozac_browser_menu_elevation)
// This is a workaround for SDK<23 to allow popup dismissal on outside or back button press
// See: https://github.com/mozilla-mobile/fenix/issues/10027
popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
customView.copy.isVisible = copyVisible
customView.paste.isVisible = !clipboard.text.isNullOrEmpty() && !isCustomTabSession
customView.paste_and_go.isVisible =
!clipboard.text.isNullOrEmpty() && !isCustomTabSession
customView.copy.setOnClickListener {
popupWindow.dismiss()
clipboard.text = getUrlForClipboard(
it.context.components.core.store,
customTabSession
)
view.get()?.let {
FenixSnackbar.make(
view = it,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)
.setText(context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar))
.show()
}
}
customView.paste.setOnClickListener {
popupWindow.dismiss()
handlePaste(clipboard.text!!)
}
customView.paste_and_go.setOnClickListener {
popupWindow.dismiss()
handlePasteAndGo(clipboard.text!!)
}
view.get()?.let {
popupWindow.showAsDropDown(
it,
context.resources.getDimensionPixelSize(R.dimen.context_menu_x_offset),
0,
Gravity.START
)
}
}
@VisibleForTesting
internal fun getUrlForClipboard(
store: BrowserStore,
customTabSession: Session? = null
): String? {
return if (customTabSession != null) {
customTabSession.url
} else {
val selectedTab = store.state.selectedTab
selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
}
}
}
......@@ -12,8 +12,9 @@ import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
......@@ -24,13 +25,19 @@ import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.TopSiteStorage
import org.mozilla.fenix.components.metrics.Event
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.metrics
import org.mozilla.fenix.ext.searchEngineManager
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
import mozilla.components.feature.tab.collections.Tab as ComponentTab
@OptIn(ExperimentalCoroutinesApi::class)
......@@ -44,7 +51,6 @@ class DefaultSessionControlControllerTest {
private val navController: NavController = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true)
private val store: BrowserStore = mockk(relaxed = true)
private val engine: Engine = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val topSiteStorage: TopSiteStorage = mockk(relaxed = true)
......@@ -55,6 +61,10 @@ class DefaultSessionControlControllerTest {
private val showTabTray: () -> Unit = mockk(relaxed = true)
private val showDeleteCollectionPrompt: (tabCollection: TabCollection, title: String?, message: String) -> Unit =
mockk(relaxed = true)
private val searchEngine = mockk<SearchEngine>(relaxed = true)
private val searchEngineManager = mockk<SearchEngineManager>(relaxed = true)
private val settings: Settings = mockk(relaxed = true)
private val analytics: Analytics = mockk(relaxed = true)
private lateinit var controller: DefaultSessionControlController
......@@ -70,6 +80,13 @@ class DefaultSessionControlControllerTest {
every { navController.currentDestination } returns mockk {
every { id } returns R.id.homeFragment
}
every { activity.components.settings } returns settings
every { activity.components.search.provider.getDefaultEngine(activity) } returns searchEngine
every { activity.settings() } returns settings
every { activity.searchEngineManager } returns searchEngineManager
every { searchEngineManager.defaultSearchEngine } returns searchEngine
every { activity.components.analytics } returns analytics
every { analytics.metrics } returns metrics
controller = DefaultSessionControlController(
activity = activity,
......@@ -155,7 +172,10 @@ class DefaultSessionControlControllerTest {
}
val tab = mockk<ComponentTab>()
every {
activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, "Collection")
activity.resources.getString(
R.string.delete_tab_and_collection_dialog_title,
"Collection"
)
} returns "Delete Collection?"
every {
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
......@@ -252,11 +272,13 @@ class DefaultSessionControlControllerTest {
controller.handleSelectTopSite(topSiteUrl, true)
verify { metrics.track(Event.TopSiteOpenInNewTab) }
verify { metrics.track(Event.TopSiteOpenDefault) }
verify { tabsUseCases.addTab.invoke(
topSiteUrl,
selectTab = true,
startLoading = true
) }
verify {
tabsUseCases.addTab.invoke(
topSiteUrl,
selectTab = true,
startLoading = true
)
}
verify { activity.openToBrowser(BrowserDirection.FromHome) }
}
......@@ -266,11 +288,13 @@ class DefaultSessionControlControllerTest {
controller.handleSelectTopSite(topSiteUrl, false)
verify { metrics.track(Event.TopSiteOpenInNewTab) }
verify { tabsUseCases.addTab.invoke(
topSiteUrl,
selectTab = true,
startLoading = true
) }
verify {
tabsUseCases.addTab.invoke(
topSiteUrl,
selectTab = true,
startLoading = true
)
}
verify { activity.openToBrowser(BrowserDirection.FromHome) }
}
......@@ -341,4 +365,43 @@ class DefaultSessionControlControllerTest {
)
}
}
@Test
fun handlePasteAndGo() {
controller.handlePasteAndGo("text")
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = "text",
newTab = true,
from = BrowserDirection.FromHome,
engine = searchEngine
)
settings.incrementActiveSearchCount()
metrics.track(any<Event.PerformedSearch>())
}
controller.handlePasteAndGo("https://mozilla.org")
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = "https://mozilla.org",
newTab = true,
from = BrowserDirection.FromHome,
engine = searchEngine
)
metrics.track(any<Event.EnteredUrl>())
}
}
@Test
fun handlePaste() {
controller.handlePaste("text")
verify {
navController.navigate(
match<NavDirections> { it.actionId == R.id.action_global_search },
null
)
}
}
}
......@@ -98,4 +98,16 @@ class SessionControlInteractorTest {
interactor.onAddTabsToCollectionTapped()
verify { controller.handleCreateCollection() }
}
@Test
fun onPaste() {
interactor.onPaste("text")
verify { controller.handlePaste("text") }
}
@Test
fun onPasteAndGo() {
interactor.onPasteAndGo("text")
verify { controller.handlePasteAndGo("text") }
}
}