1
0
Fork 0

4281 remove qab (#6310)

* For #4281: small ToolbarMenu refactor

This makes it easier to see how items are ordered in the menuItems list

* For 4281: add QAB buttons to menu

* For 4281: removed menu back button per mocks

I double checked with UX, and we'll be relying on the hardware back button for its functionality

* For 4281: add content descriptions for bookmarking

* For 4281: updated BrowserToolbarController for new functionality

* For 4281: provided simple dependencies to browser controller

More complex changes will be in a following commit, for review readability

* For 4281: move toolbar controller dependencies up to BaseBrowserFragment

The functionality they control is being moved into the toolbar menu, which is shared by both normal tabs and custom ones

* For 4281: removed (now unused) code related to QAB

* For 4281: fix test compilation after QAB removal

Tests still need to be expanded to include added functionality

* For 4281: updated menu to show if url is bookmarked

This sloppy workaround is required because TwoStateButton requires that `isInPrimaryState` be a synchronous call, and checking whether or not the current site is bookmarked is quite slow (10-50 MS, in my tests).  After days of work and many attempted solutions, this was the least abhorrent among them.

https://github.com/mozilla-mobile/android-components/issues/4915 was opened against AC to evaluate potentially supporting async `isInPrimaryState` functions.
https://github.com/mozilla-mobile/fenix/issues/6370 was opened against Fenix to investigate the unexpectedly slow call to `BookmarkStorage`.

* For 4281: update reader mode switch

* For 4281: selectively show/hide menu items

* For 4281: add reader mode appearance

* For 4281: update bookmark button when it is clicked

* For 4281: removed unused QAB code

* For 4281: removed QAB robot, updated UI tests

* For 4281: removed QuickActionSheet metrics

Since this behavior now lives in the toolbar, it is tracked via Event.BrowserMenuItemTapped

* For 4281: fixed lint errors

* For 4281: add new strings for buttons added to menu

This is necessary because the location change (from QAB to toolbar menu) could affect the grammar in some languages

* For 4281: remove outdated TODOs

* For 4281: removed QAB container

* For 4281: removed back button reference from UI test

This button no longer exists

* For 4821: Fixes a visual defect (extra padding on top of toolbar)

* For 4281: update copy on reader mode

* For 4281: fixed review nits
master
Severin Rudie 2019-11-11 17:10:14 -08:00 committed by GitHub
parent 11ad1010a9
commit 6909a76bcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 516 additions and 3137 deletions

View File

@ -276,74 +276,6 @@ find_in_page:
- fenix-core@mozilla.com
expires: "2020-03-01"
quick_action_sheet:
opened:
type: event
description: >
A user opened the quick action sheet UI
bugs:
- https://github.com/mozilla-mobile/fenix/issue/1195
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"
closed:
type: event
description: >
A user closed the quick action sheet UI
bugs:
- https://github.com/mozilla-mobile/fenix/issue/1195
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"
share_tapped:
type: event
description: >
A user tapped the share button
bugs:
- https://github.com/mozilla-mobile/fenix/issue/1195
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"
bookmark_tapped:
type: event
description: >
A user tapped the bookmark button
bugs:
- https://github.com/mozilla-mobile/fenix/issue/1195
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"
download_tapped:
type: event
description: >
A user tapped the download button
bugs:
- https://github.com/mozilla-mobile/fenix/issue/1195
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"
open_app_tapped:
type: event
description: >
A user tapped the open in app button
bugs:
- https://github.com/mozilla-mobile/fenix/issue/1195
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/4629
notification_emails:
- fenix-core@mozilla.com
expires: "2020-03-01"
metrics:
default_browser:
type: boolean

View File

@ -18,7 +18,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.quickActionBar
/**
* Tests for verifying basic functionality of bookmarks
@ -66,12 +65,12 @@ class BookmarksTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openQuickActionBar {
}.openThreeDotMenu {
verifyAddBookmarkButton()
clickBookmarkButton()
clickAddBookmarkButton()
}
browserScreen {
}.openQuickActionBar {
}.openThreeDotMenu {
verifyEditBookmarkButton()
}
}
@ -80,11 +79,8 @@ class BookmarksTest {
fun addBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar {
browserScreen {
createBookmark(defaultWebPage.url)
}
navigationToolbar {
}.openThreeDotMenu {
}.openLibrary {
}.openBookmarks {
@ -109,9 +105,8 @@ class BookmarksTest {
fun editBookmarkViewTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) }
navigationToolbar {
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openLibrary {
}.openBookmarks {
@ -130,9 +125,8 @@ class BookmarksTest {
fun copyBookmarkURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) }
navigationToolbar {
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openLibrary {
}.openBookmarks {
@ -146,9 +140,8 @@ class BookmarksTest {
fun openBookmarkInNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) }
navigationToolbar {
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openLibrary {
}.openBookmarks {
@ -164,9 +157,8 @@ class BookmarksTest {
fun openBookmarkInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) }
navigationToolbar {
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openLibrary {
}.openBookmarks {
@ -182,9 +174,8 @@ class BookmarksTest {
fun deleteBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) }
navigationToolbar {
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openLibrary {
}.openBookmarks {

View File

@ -65,7 +65,6 @@ class NavigationToolbarTest {
navigationToolbar {
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyBackButton()
}.goBack {
verifyPageContent(defaultWebPage.content)
}

View File

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots
import android.net.Uri
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -67,6 +68,14 @@ class BrowserRobot {
TestAssetHelper.waitingTime)
}
fun createBookmark(url: Uri) {
navigationToolbar {
}.enterURLAndEnterToBrowser(url) {
}.openThreeDotMenu {
clickAddBookmarkButton()
}
}
class Transition {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private fun threeDotButton() = onView(
@ -106,16 +115,6 @@ class BrowserRobot {
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun openQuickActionBar(interact: QuickActionBarRobot.() -> Unit): QuickActionBarRobot.Transition {
mDevice.waitNotNull(Until.gone(By.res("org.mozilla.fenix.debug:id/quick_action_sheet")),
TestAssetHelper.waitingTime
)
quickActionBarHandle().click()
QuickActionBarRobot().interact()
return QuickActionBarRobot.Transition()
}
}
}
@ -133,4 +132,3 @@ fun dismissTrackingOnboarding() {
fun navURLBar() = onView(withId(R.id.mozac_browser_toolbar_url_view))
private fun tabsCounter() = onView(withId(R.id.counter_box))
private fun quickActionBarHandle() = onView(withId(R.id.quick_action_sheet_handle))

View File

@ -1,81 +0,0 @@
/* 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/. */
@file:Suppress("TooManyFunctions")
package org.mozilla.fenix.ui.robots
import android.net.Uri
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isSelected
/**
* Implementation of Robot Pattern for the quick action bar.
*/
class QuickActionBarRobot {
fun verifyAddBookmarkButton() = assertAddBookmarkButton()
fun verifyEditBookmarkButton() = assertEditBookmarkButton()
fun clickBookmarkButton() {
addBookmarkButton().click()
}
fun createBookmark(url: Uri) {
navigationToolbar {
}.enterURLAndEnterToBrowser(url) {
}.openQuickActionBar {
clickBookmarkButton()
}
}
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun closeQuickActionBar(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
quickActionBarHandle().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
fun quickActionBar(interact: QuickActionBarRobot.() -> Unit): QuickActionBarRobot.Transition {
QuickActionBarRobot().interact()
return QuickActionBarRobot.Transition()
}
private fun quickActionBarHandle() = onView(withId(R.id.quick_action_sheet_handle))
private fun addBookmarkButton() =
onView(allOf(withId(R.id.quick_action_bookmark), isSelected(false)))
private fun editBookmarkButton() =
onView(allOf(withId(R.id.quick_action_bookmark), isSelected(true)))
private fun snackBarText() = onView(withId(R.id.snackbar_text))
private fun assertAddBookmarkButton() = addBookmarkButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(withText("Bookmark")))
private fun assertBookmarkSavedSnackBarText() =
snackBarText().check(matches(withText("Bookmark saved!")))
private fun assertEditBookmarkButton() = editBookmarkButton().check(matches(withEffectiveVisibility(
ViewMatchers.Visibility.VISIBLE)))
.check(matches(withText("Edit Bookmark")))

View File

@ -11,11 +11,11 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasFocus
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
@ -38,7 +38,8 @@ class ThreeDotMenuMainRobot {
fun verifyHelpButton() = assertHelpButton()
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyForwardButton() = assertForwardButton()
fun verifyBackButton() = assertBackButton()
fun verifyAddBookmarkButton() = assertAddBookmarkButton()
fun verifyEditBookmarkButton() = assertEditBookmarkButton()
fun verifyRefreshButton() = assertRefreshButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyShareButton() = assertShareButton()
@ -55,6 +56,7 @@ class ThreeDotMenuMainRobot {
fun clickAddNewCollection() {
addNewCollectionButton().click()
}
fun clickAddBookmarkButton() = addBookmarkButton().click()
fun verifyCollectionNameTextField() = assertCollectionNameTextField()
fun verifyFindInPageButton() = assertFindInPageButton()
fun verifyShareScrim() = assertShareScrim()
@ -115,8 +117,10 @@ class ThreeDotMenuMainRobot {
}
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Back")), waitingTime)
backButton().click()
// Close three dot
mDevice.pressBack()
// Nav back to previous page
mDevice.pressBack()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -194,8 +198,12 @@ private fun forwardButton() = onView(ViewMatchers.withContentDescription("Forwar
private fun assertForwardButton() = forwardButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun backButton() = onView(ViewMatchers.withContentDescription("Back"))
private fun assertBackButton() = backButton()
private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark"))
private fun assertAddBookmarkButton() = addBookmarkButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark"))
private fun assertEditBookmarkButton() = editBookmarkButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh"))
@ -210,7 +218,7 @@ private fun shareTabButton() = onView(allOf(withText("Share tabs")))
private fun assertShareTabButton() = shareTabButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun shareButton() = onView(allOf(withText("Share")))
private fun shareButton() = onView(ViewMatchers.withContentDescription("Share"))
private fun assertShareButton() = shareButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.component_search.*
import kotlinx.android.synthetic.main.component_search.toolbar
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.Dispatchers.IO
@ -32,6 +32,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.accounts.FxaCapability
@ -44,6 +45,7 @@ import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SessionUseCases
@ -61,17 +63,17 @@ import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.BrowserFragmentState
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
import org.mozilla.fenix.components.toolbar.BrowserToolbarController
import org.mozilla.fenix.components.toolbar.BrowserInteractor
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.QuickActionSheetState
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.downloads.DownloadNotificationBottomSheetDialog
import org.mozilla.fenix.downloads.DownloadService
@ -84,7 +86,6 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.isInExperiment
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.ThemeManager
@ -95,10 +96,12 @@ import org.mozilla.fenix.theme.ThemeManager
*/
@Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Observer {
protected lateinit var browserStore: BrowserFragmentStore
protected lateinit var browserFragmentStore: BrowserFragmentStore
protected lateinit var browserInteractor: BrowserToolbarViewInteractor
protected lateinit var browserToolbarView: BrowserToolbarView
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
@ -131,20 +134,9 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
val activity = activity as HomeActivity
activity.themeManager.applyStatusBarTheme(activity)
val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect
browserStore = StoreProvider.get(this) {
browserFragmentStore = StoreProvider.get(this) {
BrowserFragmentStore(
BrowserFragmentState(
quickActionSheetState = QuickActionSheetState(
readable = getSessionById()?.readerable ?: false,
bookmarked = false,
readerActive = getSessionById()?.readerMode ?: false,
bounceNeeded = false,
isAppLink = getSessionById()?.let {
appLink.invoke(it.url).hasExternalApp()
} ?: false
)
)
BrowserFragmentState()
)
}
@ -173,10 +165,13 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
}
val browserToolbarController = DefaultBrowserToolbarController(
requireActivity(),
snackbar,
findNavController(),
(activity as HomeActivity).browsingModeManager,
store = browserFragmentStore,
activity = requireActivity(),
snackbar = snackbar,
navController = findNavController(),
readerModeController = DefaultReaderModeController(readerViewFeature),
browsingModeManager = (activity as HomeActivity).browsingModeManager,
sessionManager = requireComponents.core.sessionManager,
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
browserLayout = view.browserLayout,
engineView = engineView,
@ -193,15 +188,14 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK
},
bottomSheetBehavior = QuickActionSheetBehavior.from(nestedScrollQuickAction),
bookmarkTapped = { lifecycleScope.launch { bookmarkTapped(it) } },
scope = lifecycleScope,
tabCollectionStorage = requireComponents.core.tabCollectionStorage
)
browserInteractor =
createBrowserToolbarViewInteractor(
browserToolbarController,
customTabSessionId?.let { sessionManager.findSessionById(it) })
browserInteractor = BrowserInteractor(
browserToolbarController = browserToolbarController
)
browserToolbarView = BrowserToolbarView(
container = view.browserLayout,
@ -368,7 +362,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
activity?.enterToImmersiveMode()
toolbar.visibility = View.GONE
nestedScrollQuickAction.visibility = View.GONE
} else {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
activity?.exitImmersiveModeIfNeeded()
@ -376,7 +369,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
activity.themeManager.applyStatusBarTheme(activity)
}
toolbar.visibility = View.VISIBLE
nestedScrollQuickAction.visibility = View.VISIBLE
}
updateLayoutMargins(inFullScreen)
},
@ -557,11 +549,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
return false
}
protected abstract fun createBrowserToolbarViewInteractor(
browserToolbarController: BrowserToolbarController,
session: Session?
): BrowserToolbarViewInteractor
protected abstract fun navToQuickSettingsSheet(
session: Session,
sitePermissions: SitePermissions?
@ -644,6 +631,48 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
}
}
private suspend fun bookmarkTapped(session: Session) = withContext(IO) {
val bookmarksStorage = requireComponents.core.bookmarksStorage
val existing =
bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url }
if (existing != null) {
// Bookmark exists, go to edit fragment
withContext(Main) {
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid)
)
}
} else {
// Save bookmark, then go to edit fragment
val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
url = session.url,
title = session.title,
position = null
)
withContext(Main) {
requireComponents.analytics.metrics.track(Event.AddBookmark)
view?.let { view ->
FenixSnackbar.make(view, Snackbar.LENGTH_LONG)
.setAnchorView(browserToolbarView.view)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(
guid
)
)
}
.setText(getString(R.string.bookmark_saved_snackbar))
.show()
}
}
}
}
companion object {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1

View File

@ -20,19 +20,13 @@ import android.widget.RadioButton
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.android.synthetic.main.fragment_browser.view.browserLayout
import kotlinx.android.synthetic.main.fragment_browser.view.readerViewControlsBar
import kotlinx.android.synthetic.main.fragment_home.bottom_bar
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.onboarding_message
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature
@ -40,18 +34,12 @@ import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.jetbrains.anko.dimen
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.BrowserInteractor
import org.mozilla.fenix.components.toolbar.BrowserToolbarController
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.QuickActionSheetAction
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getDimenInDip
import org.mozilla.fenix.ext.increaseTapArea
@ -61,9 +49,6 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.quickactionsheet.DefaultQuickActionSheetController
import org.mozilla.fenix.quickactionsheet.QuickActionSheetSessionObserver
import org.mozilla.fenix.quickactionsheet.QuickActionSheetView
/**
* Fragment used for browsing the web within the main app.
@ -71,10 +56,6 @@ import org.mozilla.fenix.quickactionsheet.QuickActionSheetView
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
class BrowserFragment : BaseBrowserFragment(), BackHandler {
private lateinit var quickActionSheetView: QuickActionSheetView
private var quickActionSheetSessionObserver: QuickActionSheetSessionObserver? = null
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -114,15 +95,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
if (available) {
context.components.analytics.metrics.track(Event.ReaderModeAvailable)
}
browserStore.apply {
dispatch(QuickActionSheetAction.ReadableStateChange(available))
dispatch(
QuickActionSheetAction.ReaderActiveStateChange(
sessionManager.selectedSession?.readerMode ?: false
)
)
}
},
owner = this,
view = view
@ -134,8 +106,7 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
themeReaderViewControlsForPrivateMode(view.readerViewControlsBar)
}
consumeFrom(browserStore) {
quickActionSheetView.update(it)
consumeFrom(browserFragmentStore) {
browserToolbarView.update(it)
}
}
@ -144,13 +115,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
override fun onStart() {
super.onStart()
subscribeToTabCollections()
quickActionSheetSessionObserver = QuickActionSheetSessionObserver(
lifecycleScope,
requireComponents,
dispatch = { action -> browserStore.dispatch(action) }
).also { observer ->
getSessionById()?.register(observer, this, autoPause = true)
}
getSessionById()?.register(toolbarSessionObserver, this, autoPause = true)
}
@ -173,7 +137,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
* This fixes issue #5254.
*/
(activity as HomeActivity).updateThemeForSession(it)
quickActionSheetSessionObserver?.updateBookmarkState(it)
}
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
}
@ -182,34 +145,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
return readerViewFeature.onBackPressed() || super.onBackPressed()
}
override fun createBrowserToolbarViewInteractor(
browserToolbarController: BrowserToolbarController,
session: Session?
): BrowserToolbarViewInteractor {
val context = requireContext()
val interactor = BrowserInteractor(
context = context,
store = browserStore,
browserToolbarController = browserToolbarController,
quickActionSheetController = DefaultQuickActionSheetController(
context = context,
navController = findNavController(),
sessionManager = context.components.core.sessionManager,
appLinksUseCases = context.components.useCases.appLinksUseCases,
bookmarkTapped = {
lifecycleScope.launch { bookmarkTapped(it) }
}
),
readerModeController = DefaultReaderModeController(readerViewFeature),
currentSession = session
)
quickActionSheetView = QuickActionSheetView(view!!.nestedScrollQuickAction, interactor)
return interactor
}
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
@ -241,8 +176,8 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
}
override fun getEngineMargins(): Pair<Int, Int> {
val toolbarAndQASSize = resources.getDimensionPixelSize(R.dimen.toolbar_and_qab_height)
return 0 to toolbarAndQASSize
val toolbarSize = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
return 0 to toolbarSize
}
override fun getAppropriateLayoutGravity() = Gravity.BOTTOM
@ -277,51 +212,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
}
}
private suspend fun bookmarkTapped(session: Session) = withContext(IO) {
val bookmarksStorage = requireComponents.core.bookmarksStorage
val existing =
bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url }
if (existing != null) {
// Bookmark exists, go to edit fragment
withContext(Main) {
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid)
)
}
} else {
// Save bookmark, then go to edit fragment
val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
url = session.url,
title = session.title,
position = null
)
withContext(Main) {
browserStore.dispatch(
QuickActionSheetAction.BookmarkedStateChange(bookmarked = true)
)
requireComponents.analytics.metrics.track(Event.AddBookmark)
view?.let { view ->
FenixSnackbar.make(view, Snackbar.LENGTH_LONG)
.setAnchorView(browserToolbarView.view)
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(
guid
)
)
}
.setText(getString(R.string.bookmark_saved_snackbar))
.show()
}
}
}
}
private fun subscribeToTabCollections() {
requireComponents.core.tabCollectionStorage.getCollections().observe(this, Observer {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
@ -333,11 +223,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
})
}
override fun onSessionSelected(session: Session) {
super.onSessionSelected(session)
quickActionSheetSessionObserver?.updateBookmarkState(session)
}
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) {
showTabSavedToCollectionSnackbar()
@ -415,7 +300,7 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
view,
FenixSnackbarDelegate(
view,
nestedScrollQuickAction
bottom_bar
)
)

