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 - fenix-core@mozilla.com
expires: "2020-03-01" 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: metrics:
default_browser: default_browser:
type: boolean 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.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.quickActionBar
/** /**
* Tests for verifying basic functionality of bookmarks * Tests for verifying basic functionality of bookmarks
@ -66,12 +65,12 @@ class BookmarksTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openQuickActionBar { }.openThreeDotMenu {
verifyAddBookmarkButton() verifyAddBookmarkButton()
clickBookmarkButton() clickAddBookmarkButton()
} }
browserScreen { browserScreen {
}.openQuickActionBar { }.openThreeDotMenu {
verifyEditBookmarkButton() verifyEditBookmarkButton()
} }
} }
@ -80,11 +79,8 @@ class BookmarksTest {
fun addBookmarkTest() { fun addBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { browserScreen {
createBookmark(defaultWebPage.url) createBookmark(defaultWebPage.url)
}
navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openLibrary { }.openLibrary {
}.openBookmarks { }.openBookmarks {
@ -109,9 +105,8 @@ class BookmarksTest {
fun editBookmarkViewTest() { fun editBookmarkViewTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) } browserScreen {
createBookmark(defaultWebPage.url)
navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openLibrary { }.openLibrary {
}.openBookmarks { }.openBookmarks {
@ -130,9 +125,8 @@ class BookmarksTest {
fun copyBookmarkURLTest() { fun copyBookmarkURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) } browserScreen {
createBookmark(defaultWebPage.url)
navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openLibrary { }.openLibrary {
}.openBookmarks { }.openBookmarks {
@ -146,9 +140,8 @@ class BookmarksTest {
fun openBookmarkInNewTabTest() { fun openBookmarkInNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) } browserScreen {
createBookmark(defaultWebPage.url)
navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openLibrary { }.openLibrary {
}.openBookmarks { }.openBookmarks {
@ -164,9 +157,8 @@ class BookmarksTest {
fun openBookmarkInPrivateTabTest() { fun openBookmarkInPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) } browserScreen {
createBookmark(defaultWebPage.url)
navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openLibrary { }.openLibrary {
}.openBookmarks { }.openBookmarks {
@ -182,9 +174,8 @@ class BookmarksTest {
fun deleteBookmarkTest() { fun deleteBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
quickActionBar { createBookmark(defaultWebPage.url) } browserScreen {
createBookmark(defaultWebPage.url)
navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openLibrary { }.openLibrary {
}.openBookmarks { }.openBookmarks {

View File

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

View File

@ -6,6 +6,7 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.net.Uri
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
@ -67,6 +68,14 @@ class BrowserRobot {
TestAssetHelper.waitingTime) TestAssetHelper.waitingTime)
} }
fun createBookmark(url: Uri) {
navigationToolbar {
}.enterURLAndEnterToBrowser(url) {
}.openThreeDotMenu {
clickAddBookmarkButton()
}
}
class Transition { class Transition {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private fun threeDotButton() = onView( private fun threeDotButton() = onView(
@ -106,16 +115,6 @@ class BrowserRobot {
HomeScreenRobot().interact() HomeScreenRobot().interact()
return HomeScreenRobot.Transition() 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)) fun navURLBar() = onView(withId(R.id.mozac_browser_toolbar_url_view))
private fun tabsCounter() = onView(withId(R.id.counter_box)) 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.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasFocus 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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility 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.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
@ -38,7 +38,8 @@ class ThreeDotMenuMainRobot {
fun verifyHelpButton() = assertHelpButton() fun verifyHelpButton() = assertHelpButton()
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyForwardButton() = assertForwardButton() fun verifyForwardButton() = assertForwardButton()
fun verifyBackButton() = assertBackButton() fun verifyAddBookmarkButton() = assertAddBookmarkButton()
fun verifyEditBookmarkButton() = assertEditBookmarkButton()
fun verifyRefreshButton() = assertRefreshButton() fun verifyRefreshButton() = assertRefreshButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton() fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyShareButton() = assertShareButton() fun verifyShareButton() = assertShareButton()
@ -55,6 +56,7 @@ class ThreeDotMenuMainRobot {
fun clickAddNewCollection() { fun clickAddNewCollection() {
addNewCollectionButton().click() addNewCollectionButton().click()
} }
fun clickAddBookmarkButton() = addBookmarkButton().click()
fun verifyCollectionNameTextField() = assertCollectionNameTextField() fun verifyCollectionNameTextField() = assertCollectionNameTextField()
fun verifyFindInPageButton() = assertFindInPageButton() fun verifyFindInPageButton() = assertFindInPageButton()
fun verifyShareScrim() = assertShareScrim() fun verifyShareScrim() = assertShareScrim()
@ -115,8 +117,10 @@ class ThreeDotMenuMainRobot {
} }
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Back")), waitingTime) // Close three dot
backButton().click() mDevice.pressBack()
// Nav back to previous page
mDevice.pressBack()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -194,8 +198,12 @@ private fun forwardButton() = onView(ViewMatchers.withContentDescription("Forwar
private fun assertForwardButton() = forwardButton() private fun assertForwardButton() = forwardButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun backButton() = onView(ViewMatchers.withContentDescription("Back")) private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark"))
private fun assertBackButton() = backButton() 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))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh")) private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh"))
@ -210,7 +218,7 @@ private fun shareTabButton() = onView(allOf(withText("Share tabs")))
private fun assertShareTabButton() = shareTabButton() private fun assertShareTabButton() = shareTabButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun shareButton() = onView(allOf(withText("Share"))) private fun shareButton() = onView(ViewMatchers.withContentDescription("Share"))
private fun assertShareButton() = shareButton() private fun assertShareButton() = shareButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar 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.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -32,6 +32,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.accounts.FxaCapability 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.downloads.manager.FetchDownloadManager
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
import mozilla.components.feature.prompts.PromptFeature import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
@ -61,17 +63,17 @@ import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.BrowserFragmentState import org.mozilla.fenix.components.toolbar.BrowserFragmentState
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore 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.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController 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.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.downloads.DownloadNotificationBottomSheetDialog import org.mozilla.fenix.downloads.DownloadNotificationBottomSheetDialog
import org.mozilla.fenix.downloads.DownloadService 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.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.isInExperiment import org.mozilla.fenix.isInExperiment
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
@ -95,10 +96,12 @@ import org.mozilla.fenix.theme.ThemeManager
*/ */
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Observer { 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 browserInteractor: BrowserToolbarViewInteractor
protected lateinit var browserToolbarView: BrowserToolbarView protected lateinit var browserToolbarView: BrowserToolbarView
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>() private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>() private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>() private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
@ -131,20 +134,9 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
val activity = activity as HomeActivity val activity = activity as HomeActivity
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect browserFragmentStore = StoreProvider.get(this) {
browserStore = StoreProvider.get(this) {
BrowserFragmentStore( BrowserFragmentStore(
BrowserFragmentState( BrowserFragmentState()
quickActionSheetState = QuickActionSheetState(
readable = getSessionById()?.readerable ?: false,
bookmarked = false,
readerActive = getSessionById()?.readerMode ?: false,
bounceNeeded = false,
isAppLink = getSessionById()?.let {
appLink.invoke(it.url).hasExternalApp()
} ?: false
)
)
) )
} }
@ -173,10 +165,13 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
} }
val browserToolbarController = DefaultBrowserToolbarController( val browserToolbarController = DefaultBrowserToolbarController(
requireActivity(), store = browserFragmentStore,
snackbar, activity = requireActivity(),
findNavController(), snackbar = snackbar,
(activity as HomeActivity).browsingModeManager, navController = findNavController(),
readerModeController = DefaultReaderModeController(readerViewFeature),
browsingModeManager = (activity as HomeActivity).browsingModeManager,
sessionManager = requireComponents.core.sessionManager,
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
browserLayout = view.browserLayout, browserLayout = view.browserLayout,
engineView = engineView, engineView = engineView,
@ -193,15 +188,14 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
}, },
bottomSheetBehavior = QuickActionSheetBehavior.from(nestedScrollQuickAction), bookmarkTapped = { lifecycleScope.launch { bookmarkTapped(it) } },
scope = lifecycleScope, scope = lifecycleScope,
tabCollectionStorage = requireComponents.core.tabCollectionStorage tabCollectionStorage = requireComponents.core.tabCollectionStorage
) )
browserInteractor = browserInteractor = BrowserInteractor(
createBrowserToolbarViewInteractor( browserToolbarController = browserToolbarController
browserToolbarController, )
customTabSessionId?.let { sessionManager.findSessionById(it) })
browserToolbarView = BrowserToolbarView( browserToolbarView = BrowserToolbarView(
container = view.browserLayout, container = view.browserLayout,
@ -368,7 +362,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
activity?.enterToImmersiveMode() activity?.enterToImmersiveMode()
toolbar.visibility = View.GONE toolbar.visibility = View.GONE
nestedScrollQuickAction.visibility = View.GONE
} else { } else {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
activity?.exitImmersiveModeIfNeeded() activity?.exitImmersiveModeIfNeeded()
@ -376,7 +369,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
} }
toolbar.visibility = View.VISIBLE toolbar.visibility = View.VISIBLE
nestedScrollQuickAction.visibility = View.VISIBLE
} }
updateLayoutMargins(inFullScreen) updateLayoutMargins(inFullScreen)
}, },
@ -557,11 +549,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
return false return false
} }
protected abstract fun createBrowserToolbarViewInteractor(
browserToolbarController: BrowserToolbarController,
session: Session?
): BrowserToolbarViewInteractor
protected abstract fun navToQuickSettingsSheet( protected abstract fun navToQuickSettingsSheet(
session: Session, session: Session,
sitePermissions: SitePermissions? 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 { companion object {
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id" private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 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.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.view.browserLayout
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.readerViewControlsBar
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.* import kotlinx.android.synthetic.main.fragment_home.bottom_bar
import kotlinx.coroutines.Dispatchers.IO import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.onboarding_message
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi 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.browser.session.Session
import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.readerview.ReaderViewFeature 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.feature.sitepermissions.SitePermissions
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.jetbrains.anko.dimen import org.jetbrains.anko.dimen
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event 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.components
import org.mozilla.fenix.ext.getDimenInDip import org.mozilla.fenix.ext.getDimenInDip
import org.mozilla.fenix.ext.increaseTapArea 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.SessionControlChange
import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.mvi.getManagedEmitter 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. * Fragment used for browsing the web within the main app.
@ -71,10 +56,6 @@ import org.mozilla.fenix.quickactionsheet.QuickActionSheetView
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
class BrowserFragment : BaseBrowserFragment(), BackHandler { class BrowserFragment : BaseBrowserFragment(), BackHandler {
private lateinit var quickActionSheetView: QuickActionSheetView
private var quickActionSheetSessionObserver: QuickActionSheetSessionObserver? = null
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -114,15 +95,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
if (available) { if (available) {
context.components.analytics.metrics.track(Event.ReaderModeAvailable) context.components.analytics.metrics.track(Event.ReaderModeAvailable)
} }
browserStore.apply {
dispatch(QuickActionSheetAction.ReadableStateChange(available))
dispatch(
QuickActionSheetAction.ReaderActiveStateChange(
sessionManager.selectedSession?.readerMode ?: false
)
)
}
}, },
owner = this, owner = this,
view = view view = view
@ -134,8 +106,7 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
themeReaderViewControlsForPrivateMode(view.readerViewControlsBar) themeReaderViewControlsForPrivateMode(view.readerViewControlsBar)
} }
consumeFrom(browserStore) { consumeFrom(browserFragmentStore) {
quickActionSheetView.update(it)
browserToolbarView.update(it) browserToolbarView.update(it)
} }
} }
@ -144,13 +115,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
subscribeToTabCollections() subscribeToTabCollections()
quickActionSheetSessionObserver = QuickActionSheetSessionObserver(
lifecycleScope,
requireComponents,
dispatch = { action -> browserStore.dispatch(action) }
).also { observer ->
getSessionById()?.register(observer, this, autoPause = true)
}
getSessionById()?.register(toolbarSessionObserver, this, autoPause = true) getSessionById()?.register(toolbarSessionObserver, this, autoPause = true)
} }
@ -173,7 +137,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
* This fixes issue #5254. * This fixes issue #5254.
*/ */
(activity as HomeActivity).updateThemeForSession(it) (activity as HomeActivity).updateThemeForSession(it)
quickActionSheetSessionObserver?.updateBookmarkState(it)
} }
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
} }
@ -182,34 +145,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
return readerViewFeature.onBackPressed() || super.onBackPressed() 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?) { override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
val directions = val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
@ -241,8 +176,8 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
} }
override fun getEngineMargins(): Pair<Int, Int> { override fun getEngineMargins(): Pair<Int, Int> {
val toolbarAndQASSize = resources.getDimensionPixelSize(R.dimen.toolbar_and_qab_height) val toolbarSize = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
return 0 to toolbarAndQASSize return 0 to toolbarSize
} }
override fun getAppropriateLayoutGravity() = Gravity.BOTTOM 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() { private fun subscribeToTabCollections() {
requireComponents.core.tabCollectionStorage.getCollections().observe(this, Observer { requireComponents.core.tabCollectionStorage.getCollections().observe(this, Observer {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it 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 { private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) { override fun onCollectionCreated(title: String, sessions: List<Session>) {
showTabSavedToCollectionSnackbar() showTabSavedToCollectionSnackbar()
@ -415,7 +300,7 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler {
view, view,
FenixSnackbarDelegate( FenixSnackbarDelegate(
view, 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.PrivateBrowsingMode
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
import org.mozilla.fenix.GleanMetrics.QrScanner import org.mozilla.fenix.GleanMetrics.QrScanner
import org.mozilla.fenix.GleanMetrics.QuickActionSheet
import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.SearchShortcuts
@ -138,24 +137,6 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.browserMenuAction.record(it) }, { Events.browserMenuAction.record(it) },
{ Events.browserMenuActionKeys.valueOf(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>( is Event.OpenedBookmarkInNewTab -> EventWrapper<NoExtraKeys>(
{ BookmarksManagement.openInNewTab.record(it) } { BookmarksManagement.openInNewTab.record(it) }
) )

View File

@ -50,12 +50,6 @@ sealed class Event {
object AddBookmarkFolder : Event() object AddBookmarkFolder : Event()
object RemoveBookmarkFolder : Event() object RemoveBookmarkFolder : Event()
object RemoveBookmarks : 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 CustomTabsClosed : Event()
object CustomTabsActionTapped : Event() object CustomTabsActionTapped : Event()
object CustomTabsMenuOpened : Event() object CustomTabsMenuOpened : Event()
@ -300,7 +294,8 @@ sealed class Event {
enum class Item { enum class Item {
SETTINGS, LIBRARY, HELP, DESKTOP_VIEW_ON, DESKTOP_VIEW_OFF, FIND_IN_PAGE, NEW_TAB, 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, 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>? 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 * 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, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:Suppress("unused", "UNUSED_PARAMETER")
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store 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) : class BrowserFragmentStore(initialState: BrowserFragmentState) :
Store<BrowserFragmentState, BrowserFragmentAction>(initialState, ::browserStateReducer) Store<BrowserFragmentState, BrowserFragmentAction>(initialState, ::browserStateReducer)
/** /**
* The state for the Browser Screen * The state for the Browser Screen
* @property quickActionSheetState: state of the quick action sheet
*/ */
data class BrowserFragmentState( class BrowserFragmentState : State
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
sealed class BrowserFragmentAction : Action 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]. * Reducers for [BrowserFragmentStore].
* *
@ -57,30 +33,7 @@ private fun browserStateReducer(
state: BrowserFragmentState, state: BrowserFragmentState,
action: BrowserFragmentAction action: BrowserFragmentAction
): BrowserFragmentState { ): BrowserFragmentState {
return when (action) { return when {
is QuickActionSheetAction -> { else -> BrowserFragmentState()
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))
}
}
} }
} }

View File

@ -4,16 +4,7 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import android.content.Context open class BrowserInteractor(
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(
private val browserToolbarController: BrowserToolbarController private val browserToolbarController: BrowserToolbarController
) : BrowserToolbarViewInteractor { ) : BrowserToolbarViewInteractor {
@ -37,56 +28,3 @@ open class BrowserToolbarInteractor(
browserToolbarController.handleToolbarItemInteraction(item) 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.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.fragment.FragmentNavigator import androidx.navigation.fragment.FragmentNavigator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.NavGraphDirections 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.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager 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.collections.SaveCollectionStep
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.lib.Do import org.mozilla.fenix.lib.Do
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
/** /**
@ -54,10 +53,13 @@ interface BrowserToolbarController {
@Suppress("LargeClass") @Suppress("LargeClass")
class DefaultBrowserToolbarController( class DefaultBrowserToolbarController(
private val store: BrowserFragmentStore,
private val activity: Activity, private val activity: Activity,
private val snackbar: FenixSnackbar?, private val snackbar: FenixSnackbar?,
private val navController: NavController, private val navController: NavController,
private val readerModeController: ReaderModeController,
private val browsingModeManager: BrowsingModeManager, private val browsingModeManager: BrowsingModeManager,
private val sessionManager: SessionManager,
private val findInPageLauncher: () -> Unit, private val findInPageLauncher: () -> Unit,
private val browserLayout: ViewGroup, private val browserLayout: ViewGroup,
private val engineView: EngineView, private val engineView: EngineView,
@ -66,7 +68,7 @@ class DefaultBrowserToolbarController(
private val customTabSession: Session?, private val customTabSession: Session?,
private val getSupportUrl: () -> String, private val getSupportUrl: () -> String,
private val openInFenixIntent: Intent, private val openInFenixIntent: Intent,
private val bottomSheetBehavior: QuickActionSheetBehavior<NestedScrollView>, private val bookmarkTapped: (Session) -> Unit,
private val scope: LifecycleCoroutineScope, private val scope: LifecycleCoroutineScope,
private val tabCollectionStorage: TabCollectionStorage private val tabCollectionStorage: TabCollectionStorage
) : BrowserToolbarController { ) : BrowserToolbarController {
@ -163,7 +165,6 @@ class DefaultBrowserToolbarController(
browsingModeManager.mode = BrowsingMode.Private browsingModeManager.mode = BrowsingMode.Private
} }
ToolbarMenu.Item.FindInPage -> { ToolbarMenu.Item.FindInPage -> {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
findInPageLauncher() findInPageLauncher()
activity.components.analytics.metrics.track(Event.FindInPageOpened) activity.components.analytics.metrics.track(Event.FindInPageOpened)
} }
@ -210,6 +211,35 @@ class DefaultBrowserToolbarController(
activity.finish() activity.finish()
} }
ToolbarMenu.Item.Quit -> deleteAndQuit(activity, scope, snackbar) 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.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION
ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT 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)) activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem))

View File

@ -10,11 +10,14 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.extensions.LayoutContainer 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.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
@ -24,6 +27,7 @@ import org.jetbrains.anko.dimen
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
@ -168,12 +172,18 @@ class BrowserToolbarView(
) )
} else { } else {
DefaultToolbarMenu( DefaultToolbarMenu(
this, context = this,
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(), hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
requestDesktopStateProvider = { requestDesktopStateProvider = {
sessionManager.selectedSession?.desktopMode ?: false 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 package org.mozilla.fenix.components.toolbar
import android.content.Context 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.BrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch 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.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R 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.theme.ThemeManager
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@Suppress("LargeClass") // While large, most of the class is very simple
class DefaultToolbarMenu( class DefaultToolbarMenu(
private val context: Context, private val context: Context,
private val hasAccountProblem: Boolean = false, private val hasAccountProblem: Boolean = false,
private val requestDesktopStateProvider: () -> 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 { ) : ToolbarMenu {
private var currentUrlIsBookmarked = false
private var isBookmarkedJob: Job? = null
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems, endOfMenuAlwaysVisible = true) } override val menuBuilder by lazy { BrowserMenuBuilder(menuItems, endOfMenuAlwaysVisible = true) }
override val menuToolbar by lazy { 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( val forward = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward, primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward,
primaryContentDescription = context.getString(R.string.browser_menu_forward), primaryContentDescription = context.getString(R.string.browser_menu_forward),
@ -93,143 +89,227 @@ class DefaultToolbarMenu(
} }
} }
BrowserMenuItemToolbar(listOf(back, forward, refresh)) val share = BrowserMenuItemToolbar.Button(
} imageResource = R.drawable.mozac_ic_share,
contentDescription = context.getString(R.string.browser_menu_share),
private val menuItems by lazy { iconTintColorResource = primaryTextColor(),
val items = mutableListOf( listener = {
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)
) {
onItemTapped.invoke(ToolbarMenu.Item.Share) 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) { registerForIsBookmarkedUpdates(sessionManager)
items.add( val bookmark = BrowserMenuItemToolbar.TwoStateButton(
BrowserMenuImageText( primaryImageResource = R.drawable.ic_bookmark_filled,
context.getString(R.string.browser_menu_save_to_collection), primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark),
R.drawable.ic_tab_collection, primaryImageTintResource = ThemeManager.resolveAttribute(
ThemeManager.resolveAttribute(R.attr.primaryText, context) R.attr.primaryText,
) { context
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) ),
} // 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) { BrowserMenuItemToolbar(listOf(forward, bookmark, share, refresh))
items.add( }
BrowserMenuImageText(
context.getString(R.string.delete_browsing_data_on_quit_action), private val menuItems by lazy {
R.drawable.ic_exit, // Predicates that are called once, during screen init
ThemeManager.resolveAttribute(R.attr.primaryText, context) val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity)
) { ?.browsingModeManager?.mode == BrowsingMode.Normal
onItemTapped.invoke(ToolbarMenu.Item.Quit) 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( listOfNotNull(
BrowserMenuDivider() help,
) settings,
library,
items.add( 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 menuToolbar
) )
items
} }
private fun shouldShowAddToHomescreen(): Boolean { private val help = BrowserMenuImageText(
return context.components.useCases.webAppUseCases.isPinningSupported() && context.getString(R.string.browser_menu_help),
context.components.core.sessionManager.selectedSession != null 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 SaveToCollection : Item()
object AddToHomeScreen : Item() object AddToHomeScreen : Item()
object Quit : Item() object Quit : Item()
data class ReaderMode(val isChecked: Boolean) : Item()
object OpenInApp : Item()
object Bookmark : Item()
object ReaderModeAppearance : Item()
} }
val menuBuilder: BrowserMenuBuilder val menuBuilder: BrowserMenuBuilder

View File

@ -99,60 +99,58 @@ class CustomTabToolbarMenu(
private val menuItems by lazy { private val menuItems by lazy {
listOf( listOf(
menuToolbar, menuToolbar,
BrowserMenuDivider(), BrowserMenuDivider(),
share,
BrowserMenuImageText( desktopMode,
context.getString(R.string.browser_menu_share), findInPage,
R.drawable.mozac_ic_share, openInFenix,
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)
},
BrowserMenuDivider(), BrowserMenuDivider(),
poweredBy
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)
)
) )
} }
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 android.view.View
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable import com.airbnb.lottie.LottieDrawable
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
@ -28,7 +27,6 @@ class CustomTabsIntegration(
toolbar: BrowserToolbar, toolbar: BrowserToolbar,
sessionId: String, sessionId: String,
activity: Activity, activity: Activity,
quickActionbar: NestedScrollView,
engineLayout: View, engineLayout: View,
onItemTapped: (ToolbarMenu.Item) -> Unit = {} onItemTapped: (ToolbarMenu.Item) -> Unit = {}
) : LifecycleAwareFeature, BackHandler { ) : LifecycleAwareFeature, BackHandler {
@ -52,9 +50,6 @@ class CustomTabsIntegration(
} }
} }
// Hide the Quick Action Bar.
quickActionbar.visibility = View.GONE
val task = LottieCompositionFactory val task = LottieCompositionFactory
.fromRawRes( .fromRawRes(
activity, activity,

View File

@ -32,8 +32,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BaseBrowserFragment import org.mozilla.fenix.browser.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate 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.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
@ -63,12 +61,11 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler {
customTabSessionId?.let { customTabSessionId -> customTabSessionId?.let { customTabSessionId ->
customTabsIntegration.set( customTabsIntegration.set(
feature = CustomTabsIntegration( feature = CustomTabsIntegration(
requireComponents.core.sessionManager, sessionManager = requireComponents.core.sessionManager,
toolbar, toolbar = toolbar,
customTabSessionId, sessionId = customTabSessionId,
activity, activity = activity,
view.nestedScrollQuickAction, engineLayout = view.swipeRefresh,
view.swipeRefresh,
onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) } onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }
), ),
owner = this, owner = this,
@ -123,7 +120,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler {
} }
} }
consumeFrom(browserStore) { consumeFrom(browserFragmentStore) {
browserToolbarView.update(it) browserToolbarView.update(it)
} }
@ -146,11 +143,6 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler {
return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded() return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
} }
override fun createBrowserToolbarViewInteractor(
browserToolbarController: BrowserToolbarController,
session: Session?
) = BrowserToolbarInteractor(browserToolbarController)
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) { override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
val directions = ExternalAppBrowserFragmentDirections val directions = ExternalAppBrowserFragmentDirections
.actionExternalAppBrowserFragmentToQuickSettingsSheetDialogFragment( .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 && get() = trackingProtectionOnboardingCount < trackingProtectionOnboardingMaximumCount &&
!trackingProtectionOnboardingShownThisSession !trackingProtectionOnboardingShownThisSession
val shouldAutoBounceQuickActionSheet: Boolean
get() = autoBounceQuickActionSheetCount < autoBounceMaximumCount
val shouldShowSecurityPinWarningSync: Boolean val shouldShowSecurityPinWarningSync: Boolean
get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount
@ -225,12 +222,6 @@ class Settings private constructor(
else -> appContext.getString(R.string.preference_light_theme) 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) @VisibleForTesting(otherwise = PRIVATE)
internal val loginsSecureWarningSyncCount by intPreference( internal val loginsSecureWarningSyncCount by intPreference(
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync), appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync),
@ -257,13 +248,6 @@ class Settings private constructor(
).apply() ).apply()
} }
fun incrementAutomaticBounceQuickActionSheetCount() {
preferences.edit().putInt(
appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action),
autoBounceQuickActionSheetCount + 1
).apply()
}
val shouldShowSearchSuggestions by booleanPreference( val shouldShowSearchSuggestions by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_show_search_suggestions), appContext.getPreferenceKey(R.string.pref_key_show_search_suggestions),
default = true 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:id="@+id/swipeRefresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <mozilla.components.concept.engine.EngineView
android:id="@+id/engineView" android:id="@+id/engineView"
@ -22,14 +22,6 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </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 <ViewStub
android:id="@+id/stubFindInPage" android:id="@+id/stubFindInPage"
android:inflatedId="@+id/findInPageView" 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" /> <attr name="onboardingKey" format="reference" />
</declare-styleable> </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> </resources>

View File

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

View File

@ -59,6 +59,10 @@
<string name="browser_menu_refresh">Refresh</string> <string name="browser_menu_refresh">Refresh</string>
<!-- Content description (not visible, for screen readers etc.): Stop loading current website --> <!-- Content description (not visible, for screen readers etc.): Stop loading current website -->
<string name="browser_menu_stop">Stop</string> <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 --> <!-- Browser menu button that sends a user to help articles -->
<string name="browser_menu_help">Help</string> <string name="browser_menu_help">Help</string>
<!-- Browser menu button that sends a to a the what's new article --> <!-- 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 <!-- 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) --> 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> <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 --> <!-- Search Fragment -->
<!-- Button in the search view that lets a user search by scanning a QR code --> <!-- 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 --> <!-- Preference for using following device theme -->
<string name="preference_follow_device_theme">Follow device theme</string> <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 --> <!-- Library -->
<!-- Option in Library to open Sessions page --> <!-- Option in Library to open Sessions page -->
<string name="library_sessions">Sessions</string> <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 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.mockk
import io.mockk.verify import io.mockk.verify
import mozilla.components.browser.session.Session
import org.junit.Before import org.junit.Before
import org.junit.Test 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 { class BrowserInteractorTest {
lateinit var metrics: MetricController
lateinit var context: Context
lateinit var browserStore: BrowserFragmentStore
lateinit var browserToolbarController: BrowserToolbarController lateinit var browserToolbarController: BrowserToolbarController
lateinit var quickActionSheetController: QuickActionSheetController
lateinit var readerModeController: ReaderModeController
lateinit var session: Session
lateinit var interactor: BrowserInteractor lateinit var interactor: BrowserInteractor
@Before @Before
fun setup() { fun setup() {
metrics = mockk()
context = mockk()
browserStore = mockk(relaxed = true)
browserToolbarController = mockk(relaxed = true) browserToolbarController = mockk(relaxed = true)
quickActionSheetController = mockk(relaxed = true)
readerModeController = mockk(relaxed = true)
session = mockk()
interactor = BrowserInteractor( interactor = BrowserInteractor(
context, browserToolbarController
browserStore,
browserToolbarController,
quickActionSheetController,
readerModeController,
session
) )
every { context.metrics } returns metrics
every { context.components.core.sessionManager.selectedSession } returns session
} }
@Test @Test
@ -85,86 +54,4 @@ class BrowserInteractorTest {
verify { browserToolbarController.handleToolbarItemInteraction(item) } 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.Context
import android.content.Intent import android.content.Intent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@ -52,7 +51,6 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.toTab import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.Tab
import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.home.sessioncontrol.TabCollection
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@ -72,8 +70,6 @@ class DefaultBrowserToolbarControllerTest {
private val getSupportUrl: () -> String = { "https://supportUrl.org" } private val getSupportUrl: () -> String = { "https://supportUrl.org" }
private val openInFenixIntent: Intent = mockk(relaxed = true) private val openInFenixIntent: Intent = mockk(relaxed = true)
private val currentSessionAsTab: Tab = 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 metrics: MetricController = mockk(relaxed = true)
private val searchUseCases: SearchUseCases = mockk(relaxed = true) private val searchUseCases: SearchUseCases = mockk(relaxed = true)
private val sessionUseCases: SessionUseCases = mockk(relaxed = true) private val sessionUseCases: SessionUseCases = mockk(relaxed = true)
@ -99,11 +95,14 @@ class DefaultBrowserToolbarControllerTest {
customTabSession = null, customTabSession = null,
getSupportUrl = getSupportUrl, getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
bottomSheetBehavior = bottomSheetBehavior,
scope = scope, scope = scope,
browserLayout = browserLayout, browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout, swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage tabCollectionStorage = tabCollectionStorage,
bookmarkTapped = mockk(),
readerModeController = mockk(),
sessionManager = mockk(),
store = mockk()
) )
mockkStatic( mockkStatic(
@ -338,7 +337,6 @@ class DefaultBrowserToolbarControllerTest {
controller.handleToolbarItemInteraction(item) controller.handleToolbarItemInteraction(item)
verify { bottomSheetBehavior.state = QuickActionSheetBehavior.STATE_COLLAPSED }
verify { findInPageLauncher() } verify { findInPageLauncher() }
verify { metrics.track(Event.FindInPageOpened) } verify { metrics.track(Event.FindInPageOpened) }
} }
@ -466,11 +464,14 @@ class DefaultBrowserToolbarControllerTest {
customTabSession = currentSession, customTabSession = currentSession,
getSupportUrl = getSupportUrl, getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
bottomSheetBehavior = bottomSheetBehavior,
scope = scope, scope = scope,
browserLayout = browserLayout, browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout, swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage tabCollectionStorage = tabCollectionStorage,
bookmarkTapped = mockk(),
readerModeController = mockk(),
sessionManager = mockk(),
store = mockk()
) )
val sessionManager: SessionManager = mockk(relaxed = true) 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) 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 @Test
fun showLoginsDialogWarningSync() { fun showLoginsDialogWarningSync() {
// When just created // 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.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.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 | | 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.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.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 | | 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 |