View File

@ -31,7 +31,6 @@ import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
import org.mozilla.fenix.GleanMetrics.QrScanner
import org.mozilla.fenix.GleanMetrics.QuickActionSheet
import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.GleanMetrics.SearchShortcuts
@ -138,24 +137,6 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.browserMenuAction.record(it) },
{ Events.browserMenuActionKeys.valueOf(it) }
)
is Event.QuickActionSheetOpened -> EventWrapper<NoExtraKeys>(
{ QuickActionSheet.opened.record(it) }
)
is Event.QuickActionSheetClosed -> EventWrapper<NoExtraKeys>(
{ QuickActionSheet.closed.record(it) }
)
is Event.QuickActionSheetShareTapped -> EventWrapper<NoExtraKeys>(
{ QuickActionSheet.shareTapped.record(it) }
)
is Event.QuickActionSheetBookmarkTapped -> EventWrapper<NoExtraKeys>(
{ QuickActionSheet.bookmarkTapped.record(it) }
)
is Event.QuickActionSheetDownloadTapped -> EventWrapper<NoExtraKeys>(
{ QuickActionSheet.downloadTapped.record(it) }
)
is Event.QuickActionSheetOpenInAppTapped -> EventWrapper<NoExtraKeys>(
{ QuickActionSheet.openAppTapped.record(it) }
)
is Event.OpenedBookmarkInNewTab -> EventWrapper<NoExtraKeys>(
{ BookmarksManagement.openInNewTab.record(it) }
)

View File

@ -50,12 +50,6 @@ sealed class Event {
object AddBookmarkFolder : Event()
object RemoveBookmarkFolder : Event()
object RemoveBookmarks : Event()
object QuickActionSheetOpened : Event()
object QuickActionSheetClosed : Event()
object QuickActionSheetShareTapped : Event()
object QuickActionSheetBookmarkTapped : Event()
object QuickActionSheetDownloadTapped : Event()
object QuickActionSheetOpenInAppTapped : Event()
object CustomTabsClosed : Event()
object CustomTabsActionTapped : Event()
object CustomTabsMenuOpened : Event()
@ -300,7 +294,8 @@ sealed class Event {
enum class Item {
SETTINGS, LIBRARY, HELP, DESKTOP_VIEW_ON, DESKTOP_VIEW_OFF, FIND_IN_PAGE, NEW_TAB,
NEW_PRIVATE_TAB, SHARE, REPORT_SITE_ISSUE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
SAVE_TO_COLLECTION, ADD_TO_HOMESCREEN, QUIT
SAVE_TO_COLLECTION, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON, READER_MODE_OFF, OPEN_IN_APP,
BOOKMARK, READER_MODE_APPEARANCE
}
override val extras: Map<Events.browserMenuActionKeys, String>?

View File

@ -2,51 +2,27 @@
* 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/. */
@file:Suppress("unused", "UNUSED_PARAMETER")
package org.mozilla.fenix.components.toolbar
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
// The state that used to live in this class was moved into another component in #4281. Keeping
// the shell of this file because we will need to expand it as we add additional features to
// the browser.
class BrowserFragmentStore(initialState: BrowserFragmentState) :
Store<BrowserFragmentState, BrowserFragmentAction>(initialState, ::browserStateReducer)
/**
* The state for the Browser Screen
* @property quickActionSheetState: state of the quick action sheet
*/
data class BrowserFragmentState(
val quickActionSheetState: QuickActionSheetState
) : State
/**
* The state for the QuickActionSheet
* @property readable Whether or not the current session can display a reader view
* @property bookmarked Whether or not the current session is already bookmarked
* @property readerActive Whether or not the current session is in reader mode
* @property bounceNeeded Whether or not the quick action sheet should bounce
*/
data class QuickActionSheetState(
val readable: Boolean,
val bookmarked: Boolean,
val readerActive: Boolean,
val bounceNeeded: Boolean,
val isAppLink: Boolean
) : State
class BrowserFragmentState : State
sealed class BrowserFragmentAction : Action
/**
* Actions to dispatch through the [QuickActionSheetStore] to modify [QuickActionSheetState] through the reducer.
*/
sealed class QuickActionSheetAction : BrowserFragmentAction() {
data class BookmarkedStateChange(val bookmarked: Boolean) : QuickActionSheetAction()
data class ReadableStateChange(val readable: Boolean) : QuickActionSheetAction()
data class ReaderActiveStateChange(val active: Boolean) : QuickActionSheetAction()
data class AppLinkStateChange(val isAppLink: Boolean) : QuickActionSheetAction()
object BounceNeededChange : QuickActionSheetAction()
}
/**
* Reducers for [BrowserFragmentStore].
*
@ -57,30 +33,7 @@ private fun browserStateReducer(
state: BrowserFragmentState,
action: BrowserFragmentAction
): BrowserFragmentState {
return when (action) {
is QuickActionSheetAction -> {
QuickActionSheetStateReducer.reduce(state, action)
}
}
}
/**
* Reduces [QuickActionSheetAction]s to update [BrowserFragmentState].
*/
internal object QuickActionSheetStateReducer {
fun reduce(state: BrowserFragmentState, action: QuickActionSheetAction): BrowserFragmentState {
return when (action) {
is QuickActionSheetAction.BookmarkedStateChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(bookmarked = action.bookmarked))
is QuickActionSheetAction.ReadableStateChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(readable = action.readable))
is QuickActionSheetAction.ReaderActiveStateChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(readerActive = action.active))
is QuickActionSheetAction.BounceNeededChange ->
state.copy(quickActionSheetState = state.quickActionSheetState.copy(bounceNeeded = true))
is QuickActionSheetAction.AppLinkStateChange -> {
state.copy(quickActionSheetState = state.quickActionSheetState.copy(isAppLink = action.isAppLink))
}
}
return when {
else -> BrowserFragmentState()
}
}

View File

@ -4,16 +4,7 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import mozilla.components.browser.session.Session
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.quickactionsheet.QuickActionSheetController
import org.mozilla.fenix.quickactionsheet.QuickActionSheetViewInteractor
open class BrowserToolbarInteractor(
open class BrowserInteractor(
private val browserToolbarController: BrowserToolbarController
) : BrowserToolbarViewInteractor {
@ -37,56 +28,3 @@ open class BrowserToolbarInteractor(
browserToolbarController.handleToolbarItemInteraction(item)
}
}
class BrowserInteractor(
private val context: Context,
private val store: BrowserFragmentStore,
browserToolbarController: BrowserToolbarController,
private val quickActionSheetController: QuickActionSheetController,
private val readerModeController: ReaderModeController,
private val currentSession: Session?
) : BrowserToolbarInteractor(browserToolbarController), QuickActionSheetViewInteractor {
override fun onQuickActionSheetOpened() {
context.metrics.track(Event.QuickActionSheetOpened)
}
override fun onQuickActionSheetClosed() {
context.metrics.track(Event.QuickActionSheetClosed)
}
override fun onQuickActionSheetSharePressed() {
quickActionSheetController.handleShare()
}
override fun onQuickActionSheetDownloadPressed() {
quickActionSheetController.handleDownload()
}
override fun onQuickActionSheetBookmarkPressed() {
quickActionSheetController.handleBookmark()
}
override fun onQuickActionSheetReadPressed() {
val enabled =
currentSession?.readerMode ?: context.components.core.sessionManager.selectedSession?.readerMode ?: false
if (enabled) {
context.metrics.track(Event.QuickActionSheetClosed)
readerModeController.hideReaderView()
} else {
context.metrics.track(Event.QuickActionSheetOpened)
readerModeController.showReaderView()
}
store.dispatch(QuickActionSheetAction.ReaderActiveStateChange(!enabled))
}
override fun onQuickActionSheetOpenLinkPressed() {
quickActionSheetController.handleOpenLink()
}
override fun onQuickActionSheetAppearancePressed() {
context.metrics.track(Event.ReaderModeAppearanceOpened)
readerModeController.showControls()
}
}

View File

@ -11,18 +11,17 @@ import android.graphics.drawable.ColorDrawable
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import androidx.navigation.fragment.FragmentNavigator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.NavGraphDirections
@ -31,14 +30,14 @@ import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
/**
@ -54,10 +53,13 @@ interface BrowserToolbarController {
@Suppress("LargeClass")
class DefaultBrowserToolbarController(
private val store: BrowserFragmentStore,
private val activity: Activity,
private val snackbar: FenixSnackbar?,
private val navController: NavController,
private val readerModeController: ReaderModeController,
private val browsingModeManager: BrowsingModeManager,
private val sessionManager: SessionManager,
private val findInPageLauncher: () -> Unit,
private val browserLayout: ViewGroup,
private val engineView: EngineView,
@ -66,7 +68,7 @@ class DefaultBrowserToolbarController(
private val customTabSession: Session?,
private val getSupportUrl: () -> String,
private val openInFenixIntent: Intent,
private val bottomSheetBehavior: QuickActionSheetBehavior<NestedScrollView>,
private val bookmarkTapped: (Session) -> Unit,
private val scope: LifecycleCoroutineScope,
private val tabCollectionStorage: TabCollectionStorage
) : BrowserToolbarController {
@ -163,7 +165,6 @@ class DefaultBrowserToolbarController(
browsingModeManager.mode = BrowsingMode.Private
}
ToolbarMenu.Item.FindInPage -> {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
findInPageLauncher()
activity.components.analytics.metrics.track(Event.FindInPageOpened)
}
@ -210,6 +211,35 @@ class DefaultBrowserToolbarController(
activity.finish()
}
ToolbarMenu.Item.Quit -> deleteAndQuit(activity, scope, snackbar)
is ToolbarMenu.Item.ReaderMode -> {
val enabled = currentSession?.readerMode
?: activity.components.core.sessionManager.selectedSession?.readerMode
?: false
if (enabled) {
readerModeController.hideReaderView()
} else {
readerModeController.showReaderView()
}
}
ToolbarMenu.Item.ReaderModeAppearance -> {
readerModeController.showControls()
}
ToolbarMenu.Item.OpenInApp -> {
val appLinksUseCases =
activity.components.useCases.appLinksUseCases
val getRedirect = appLinksUseCases.appLinkRedirect
sessionManager.selectedSession?.let {
val redirect = getRedirect.invoke(it.url)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect)
}
}
ToolbarMenu.Item.Bookmark -> {
sessionManager.selectedSession?.let {
bookmarkTapped(it)
}
}
}
}
@ -260,6 +290,16 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION
ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT
is ToolbarMenu.Item.ReaderMode ->
if (item.isChecked) {
Event.BrowserMenuItemTapped.Item.READER_MODE_ON
} else {
Event.BrowserMenuItemTapped.Item.READER_MODE_OFF
}
ToolbarMenu.Item.ReaderModeAppearance ->
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
}
activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

View File

@ -10,11 +10,14 @@ import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
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.browser_toolbar_popup_window.view.copy
import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.paste
import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.paste_and_go
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.toolbar.BrowserToolbar
@ -24,6 +27,7 @@ import org.jetbrains.anko.dimen
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.theme.ThemeManager
@ -168,12 +172,18 @@ class BrowserToolbarView(
)
} else {
DefaultToolbarMenu(
this,
context = this,
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
requestDesktopStateProvider = {
sessionManager.selectedSession?.desktopMode ?: false
},
onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) }
readerModeStateProvider = {
sessionManager.selectedSession?.readerMode ?: false
},
onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) },
lifecycleOwner = container.context as AppCompatActivity,
sessionManager = sessionManager,
bookmarksStorage = bookmarkStorage
)
}

View File

@ -5,12 +5,19 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.storage.BookmarksStorage
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -19,35 +26,24 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.Settings
@Suppress("LargeClass") // While large, most of the class is very simple
class DefaultToolbarMenu(
private val context: Context,
private val hasAccountProblem: Boolean = false,
private val requestDesktopStateProvider: () -> Boolean = { false },
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {}
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {},
private val lifecycleOwner: LifecycleOwner,
private val bookmarksStorage: BookmarksStorage,
readerModeStateProvider: () -> Boolean = { false },
sessionManager: SessionManager
) : ToolbarMenu {
private var currentUrlIsBookmarked = false
private var isBookmarkedJob: Job? = null
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems, endOfMenuAlwaysVisible = true) }
override val menuToolbar by lazy {
val back = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = ThemeManager.resolveAttribute(
R.attr.primaryText,
context
),
isInPrimaryState = {
context.components.core.sessionManager.selectedSession?.canGoBack ?: true
},
secondaryImageTintResource = ThemeManager.resolveAttribute(
R.attr.disabled,
context
),
disableInSecondaryState = true
) {
onItemTapped.invoke(ToolbarMenu.Item.Back)
}
val forward = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
@ -93,143 +89,227 @@ class DefaultToolbarMenu(
}
}
BrowserMenuItemToolbar(listOf(back, forward, refresh))
}
private val menuItems by lazy {
val items = mutableListOf(
BrowserMenuImageText(
context.getString(R.string.browser_menu_help),
R.drawable.ic_help,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.Help)
},
BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
imageResource = R.drawable.ic_settings,
iconTintColorResource = if (hasAccountProblem)
R.color.sync_error_text_color else
ThemeManager.resolveAttribute(R.attr.primaryText, context),
textColorResource = if (hasAccountProblem)
R.color.sync_error_text_color else
ThemeManager.resolveAttribute(R.attr.primaryText, context),
highlight = if (hasAccountProblem) {
BrowserMenuHighlightableItem.Highlight(
endImageResource = R.drawable.ic_alert,
backgroundResource = R.drawable.sync_error_background_with_ripple,
colorResource = R.color.sync_error_background_color
)
} else null
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_your_library),
R.drawable.ic_library,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.Library)
},
BrowserMenuImageSwitch(
R.drawable.ic_desktop,
context.getString(R.string.browser_menu_desktop_site),
requestDesktopStateProvider
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_add_to_homescreen),
R.drawable.ic_add_to_homescreen,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
}.apply {
visible = ::shouldShowAddToHomescreen
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_find_in_page),
R.drawable.mozac_ic_search,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_private_tab),
R.drawable.ic_private_browsing,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.NewPrivateTab)
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_new_tab),
R.drawable.ic_new,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.NewTab)
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_share),
R.drawable.mozac_ic_share,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
val share = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.mozac_ic_share,
contentDescription = context.getString(R.string.browser_menu_share),
iconTintColorResource = primaryTextColor(),
listener = {
onItemTapped.invoke(ToolbarMenu.Item.Share)
},
BrowserMenuImageText(
context.getString(R.string.browser_menu_report_issue),
R.drawable.ic_report_issues,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.ReportIssue)
}
)
if ((context.asActivity() as? HomeActivity)?.browsingModeManager?.mode == BrowsingMode.Normal) {
items.add(
BrowserMenuImageText(
context.getString(R.string.browser_menu_save_to_collection),
R.drawable.ic_tab_collection,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
}
)
registerForIsBookmarkedUpdates(sessionManager)
val bookmark = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = R.drawable.ic_bookmark_filled,
primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark),
primaryImageTintResource = ThemeManager.resolveAttribute(
R.attr.primaryText,
context
),
// TwoStateButton.isInPrimaryState must be synchronous, and checking bookmark state is
// relatively slow. The best we can do here is periodically compute and cache a new "is
// bookmarked" state, and use that whenever the menu has been opened.
isInPrimaryState = { currentUrlIsBookmarked },
secondaryImageResource = R.drawable.ic_bookmark_outline,
secondaryContentDescription = context.getString(R.string.browser_menu_bookmark),
secondaryImageTintResource = ThemeManager.resolveAttribute(
R.attr.primaryText,
context
),
disableInSecondaryState = false
) {
if (!currentUrlIsBookmarked) currentUrlIsBookmarked = true
onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
}
if (Settings.getInstance(context).shouldDeleteBrowsingDataOnQuit) {
items.add(
BrowserMenuImageText(
context.getString(R.string.delete_browsing_data_on_quit_action),
R.drawable.ic_exit,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.Quit)
}
)
BrowserMenuItemToolbar(listOf(forward, bookmark, share, refresh))
}
private val menuItems by lazy {
// Predicates that are called once, during screen init
val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity)
?.browsingModeManager?.mode == BrowsingMode.Normal
val shouldDeleteDataOnQuit = Settings.getInstance(context)
.shouldDeleteBrowsingDataOnQuit
// Predicates that need to be repeatedly called as the session changes
fun shouldShowAddToHomescreen(): Boolean {
return context.components.useCases.webAppUseCases.isPinningSupported() &&
context.components.core.sessionManager.selectedSession != null
}
fun shouldShowReaderMode(): Boolean = sessionManager.selectedSession?.readerable ?: false
fun shouldShowOpenInApp(): Boolean = sessionManager.selectedSession?.let { session ->
val appLink =
context.components.useCases.appLinksUseCases.appLinkRedirect
appLink(session.url).hasExternalApp()
} ?: false
fun shouldShowReaderAppearance(): Boolean =
sessionManager.selectedSession?.readerMode ?: false
items.add(
BrowserMenuDivider()
)
items.add(
listOfNotNull(
help,
settings,
library,
desktopMode,
addToHomescreen.apply { visible = ::shouldShowAddToHomescreen },
findInPage,
privateTab,
newTab,
reportIssue,
if (shouldShowSaveToCollection) saveToCollection else null,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
readerMode.apply { visible = ::shouldShowReaderMode },
readerAppearance.apply { visible = ::shouldShowReaderAppearance },
openInApp.apply { visible = ::shouldShowOpenInApp },
BrowserMenuDivider(),
menuToolbar
)
items
}
private fun shouldShowAddToHomescreen(): Boolean {
return context.components.useCases.webAppUseCases.isPinningSupported() &&
context.components.core.sessionManager.selectedSession != null
private val help = BrowserMenuImageText(
context.getString(R.string.browser_menu_help),
R.drawable.ic_help,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Help)
}
private val settings = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
imageResource = R.drawable.ic_settings,
iconTintColorResource = if (hasAccountProblem)
R.color.sync_error_text_color else
primaryTextColor(),
textColorResource = if (hasAccountProblem)
R.color.sync_error_text_color else
primaryTextColor(),
highlight = if (hasAccountProblem) {
BrowserMenuHighlightableItem.Highlight(
endImageResource = R.drawable.ic_alert,
backgroundResource = R.drawable.sync_error_background_with_ripple,
colorResource = R.color.sync_error_background_color
)
} else null
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
private val library = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_your_library),
imageResource = R.drawable.ic_library,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Library)
}
private val desktopMode = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
initialState = requestDesktopStateProvider
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
private val addToHomescreen = 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)
}
private val findInPage = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
private val privateTab = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_private_tab),
imageResource = R.drawable.ic_private_browsing,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.NewPrivateTab)
}
private val newTab = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_new_tab),
imageResource = R.drawable.ic_new,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.NewTab)
}
private val reportIssue = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_report_issue),
imageResource = R.drawable.ic_report_issues,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.ReportIssue)
}
private val saveToCollection = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection),
imageResource = R.drawable.ic_tab_collection,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
}
private 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)
}
private val readerMode = BrowserMenuImageSwitch(
label = context.getString(R.string.browser_menu_read),
imageResource = R.drawable.ic_readermode,
initialState = readerModeStateProvider
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.ReaderMode(checked))
}
private val readerAppearance = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_read_appearance),
imageResource = R.drawable.ic_readermode_appearance,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance)
}
private val openInApp = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_open_app_link),
imageResource = R.drawable.ic_app_links,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
private fun registerForIsBookmarkedUpdates(sessionManager: SessionManager) {
val observer = object : Session.Observer {
override fun onUrlChanged(session: Session, url: String) {
currentUrlIsBookmarked = false
updateCurrentUrlIsBookmarked(url)
}
}
sessionManager.selectedSession?.url?.let { updateCurrentUrlIsBookmarked(it) }
sessionManager.selectedSession?.register(observer, lifecycleOwner)
}
private fun updateCurrentUrlIsBookmarked(newUrl: String) {
isBookmarkedJob?.cancel()
isBookmarkedJob = lifecycleOwner.lifecycleScope.launch {
currentUrlIsBookmarked = bookmarksStorage
.getBookmarksWithUrl(newUrl)
.any { it.url == newUrl }
}
}
}

View File

@ -26,6 +26,10 @@ interface ToolbarMenu {
object SaveToCollection : Item()
object AddToHomeScreen : Item()
object Quit : Item()
data class ReaderMode(val isChecked: Boolean) : Item()
object OpenInApp : Item()
object Bookmark : Item()
object ReaderModeAppearance : Item()
}
val menuBuilder: BrowserMenuBuilder

View File

@ -99,60 +99,58 @@ class CustomTabToolbarMenu(
private val menuItems by lazy {
listOf(
menuToolbar,
BrowserMenuDivider(),
BrowserMenuImageText(
context.getString(R.string.browser_menu_share),
R.drawable.mozac_ic_share,
textColorResource = ThemeManager.resolveAttribute(
R.attr.primaryText,
context
),
iconTintColorResource = ThemeManager.resolveAttribute(
R.attr.primaryText,
context
)
) {
onItemTapped.invoke(ToolbarMenu.Item.Share)
},
BrowserMenuSwitch(context.getString(R.string.browser_menu_desktop_site),
{ session?.desktopMode ?: false }, { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}),
BrowserMenuImageText(
context.getString(R.string.browser_menu_find_in_page),
R.drawable.mozac_ic_search,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
},
SimpleBrowserMenuItem(
{
val appName = context.getString(R.string.app_name)
context.getString(R.string.browser_menu_open_in_fenix, appName)
}(),
textColorResource = ThemeManager.resolveAttribute(
R.attr.primaryText,
context
)
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInFenix)
},
share,
desktopMode,
findInPage,
openInFenix,
BrowserMenuDivider(),
SimpleBrowserMenuItem(
{
val appName = context.getString(R.string.app_name)
context.getString(R.string.browser_menu_powered_by2, appName).toUpperCase()
}(),
ToolbarMenu.CAPTION_TEXT_SIZE,
ThemeManager.resolveAttribute(R.attr.primaryText, context)
)
poweredBy
)
}
private val share = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_share),
imageResource = R.drawable.mozac_ic_share,
textColorResource = primaryTextColor(),
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Share)
}
private val desktopMode = BrowserMenuSwitch(
label = context.getString(R.string.browser_menu_desktop_site),
initialState = { session?.desktopMode ?: false }, listener = { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
})
private val findInPage = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
private val openInFenix = SimpleBrowserMenuItem(
label = {
val appName = context.getString(R.string.app_name)
context.getString(R.string.browser_menu_open_in_fenix, appName)
}(),
textSize = ToolbarMenu.CAPTION_TEXT_SIZE,
textColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInFenix)
}
private val poweredBy = SimpleBrowserMenuItem(
label = {
val appName = context.getString(R.string.app_name)
context.getString(R.string.browser_menu_powered_by, appName).toUpperCase()
}(),
textSize = ToolbarMenu.CAPTION_TEXT_SIZE,
textColorResource = primaryTextColor()
)
private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
}

View File

@ -9,7 +9,6 @@ import android.view.Gravity
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import mozilla.components.browser.session.SessionManager
@ -28,7 +27,6 @@ class CustomTabsIntegration(
toolbar: BrowserToolbar,
sessionId: String,
activity: Activity,
quickActionbar: NestedScrollView,
engineLayout: View,
onItemTapped: (ToolbarMenu.Item) -> Unit = {}
) : LifecycleAwareFeature, BackHandler {
@ -52,9 +50,6 @@ class CustomTabsIntegration(
}
}
// Hide the Quick Action Bar.
quickActionbar.visibility = View.GONE
val task = LottieCompositionFactory
.fromRawRes(
activity,

View File

@ -32,8 +32,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.toolbar.BrowserToolbarController
import org.mozilla.fenix.components.toolbar.BrowserToolbarInteractor
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
@ -63,12 +61,11 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler {
customTabSessionId?.let { customTabSessionId ->
customTabsIntegration.set(
feature = CustomTabsIntegration(
requireComponents.core.sessionManager,
toolbar,
customTabSessionId,
activity,
view.nestedScrollQuickAction,
view.swipeRefresh,
sessionManager = requireComponents.core.sessionManager,
toolbar = toolbar,
sessionId = customTabSessionId,
activity = activity,
engineLayout = view.swipeRefresh,
onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }
),
owner = this,
@ -123,7 +120,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler {
}
}
consumeFrom(browserStore) {
consumeFrom(browserFragmentStore) {
browserToolbarView.update(it)
}
@ -146,11 +143,6 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler {
return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
}
override fun createBrowserToolbarViewInteractor(
browserToolbarController: BrowserToolbarController,
session: Session?
) = BrowserToolbarInteractor(browserToolbarController)
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
val directions = ExternalAppBrowserFragmentDirections
.actionExternalAppBrowserFragmentToQuickSettingsSheetDialogFragment(

View File

@ -1,128 +0,0 @@
/* 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.quickactionsheet
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.NestedScrollView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
const val POSITION_SNAP_BUFFER = 1f
class QuickActionSheet @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
defStyleRes: Int = 0
) : ConstraintLayout(context, attrs, defStyle, defStyleRes) {
private val scope = MainScope()
private lateinit var quickActionSheetBehavior: QuickActionSheetBehavior<NestedScrollView>
init {
inflate(context, R.layout.layout_quick_action_sheet, this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
quickActionSheetBehavior =
QuickActionSheetBehavior.from(quick_action_sheet.parent as NestedScrollView)
quickActionSheetBehavior.isHideable = false
setupHandle()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel()
}
private fun setupHandle() {
quick_action_sheet_handle.setOnClickListener {
quickActionSheetBehavior.state = when (quickActionSheetBehavior.state) {
BottomSheetBehavior.STATE_EXPANDED -> BottomSheetBehavior.STATE_COLLAPSED
else -> BottomSheetBehavior.STATE_EXPANDED
}
}
quick_action_sheet_handle.setAccessibilityDelegate(HandleAccessibilityDelegate(quickActionSheetBehavior))
}
fun bounceSheet() {
context.settings().incrementAutomaticBounceQuickActionSheetCount()
scope.launch(Dispatchers.Main) {
delay(BOUNCE_ANIMATION_DELAY_LENGTH)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
delay(BOUNCE_ANIMATION_PAUSE_LENGTH)
quickActionSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
class HandleAccessibilityDelegate(
private val quickActionSheetBehavior: QuickActionSheetBehavior<NestedScrollView>
) : View.AccessibilityDelegate() {
private var finalState = BottomSheetBehavior.STATE_COLLAPSED
get() = when (quickActionSheetBehavior.state) {
BottomSheetBehavior.STATE_EXPANDED,
BottomSheetBehavior.STATE_HIDDEN,
BottomSheetBehavior.STATE_COLLAPSED -> {
quickActionSheetBehavior.state
}
else -> field
}
set(value) {
field = value
quickActionSheetBehavior.state = value
}
override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
finalState = when (action) {
AccessibilityNodeInfo.ACTION_CLICK ->
when (quickActionSheetBehavior.state) {
BottomSheetBehavior.STATE_EXPANDED -> BottomSheetBehavior.STATE_COLLAPSED
else -> BottomSheetBehavior.STATE_EXPANDED
}
AccessibilityNodeInfo.ACTION_COLLAPSE ->
BottomSheetBehavior.STATE_COLLAPSED
AccessibilityNodeInfo.ACTION_EXPAND ->
BottomSheetBehavior.STATE_EXPANDED
else -> return super.performAccessibilityAction(host, action, args)
}
host?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED)
return true
}
override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.addAction(
when (finalState) {
BottomSheetBehavior.STATE_COLLAPSED,
BottomSheetBehavior.STATE_HIDDEN -> AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND
else -> AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE
}
)
}
}
companion object {
const val BOUNCE_ANIMATION_DELAY_LENGTH = 1000L
const val BOUNCE_ANIMATION_PAUSE_LENGTH = 2000L
}
}

View File

@ -1,68 +0,0 @@
/* 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.quickactionsheet
import android.content.Context
import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.app.links.AppLinksUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.ItsNotBrokenSnack
/**
* An interface that handles the view manipulation of the QuickActionSheet, triggered by the Interactor
*/
interface QuickActionSheetController {
fun handleShare()
fun handleDownload()
fun handleBookmark()
fun handleOpenLink()
}
class DefaultQuickActionSheetController(
private val context: Context,
private val navController: NavController,
private val sessionManager: SessionManager,
private val appLinksUseCases: AppLinksUseCases,
private val bookmarkTapped: (Session) -> Unit
) : QuickActionSheetController {
override fun handleShare() {
context.metrics.track(Event.QuickActionSheetShareTapped)
sessionManager.selectedSession?.url.let {
val directions = BrowserFragmentDirections.actionBrowserFragmentToShareFragment(it)
navController.nav(R.id.browserFragment, directions)
}
}
override fun handleDownload() {
context.metrics.track(Event.QuickActionSheetDownloadTapped)
ItsNotBrokenSnack(context).showSnackbar(issueNumber = "348")
}
override fun handleBookmark() {
context.metrics.track(Event.QuickActionSheetBookmarkTapped)
sessionManager.selectedSession?.let {
bookmarkTapped(it)
}
}
override fun handleOpenLink() {
context.metrics.track(Event.QuickActionSheetOpenInAppTapped)
val getRedirect = appLinksUseCases.appLinkRedirect
sessionManager.selectedSession?.let {
val redirect = getRedirect.invoke(it.url)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect)
}
}
}

View File

@ -1,68 +0,0 @@
/* 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.quickactionsheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.session.Session
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.toolbar.QuickActionSheetAction
import java.net.MalformedURLException
import java.net.URL
class QuickActionSheetSessionObserver(
private val parentScope: CoroutineScope,
private val components: Components,
private val dispatch: (QuickActionSheetAction) -> Unit
) : Session.Observer {
private var findBookmarkJob: Job? = null
override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (!loading) {
updateBookmarkState(session)
dispatch(QuickActionSheetAction.BounceNeededChange)
}
}
override fun onUrlChanged(session: Session, url: String) {
updateBookmarkState(session)
updateAppLinksState(session)
}
/**
* Launches job to update the bookmark button on the quick action sheet.
*/
fun updateBookmarkState(session: Session) {
findBookmarkJob?.cancel()
findBookmarkJob = parentScope.launch(Main) {
val found = findBookmarkedURL(session)
dispatch(QuickActionSheetAction.BookmarkedStateChange(found))
}
}
/**
* Updates the app link button on the quick action sheet.
*/
private fun updateAppLinksState(session: Session) {
val url = session.url
val appLinks = components.useCases.appLinksUseCases.appLinkRedirect
dispatch(QuickActionSheetAction.AppLinkStateChange(appLinks(url).hasExternalApp()))
}
private suspend fun findBookmarkedURL(session: Session): Boolean = withContext(IO) {
try {
val url = URL(session.url).toString()
val list = components.core.bookmarksStorage.getBookmarksWithUrl(url)
list.isNotEmpty() && list[0].url == url
} catch (e: MalformedURLException) {
false
}
}
}

View File

@ -1,159 +0,0 @@
/* 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.quickactionsheet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.layout_quick_action_sheet.*
import kotlinx.android.synthetic.main.layout_quick_action_sheet.view.*
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.components.toolbar.BrowserFragmentState
interface QuickActionSheetViewInteractor {
fun onQuickActionSheetOpened()
fun onQuickActionSheetClosed()
fun onQuickActionSheetSharePressed()
fun onQuickActionSheetDownloadPressed()
fun onQuickActionSheetBookmarkPressed()
fun onQuickActionSheetReadPressed()
fun onQuickActionSheetAppearancePressed()
fun onQuickActionSheetOpenLinkPressed()
}
/**
* View for the quick action sheet that slides out from the toolbar.
*/
class QuickActionSheetView(
override val containerView: ViewGroup,
private val interactor: QuickActionSheetViewInteractor
) : LayoutContainer, View.OnClickListener {
val view: NestedScrollView = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_quick_action_sheet, containerView, true)
.findViewById(R.id.nestedScrollQuickAction)
private val quickActionSheet = view.quick_action_sheet as QuickActionSheet
private val quickActionSheetBehavior = QuickActionSheetBehavior.from(nestedScrollQuickAction)
init {
quickActionSheetBehavior.setQuickActionSheetCallback(object :
QuickActionSheetBehavior.QuickActionSheetCallback {
override fun onStateChanged(bottomSheet: View, newState: Int) {
updateImportantForAccessibility(newState)
if (newState == QuickActionSheetBehavior.STATE_EXPANDED) {
interactor.onQuickActionSheetOpened()
} else if (newState == QuickActionSheetBehavior.STATE_COLLAPSED) {
interactor.onQuickActionSheetClosed()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
animateOverlay(slideOffset)
}
})
updateImportantForAccessibility(quickActionSheetBehavior.state)
quick_action_share.setOnClickListener(this)
quick_action_downloads.setOnClickListener(this)
quick_action_bookmark.setOnClickListener(this)
quick_action_read.setOnClickListener(this)
quick_action_appearance.setOnClickListener(this)
quick_action_open_app_link.setOnClickListener(this)
}
/**
* Handles clicks from quick action buttons
*/
override fun onClick(button: View) {
when (button.id) {
R.id.quick_action_share -> interactor.onQuickActionSheetSharePressed()
R.id.quick_action_downloads -> interactor.onQuickActionSheetDownloadPressed()
R.id.quick_action_bookmark -> interactor.onQuickActionSheetBookmarkPressed()
R.id.quick_action_read -> interactor.onQuickActionSheetReadPressed()
R.id.quick_action_appearance -> interactor.onQuickActionSheetAppearancePressed()
R.id.quick_action_open_app_link -> interactor.onQuickActionSheetOpenLinkPressed()
else -> return
}
quickActionSheetBehavior.state = QuickActionSheetBehavior.STATE_COLLAPSED
}
/**
* Changes alpha of overlay based on new offset of this sheet within [-1,1] range.
*/
private fun animateOverlay(offset: Float) {
quick_action_overlay.alpha = (1 - offset)
}
/**
* Updates the important for accessibility flag on the buttons container,
* depending on if the sheet is opened or closed.
*/
private fun updateImportantForAccessibility(state: Int) {
view.quick_action_buttons_layout.importantForAccessibility = when (state) {
QuickActionSheetBehavior.STATE_COLLAPSED, QuickActionSheetBehavior.STATE_HIDDEN ->
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
else ->
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
}
fun update(state: BrowserFragmentState) {
val quickActionSheetState = state.quickActionSheetState
view.quick_action_read.isVisible = quickActionSheetState.readable
view.quick_action_read.isSelected = quickActionSheetState.readerActive
view.quick_action_read.text = view.context.getString(
if (quickActionSheetState.readerActive) R.string.quick_action_read_close else R.string.quick_action_read
)
notifyReaderModeButton(quickActionSheetState.readable)
view.quick_action_appearance.isVisible = quickActionSheetState.readerActive
view.quick_action_bookmark.isSelected = quickActionSheetState.bookmarked
view.quick_action_bookmark.text = view.context.getString(
if (quickActionSheetState.bookmarked) {
R.string.quick_action_bookmark_edit
} else {
R.string.quick_action_bookmark
}
)
if (quickActionSheetState.bounceNeeded && view.context.settings().shouldAutoBounceQuickActionSheet) {
quickActionSheet.bounceSheet()
}
view.quick_action_open_app_link.apply {
visibility = if (quickActionSheetState.isAppLink) View.VISIBLE else View.GONE
}
}
private fun notifyReaderModeButton(readable: Boolean) {
val settings = view.context.settings().preferences
val shouldNotifyKey = view.context.getString(R.string.pref_key_reader_mode_notification)
@DrawableRes
val readerTwoStateDrawableRes = if (readable && settings.getBoolean(shouldNotifyKey, true)) {
quickActionSheet.bounceSheet()
settings.edit { putBoolean(shouldNotifyKey, false) }
R.drawable.reader_two_state_with_notification
} else {
R.drawable.reader_two_state
}
view.quick_action_read.putCompoundDrawablesRelativeWithIntrinsicBounds(
top = view.context.getDrawable(readerTwoStateDrawableRes)
)
}
}

View File

@ -132,9 +132,6 @@ class Settings private constructor(
get() = trackingProtectionOnboardingCount < trackingProtectionOnboardingMaximumCount &&
!trackingProtectionOnboardingShownThisSession
val shouldAutoBounceQuickActionSheet: Boolean
get() = autoBounceQuickActionSheetCount < autoBounceMaximumCount
val shouldShowSecurityPinWarningSync: Boolean
get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount
@ -225,12 +222,6 @@ class Settings private constructor(
else -> appContext.getString(R.string.preference_light_theme)
}
@VisibleForTesting(otherwise = PRIVATE)
internal val autoBounceQuickActionSheetCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action),
default = 0
)
@VisibleForTesting(otherwise = PRIVATE)
internal val loginsSecureWarningSyncCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync),
@ -257,13 +248,6 @@ class Settings private constructor(
).apply()
}
fun incrementAutomaticBounceQuickActionSheetCount() {
preferences.edit().putInt(
appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action),
autoBounceQuickActionSheetCount + 1
).apply()
}
val shouldShowSearchSuggestions by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_show_search_suggestions),
default = true

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<org.mozilla.fenix.quickactionsheet.QuickActionSheet xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/quick_action_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />

View File

@ -14,7 +14,7 @@
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/toolbar_and_qab_height">
android:layout_marginBottom="@dimen/browser_toolbar_height">
<mozilla.components.concept.engine.EngineView
android:id="@+id/engineView"
@ -22,14 +22,6 @@
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollQuickAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:mozac_behavior_hideable="true"
app:mozac_behavior_peekHeight="12dp"
app:layout_behavior="org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior" />
<ViewStub
android:id="@+id/stubFindInPage"
android:inflatedId="@+id/findInPageView"

View File

@ -1,141 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/quick_action_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?foundation">
<View
android:id="@+id/quick_action_sheet_faded_handle"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/neutralFaded"
android:focusable="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/quick_action_sheet_handle"
android:layout_width="match_parent"
android:layout_height="12dp"
android:background="@null"
android:contentDescription="@string/quick_action_sheet_handle"
android:paddingTop="7dp"
android:src="@drawable/ic_drawer_pull_tab"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/quick_action_sheet_faded_handle" />
<LinearLayout
android:id="@+id/quick_action_buttons_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="12dp"
android:background="@null"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/quick_action_sheet_handle">
<Button
android:id="@+id/quick_action_share"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:drawableTop="@drawable/quick_action_icon_share"
android:drawablePadding="5dp"
android:text="@string/quick_action_share"
android:textAllCaps="false"
android:textColor="?primaryText"
android:textSize="12sp" />
<Button
android:id="@+id/quick_action_downloads"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:drawableTop="@drawable/library_icon_downloads_circle_background"
android:drawablePadding="5dp"
android:text="@string/quick_action_download"
android:textAllCaps="false"
android:textColor="?primaryText"
android:textSize="12sp"
android:visibility="gone" />
<Button
android:id="@+id/quick_action_bookmark"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:drawableTop="@drawable/bookmark_two_state"
android:drawablePadding="5dp"
android:text="@string/quick_action_bookmark"
android:textAllCaps="false"
android:textColor="?primaryText"
android:textSize="12sp" />
<Button
android:id="@+id/quick_action_appearance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:drawableTop="@drawable/quick_action_icon_appearance"
android:drawablePadding="5dp"
android:text="@string/quick_action_read_appearance"
android:textAllCaps="false"
android:textColor="?primaryText"
android:textSize="12sp"
android:visibility="gone" />
<Button
android:id="@+id/quick_action_open_app_link"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:drawableTop="@drawable/library_icon_app_links_circle_background"
android:drawablePadding="5dp"
android:text="@string/quick_action_open_app_link"
android:textAllCaps="false"
android:textColor="?primaryText"
android:textSize="12sp"
android:visibility="gone" />
<Button
android:id="@+id/quick_action_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="?selectableItemBackgroundBorderless"
android:drawableTop="@drawable/reader_two_state"
android:drawablePadding="5dp"
android:text="@string/quick_action_read"
android:textAllCaps="false"
android:textColor="?primaryText"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
<View
android:id="@+id/quick_action_overlay"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.0"
android:background="?foundation"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/quick_action_buttons_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -67,55 +67,4 @@
<attr name="onboardingKey" format="reference" />
</declare-styleable>
<declare-styleable name="QuickActionSheetBehavior_Layout">
<!-- The height of the bottom sheet when it is collapsed. -->
<attr format="dimension" name="mozac_behavior_peekHeight">
<!-- Peek at the 16:9 ratio keyline of its parent -->
<enum name="auto" value="-1"/>
</attr>
<!-- Whether this bottom sheet can be hidden by dragging it further downwards -->
<attr format="boolean" name="mozac_behavior_hideable"/>
<!-- Skip the collapsed state once expanded; no effect unless it is hideable -->
<attr format="boolean" name="mozac_behavior_skipCollapsed"/>
<!-- Whether height of expanded sheet wraps content or not -->
<attr format="boolean" name="mozac_behavior_fitToContents"/>
<!-- The ratio to be used to set the height of half-expanded state in proportion to parent, when
fitToContents is false. Defaults to true half, 0.5, if not explicitly set. Ratio must be a
float value between 0 and 1 and produce a half-expanded state height larger than the
peek height for the half-expanded state to be operational -->
<attr format="reference|float" name="mozac_behavior_halfExpandedRatio"/>
<!-- The top offset of the BottomSheet in the expanded-state when fitsToContent is false.
The default value is 0, which results in the sheet matching the parent's top. -->
<attr format="reference|integer" name="mozac_behavior_expandedOffset"/>
<!-- Shape appearance style reference for BottomSheet. Attribute declaration is in the shape
package. -->
<attr name="shapeAppearance"/>
<!-- Shape appearance overlay style reference for BottomSheet. To be used to augment attributes
declared in the shapeAppearance. Attribute declaration is in the shape package. -->
<attr name="shapeAppearanceOverlay"/>
<!-- Background color used by the BottomSheetBehavior background drawable when shape theming is
enabled. Accepts a ColorStateList or ColorInt. If shape theming is not enabled,
android:background should instead be utilized to set the background resource. -->
<attr name="backgroundTint"/>
<!-- Behavior properties will be saved and restored by evaluating each flag.
usage: app:behavior_saveFlags=”hideable|skipCollapsed” -->
<attr name="mozac_behavior_saveFlags">
<!-- This flag will preserve the peekHeight on configuration change. -->
<flag name="peekHeight" value="0x1"/>
<!-- This flag will preserve the fitToContents boolean value on configuration change. -->
<flag name="fitToContents" value="0x2"/>
<!-- This flag will preserve the hideable boolean value on configuration change. -->
<flag name="hideable" value="0x4"/>
<!-- This flag will preserve the skipCollapsed boolean value on configuration change. -->
<flag name="skipCollapsed" value="0x8"/>
<!-- This flag will preserve the all the aforementioned values on configuration change. -->
<flag name="all" value="-1"/>
<!-- This flag will not preserve the aforementioned values on configuration change. The only
value preserved will be the positional state, e.g. collapsed, hidden, expanded, etc.
This is the default behavior. -->
<flag name="none" value="0"/>
</attr>
<attr name="android:elevation"/>
</declare-styleable>
</resources>

View File

@ -54,7 +54,6 @@
<!-- Browser Toolbar -->
<dimen name="browser_toolbar_height">56dp</dimen>
<dimen name="toolbar_and_qab_height">67dp</dimen>
<!-- SignIn Component -->
<dimen name="sign_in_button_padding">10dp</dimen>

View File

@ -59,6 +59,10 @@
<string name="browser_menu_refresh">Refresh</string>
<!-- Content description (not visible, for screen readers etc.): Stop loading current website -->
<string name="browser_menu_stop">Stop</string>
<!-- Content description (not visible, for screen readers etc.): Bookmark the current page -->
<string name="browser_menu_bookmark">Bookmark</string>
<!-- Content description (not visible, for screen readers etc.): Un-bookmark the current page -->
<string name="browser_menu_edit_bookmark">Edit bookmark</string>
<!-- Browser menu button that sends a user to help articles -->
<string name="browser_menu_help">Help</string>
<!-- Browser menu button that sends a to a the what's new article -->
@ -94,6 +98,12 @@
<!-- Browser menu text shown in custom tabs to indicate this is a Fenix tab
The first parameter is the name of the app defined in app_name (for example: Fenix) -->
<string name="browser_menu_powered_by2">Powered by %1$s</string>
<!-- Browser menu button to put the the current page in reader mode -->
<string name="browser_menu_read">Reader view</string>
<!-- Browser menu button to open the current page in an external app -->
<string name="browser_menu_open_app_link">Open in App</string>
<!-- Browser menu button to configure reader mode appearance e.g. the used font type and size -->
<string name="browser_menu_read_appearance">Appearance</string>
<!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code -->
@ -302,27 +312,6 @@
<!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Follow device theme</string>
<!-- Quick Action Sheet -->
<!-- Button in the browser chrome to share the current page -->
<string name="quick_action_share">Share</string>
<!-- Button in the browser chrome to download the current page -->
<string name="quick_action_download">Download</string>
<!-- Button in the browser chrome in the browser to bookmark the current page -->
<string name="quick_action_bookmark">Bookmark</string>
<!-- Button in the browser chrome in the browser to edit the bookmark of the current page -->
<string name="quick_action_bookmark_edit">Edit Bookmark</string>
<!-- Button in the browser chrome in the browser to put the the current page in reader mode -->
<string name="quick_action_read">Read</string>
<!-- Button in the browser chrome to exit reader mode -->
<string name="quick_action_read_close">Close</string>
<!-- Button in the browser chrome to open the current page in an external app -->
<string name="quick_action_open_app_link">Open in App</string>
<!-- Button in the browser chrome to configure reader mode appearance e.g. the used font type and size -->
<string name="quick_action_read_appearance">Appearance</string>
<!-- Content description (not visible, for screen readers etc.): Quick action sheet handle to provide users
with shortcuts to commonly used actions such as Share, Bookmark etc.. -->
<string name="quick_action_sheet_handle">Quick actions</string>
<!-- Library -->
<!-- Option in Library to open Sessions page -->
<string name="library_sessions">Sessions</string>

View File

@ -1,75 +0,0 @@
/* 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 kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class BrowserFragmentStoreTest {
@Test
fun bookmarkStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserFragmentStore(initialState)
store.dispatch(QuickActionSheetAction.BookmarkedStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.bookmarked, true)
}
@Test
fun readableStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserFragmentStore(initialState)
store.dispatch(QuickActionSheetAction.ReadableStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.readable, true)
}
@Test
fun readerActiveStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserFragmentStore(initialState)
store.dispatch(QuickActionSheetAction.ReaderActiveStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.readerActive, true)
}
@Test
fun bounceNeededChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserFragmentStore(initialState)
store.dispatch(QuickActionSheetAction.BounceNeededChange).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.bounceNeeded, true)
}
@Test
fun appLinkStateChange() = runBlocking {
val initialState = defaultBrowserState()
val store = BrowserFragmentStore(initialState)
store.dispatch(QuickActionSheetAction.AppLinkStateChange(true)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.quickActionSheetState.isAppLink, true)
}
private fun defaultBrowserState(): BrowserFragmentState = BrowserFragmentState(
quickActionSheetState = defaultQuickActionSheetState()
)
private fun defaultQuickActionSheetState(): QuickActionSheetState = QuickActionSheetState(
readable = false,
bookmarked = false,
readerActive = false,
bounceNeeded = false,
isAppLink = false
)
}

View File

@ -1,52 +1,21 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.session.Session
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.quickactionsheet.QuickActionSheetController
class BrowserInteractorTest {
lateinit var metrics: MetricController
lateinit var context: Context
lateinit var browserStore: BrowserFragmentStore
lateinit var browserToolbarController: BrowserToolbarController
lateinit var quickActionSheetController: QuickActionSheetController
lateinit var readerModeController: ReaderModeController
lateinit var session: Session
lateinit var interactor: BrowserInteractor
@Before
fun setup() {
metrics = mockk()
context = mockk()
browserStore = mockk(relaxed = true)
browserToolbarController = mockk(relaxed = true)
quickActionSheetController = mockk(relaxed = true)
readerModeController = mockk(relaxed = true)
session = mockk()
interactor = BrowserInteractor(
context,
browserStore,
browserToolbarController,
quickActionSheetController,
readerModeController,
session
browserToolbarController
)
every { context.metrics } returns metrics
every { context.components.core.sessionManager.selectedSession } returns session
}
@Test
@ -85,86 +54,4 @@ class BrowserInteractorTest {
verify { browserToolbarController.handleToolbarItemInteraction(item) }
}
@Test
fun onQuickActionSheetOpened() {
every { metrics.track(Event.QuickActionSheetOpened) } just Runs
interactor.onQuickActionSheetOpened()
verify { metrics.track(Event.QuickActionSheetOpened) }
}
@Test
fun onQuickActionSheetClosed() {
every { metrics.track(Event.QuickActionSheetClosed) } just Runs
interactor.onQuickActionSheetClosed()
verify { metrics.track(Event.QuickActionSheetClosed) }
}
@Test
fun onQuickActionSheetSharePressed() {
interactor.onQuickActionSheetSharePressed()
verify { quickActionSheetController.handleShare() }
}
@Test
fun onQuickActionSheetDownloadPressed() {
every { metrics.track(Event.QuickActionSheetDownloadTapped) } just Runs
interactor.onQuickActionSheetDownloadPressed()
verify { quickActionSheetController.handleDownload() }
}
@Test
fun onQuickActionSheetBookmarkPressed() {
interactor.onQuickActionSheetBookmarkPressed()
verify { quickActionSheetController.handleBookmark() }
}
@Test
fun onQuickActionSheetReadPressed() {
every { session.readerMode } returns false
every { metrics.track(Event.QuickActionSheetOpened) } just Runs
interactor.onQuickActionSheetReadPressed()
verify { metrics.track(Event.QuickActionSheetOpened) }
verify { readerModeController.showReaderView() }
}
@Test
fun onQuickActionSheetReadPressedWithActiveReaderMode() {
every { session.readerMode } returns true
every { metrics.track(Event.QuickActionSheetClosed) } just Runs
interactor.onQuickActionSheetReadPressed()
verify { metrics.track(Event.QuickActionSheetClosed) }
verify { readerModeController.hideReaderView() }
}
@Test
fun onQuickActionSheetOpenLinkPressed() {
interactor.onQuickActionSheetOpenLinkPressed()
verify { quickActionSheetController.handleOpenLink() }
}
@Test
fun onQuickActionSheetAppearancePressed() {
every { metrics.track(Event.ReaderModeAppearanceOpened) } just Runs
interactor.onQuickActionSheetAppearancePressed()
verify {
metrics.track(Event.ReaderModeAppearanceOpened)
readerModeController.showControls()
}
}
}

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context
import android.content.Intent
import android.view.ViewGroup
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
@ -52,7 +51,6 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
@ExperimentalCoroutinesApi
@ -72,8 +70,6 @@ class DefaultBrowserToolbarControllerTest {
private val getSupportUrl: () -> String = { "https://supportUrl.org" }
private val openInFenixIntent: Intent = mockk(relaxed = true)
private val currentSessionAsTab: Tab = mockk(relaxed = true)
private val bottomSheetBehavior: QuickActionSheetBehavior<NestedScrollView> =
mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private val searchUseCases: SearchUseCases = mockk(relaxed = true)
private val sessionUseCases: SessionUseCases = mockk(relaxed = true)
@ -99,11 +95,14 @@ class DefaultBrowserToolbarControllerTest {
customTabSession = null,
getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent,
bottomSheetBehavior = bottomSheetBehavior,
scope = scope,
browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage
tabCollectionStorage = tabCollectionStorage,
bookmarkTapped = mockk(),
readerModeController = mockk(),
sessionManager = mockk(),
store = mockk()
)
mockkStatic(
@ -338,7 +337,6 @@ class DefaultBrowserToolbarControllerTest {
controller.handleToolbarItemInteraction(item)
verify { bottomSheetBehavior.state = QuickActionSheetBehavior.STATE_COLLAPSED }
verify { findInPageLauncher() }
verify { metrics.track(Event.FindInPageOpened) }
}
@ -466,11 +464,14 @@ class DefaultBrowserToolbarControllerTest {
customTabSession = currentSession,
getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent,
bottomSheetBehavior = bottomSheetBehavior,
scope = scope,
browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage
tabCollectionStorage = tabCollectionStorage,
bookmarkTapped = mockk(),
readerModeController = mockk(),
sessionManager = mockk(),
store = mockk()
)
val sessionManager: SessionManager = mockk(relaxed = true)

View File

@ -1,89 +0,0 @@
/*
* 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.quickactionsheet
import androidx.navigation.NavController
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.app.links.AppLinksUseCases
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.ItsNotBrokenSnack
class DefaultQuickActionSheetControllerTest {
private val context: HomeActivity = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true)
private val currentSession: Session = mockk(relaxed = true)
private val appLinksUseCases: AppLinksUseCases = mockk(relaxed = true)
private val bookmarkTapped: (Session) -> Unit = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
private lateinit var controller: DefaultQuickActionSheetController
@Before
fun setUp() {
controller = DefaultQuickActionSheetController(
context,
navController,
sessionManager,
appLinksUseCases,
bookmarkTapped
)
every { sessionManager.selectedSession } returns currentSession
every { context.metrics } returns metrics
}
@Test
fun handleShare() {
controller.handleShare()
verify { metrics.track(Event.QuickActionSheetShareTapped) }
verify {
navController.nav(
R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToShareFragment(currentSession.url)
)
}
}
@Test
fun handleDownload() {
controller.handleDownload()
verify { metrics.track(Event.QuickActionSheetDownloadTapped) }
verify { ItsNotBrokenSnack(context).showSnackbar(issueNumber = "348") }
}
@Test
fun handleBookmark() {
controller.handleBookmark()
verify { metrics.track(Event.QuickActionSheetBookmarkTapped) }
verify { bookmarkTapped(currentSession) }
}
@Test
fun handleOpenLink() {
controller.handleOpenLink()
verify { metrics.track(Event.QuickActionSheetOpenInAppTapped) }
verify { appLinksUseCases.appLinkRedirect.invoke(currentSession.url) }
}
}

View File

@ -1,70 +0,0 @@
/* 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.quickactionsheet
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.feature.app.links.AppLinkRedirect
import mozilla.components.feature.app.links.AppLinksUseCases
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
import org.mozilla.fenix.components.toolbar.QuickActionSheetAction
@ExperimentalCoroutinesApi
class QuickActionSheetSessionObserverTest {
private lateinit var components: Components
private lateinit var appLinkRedirect: AppLinksUseCases.GetAppLinkRedirect
private lateinit var store: BrowserFragmentStore
private lateinit var dispatch: (QuickActionSheetAction) -> Unit
@Before
fun setup() {
components = mockk(relaxed = true)
appLinkRedirect = mockk(relaxed = true)
store = mockk(relaxed = true)
dispatch = { store.dispatch(it) }
every { components.useCases.appLinksUseCases.appLinkRedirect } returns appLinkRedirect
}
@Test
fun `onLoadingStateChanged dispatches BounceNeededChange and updates bookmark button`() {
val session: Session = mockk()
val observer = spyk(QuickActionSheetSessionObserver(mockk(), components, dispatch))
every { observer.updateBookmarkState(session) } just Runs
observer.onLoadingStateChanged(session, true)
verify(exactly = 0) { store.dispatch(QuickActionSheetAction.BounceNeededChange) }
observer.onLoadingStateChanged(session, false)
verify { observer.updateBookmarkState(session) }
verify { store.dispatch(QuickActionSheetAction.BounceNeededChange) }
}
@Test
fun `onUrlChanged updates bookmark and app link buttons`() {
val url = "https://example.com"
val session: Session = mockk()
every { session.url } returns url
val observer = spyk(QuickActionSheetSessionObserver(mockk(), components, dispatch))
every { observer.updateBookmarkState(session) } just Runs
every { appLinkRedirect.invoke(url) } returns AppLinkRedirect(mockk(), "", false)
observer.onUrlChanged(session, "")
verify { observer.updateBookmarkState(session) }
verify { appLinkRedirect.invoke("https://example.com") }
verify { store.dispatch(QuickActionSheetAction.AppLinkStateChange(true)) }
}
}

View File

@ -140,39 +140,6 @@ class SettingsTest {
assertTrue(settings.isTelemetryEnabled)
}
@Test
fun autoBounceQuickActionSheetCount() {
// When just created
// Then
assertEquals(0, settings.autoBounceQuickActionSheetCount)
// When
settings.incrementAutomaticBounceQuickActionSheetCount()
settings.incrementAutomaticBounceQuickActionSheetCount()
// Then
assertEquals(2, settings.autoBounceQuickActionSheetCount)
}
@Test
fun shouldAutoBounceQuickActionSheet() {
// When just created
// Then
assertTrue(settings.shouldAutoBounceQuickActionSheet)
// When
settings.incrementAutomaticBounceQuickActionSheetCount()
// Then
assertTrue(settings.shouldAutoBounceQuickActionSheet)
// When
settings.incrementAutomaticBounceQuickActionSheetCount()
// Then
assertFalse(settings.shouldAutoBounceQuickActionSheet)
}
@Test
fun showLoginsDialogWarningSync() {
// When just created

View File

@ -111,12 +111,6 @@ The following metrics are added to the ping:
| qr_scanner.navigation_denied |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped "deny" on the prompt, putting the user back to the scanning view |[1](https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967)||2020-03-01 |
| qr_scanner.opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the QR scanner |[1](https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967)||2020-03-01 |
| qr_scanner.prompt_displayed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user scanned a QR code, causing a confirmation prompt to display asking if they want to navigate to the page |[1](https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967)||2020-03-01 |
| quick_action_sheet.bookmark_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped the bookmark button |[1](https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466)||2020-03-01 |
| quick_action_sheet.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user closed the quick action sheet UI |[1](https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466)||2020-03-01 |
| quick_action_sheet.download_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped the download button |[1](https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466)||2020-03-01 |
| quick_action_sheet.open_app_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped the open in app button |[1](https://github.com/mozilla-mobile/fenix/pull/4629)||2020-03-01 |
| quick_action_sheet.opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the quick action sheet UI |[1](https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466)||2020-03-01 |
| quick_action_sheet.share_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped the share button |[1](https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466)||2020-03-01 |
| reader_mode.appearance |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped the appearance button |[1](https://github.com/mozilla-mobile/fenix/pull/3941)||2020-03-01 |
| reader_mode.available |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Reader mode is available for the current page |[1](https://github.com/mozilla-mobile/fenix/pull/3941)||2020-03-01 |
| reader_mode.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user closed reader mode |[1](https://github.com/mozilla-mobile/fenix/pull/4328)||2020-03-01 |