From 6909a76bcb99c8f14b0dfc097d8bab2e84cb7cf8 Mon Sep 17 00:00:00 2001 From: Severin Rudie Date: Mon, 11 Nov 2019 17:10:14 -0800 Subject: [PATCH] 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 --- app/metrics.yaml | 68 - .../org/mozilla/fenix/ui/BookmarksTest.kt | 37 +- .../mozilla/fenix/ui/NavigationToolbarTest.kt | 1 - .../mozilla/fenix/ui/robots/BrowserRobot.kt | 20 +- .../fenix/ui/robots/QuickActionBarRobot.kt | 81 - .../fenix/ui/robots/ThreeDotMenuMainRobot.kt | 24 +- .../fenix/browser/BaseBrowserFragment.kt | 97 +- .../mozilla/fenix/browser/BrowserFragment.kt | 131 +- .../components/metrics/GleanMetricsService.kt | 19 - .../fenix/components/metrics/Metrics.kt | 9 +- .../toolbar/BrowserFragmentStore.kt | 63 +- .../components/toolbar/BrowserInteractor.kt | 64 +- .../toolbar/BrowserToolbarController.kt | 52 +- .../components/toolbar/BrowserToolbarView.kt | 16 +- .../components/toolbar/DefaultToolbarMenu.kt | 372 +++-- .../fenix/components/toolbar/ToolbarMenu.kt | 4 + .../fenix/customtabs/CustomTabToolbarMenu.kt | 102 +- .../fenix/customtabs/CustomTabsIntegration.kt | 5 - .../customtabs/ExternalAppBrowserFragment.kt | 20 +- .../quickactionsheet/QuickActionSheet.kt | 128 -- .../QuickActionSheetBehavior.kt | 1351 ----------------- .../QuickActionSheetController.kt | 68 - .../QuickActionSheetSessionObserver.kt | 68 - .../quickactionsheet/QuickActionSheetView.kt | 159 -- .../java/org/mozilla/fenix/utils/Settings.kt | 16 - app/src/main/res/drawable/ic_app_links.xml | 14 + .../res/drawable/ic_readermode_appearance.xml | 14 + .../layout/component_quick_action_sheet.xml | 9 - app/src/main/res/layout/fragment_browser.xml | 10 +- .../res/layout/layout_quick_action_sheet.xml | 141 -- app/src/main/res/values/attrs.xml | 51 - app/src/main/res/values/dimens.xml | 1 - app/src/main/res/values/strings.xml | 31 +- .../toolbar/BrowserFragmentStoreTest.kt | 75 - .../toolbar/BrowserInteractorTest.kt | 115 +- .../DefaultBrowserToolbarControllerTest.kt | 19 +- .../DefaultQuickActionSheetControllerTest.kt | 89 -- .../QuickActionSheetSessionObserverTest.kt | 70 - .../org/mozilla/fenix/utils/SettingsTest.kt | 33 - docs/metrics.md | 6 - 40 files changed, 516 insertions(+), 3137 deletions(-) delete mode 100644 app/src/androidTest/java/org/mozilla/fenix/ui/robots/QuickActionBarRobot.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetBehavior.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetSessionObserver.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt create mode 100644 app/src/main/res/drawable/ic_app_links.xml create mode 100644 app/src/main/res/drawable/ic_readermode_appearance.xml delete mode 100644 app/src/main/res/layout/component_quick_action_sheet.xml delete mode 100644 app/src/main/res/layout/layout_quick_action_sheet.xml delete mode 100644 app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserFragmentStoreTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/quickactionsheet/DefaultQuickActionSheetControllerTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetSessionObserverTest.kt diff --git a/app/metrics.yaml b/app/metrics.yaml index 1c3ba9973..324a27b11 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -276,74 +276,6 @@ find_in_page: - fenix-core@mozilla.com expires: "2020-03-01" -quick_action_sheet: - opened: - type: event - description: > - A user opened the quick action sheet UI - bugs: - - https://github.com/mozilla-mobile/fenix/issue/1195 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466 - notification_emails: - - fenix-core@mozilla.com - expires: "2020-03-01" - closed: - type: event - description: > - A user closed the quick action sheet UI - bugs: - - https://github.com/mozilla-mobile/fenix/issue/1195 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466 - notification_emails: - - fenix-core@mozilla.com - expires: "2020-03-01" - share_tapped: - type: event - description: > - A user tapped the share button - bugs: - - https://github.com/mozilla-mobile/fenix/issue/1195 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466 - notification_emails: - - fenix-core@mozilla.com - expires: "2020-03-01" - bookmark_tapped: - type: event - description: > - A user tapped the bookmark button - bugs: - - https://github.com/mozilla-mobile/fenix/issue/1195 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466 - notification_emails: - - fenix-core@mozilla.com - expires: "2020-03-01" - download_tapped: - type: event - description: > - A user tapped the download button - bugs: - - https://github.com/mozilla-mobile/fenix/issue/1195 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/1362#issuecomment-479668466 - notification_emails: - - fenix-core@mozilla.com - expires: "2020-03-01" - open_app_tapped: - type: event - description: > - A user tapped the open in app button - bugs: - - https://github.com/mozilla-mobile/fenix/issue/1195 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/4629 - notification_emails: - - fenix-core@mozilla.com - expires: "2020-03-01" - metrics: default_browser: type: boolean diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt index 3eba79953..4fae148f2 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -18,7 +18,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar -import org.mozilla.fenix.ui.robots.quickActionBar /** * Tests for verifying basic functionality of bookmarks @@ -66,12 +65,12 @@ class BookmarksTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - }.openQuickActionBar { + }.openThreeDotMenu { verifyAddBookmarkButton() - clickBookmarkButton() + clickAddBookmarkButton() } browserScreen { - }.openQuickActionBar { + }.openThreeDotMenu { verifyEditBookmarkButton() } } @@ -80,11 +79,8 @@ class BookmarksTest { fun addBookmarkTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - quickActionBar { + browserScreen { createBookmark(defaultWebPage.url) - } - - navigationToolbar { }.openThreeDotMenu { }.openLibrary { }.openBookmarks { @@ -109,9 +105,8 @@ class BookmarksTest { fun editBookmarkViewTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - quickActionBar { createBookmark(defaultWebPage.url) } - - navigationToolbar { + browserScreen { + createBookmark(defaultWebPage.url) }.openThreeDotMenu { }.openLibrary { }.openBookmarks { @@ -130,9 +125,8 @@ class BookmarksTest { fun copyBookmarkURLTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - quickActionBar { createBookmark(defaultWebPage.url) } - - navigationToolbar { + browserScreen { + createBookmark(defaultWebPage.url) }.openThreeDotMenu { }.openLibrary { }.openBookmarks { @@ -146,9 +140,8 @@ class BookmarksTest { fun openBookmarkInNewTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - quickActionBar { createBookmark(defaultWebPage.url) } - - navigationToolbar { + browserScreen { + createBookmark(defaultWebPage.url) }.openThreeDotMenu { }.openLibrary { }.openBookmarks { @@ -164,9 +157,8 @@ class BookmarksTest { fun openBookmarkInPrivateTabTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - quickActionBar { createBookmark(defaultWebPage.url) } - - navigationToolbar { + browserScreen { + createBookmark(defaultWebPage.url) }.openThreeDotMenu { }.openLibrary { }.openBookmarks { @@ -182,9 +174,8 @@ class BookmarksTest { fun deleteBookmarkTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - quickActionBar { createBookmark(defaultWebPage.url) } - - navigationToolbar { + browserScreen { + createBookmark(defaultWebPage.url) }.openThreeDotMenu { }.openLibrary { }.openBookmarks { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt index c79153663..238d4c0e4 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -65,7 +65,6 @@ class NavigationToolbarTest { navigationToolbar { }.openThreeDotMenu { verifyThreeDotMenuExists() - verifyBackButton() }.goBack { verifyPageContent(defaultWebPage.content) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt index ec71d53db..b28771f77 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots +import android.net.Uri import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches @@ -67,6 +68,14 @@ class BrowserRobot { TestAssetHelper.waitingTime) } + fun createBookmark(url: Uri) { + navigationToolbar { + }.enterURLAndEnterToBrowser(url) { + }.openThreeDotMenu { + clickAddBookmarkButton() + } + } + class Transition { private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private fun threeDotButton() = onView( @@ -106,16 +115,6 @@ class BrowserRobot { HomeScreenRobot().interact() return HomeScreenRobot.Transition() } - - fun openQuickActionBar(interact: QuickActionBarRobot.() -> Unit): QuickActionBarRobot.Transition { - mDevice.waitNotNull(Until.gone(By.res("org.mozilla.fenix.debug:id/quick_action_sheet")), - TestAssetHelper.waitingTime - ) - quickActionBarHandle().click() - - QuickActionBarRobot().interact() - return QuickActionBarRobot.Transition() - } } } @@ -133,4 +132,3 @@ fun dismissTrackingOnboarding() { fun navURLBar() = onView(withId(R.id.mozac_browser_toolbar_url_view)) private fun tabsCounter() = onView(withId(R.id.counter_box)) -private fun quickActionBarHandle() = onView(withId(R.id.quick_action_sheet_handle)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/QuickActionBarRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/QuickActionBarRobot.kt deleted file mode 100644 index 16c8cd50b..000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/QuickActionBarRobot.kt +++ /dev/null @@ -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"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt index f6ca35c39..225a363d0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt @@ -11,11 +11,11 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasFocus -import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withResourceName +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice @@ -38,7 +38,8 @@ class ThreeDotMenuMainRobot { fun verifyHelpButton() = assertHelpButton() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyForwardButton() = assertForwardButton() - fun verifyBackButton() = assertBackButton() + fun verifyAddBookmarkButton() = assertAddBookmarkButton() + fun verifyEditBookmarkButton() = assertEditBookmarkButton() fun verifyRefreshButton() = assertRefreshButton() fun verifyCloseAllTabsButton() = assertCloseAllTabsButton() fun verifyShareButton() = assertShareButton() @@ -55,6 +56,7 @@ class ThreeDotMenuMainRobot { fun clickAddNewCollection() { addNewCollectionButton().click() } + fun clickAddBookmarkButton() = addBookmarkButton().click() fun verifyCollectionNameTextField() = assertCollectionNameTextField() fun verifyFindInPageButton() = assertFindInPageButton() fun verifyShareScrim() = assertShareScrim() @@ -115,8 +117,10 @@ class ThreeDotMenuMainRobot { } fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - mDevice.waitNotNull(Until.findObject(By.desc("Back")), waitingTime) - backButton().click() + // Close three dot + mDevice.pressBack() + // Nav back to previous page + mDevice.pressBack() BrowserRobot().interact() return BrowserRobot.Transition() @@ -194,8 +198,12 @@ private fun forwardButton() = onView(ViewMatchers.withContentDescription("Forwar private fun assertForwardButton() = forwardButton() .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) -private fun backButton() = onView(ViewMatchers.withContentDescription("Back")) -private fun assertBackButton() = backButton() +private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark")) +private fun assertAddBookmarkButton() = addBookmarkButton() + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + +private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark")) +private fun assertEditBookmarkButton() = editBookmarkButton() .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh")) @@ -210,7 +218,7 @@ private fun shareTabButton() = onView(allOf(withText("Share tabs"))) private fun assertShareTabButton() = shareTabButton() .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) -private fun shareButton() = onView(allOf(withText("Share"))) +private fun shareButton() = onView(ViewMatchers.withContentDescription("Share")) private fun assertShareButton() = shareButton() .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index ed6574ff0..d87dca29f 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.component_search.* +import kotlinx.android.synthetic.main.component_search.toolbar import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.coroutines.Dispatchers.IO @@ -32,6 +32,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.feature.accounts.FxaCapability @@ -44,6 +45,7 @@ import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.prompts.PromptFeature +import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionUseCases @@ -61,17 +63,17 @@ import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R +import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.toolbar.BrowserFragmentState import org.mozilla.fenix.components.toolbar.BrowserFragmentStore -import org.mozilla.fenix.components.toolbar.BrowserToolbarController +import org.mozilla.fenix.components.toolbar.BrowserInteractor import org.mozilla.fenix.components.toolbar.BrowserToolbarView import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController -import org.mozilla.fenix.components.toolbar.QuickActionSheetState import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.downloads.DownloadNotificationBottomSheetDialog import org.mozilla.fenix.downloads.DownloadService @@ -84,7 +86,6 @@ import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.isInExperiment -import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.theme.ThemeManager @@ -95,10 +96,12 @@ import org.mozilla.fenix.theme.ThemeManager */ @Suppress("TooManyFunctions", "LargeClass") abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Observer { - protected lateinit var browserStore: BrowserFragmentStore + protected lateinit var browserFragmentStore: BrowserFragmentStore protected lateinit var browserInteractor: BrowserToolbarViewInteractor protected lateinit var browserToolbarView: BrowserToolbarView + protected val readerViewFeature = ViewBoundFeatureWrapper() + private val sessionFeature = ViewBoundFeatureWrapper() private val windowFeature = ViewBoundFeatureWrapper() private val contextMenuFeature = ViewBoundFeatureWrapper() @@ -131,20 +134,9 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs val activity = activity as HomeActivity activity.themeManager.applyStatusBarTheme(activity) - val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect - browserStore = StoreProvider.get(this) { + browserFragmentStore = StoreProvider.get(this) { BrowserFragmentStore( - BrowserFragmentState( - quickActionSheetState = QuickActionSheetState( - readable = getSessionById()?.readerable ?: false, - bookmarked = false, - readerActive = getSessionById()?.readerMode ?: false, - bounceNeeded = false, - isAppLink = getSessionById()?.let { - appLink.invoke(it.url).hasExternalApp() - } ?: false - ) - ) + BrowserFragmentState() ) } @@ -173,10 +165,13 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs } val browserToolbarController = DefaultBrowserToolbarController( - requireActivity(), - snackbar, - findNavController(), - (activity as HomeActivity).browsingModeManager, + store = browserFragmentStore, + activity = requireActivity(), + snackbar = snackbar, + navController = findNavController(), + readerModeController = DefaultReaderModeController(readerViewFeature), + browsingModeManager = (activity as HomeActivity).browsingModeManager, + sessionManager = requireComponents.core.sessionManager, findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, browserLayout = view.browserLayout, engineView = engineView, @@ -193,15 +188,14 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs action = Intent.ACTION_VIEW flags = Intent.FLAG_ACTIVITY_NEW_TASK }, - bottomSheetBehavior = QuickActionSheetBehavior.from(nestedScrollQuickAction), + bookmarkTapped = { lifecycleScope.launch { bookmarkTapped(it) } }, scope = lifecycleScope, tabCollectionStorage = requireComponents.core.tabCollectionStorage ) - browserInteractor = - createBrowserToolbarViewInteractor( - browserToolbarController, - customTabSessionId?.let { sessionManager.findSessionById(it) }) + browserInteractor = BrowserInteractor( + browserToolbarController = browserToolbarController + ) browserToolbarView = BrowserToolbarView( container = view.browserLayout, @@ -368,7 +362,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE activity?.enterToImmersiveMode() toolbar.visibility = View.GONE - nestedScrollQuickAction.visibility = View.GONE } else { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER activity?.exitImmersiveModeIfNeeded() @@ -376,7 +369,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs activity.themeManager.applyStatusBarTheme(activity) } toolbar.visibility = View.VISIBLE - nestedScrollQuickAction.visibility = View.VISIBLE } updateLayoutMargins(inFullScreen) }, @@ -557,11 +549,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs return false } - protected abstract fun createBrowserToolbarViewInteractor( - browserToolbarController: BrowserToolbarController, - session: Session? - ): BrowserToolbarViewInteractor - protected abstract fun navToQuickSettingsSheet( session: Session, sitePermissions: SitePermissions? @@ -644,6 +631,48 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs } } + private suspend fun bookmarkTapped(session: Session) = withContext(IO) { + val bookmarksStorage = requireComponents.core.bookmarksStorage + val existing = + bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url } + if (existing != null) { + // Bookmark exists, go to edit fragment + withContext(Main) { + nav( + R.id.browserFragment, + BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid) + ) + } + } else { + // Save bookmark, then go to edit fragment + val guid = bookmarksStorage.addItem( + BookmarkRoot.Mobile.id, + url = session.url, + title = session.title, + position = null + ) + + withContext(Main) { + requireComponents.analytics.metrics.track(Event.AddBookmark) + + view?.let { view -> + FenixSnackbar.make(view, Snackbar.LENGTH_LONG) + .setAnchorView(browserToolbarView.view) + .setAction(getString(R.string.edit_bookmark_snackbar_action)) { + nav( + R.id.browserFragment, + BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment( + guid + ) + ) + } + .setText(getString(R.string.bookmark_saved_snackbar)) + .show() + } + } + } + } + companion object { private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id" private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 66b724627..b38020960 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -20,19 +20,13 @@ import android.widget.RadioButton import androidx.appcompat.widget.AppCompatImageView import androidx.core.content.ContextCompat import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.fragment_browser.* -import kotlinx.android.synthetic.main.fragment_browser.view.* -import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main +import kotlinx.android.synthetic.main.fragment_browser.view.browserLayout +import kotlinx.android.synthetic.main.fragment_browser.view.readerViewControlsBar +import kotlinx.android.synthetic.main.fragment_home.bottom_bar +import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.onboarding_message import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.session.Session import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.readerview.ReaderViewFeature @@ -40,18 +34,12 @@ import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.BackHandler -import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.jetbrains.anko.dimen import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.components.toolbar.BrowserInteractor -import org.mozilla.fenix.components.toolbar.BrowserToolbarController -import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor -import org.mozilla.fenix.components.toolbar.QuickActionSheetAction import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getDimenInDip import org.mozilla.fenix.ext.increaseTapArea @@ -61,9 +49,6 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.home.sessioncontrol.TabCollection import org.mozilla.fenix.mvi.getManagedEmitter -import org.mozilla.fenix.quickactionsheet.DefaultQuickActionSheetController -import org.mozilla.fenix.quickactionsheet.QuickActionSheetSessionObserver -import org.mozilla.fenix.quickactionsheet.QuickActionSheetView /** * Fragment used for browsing the web within the main app. @@ -71,10 +56,6 @@ import org.mozilla.fenix.quickactionsheet.QuickActionSheetView @ExperimentalCoroutinesApi @Suppress("TooManyFunctions", "LargeClass") class BrowserFragment : BaseBrowserFragment(), BackHandler { - private lateinit var quickActionSheetView: QuickActionSheetView - private var quickActionSheetSessionObserver: QuickActionSheetSessionObserver? = null - - private val readerViewFeature = ViewBoundFeatureWrapper() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -114,15 +95,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { if (available) { context.components.analytics.metrics.track(Event.ReaderModeAvailable) } - - browserStore.apply { - dispatch(QuickActionSheetAction.ReadableStateChange(available)) - dispatch( - QuickActionSheetAction.ReaderActiveStateChange( - sessionManager.selectedSession?.readerMode ?: false - ) - ) - } }, owner = this, view = view @@ -134,8 +106,7 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { themeReaderViewControlsForPrivateMode(view.readerViewControlsBar) } - consumeFrom(browserStore) { - quickActionSheetView.update(it) + consumeFrom(browserFragmentStore) { browserToolbarView.update(it) } } @@ -144,13 +115,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { override fun onStart() { super.onStart() subscribeToTabCollections() - quickActionSheetSessionObserver = QuickActionSheetSessionObserver( - lifecycleScope, - requireComponents, - dispatch = { action -> browserStore.dispatch(action) } - ).also { observer -> - getSessionById()?.register(observer, this, autoPause = true) - } getSessionById()?.register(toolbarSessionObserver, this, autoPause = true) } @@ -173,7 +137,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { * This fixes issue #5254. */ (activity as HomeActivity).updateThemeForSession(it) - quickActionSheetSessionObserver?.updateBookmarkState(it) } requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } @@ -182,34 +145,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { return readerViewFeature.onBackPressed() || super.onBackPressed() } - override fun createBrowserToolbarViewInteractor( - browserToolbarController: BrowserToolbarController, - session: Session? - ): BrowserToolbarViewInteractor { - val context = requireContext() - - val interactor = BrowserInteractor( - context = context, - store = browserStore, - browserToolbarController = browserToolbarController, - quickActionSheetController = DefaultQuickActionSheetController( - context = context, - navController = findNavController(), - sessionManager = context.components.core.sessionManager, - appLinksUseCases = context.components.useCases.appLinksUseCases, - bookmarkTapped = { - lifecycleScope.launch { bookmarkTapped(it) } - } - ), - readerModeController = DefaultReaderModeController(readerViewFeature), - currentSession = session - ) - - quickActionSheetView = QuickActionSheetView(view!!.nestedScrollQuickAction, interactor) - - return interactor - } - override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) { val directions = BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( @@ -241,8 +176,8 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { } override fun getEngineMargins(): Pair { - val toolbarAndQASSize = resources.getDimensionPixelSize(R.dimen.toolbar_and_qab_height) - return 0 to toolbarAndQASSize + val toolbarSize = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) + return 0 to toolbarSize } override fun getAppropriateLayoutGravity() = Gravity.BOTTOM @@ -277,51 +212,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { } } - private suspend fun bookmarkTapped(session: Session) = withContext(IO) { - val bookmarksStorage = requireComponents.core.bookmarksStorage - val existing = - bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url } - if (existing != null) { - // Bookmark exists, go to edit fragment - withContext(Main) { - nav( - R.id.browserFragment, - BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid) - ) - } - } else { - // Save bookmark, then go to edit fragment - val guid = bookmarksStorage.addItem( - BookmarkRoot.Mobile.id, - url = session.url, - title = session.title, - position = null - ) - - withContext(Main) { - browserStore.dispatch( - QuickActionSheetAction.BookmarkedStateChange(bookmarked = true) - ) - requireComponents.analytics.metrics.track(Event.AddBookmark) - - view?.let { view -> - FenixSnackbar.make(view, Snackbar.LENGTH_LONG) - .setAnchorView(browserToolbarView.view) - .setAction(getString(R.string.edit_bookmark_snackbar_action)) { - nav( - R.id.browserFragment, - BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment( - guid - ) - ) - } - .setText(getString(R.string.bookmark_saved_snackbar)) - .show() - } - } - } - } - private fun subscribeToTabCollections() { requireComponents.core.tabCollectionStorage.getCollections().observe(this, Observer { requireComponents.core.tabCollectionStorage.cachedTabCollections = it @@ -333,11 +223,6 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { }) } - override fun onSessionSelected(session: Session) { - super.onSessionSelected(session) - quickActionSheetSessionObserver?.updateBookmarkState(session) - } - private val collectionStorageObserver = object : TabCollectionStorage.Observer { override fun onCollectionCreated(title: String, sessions: List) { showTabSavedToCollectionSnackbar() @@ -415,7 +300,7 @@ class BrowserFragment : BaseBrowserFragment(), BackHandler { view, FenixSnackbarDelegate( view, - nestedScrollQuickAction + bottom_bar ) ) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index a73cf3233..cdd1ffa0e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -31,7 +31,6 @@ import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.QrScanner -import org.mozilla.fenix.GleanMetrics.QuickActionSheet import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.SearchShortcuts @@ -138,24 +137,6 @@ private val Event.wrapper: EventWrapper<*>? { Events.browserMenuAction.record(it) }, { Events.browserMenuActionKeys.valueOf(it) } ) - is Event.QuickActionSheetOpened -> EventWrapper( - { QuickActionSheet.opened.record(it) } - ) - is Event.QuickActionSheetClosed -> EventWrapper( - { QuickActionSheet.closed.record(it) } - ) - is Event.QuickActionSheetShareTapped -> EventWrapper( - { QuickActionSheet.shareTapped.record(it) } - ) - is Event.QuickActionSheetBookmarkTapped -> EventWrapper( - { QuickActionSheet.bookmarkTapped.record(it) } - ) - is Event.QuickActionSheetDownloadTapped -> EventWrapper( - { QuickActionSheet.downloadTapped.record(it) } - ) - is Event.QuickActionSheetOpenInAppTapped -> EventWrapper( - { QuickActionSheet.openAppTapped.record(it) } - ) is Event.OpenedBookmarkInNewTab -> EventWrapper( { BookmarksManagement.openInNewTab.record(it) } ) diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 92cd478c3..0cd7d3a3b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -50,12 +50,6 @@ sealed class Event { object AddBookmarkFolder : Event() object RemoveBookmarkFolder : Event() object RemoveBookmarks : Event() - object QuickActionSheetOpened : Event() - object QuickActionSheetClosed : Event() - object QuickActionSheetShareTapped : Event() - object QuickActionSheetBookmarkTapped : Event() - object QuickActionSheetDownloadTapped : Event() - object QuickActionSheetOpenInAppTapped : Event() object CustomTabsClosed : Event() object CustomTabsActionTapped : Event() object CustomTabsMenuOpened : Event() @@ -300,7 +294,8 @@ sealed class Event { enum class Item { SETTINGS, LIBRARY, HELP, DESKTOP_VIEW_ON, DESKTOP_VIEW_OFF, FIND_IN_PAGE, NEW_TAB, NEW_PRIVATE_TAB, SHARE, REPORT_SITE_ISSUE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX, - SAVE_TO_COLLECTION, ADD_TO_HOMESCREEN, QUIT + SAVE_TO_COLLECTION, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON, READER_MODE_OFF, OPEN_IN_APP, + BOOKMARK, READER_MODE_APPEARANCE } override val extras: Map? diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserFragmentStore.kt index bd9f6ae75..b820955ca 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserFragmentStore.kt @@ -2,51 +2,27 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@file:Suppress("unused", "UNUSED_PARAMETER") + package org.mozilla.fenix.components.toolbar import mozilla.components.lib.state.Action import mozilla.components.lib.state.State import mozilla.components.lib.state.Store +// The state that used to live in this class was moved into another component in #4281. Keeping +// the shell of this file because we will need to expand it as we add additional features to +// the browser. class BrowserFragmentStore(initialState: BrowserFragmentState) : Store(initialState, ::browserStateReducer) /** * The state for the Browser Screen - * @property quickActionSheetState: state of the quick action sheet */ -data class BrowserFragmentState( - val quickActionSheetState: QuickActionSheetState -) : State - -/** - * The state for the QuickActionSheet - * @property readable Whether or not the current session can display a reader view - * @property bookmarked Whether or not the current session is already bookmarked - * @property readerActive Whether or not the current session is in reader mode - * @property bounceNeeded Whether or not the quick action sheet should bounce - */ -data class QuickActionSheetState( - val readable: Boolean, - val bookmarked: Boolean, - val readerActive: Boolean, - val bounceNeeded: Boolean, - val isAppLink: Boolean -) : State +class BrowserFragmentState : State sealed class BrowserFragmentAction : Action -/** - * Actions to dispatch through the [QuickActionSheetStore] to modify [QuickActionSheetState] through the reducer. - */ -sealed class QuickActionSheetAction : BrowserFragmentAction() { - data class BookmarkedStateChange(val bookmarked: Boolean) : QuickActionSheetAction() - data class ReadableStateChange(val readable: Boolean) : QuickActionSheetAction() - data class ReaderActiveStateChange(val active: Boolean) : QuickActionSheetAction() - data class AppLinkStateChange(val isAppLink: Boolean) : QuickActionSheetAction() - object BounceNeededChange : QuickActionSheetAction() -} - /** * Reducers for [BrowserFragmentStore]. * @@ -57,30 +33,7 @@ private fun browserStateReducer( state: BrowserFragmentState, action: BrowserFragmentAction ): BrowserFragmentState { - return when (action) { - is QuickActionSheetAction -> { - QuickActionSheetStateReducer.reduce(state, action) - } - } -} - -/** - * Reduces [QuickActionSheetAction]s to update [BrowserFragmentState]. - */ -internal object QuickActionSheetStateReducer { - fun reduce(state: BrowserFragmentState, action: QuickActionSheetAction): BrowserFragmentState { - return when (action) { - is QuickActionSheetAction.BookmarkedStateChange -> - state.copy(quickActionSheetState = state.quickActionSheetState.copy(bookmarked = action.bookmarked)) - is QuickActionSheetAction.ReadableStateChange -> - state.copy(quickActionSheetState = state.quickActionSheetState.copy(readable = action.readable)) - is QuickActionSheetAction.ReaderActiveStateChange -> - state.copy(quickActionSheetState = state.quickActionSheetState.copy(readerActive = action.active)) - is QuickActionSheetAction.BounceNeededChange -> - state.copy(quickActionSheetState = state.quickActionSheetState.copy(bounceNeeded = true)) - is QuickActionSheetAction.AppLinkStateChange -> { - state.copy(quickActionSheetState = state.quickActionSheetState.copy(isAppLink = action.isAppLink)) - } - } + return when { + else -> BrowserFragmentState() } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt index c80571d6b..ca9473b34 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserInteractor.kt @@ -4,16 +4,7 @@ package org.mozilla.fenix.components.toolbar -import android.content.Context -import mozilla.components.browser.session.Session -import org.mozilla.fenix.browser.readermode.ReaderModeController -import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.metrics -import org.mozilla.fenix.quickactionsheet.QuickActionSheetController -import org.mozilla.fenix.quickactionsheet.QuickActionSheetViewInteractor - -open class BrowserToolbarInteractor( +open class BrowserInteractor( private val browserToolbarController: BrowserToolbarController ) : BrowserToolbarViewInteractor { @@ -37,56 +28,3 @@ open class BrowserToolbarInteractor( browserToolbarController.handleToolbarItemInteraction(item) } } - -class BrowserInteractor( - private val context: Context, - private val store: BrowserFragmentStore, - browserToolbarController: BrowserToolbarController, - private val quickActionSheetController: QuickActionSheetController, - private val readerModeController: ReaderModeController, - private val currentSession: Session? -) : BrowserToolbarInteractor(browserToolbarController), QuickActionSheetViewInteractor { - - override fun onQuickActionSheetOpened() { - context.metrics.track(Event.QuickActionSheetOpened) - } - - override fun onQuickActionSheetClosed() { - context.metrics.track(Event.QuickActionSheetClosed) - } - - override fun onQuickActionSheetSharePressed() { - quickActionSheetController.handleShare() - } - - override fun onQuickActionSheetDownloadPressed() { - quickActionSheetController.handleDownload() - } - - override fun onQuickActionSheetBookmarkPressed() { - quickActionSheetController.handleBookmark() - } - - override fun onQuickActionSheetReadPressed() { - val enabled = - currentSession?.readerMode ?: context.components.core.sessionManager.selectedSession?.readerMode ?: false - - if (enabled) { - context.metrics.track(Event.QuickActionSheetClosed) - readerModeController.hideReaderView() - } else { - context.metrics.track(Event.QuickActionSheetOpened) - readerModeController.showReaderView() - } - store.dispatch(QuickActionSheetAction.ReaderActiveStateChange(!enabled)) - } - - override fun onQuickActionSheetOpenLinkPressed() { - quickActionSheetController.handleOpenLink() - } - - override fun onQuickActionSheetAppearancePressed() { - context.metrics.track(Event.ReaderModeAppearanceOpened) - readerModeController.showControls() - } -} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index f575225a5..4e418e5a5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -11,18 +11,17 @@ import android.graphics.drawable.ColorDrawable import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting -import androidx.core.widget.NestedScrollView import androidx.lifecycle.LifecycleCoroutineScope import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.navigation.NavOptions import androidx.navigation.fragment.FragmentNavigator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.google.android.material.bottomsheet.BottomSheetBehavior import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.EngineView import mozilla.components.support.ktx.kotlin.isUrl import org.mozilla.fenix.NavGraphDirections @@ -31,14 +30,14 @@ import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager -import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.browser.readermode.ReaderModeController import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.TabCollectionStorage +import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.lib.Do -import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit /** @@ -54,10 +53,13 @@ interface BrowserToolbarController { @Suppress("LargeClass") class DefaultBrowserToolbarController( + private val store: BrowserFragmentStore, private val activity: Activity, private val snackbar: FenixSnackbar?, private val navController: NavController, + private val readerModeController: ReaderModeController, private val browsingModeManager: BrowsingModeManager, + private val sessionManager: SessionManager, private val findInPageLauncher: () -> Unit, private val browserLayout: ViewGroup, private val engineView: EngineView, @@ -66,7 +68,7 @@ class DefaultBrowserToolbarController( private val customTabSession: Session?, private val getSupportUrl: () -> String, private val openInFenixIntent: Intent, - private val bottomSheetBehavior: QuickActionSheetBehavior, + private val bookmarkTapped: (Session) -> Unit, private val scope: LifecycleCoroutineScope, private val tabCollectionStorage: TabCollectionStorage ) : BrowserToolbarController { @@ -163,7 +165,6 @@ class DefaultBrowserToolbarController( browsingModeManager.mode = BrowsingMode.Private } ToolbarMenu.Item.FindInPage -> { - bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED findInPageLauncher() activity.components.analytics.metrics.track(Event.FindInPageOpened) } @@ -210,6 +211,35 @@ class DefaultBrowserToolbarController( activity.finish() } ToolbarMenu.Item.Quit -> deleteAndQuit(activity, scope, snackbar) + is ToolbarMenu.Item.ReaderMode -> { + val enabled = currentSession?.readerMode + ?: activity.components.core.sessionManager.selectedSession?.readerMode + ?: false + + if (enabled) { + readerModeController.hideReaderView() + } else { + readerModeController.showReaderView() + } + } + ToolbarMenu.Item.ReaderModeAppearance -> { + readerModeController.showControls() + } + ToolbarMenu.Item.OpenInApp -> { + val appLinksUseCases = + activity.components.useCases.appLinksUseCases + val getRedirect = appLinksUseCases.appLinkRedirect + sessionManager.selectedSession?.let { + val redirect = getRedirect.invoke(it.url) + redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK + appLinksUseCases.openAppLink.invoke(redirect) + } + } + ToolbarMenu.Item.Bookmark -> { + sessionManager.selectedSession?.let { + bookmarkTapped(it) + } + } } } @@ -260,6 +290,16 @@ class DefaultBrowserToolbarController( ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT + is ToolbarMenu.Item.ReaderMode -> + if (item.isChecked) { + Event.BrowserMenuItemTapped.Item.READER_MODE_ON + } else { + Event.BrowserMenuItemTapped.Item.READER_MODE_OFF + } + ToolbarMenu.Item.ReaderModeAppearance -> + Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE + ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP + ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK } activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem)) diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index daaff60ce..37ff701e3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -10,11 +10,14 @@ import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.PopupWindow +import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.google.android.material.snackbar.Snackbar import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.* +import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.copy +import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.paste +import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.paste_and_go import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider import mozilla.components.browser.session.Session import mozilla.components.browser.toolbar.BrowserToolbar @@ -24,6 +27,7 @@ import org.jetbrains.anko.dimen import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.customtabs.CustomTabToolbarMenu +import org.mozilla.fenix.ext.bookmarkStorage import org.mozilla.fenix.ext.components import org.mozilla.fenix.theme.ThemeManager @@ -168,12 +172,18 @@ class BrowserToolbarView( ) } else { DefaultToolbarMenu( - this, + context = this, hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(), requestDesktopStateProvider = { sessionManager.selectedSession?.desktopMode ?: false }, - onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) } + readerModeStateProvider = { + sessionManager.selectedSession?.readerMode ?: false + }, + onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) }, + lifecycleOwner = container.context as AppCompatActivity, + sessionManager = sessionManager, + bookmarksStorage = bookmarkStorage ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index 3a8664061..2c2a1ced2 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -5,12 +5,19 @@ package org.mozilla.fenix.components.toolbar import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.item.BrowserMenuDivider import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuImageSwitch +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.concept.storage.BookmarksStorage import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -19,35 +26,24 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.Settings +@Suppress("LargeClass") // While large, most of the class is very simple class DefaultToolbarMenu( private val context: Context, private val hasAccountProblem: Boolean = false, private val requestDesktopStateProvider: () -> Boolean = { false }, - private val onItemTapped: (ToolbarMenu.Item) -> Unit = {} + private val onItemTapped: (ToolbarMenu.Item) -> Unit = {}, + private val lifecycleOwner: LifecycleOwner, + private val bookmarksStorage: BookmarksStorage, + readerModeStateProvider: () -> Boolean = { false }, + sessionManager: SessionManager ) : ToolbarMenu { + private var currentUrlIsBookmarked = false + private var isBookmarkedJob: Job? = null + override val menuBuilder by lazy { BrowserMenuBuilder(menuItems, endOfMenuAlwaysVisible = true) } override val menuToolbar by lazy { - val back = BrowserMenuItemToolbar.TwoStateButton( - primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_back, - primaryContentDescription = context.getString(R.string.browser_menu_back), - primaryImageTintResource = ThemeManager.resolveAttribute( - R.attr.primaryText, - context - ), - isInPrimaryState = { - context.components.core.sessionManager.selectedSession?.canGoBack ?: true - }, - secondaryImageTintResource = ThemeManager.resolveAttribute( - R.attr.disabled, - context - ), - disableInSecondaryState = true - ) { - onItemTapped.invoke(ToolbarMenu.Item.Back) - } - val forward = BrowserMenuItemToolbar.TwoStateButton( primaryImageResource = mozilla.components.ui.icons.R.drawable.mozac_ic_forward, primaryContentDescription = context.getString(R.string.browser_menu_forward), @@ -93,143 +89,227 @@ class DefaultToolbarMenu( } } - BrowserMenuItemToolbar(listOf(back, forward, refresh)) - } - - private val menuItems by lazy { - val items = mutableListOf( - BrowserMenuImageText( - context.getString(R.string.browser_menu_help), - R.drawable.ic_help, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.Help) - }, - - BrowserMenuHighlightableItem( - label = context.getString(R.string.browser_menu_settings), - imageResource = R.drawable.ic_settings, - iconTintColorResource = if (hasAccountProblem) - R.color.sync_error_text_color else - ThemeManager.resolveAttribute(R.attr.primaryText, context), - textColorResource = if (hasAccountProblem) - R.color.sync_error_text_color else - ThemeManager.resolveAttribute(R.attr.primaryText, context), - highlight = if (hasAccountProblem) { - BrowserMenuHighlightableItem.Highlight( - endImageResource = R.drawable.ic_alert, - backgroundResource = R.drawable.sync_error_background_with_ripple, - colorResource = R.color.sync_error_background_color - ) - } else null - ) { - onItemTapped.invoke(ToolbarMenu.Item.Settings) - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_your_library), - R.drawable.ic_library, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.Library) - }, - - BrowserMenuImageSwitch( - R.drawable.ic_desktop, - context.getString(R.string.browser_menu_desktop_site), - requestDesktopStateProvider - ) { checked -> - onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_add_to_homescreen), - R.drawable.ic_add_to_homescreen, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) - }.apply { - visible = ::shouldShowAddToHomescreen - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_find_in_page), - R.drawable.mozac_ic_search, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.FindInPage) - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_private_tab), - R.drawable.ic_private_browsing, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.NewPrivateTab) - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_new_tab), - R.drawable.ic_new, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.NewTab) - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_share), - R.drawable.mozac_ic_share, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { + val share = BrowserMenuItemToolbar.Button( + imageResource = R.drawable.mozac_ic_share, + contentDescription = context.getString(R.string.browser_menu_share), + iconTintColorResource = primaryTextColor(), + listener = { onItemTapped.invoke(ToolbarMenu.Item.Share) - }, - - BrowserMenuImageText( - context.getString(R.string.browser_menu_report_issue), - R.drawable.ic_report_issues, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.ReportIssue) } ) - if ((context.asActivity() as? HomeActivity)?.browsingModeManager?.mode == BrowsingMode.Normal) { - items.add( - BrowserMenuImageText( - context.getString(R.string.browser_menu_save_to_collection), - R.drawable.ic_tab_collection, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) - } - ) + registerForIsBookmarkedUpdates(sessionManager) + val bookmark = BrowserMenuItemToolbar.TwoStateButton( + primaryImageResource = R.drawable.ic_bookmark_filled, + primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark), + primaryImageTintResource = ThemeManager.resolveAttribute( + R.attr.primaryText, + context + ), + // TwoStateButton.isInPrimaryState must be synchronous, and checking bookmark state is + // relatively slow. The best we can do here is periodically compute and cache a new "is + // bookmarked" state, and use that whenever the menu has been opened. + isInPrimaryState = { currentUrlIsBookmarked }, + secondaryImageResource = R.drawable.ic_bookmark_outline, + secondaryContentDescription = context.getString(R.string.browser_menu_bookmark), + secondaryImageTintResource = ThemeManager.resolveAttribute( + R.attr.primaryText, + context + ), + disableInSecondaryState = false + ) { + if (!currentUrlIsBookmarked) currentUrlIsBookmarked = true + onItemTapped.invoke(ToolbarMenu.Item.Bookmark) } - if (Settings.getInstance(context).shouldDeleteBrowsingDataOnQuit) { - items.add( - BrowserMenuImageText( - context.getString(R.string.delete_browsing_data_on_quit_action), - R.drawable.ic_exit, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.Quit) - } - ) + BrowserMenuItemToolbar(listOf(forward, bookmark, share, refresh)) + } + + private val menuItems by lazy { + // Predicates that are called once, during screen init + val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity) + ?.browsingModeManager?.mode == BrowsingMode.Normal + val shouldDeleteDataOnQuit = Settings.getInstance(context) + .shouldDeleteBrowsingDataOnQuit + + // Predicates that need to be repeatedly called as the session changes + fun shouldShowAddToHomescreen(): Boolean { + return context.components.useCases.webAppUseCases.isPinningSupported() && + context.components.core.sessionManager.selectedSession != null } + fun shouldShowReaderMode(): Boolean = sessionManager.selectedSession?.readerable ?: false + fun shouldShowOpenInApp(): Boolean = sessionManager.selectedSession?.let { session -> + val appLink = + context.components.useCases.appLinksUseCases.appLinkRedirect + appLink(session.url).hasExternalApp() + } ?: false + fun shouldShowReaderAppearance(): Boolean = + sessionManager.selectedSession?.readerMode ?: false - items.add( - BrowserMenuDivider() - ) - - items.add( + listOfNotNull( + help, + settings, + library, + desktopMode, + addToHomescreen.apply { visible = ::shouldShowAddToHomescreen }, + findInPage, + privateTab, + newTab, + reportIssue, + if (shouldShowSaveToCollection) saveToCollection else null, + if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, + readerMode.apply { visible = ::shouldShowReaderMode }, + readerAppearance.apply { visible = ::shouldShowReaderAppearance }, + openInApp.apply { visible = ::shouldShowOpenInApp }, + BrowserMenuDivider(), menuToolbar ) - - items } - private fun shouldShowAddToHomescreen(): Boolean { - return context.components.useCases.webAppUseCases.isPinningSupported() && - context.components.core.sessionManager.selectedSession != null + private val help = BrowserMenuImageText( + context.getString(R.string.browser_menu_help), + R.drawable.ic_help, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Help) + } + + private val settings = BrowserMenuHighlightableItem( + label = context.getString(R.string.browser_menu_settings), + imageResource = R.drawable.ic_settings, + iconTintColorResource = if (hasAccountProblem) + R.color.sync_error_text_color else + primaryTextColor(), + textColorResource = if (hasAccountProblem) + R.color.sync_error_text_color else + primaryTextColor(), + highlight = if (hasAccountProblem) { + BrowserMenuHighlightableItem.Highlight( + endImageResource = R.drawable.ic_alert, + backgroundResource = R.drawable.sync_error_background_with_ripple, + colorResource = R.color.sync_error_background_color + ) + } else null + ) { + onItemTapped.invoke(ToolbarMenu.Item.Settings) + } + + private val library = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_your_library), + imageResource = R.drawable.ic_library, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Library) + } + + private val desktopMode = BrowserMenuImageSwitch( + imageResource = R.drawable.ic_desktop, + label = context.getString(R.string.browser_menu_desktop_site), + initialState = requestDesktopStateProvider + ) { checked -> + onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) + } + + private val addToHomescreen = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_add_to_homescreen), + imageResource = R.drawable.ic_add_to_homescreen, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) + } + + private val findInPage = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_find_in_page), + imageResource = R.drawable.mozac_ic_search, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.FindInPage) + } + + private val privateTab = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_private_tab), + imageResource = R.drawable.ic_private_browsing, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.NewPrivateTab) + } + + private val newTab = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_new_tab), + imageResource = R.drawable.ic_new, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.NewTab) + } + + private val reportIssue = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_report_issue), + imageResource = R.drawable.ic_report_issues, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.ReportIssue) + } + + private val saveToCollection = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_save_to_collection), + imageResource = R.drawable.ic_tab_collection, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) + } + + private val deleteDataOnQuit = BrowserMenuImageText( + label = context.getString(R.string.delete_browsing_data_on_quit_action), + imageResource = R.drawable.ic_exit, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Quit) + } + + private val readerMode = BrowserMenuImageSwitch( + label = context.getString(R.string.browser_menu_read), + imageResource = R.drawable.ic_readermode, + initialState = readerModeStateProvider + ) { checked -> + onItemTapped.invoke(ToolbarMenu.Item.ReaderMode(checked)) + } + + private val readerAppearance = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_read_appearance), + imageResource = R.drawable.ic_readermode_appearance, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance) + } + + private val openInApp = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_open_app_link), + imageResource = R.drawable.ic_app_links, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.OpenInApp) + } + + private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) + + private fun registerForIsBookmarkedUpdates(sessionManager: SessionManager) { + val observer = object : Session.Observer { + override fun onUrlChanged(session: Session, url: String) { + currentUrlIsBookmarked = false + updateCurrentUrlIsBookmarked(url) + } + } + + sessionManager.selectedSession?.url?.let { updateCurrentUrlIsBookmarked(it) } + sessionManager.selectedSession?.register(observer, lifecycleOwner) + } + + private fun updateCurrentUrlIsBookmarked(newUrl: String) { + isBookmarkedJob?.cancel() + isBookmarkedJob = lifecycleOwner.lifecycleScope.launch { + currentUrlIsBookmarked = bookmarksStorage + .getBookmarksWithUrl(newUrl) + .any { it.url == newUrl } + } } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index 29cdeba66..acf2eaeeb 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -26,6 +26,10 @@ interface ToolbarMenu { object SaveToCollection : Item() object AddToHomeScreen : Item() object Quit : Item() + data class ReaderMode(val isChecked: Boolean) : Item() + object OpenInApp : Item() + object Bookmark : Item() + object ReaderModeAppearance : Item() } val menuBuilder: BrowserMenuBuilder diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt index a67263889..0312599ee 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt @@ -99,60 +99,58 @@ class CustomTabToolbarMenu( private val menuItems by lazy { listOf( menuToolbar, - BrowserMenuDivider(), - - BrowserMenuImageText( - context.getString(R.string.browser_menu_share), - R.drawable.mozac_ic_share, - textColorResource = ThemeManager.resolveAttribute( - R.attr.primaryText, - context - ), - iconTintColorResource = ThemeManager.resolveAttribute( - R.attr.primaryText, - context - ) - ) { - onItemTapped.invoke(ToolbarMenu.Item.Share) - }, - - BrowserMenuSwitch(context.getString(R.string.browser_menu_desktop_site), - { session?.desktopMode ?: false }, { checked -> - onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) - }), - - BrowserMenuImageText( - context.getString(R.string.browser_menu_find_in_page), - R.drawable.mozac_ic_search, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) { - onItemTapped.invoke(ToolbarMenu.Item.FindInPage) - }, - - SimpleBrowserMenuItem( - { - val appName = context.getString(R.string.app_name) - context.getString(R.string.browser_menu_open_in_fenix, appName) - }(), - textColorResource = ThemeManager.resolveAttribute( - R.attr.primaryText, - context - ) - ) { - onItemTapped.invoke(ToolbarMenu.Item.OpenInFenix) - }, - + share, + desktopMode, + findInPage, + openInFenix, BrowserMenuDivider(), - - SimpleBrowserMenuItem( - { - val appName = context.getString(R.string.app_name) - context.getString(R.string.browser_menu_powered_by2, appName).toUpperCase() - }(), - ToolbarMenu.CAPTION_TEXT_SIZE, - ThemeManager.resolveAttribute(R.attr.primaryText, context) - ) + poweredBy ) } + + private val share = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_share), + imageResource = R.drawable.mozac_ic_share, + textColorResource = primaryTextColor(), + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Share) + } + + private val desktopMode = BrowserMenuSwitch( + label = context.getString(R.string.browser_menu_desktop_site), + initialState = { session?.desktopMode ?: false }, listener = { checked -> + onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) + }) + + private val findInPage = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_find_in_page), + imageResource = R.drawable.mozac_ic_search, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.FindInPage) + } + + private val openInFenix = SimpleBrowserMenuItem( + label = { + val appName = context.getString(R.string.app_name) + context.getString(R.string.browser_menu_open_in_fenix, appName) + }(), + textSize = ToolbarMenu.CAPTION_TEXT_SIZE, + textColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.OpenInFenix) + } + + private val poweredBy = SimpleBrowserMenuItem( + label = { + val appName = context.getString(R.string.app_name) + context.getString(R.string.browser_menu_powered_by, appName).toUpperCase() + }(), + textSize = ToolbarMenu.CAPTION_TEXT_SIZE, + textColorResource = primaryTextColor() + ) + + private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) } diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt index 96a01a274..b93c7d7b2 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt @@ -9,7 +9,6 @@ import android.view.Gravity import android.view.View import androidx.appcompat.content.res.AppCompatResources import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.widget.NestedScrollView import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieDrawable import mozilla.components.browser.session.SessionManager @@ -28,7 +27,6 @@ class CustomTabsIntegration( toolbar: BrowserToolbar, sessionId: String, activity: Activity, - quickActionbar: NestedScrollView, engineLayout: View, onItemTapped: (ToolbarMenu.Item) -> Unit = {} ) : LifecycleAwareFeature, BackHandler { @@ -52,9 +50,6 @@ class CustomTabsIntegration( } } - // Hide the Quick Action Bar. - quickActionbar.visibility = View.GONE - val task = LottieCompositionFactory .fromRawRes( activity, diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 5e2683783..c10aa095e 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -32,8 +32,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.browser.BaseBrowserFragment import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.FenixSnackbarDelegate -import org.mozilla.fenix.components.toolbar.BrowserToolbarController -import org.mozilla.fenix.components.toolbar.BrowserToolbarInteractor import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents @@ -63,12 +61,11 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { customTabSessionId?.let { customTabSessionId -> customTabsIntegration.set( feature = CustomTabsIntegration( - requireComponents.core.sessionManager, - toolbar, - customTabSessionId, - activity, - view.nestedScrollQuickAction, - view.swipeRefresh, + sessionManager = requireComponents.core.sessionManager, + toolbar = toolbar, + sessionId = customTabSessionId, + activity = activity, + engineLayout = view.swipeRefresh, onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) } ), owner = this, @@ -123,7 +120,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { } } - consumeFrom(browserStore) { + consumeFrom(browserFragmentStore) { browserToolbarView.update(it) } @@ -146,11 +143,6 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded() } - override fun createBrowserToolbarViewInteractor( - browserToolbarController: BrowserToolbarController, - session: Session? - ) = BrowserToolbarInteractor(browserToolbarController) - override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) { val directions = ExternalAppBrowserFragmentDirections .actionExternalAppBrowserFragmentToQuickSettingsSheetDialogFragment( diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt deleted file mode 100644 index 1963dd569..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheet.kt +++ /dev/null @@ -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 - - 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 - ) : 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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetBehavior.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetBehavior.kt deleted file mode 100644 index d22639bdc..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetBehavior.kt +++ /dev/null @@ -1,1351 +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/. */ - -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.mozilla.fenix.quickactionsheet - -import android.animation.ValueAnimator -import android.content.Context -import android.os.Parcel -import android.os.Parcelable -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.View -import android.view.ViewConfiguration -import android.view.ViewGroup -import android.view.accessibility.AccessibilityEvent -import androidx.annotation.IntDef -import androidx.annotation.RestrictTo -import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP -import androidx.annotation.VisibleForTesting -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams -import androidx.core.math.MathUtils -import androidx.core.view.ViewCompat -import androidx.customview.widget.ViewDragHelper -import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.shape.ShapeAppearanceModel -import mozilla.components.browser.toolbar.BrowserToolbar -import org.mozilla.fenix.R -import java.lang.ref.WeakReference -import kotlin.math.abs -import kotlin.math.max - -/** - * An interaction behavior plugin for a child view of [CoordinatorLayout] to make it work as a - * bottom sheet. This custom behavior is for non-modal bottom sheets that should not block accessibility - * access to the rest of the screen controls. - */ -@Suppress("TooManyFunctions", "ComplexMethod", "LargeClass") -open class QuickActionSheetBehavior(context: Context, attrs: AttributeSet) : - CoordinatorLayout.Behavior(context, attrs) { - - /** - * Save flags to be preserved in bottomsheet on configuration change. - * - * @param flags bitwise int of [.SAVE_PEEK_HEIGHT], [.SAVE_FIT_TO_CONTENTS], - * [.SAVE_HIDEABLE], [.SAVE_SKIP_COLLAPSED], [.SAVE_ALL] and - * [.SAVE_NONE]. - * @see .getSaveFlags - * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_saveFlags - */ - var saveFlags = SAVE_NONE - - private var fitToContents = true - - private val maximumVelocity: Float - - /** Peek height set by the user. */ - private var peekHeight: Int = 0 - - /** Whether or not to use automatic peek height. */ - private var peekHeightAuto: Boolean = false - - /** Minimum peek height permitted. */ - @get:VisibleForTesting - internal var peekHeightMin: Int = 0 - private set - - /** True if Behavior has a non-null value for the @shapeAppearance attribute */ -// private val shapeThemingEnabled: Boolean - - private var materialShapeDrawable: MaterialShapeDrawable? = null - - /** Default Shape Appearance to be used in bottomsheet */ - private var shapeAppearanceModelDefault: ShapeAppearanceModel? = null - - private var interpolatorAnimator: ValueAnimator? = null - - internal var expandedOffset: Int = 0 - - internal var fitToContentsOffset: Int = 0 - - internal var halfExpandedOffset: Int = 0 - - internal var halfExpandedRatio = HALF_EXPANDED_RATIO_DEFAULT - - internal var collapsedOffset: Int = 0 - - internal var elevation = -1f - - internal var hideable: Boolean = false - - /** - * Sets whether this bottom sheet should skip the collapsed state when it is being hidden after it - * is expanded once. Setting this to true has no effect unless the sheet is hideable. - * - * @param skipCollapsed True if the bottom sheet should skip the collapsed state. - * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_skipCollapsed - */ - var skipCollapsed: Boolean = false - - @State - var state - get() = internalState - set(value) { - @State val previousState = this.internalState - if (value == this.internalState) { - return - } - if (viewRef == null) { - // The view is not laid out yet; modify mState and let onLayoutChild handle it later - @Suppress("ComplexCondition") - if ((value == STATE_COLLAPSED || value == STATE_EXPANDED || - value == STATE_HALF_EXPANDED || (hideable && value == STATE_HIDDEN)) - ) { - this.internalState = value - } - return - } - startSettlingAnimationPendingLayout(value) - updateDrawableOnStateChange(value, previousState) - } - - @State - internal var internalState = STATE_COLLAPSED - - internal var viewDragHelper: ViewDragHelper? = null - - private var ignoreEvents: Boolean = false - - private var lastNestedScrollDy: Int = 0 - - private var nestedScrolled: Boolean = false - - internal var parentWidth: Int = 0 - internal var parentHeight: Int = 0 - - internal var viewRef: WeakReference? = null - - internal var nestedScrollingChildRef: WeakReference? = null - - private var callback: QuickActionSheetCallback? = null - - private var velocityTracker: VelocityTracker? = null - - internal var activePointerId: Int = 0 - - private var initialY: Int = 0 - - internal var touchingScrollingChild: Boolean = false - - /** - * Sets whether the height of the expanded sheet is determined by the height of its contents, or - * if it is expanded in two stages (half the height of the parent container, full height of parent - * container). Default value is true. - * - * @param fitToContents whether or not to fit the expanded sheet to its contents. - */ - // If sheet is already laid out, recalculate the collapsed offset based on new setting. - // Otherwise, let onLayoutChild handle this later. - // Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents. - var isFitToContents: Boolean - get() = fitToContents - set(fitToContents) { - if (this.fitToContents == fitToContents) { - return - } - this.fitToContents = fitToContents - if (viewRef != null) { - calculateCollapsedOffset() - } - setStateInternal( - if (this.fitToContents && internalState == STATE_HALF_EXPANDED) - STATE_EXPANDED else internalState - ) - } - - /** - * Sets whether this bottom sheet can hide when it is swiped down. - * - * @param hideable `true` to make this bottom sheet hideable. - * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_hideable - */ - // Lift up to collapsed state - var isHideable: Boolean - get() = hideable - set(hideable) { - if (this.hideable != hideable) { - this.hideable = hideable - if (!hideable && internalState == STATE_HIDDEN) { - state = STATE_COLLAPSED - } - } - } - - private val yVelocity: Float - get() { - if (velocityTracker == null) { - return 0f - } - velocityTracker!!.computeCurrentVelocity(PIXELS_PER_SECOND_IN_MS, maximumVelocity) - return velocityTracker!!.getYVelocity(activePointerId) - } - - private val dragCallback = object : ViewDragHelper.Callback() { - - @Suppress("ReturnCount") - override fun tryCaptureView(child: View, pointerId: Int): Boolean { - if (internalState == STATE_DRAGGING) { - return false - } - if (touchingScrollingChild) { - return false - } - if (internalState == STATE_EXPANDED && activePointerId == pointerId) { - val scroll = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null - if (scroll != null && scroll.canScrollVertically(-1)) { - // Let the content scroll up - return false - } - } - return viewRef != null && viewRef!!.get() === child - } - - override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) { - dispatchOnSlide(top) - } - - override fun onViewDragStateChanged(state: Int) { - if (state == ViewDragHelper.STATE_DRAGGING) { - setStateInternal(STATE_DRAGGING) - } - } - - @Suppress("ComplexCondition", "LongMethod") - override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) { - val top: Int - @State val targetState: Int - if (yvel < 0) { // Moving up - if (fitToContents) { - top = fitToContentsOffset - targetState = STATE_EXPANDED - } else { - val currentTop = releasedChild.top - if (currentTop > halfExpandedOffset) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = expandedOffset - targetState = STATE_EXPANDED - } - } - } else if ((hideable && shouldHide( - releasedChild, - yvel - ) && (releasedChild.top > collapsedOffset || abs(xvel) < abs(yvel))) - ) { - // Hide if we shouldn't collapse and the view was either released low or it was a - // vertical swipe. - top = parentHeight - targetState = STATE_HIDDEN - } else if (yvel == 0f || abs(xvel) > abs(yvel)) { - // If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity - // being greater than the Y velocity, settle to the nearest correct height. - val currentTop = releasedChild.top - if (fitToContents) { - if ((abs(currentTop - fitToContentsOffset) < abs(currentTop - collapsedOffset))) { - top = fitToContentsOffset - targetState = STATE_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } else { - if (currentTop < halfExpandedOffset) { - if (currentTop < abs(currentTop - collapsedOffset)) { - top = expandedOffset - targetState = STATE_EXPANDED - } else { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } - } else { - if ((abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset))) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } - } - } else { // Moving Down - if (fitToContents) { - top = collapsedOffset - targetState = STATE_COLLAPSED - } else { - // Settle to the nearest correct height. - val currentTop = releasedChild.top - if ((abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset))) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } - } - if (viewDragHelper!!.settleCapturedViewAt(releasedChild.left, top)) { - setStateInternal(STATE_SETTLING) - if (targetState == STATE_EXPANDED && interpolatorAnimator != null) { - interpolatorAnimator!!.reverse() - } - ViewCompat.postOnAnimation( - releasedChild, SettleRunnable(releasedChild, targetState) - ) - } else { - if (targetState == STATE_EXPANDED && interpolatorAnimator != null) { - interpolatorAnimator!!.reverse() - } - setStateInternal(targetState) - } - } - - override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { - return MathUtils.clamp( - top, getExpandedOffset(), if (hideable) parentHeight else collapsedOffset - ) - } - - override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int { - return child.left - } - - override fun getViewVerticalDragRange(child: View): Int { - return if (hideable) { - parentHeight - } else { - collapsedOffset - } - } - } - - /** Callback for monitoring events about bottom sheets. */ - interface QuickActionSheetCallback { - - /** - * Called when the bottom sheet changes its state. - * - * @param bottomSheet The bottom sheet view. - * @param newState The new state. This will be one of [.STATE_DRAGGING], [ ][.STATE_SETTLING], - * [.STATE_EXPANDED], [.STATE_COLLAPSED], [ ][.STATE_HIDDEN], or [.STATE_HALF_EXPANDED]. - */ - fun onStateChanged(bottomSheet: View, @State newState: Int) - - /** - * Called when the bottom sheet is being dragged. - * - * @param bottomSheet The bottom sheet view. - * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset increases - * as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and - * expanded states and from -1 to 0 it is between hidden and collapsed states. - */ - fun onSlide(bottomSheet: View, slideOffset: Float) - } - - /** @hide - */ - @RestrictTo(LIBRARY_GROUP) - @IntDef(STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN, STATE_HALF_EXPANDED) - @kotlin.annotation.Retention(AnnotationRetention.SOURCE) - annotation class State - - init { - val a = context.obtainStyledAttributes(attrs, R.styleable.QuickActionSheetBehavior_Layout) -// this.shapeThemingEnabled = a.hasValue(R.styleable.QuickActionSheetBehavior_Layout_shapeAppearance) -// val hasBackgroundTint = a.hasValue(R.styleable.QuickActionSheetBehavior_Layout_backgroundTint) -// if (hasBackgroundTint) { -// val bottomSheetColor = MaterialResources.getColorStateList( -// context, a, R.styleable.QuickActionSheetBehavior_Layout_backgroundTint -// ) -// createMaterialShapeDrawable(context, attrs, hasBackgroundTint, bottomSheetColor) -// } else { -// createMaterialShapeDrawable(context, attrs, hasBackgroundTint) -// } - createShapeValueAnimator() - this.elevation = a.getDimension(R.styleable.QuickActionSheetBehavior_Layout_android_elevation, -1f) - val value = a.peekValue(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_peekHeight) - if (value != null && value.data == PEEK_HEIGHT_AUTO) { - setPeekHeight(value.data) - } else { - setPeekHeight( - a.getDimensionPixelSize( - R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_peekHeight, PEEK_HEIGHT_AUTO - ) - ) - } - isHideable = a.getBoolean(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_hideable, false) - isFitToContents = a.getBoolean(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_fitToContents, true) - skipCollapsed = a.getBoolean(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_skipCollapsed, false) - saveFlags = a.getInt(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_saveFlags, SAVE_NONE) - setHalfExpandedRatio( - a.getFloat( - R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_halfExpandedRatio, - HALF_EXPANDED_RATIO_DEFAULT - ) - ) - setExpandedOffset(a.getInt(R.styleable.QuickActionSheetBehavior_Layout_mozac_behavior_expandedOffset, 0)) - a.recycle() - val configuration = ViewConfiguration.get(context) - maximumVelocity = configuration.scaledMaximumFlingVelocity.toFloat() - } - - override fun onSaveInstanceState(parent: CoordinatorLayout, child: V): Parcelable? { - return super.onSaveInstanceState(parent, child)?.let { SavedState(it, this) } - } - - override fun onRestoreInstanceState(parent: CoordinatorLayout, child: V, state: Parcelable) { - val ss = state as SavedState - super.onRestoreInstanceState(parent, child, ss.superState!!) - // Restore Optional State values designated by saveFlags - restoreOptionalState(ss) - // Intermediate states are restored as collapsed state - if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { - this.internalState = STATE_COLLAPSED - } else { - this.internalState = ss.state - } - } - - override fun onAttachedToLayoutParams(layoutParams: LayoutParams) { - super.onAttachedToLayoutParams(layoutParams) - // These may already be null, but just be safe, explicitly assign them. This lets us know the - // first time we layout with this behavior by checking (viewRef == null). - viewRef = null - viewDragHelper = null - } - - override fun onDetachedFromLayoutParams() { - super.onDetachedFromLayoutParams() - // Release references so we don't run unnecessary codepaths while not attached to a view. - viewRef = null - viewDragHelper = null - } - - override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { - if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) { - child.fitsSystemWindows = true - } - // Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will - // default to android:background declared in styles or layout. -// if (shapeThemingEnabled && materialShapeDrawable != null) { -// ViewCompat.setBackground(child, materialShapeDrawable) -// } - // Set elevation on MaterialShapeDrawable - if (materialShapeDrawable != null) { - // Use elevation attr if set on bottomsheet; otherwise, use elevation of child view. - materialShapeDrawable!!.elevation = if (elevation == -1f) ViewCompat.getElevation(child) else elevation - } - - if (viewRef == null) { - // First layout with this behavior. - peekHeightMin = parent.resources.getDimensionPixelSize(R.dimen.design_quick_action_sheet_peek_height_min) - viewRef = WeakReference(child) - } - if (viewDragHelper == null) { - viewDragHelper = ViewDragHelper.create(parent, dragCallback) - } - - val savedTop = child.top - // First let the parent lay it out - parent.onLayoutChild(child, layoutDirection) - // Offset the bottom sheet - parentWidth = parent.width - parentHeight = parent.height - fitToContentsOffset = max(0, parentHeight - child.height) - calculateHalfExpandedOffset() - calculateCollapsedOffset() - - if (internalState == STATE_EXPANDED) { - ViewCompat.offsetTopAndBottom(child, getExpandedOffset()) - } else if (internalState == STATE_HALF_EXPANDED) { - ViewCompat.offsetTopAndBottom(child, halfExpandedOffset) - } else if (hideable && internalState == STATE_HIDDEN) { - ViewCompat.offsetTopAndBottom(child, parentHeight) - } else if (internalState == STATE_COLLAPSED) { - ViewCompat.offsetTopAndBottom(child, collapsedOffset) - } else if (internalState == STATE_DRAGGING || internalState == STATE_SETTLING) { - ViewCompat.offsetTopAndBottom(child, savedTop - child.top) - } - - nestedScrollingChildRef = WeakReference(findScrollingChild(child)!!) - return true - } - - @Suppress("ReturnCount") - override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean { - if (!child.isShown) { - ignoreEvents = true - return false - } - val action = event.actionMasked - // Record the velocity - if (action == MotionEvent.ACTION_DOWN) { - reset() - } - if (velocityTracker == null) { - velocityTracker = VelocityTracker.obtain() - } - velocityTracker!!.addMovement(event) - when (action) { - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - touchingScrollingChild = false - activePointerId = MotionEvent.INVALID_POINTER_ID - // Reset the ignore flag - if (ignoreEvents) { - ignoreEvents = false - return false - } - } - MotionEvent.ACTION_DOWN -> { - val initialX = event.x.toInt() - initialY = event.y.toInt() - // Only intercept nested scrolling events here if the view not being moved by the - // ViewDragHelper. - if (internalState != STATE_SETTLING) { - val scroll = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null - if (scroll != null && parent.isPointInChildBounds(scroll, initialX, initialY)) { - activePointerId = event.getPointerId(event.actionIndex) - touchingScrollingChild = true - } - } - ignoreEvents = (activePointerId == MotionEvent.INVALID_POINTER_ID && !parent.isPointInChildBounds( - child, - initialX, - initialY - )) - } - } // fall out - if ((!ignoreEvents && viewDragHelper != null && viewDragHelper!!.shouldInterceptTouchEvent(event)) - ) { - return true - } - // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because - // it is not the top most view of its parent. This is not necessary when the touch event is - // happening over the scrolling content as nested scrolling logic handles that case. - val scroll = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null - return (action == MotionEvent.ACTION_MOVE && scroll != null && - !ignoreEvents && internalState != STATE_DRAGGING && !parent.isPointInChildBounds( - scroll, - event.x.toInt(), - event.y.toInt() - ) && viewDragHelper != null && abs(initialY - event.y) > viewDragHelper!!.touchSlop) - } - - @Suppress("CollapsibleIfStatements") - override fun onTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean { - val action = event.actionMasked - when { - !child.isShown -> return false - internalState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN -> return true - } - viewDragHelper?.processTouchEvent(event) - - // Record the velocity - if (action == MotionEvent.ACTION_DOWN) { - reset() - } - if (velocityTracker == null) { - velocityTracker = VelocityTracker.obtain() - } - velocityTracker!!.addMovement(event) - // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it - // to capture the bottom sheet in case it is not captured and the touch slop is passed. - if (action == MotionEvent.ACTION_MOVE && !ignoreEvents) { - if (abs(initialY - event.y) > viewDragHelper!!.touchSlop) { - viewDragHelper!!.captureChildView(child, event.getPointerId(event.actionIndex)) - } - } - return !ignoreEvents - } - - override fun onStartNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - directTargetChild: View, - target: View, - axes: Int, - type: Int - ): Boolean { - lastNestedScrollDy = 0 - nestedScrolled = false - return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0 - } - - override fun onNestedPreScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - dx: Int, - dy: Int, - consumed: IntArray, - type: Int - ) { - if (type == ViewCompat.TYPE_NON_TOUCH) { - // Ignore fling here. The ViewDragHelper handles it. - return - } - val scrollingChild = if (nestedScrollingChildRef != null) nestedScrollingChildRef!!.get() else null - if (target !== scrollingChild) { - return - } - val currentTop = child.top - val newTop = currentTop - dy - if (dy > 0) { // Upward - if (newTop < getExpandedOffset()) { - consumed[1] = currentTop - getExpandedOffset() - ViewCompat.offsetTopAndBottom(child, -consumed[1]) - setStateInternal(STATE_EXPANDED) - } else { - consumed[1] = dy - ViewCompat.offsetTopAndBottom(child, -dy) - setStateInternal(STATE_DRAGGING) - } - } else if (dy < 0) { // Downward - if (!target.canScrollVertically(-1)) { - if (newTop <= collapsedOffset || hideable) { - consumed[1] = dy - ViewCompat.offsetTopAndBottom(child, -dy) - setStateInternal(STATE_DRAGGING) - } else { - consumed[1] = currentTop - collapsedOffset - ViewCompat.offsetTopAndBottom(child, -consumed[1]) - setStateInternal(STATE_COLLAPSED) - } - } - } - dispatchOnSlide(child.top) - lastNestedScrollDy = dy - nestedScrolled = true - } - - override fun onStopNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - type: Int - ) { - if (child.top == getExpandedOffset()) { - setStateInternal(STATE_EXPANDED) - return - } - if ((nestedScrollingChildRef == null || target !== nestedScrollingChildRef!!.get() || !nestedScrolled) - ) { - return - } - val top: Int - val targetState: Int - if (lastNestedScrollDy > 0) { - top = getExpandedOffset() - targetState = STATE_EXPANDED - } else if (hideable && shouldHide(child, yVelocity)) { - top = parentHeight - targetState = STATE_HIDDEN - } else if (lastNestedScrollDy == 0) { - val currentTop = child.top - if (fitToContents) { - if (abs(currentTop - fitToContentsOffset) < abs(currentTop - collapsedOffset)) { - top = fitToContentsOffset - targetState = STATE_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } else { - if (currentTop < halfExpandedOffset) { - if (currentTop < abs(currentTop - collapsedOffset)) { - top = expandedOffset - targetState = STATE_EXPANDED - } else { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } - } else { - if (abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset)) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } - } - } else { - if (fitToContents) { - top = collapsedOffset - targetState = STATE_COLLAPSED - } else { - // Settle to nearest height. - val currentTop = child.top - if (abs(currentTop - halfExpandedOffset) < abs(currentTop - collapsedOffset)) { - top = halfExpandedOffset - targetState = STATE_HALF_EXPANDED - } else { - top = collapsedOffset - targetState = STATE_COLLAPSED - } - } - } - if (viewDragHelper!!.smoothSlideViewTo(child, child.left, top)) { - setStateInternal(STATE_SETTLING) - ViewCompat.postOnAnimation(child, SettleRunnable(child, targetState)) - } else { - setStateInternal(targetState) - } - nestedScrolled = false - } - - override fun onNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - dxConsumed: Int, - dyConsumed: Int, - dxUnconsumed: Int, - dyUnconsumed: Int, - type: Int, - consumed: IntArray - ) { - // Overridden to prevent the default consumption of the entire scroll distance. - } - - override fun onNestedPreFling( - coordinatorLayout: CoordinatorLayout, - child: V, - target: View, - velocityX: Float, - velocityY: Float - ): Boolean { - return if (nestedScrollingChildRef != null) { - (target === nestedScrollingChildRef!!.get() && ((internalState != STATE_EXPANDED || super.onNestedPreFling( - coordinatorLayout, - child, - target, - velocityX, - velocityY - )))) - } else { - false - } - } - - /** - * Sets the height of the bottom sheet when it is collapsed while optionally animating between the - * old height and the new height. - * - * @param peekHeight The height of the collapsed bottom sheet in pixels, - * or [ ][.PEEK_HEIGHT_AUTO] to configure the sheet to peek automatically at 16:9 ratio keyline. - * @param animate Whether to animate between the old height and the new height. - * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_peekHeight - */ - @Suppress("NestedBlockDepth") - @JvmOverloads - fun setPeekHeight(peekHeight: Int, animate: Boolean = false) { - var layout = false - if (peekHeight == PEEK_HEIGHT_AUTO) { - if (!peekHeightAuto) { - peekHeightAuto = true - layout = true - } - } else if (peekHeightAuto || this.peekHeight != peekHeight) { - peekHeightAuto = false - this.peekHeight = max(0, peekHeight) - layout = true - } - // If sheet is already laid out, recalculate the collapsed offset based on new setting. - // Otherwise, let onLayoutChild handle this later. - if (layout && viewRef != null) { - calculateCollapsedOffset() - if (internalState == STATE_COLLAPSED) { - val view = viewRef!!.get() - if (view != null) { - if (animate) { - startSettlingAnimationPendingLayout(internalState) - } else { - view.requestLayout() - } - } - } - } - } - - /** - * Gets the height of the bottom sheet when it is collapsed. - * - * @return The height of the collapsed bottom sheet in pixels, or [.PEEK_HEIGHT_AUTO] if the - * sheet is configured to peek automatically at 16:9 ratio keyline - * @attr ref com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_peekHeight - */ - @Suppress("unused") - fun getPeekHeight(): Int { - return if (peekHeightAuto) PEEK_HEIGHT_AUTO else peekHeight - } - - /** - * Determines the height of the QuickActionSheet in the [.STATE_HALF_EXPANDED] state. The - * material guidelines recommended a value of 0.5, which results in the sheet filling half of the - * parent. The height of the QuickActionSheet will be smaller as this ratio is decreased and taller as - * it is increased. The default value is 0.5. - * - * @param ratio a float between 0 and 1, representing the [.STATE_HALF_EXPANDED] ratio. - * @attr com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_halfExpandedRatio - */ - fun setHalfExpandedRatio(ratio: Float) { - - if ((ratio <= 0) || (ratio >= 1)) { - throw IllegalArgumentException("ratio must be a float value between 0 and 1") - } - this.halfExpandedRatio = ratio - } - - /** - * Determines the top offset of the QuickActionSheet in the [.STATE_EXPANDED] state when - * fitsToContent is false. The default value is 0, which results in the sheet matching the - * parent's top. - * - * @param offset an integer value greater than equal to 0, representing the [ ][.STATE_EXPANDED] offset. - * Value must not exceed the offset in the half expanded state. - * @attr com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_expandedOffset - */ - fun setExpandedOffset(offset: Int) { - if (offset < 0) { - throw IllegalArgumentException("offset must be greater than or equal to 0") - } - this.expandedOffset = offset - } - - /** - * Gets the ratio for the height of the QuickActionSheet in the [.STATE_HALF_EXPANDED] state. - * - * @attr com.google.android.material.R.styleable#QuickActionSheetBehavior_Layout_behavior_halfExpandedRatio - */ - @Suppress("unused") - fun getHalfExpandedRatio(): Float { - return halfExpandedRatio - } - - /** - * Sets a callback to be notified of bottom sheet events. - * - * @param callback The callback to notify when bottom sheet events occur. - */ - fun setQuickActionSheetCallback(callback: QuickActionSheetCallback) { - this.callback = callback - } - - private fun startSettlingAnimationPendingLayout(@State state: Int) { - val child = viewRef?.get() ?: return - // Start the animation; wait until a pending layout if there is one. - val parent = child.parent - if (parent != null && parent.isLayoutRequested && ViewCompat.isAttachedToWindow(child)) { - val finalState = state - child.post { startSettlingAnimation(child, finalState) } - } else { - startSettlingAnimation(child, state) - } - } - - internal fun setStateInternal(@State state: Int) { - val previousState = this.internalState - - if (this.internalState == state) { - return - } - this.internalState = state - - if (viewRef == null) { - return - } - - val bottomSheet = viewRef!!.get() ?: return - - ViewCompat.setImportantForAccessibility( - bottomSheet, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES - ) - bottomSheet.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) - - updateDrawableOnStateChange(state, previousState) - if (callback != null) { - callback!!.onStateChanged(bottomSheet, state) - } - } - - private fun updateDrawableOnStateChange(@State state: Int, @State previousState: Int) { - if (materialShapeDrawable != null) { - val isOpening = - state == STATE_EXPANDED && (previousState == STATE_HIDDEN || previousState == STATE_COLLAPSED) - // If the QuickActionSheetBehavior's state is set directly to STATE_EXPANDED from - // STATE_HIDDEN or STATE_COLLAPSED, bypassing STATE_DRAGGING, the corner transition animation - // will not be triggered automatically, so we will trigger it here. - if ((isOpening && interpolatorAnimator != null && interpolatorAnimator!!.animatedFraction == 1f) - ) { - interpolatorAnimator!!.reverse() - } - if ((state == STATE_DRAGGING && previousState == STATE_EXPANDED && interpolatorAnimator != null) - ) { - interpolatorAnimator!!.start() - } - } - } - - private fun calculateCollapsedOffset() { - val peek: Int = if (peekHeightAuto) { - max(peekHeightMin, parentHeight - parentWidth * AUTO_ASPECT_RATIO_SHORT / AUTO_ASPECT_RATIO_LONG) - } else { - peekHeight - } - - collapsedOffset = if (fitToContents) { - max(parentHeight - peek, fitToContentsOffset) - } else { - parentHeight - peek - } - } - - private fun calculateHalfExpandedOffset() { - this.halfExpandedOffset = (parentHeight * (1 - halfExpandedRatio)).toInt() - } - - private fun reset() { - activePointerId = ViewDragHelper.INVALID_POINTER - if (velocityTracker != null) { - velocityTracker!!.recycle() - velocityTracker = null - } - } - - private fun restoreOptionalState(ss: SavedState) { - if (this.saveFlags == SAVE_NONE) { - return - } - if (this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_PEEK_HEIGHT) == SAVE_PEEK_HEIGHT) { - this.peekHeight = ss.peekHeight - } - if ((this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_FIT_TO_CONTENTS) == SAVE_FIT_TO_CONTENTS)) { - this.fitToContents = ss.fitToContents - } - if (this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_HIDEABLE) == SAVE_HIDEABLE) { - this.hideable = ss.hideable - } - if ((this.saveFlags == SAVE_ALL || (this.saveFlags and SAVE_SKIP_COLLAPSED) == SAVE_SKIP_COLLAPSED)) { - this.skipCollapsed = ss.skipCollapsed - } - } - - internal fun shouldHide(child: View, yvel: Float): Boolean { - if (skipCollapsed) { - return true - } - if (child.top < collapsedOffset) { - // It should not hide, but collapse. - return false - } - val newTop = child.top + yvel * HIDE_FRICTION - return abs(newTop - collapsedOffset) / peekHeight.toFloat() > HIDE_THRESHOLD - } - - @VisibleForTesting - internal fun findScrollingChild(view: View): View? { - if (ViewCompat.isNestedScrollingEnabled(view)) { - return view - } - if (view is ViewGroup) { - var i = 0 - val count = view.childCount - while (i < count) { - val scrollingChild = findScrollingChild(view.getChildAt(i)) - if (scrollingChild != null) { - return scrollingChild - } - i++ - } - } - return null - } - -// private fun createMaterialShapeDrawable( -// context: Context, attrs: AttributeSet, hasBackgroundTint: Boolean -// ) { -// this.createMaterialShapeDrawable(context, attrs, hasBackgroundTint, null) -// } - -// private fun createMaterialShapeDrawable( -// context: Context, -// attrs: AttributeSet, -// hasBackgroundTint: Boolean, -// bottomSheetColor: ColorStateList? -// ) { -// if (this.shapeThemingEnabled) { -// this.shapeAppearanceModelDefault = -// ShapeAppearanceModel(context, attrs, R.attr.bottomSheetStyle, DEF_STYLE_RES) -// -// this.materialShapeDrawable = MaterialShapeDrawable(shapeAppearanceModelDefault) -// this.materialShapeDrawable!!.initializeElevationOverlay(context) -// -// if (hasBackgroundTint && bottomSheetColor != null) { -// materialShapeDrawable!!.fillColor = bottomSheetColor -// } else { -// // If the tint isn't set, use the theme default background color. -// val defaultColor = TypedValue() -// context.theme.resolveAttribute(android.R.attr.colorBackground, defaultColor, true) -// materialShapeDrawable!!.setTint(defaultColor.data) -// } -// } -// } - - private fun createShapeValueAnimator() { - interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f) - interpolatorAnimator!!.duration = CORNER_ANIMATION_DURATION.toLong() - interpolatorAnimator!!.addUpdateListener { animation -> - val value = animation.animatedValue as Float - if (materialShapeDrawable != null) { - materialShapeDrawable!!.interpolation = value - } - } - } - - private fun getExpandedOffset(): Int { - return if (fitToContents) fitToContentsOffset else expandedOffset - } - - internal fun startSettlingAnimation(child: View?, state: Int) { - var localState = state - var top: Int - if (localState == STATE_COLLAPSED) { - top = collapsedOffset - } else if (localState == STATE_HALF_EXPANDED) { - top = halfExpandedOffset - if (fitToContents && top <= fitToContentsOffset) { - // Skip to the expanded state if we would scroll past the height of the contents. - localState = STATE_EXPANDED - top = fitToContentsOffset - } - } else if (localState == STATE_EXPANDED) { - top = getExpandedOffset() - } else if (hideable && localState == STATE_HIDDEN) { - top = parentHeight - } else { - throw IllegalArgumentException("Illegal state argument: $state") - } - if (viewDragHelper!!.smoothSlideViewTo(child!!, child.left, top)) { - setStateInternal(STATE_SETTLING) - ViewCompat.postOnAnimation(child, SettleRunnable(child, localState)) - } else { - setStateInternal(localState) - } - } - - internal fun dispatchOnSlide(top: Int) { - val bottomSheet = viewRef!!.get() - if (bottomSheet != null && callback != null) { - if (top > collapsedOffset) { - callback!!.onSlide( - bottomSheet, (collapsedOffset - top).toFloat() / (parentHeight - collapsedOffset) - ) - } else { - callback!!.onSlide( - bottomSheet, (collapsedOffset - top).toFloat() / (collapsedOffset - getExpandedOffset()) - ) - } - } - } - - /** - * Disables the shaped corner [ShapeAppearanceModel] interpolation transition animations. - * Will have no effect unless the sheet utilizes a [MaterialShapeDrawable] with set shape - * theming properties. Only For use in UI testing. - */ - @Suppress("unused") - @VisibleForTesting - fun disableShapeAnimations() { - // Sets the shape value animator to null, prevents animations from occuring during testing. - interpolatorAnimator = null - } - - private inner class SettleRunnable internal constructor( - private val view: View, - @param:State @field:State private val targetState: Int - ) : - Runnable { - - override fun run() { - if (viewDragHelper != null && viewDragHelper!!.continueSettling(true)) { - ViewCompat.postOnAnimation(view, this) - } else { - if (internalState == STATE_SETTLING) { - setStateInternal(targetState) - } - } - } - } - - /** State persisted across instances */ - protected class SavedState : QuickActionSavedState { - @State - internal var state: Int = STATE_COLLAPSED - internal var peekHeight: Int = 0 - internal var fitToContents: Boolean = false - internal var hideable: Boolean = false - internal var skipCollapsed: Boolean = false - - @JvmOverloads - constructor(source: Parcel, loader: ClassLoader? = null) : super(source, loader) { - - state = source.readInt() - peekHeight = source.readInt() - fitToContents = source.readInt() == 1 - hideable = source.readInt() == 1 - skipCollapsed = source.readInt() == 1 - } - - constructor(superState: Parcelable, behavior: QuickActionSheetBehavior<*>) : super(superState) { - this.state = behavior.internalState - this.peekHeight = behavior.peekHeight - this.fitToContents = behavior.fitToContents - this.hideable = behavior.hideable - this.skipCollapsed = behavior.skipCollapsed - } - - override fun writeToParcel(dest: Parcel, flags: Int) { - super.writeToParcel(dest, flags) - dest.writeInt(state) - dest.writeInt(peekHeight) - dest.writeInt(if (fitToContents) 1 else 0) - dest.writeInt(if (hideable) 1 else 0) - dest.writeInt(if (skipCollapsed) 1 else 0) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): SavedState { - return SavedState(parcel) - } - - override fun newArray(size: Int): Array { - return newArray(size) - } - } - } - - override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean { - if (dependency is BrowserToolbar) { - return true - } - - return super.layoutDependsOn(parent, child, dependency) - } - - override fun onDependentViewChanged(parent: CoordinatorLayout, child: V, dependency: View): Boolean { - return if (dependency is BrowserToolbar) { - repositionQuickActionSheet(child, dependency) - true - } else { - false - } - } - - private fun repositionQuickActionSheet(quickActionSheetContainer: V, toolbar: BrowserToolbar) { - if (toolbar.translationY >= toolbar.height.toFloat() - POSITION_SNAP_BUFFER) { - internalState = STATE_HIDDEN - } else if (internalState == STATE_HIDDEN || internalState == STATE_SETTLING) { - internalState = STATE_COLLAPSED - } - quickActionSheetContainer.translationY = toolbar.translationY + toolbar.height * -1.0f - } - - companion object { - - /** The bottom sheet is dragging. */ - const val STATE_DRAGGING = 1 - - /** The bottom sheet is settling. */ - const val STATE_SETTLING = 2 - - /** The bottom sheet is expanded. */ - const val STATE_EXPANDED = 3 - - /** The bottom sheet is collapsed. */ - const val STATE_COLLAPSED = 4 - - /** The bottom sheet is hidden. */ - const val STATE_HIDDEN = 5 - - /** The bottom sheet is half-expanded (used when mFitToContents is false). */ - const val STATE_HALF_EXPANDED = 6 - - /** - * Peek at the 16:9 ratio keyline of its parent. - * - * - * This can be used as a parameter for [.setPeekHeight]. [.getPeekHeight] - * will return this when the value is set. - */ - const val PEEK_HEIGHT_AUTO = -1 - - /** - * This flag will preserve the peekHeight int value on configuration change. - */ - const val SAVE_PEEK_HEIGHT = 0x1 - - /** - * This flag will preserve the fitToContents boolean value on configuration change. - */ - const val SAVE_FIT_TO_CONTENTS = 0x2 - - /** - * This flag will preserve the hideable boolean value on configuration change. - */ - const val SAVE_HIDEABLE = 0x4 - - /** - * This flag will preserve the skipCollapsed boolean value on configuration change. - */ - const val SAVE_SKIP_COLLAPSED = 0x8 - - /** - * This flag will preserve all aforementioned values on configuration change. - */ - const val SAVE_ALL = -1 - - /** - * This flag will not preserve the aforementioned values set at runtime if the view is - * destroyed and recreated. The only value preserved will be the positional state, - * e.g. collapsed, hidden, expanded, etc. This is the default behavior. - */ - const val SAVE_NONE = 0 - - private const val HIDE_THRESHOLD = 0.5f - - private const val HIDE_FRICTION = 0.1f - - private const val CORNER_ANIMATION_DURATION = 500 - - private const val PIXELS_PER_SECOND_IN_MS = 1000 - - private const val HALF_EXPANDED_RATIO_DEFAULT = 0.5f - - private const val AUTO_ASPECT_RATIO_SHORT = 9 - - private const val AUTO_ASPECT_RATIO_LONG = 16 - - private const val DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal - - /** - * A utility function to get the [QuickActionSheetBehavior] associated with the `view`. - * - * @param view The [View] with [QuickActionSheetBehavior]. - * @return The [QuickActionSheetBehavior] associated with the `view`. - */ - fun from(view: V): QuickActionSheetBehavior { - val params = view.layoutParams as? LayoutParams - ?: throw IllegalArgumentException("The view is not a child of CoordinatorLayout") - val behavior = params.behavior as? QuickActionSheetBehavior<*> - ?: throw IllegalArgumentException("The view is not associated with QuickActionSheetBehavior") - @Suppress("UNCHECKED_CAST") - return behavior as QuickActionSheetBehavior - } - } -} - -/** - * A [Parcelable] implementation that should be used by inheritance - * hierarchies to ensure the state of all classes along the chain is saved. - */ -abstract class QuickActionSavedState : Parcelable { - - var superState: Parcelable? = null - - /** - * Constructor called by derived classes when creating their SavedState objects - * - * @param superState The state of the superclass of this view - */ - protected constructor(superState: Parcelable? = null) { - this.superState = if (superState !== EMPTY_STATE) superState else null - } - - /** - * Constructor used when reading from a parcel. Reads the state of the superclass. - * - * @param source parcel to read from - * @param loader ClassLoader to use for reading - */ - @JvmOverloads - protected constructor(source: Parcel, loader: ClassLoader? = null) { - val superState = source.readParcelable(loader) - this.superState = superState ?: EMPTY_STATE - } - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeParcelable(superState, flags) - } - - companion object { - val EMPTY_STATE: QuickActionSavedState = object : QuickActionSavedState() {} - - @Suppress("unused") - val CREATOR: Parcelable.Creator = - object : Parcelable.ClassLoaderCreator { - override fun createFromParcel(`in`: Parcel, loader: ClassLoader?): QuickActionSavedState { - val superState = `in`.readParcelable(loader) - if (superState != null) { - throw IllegalStateException("superState must be null") - } - return EMPTY_STATE - } - - override fun createFromParcel(`in`: Parcel): QuickActionSavedState { - return createFromParcel(`in`, null) - } - - override fun newArray(size: Int): Array { - return newArray(size) - } - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt deleted file mode 100644 index 577d24f60..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetController.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetSessionObserver.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetSessionObserver.kt deleted file mode 100644 index e1818cc15..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetSessionObserver.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt b/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt deleted file mode 100644 index 481e21462..000000000 --- a/app/src/main/java/org/mozilla/fenix/quickactionsheet/QuickActionSheetView.kt +++ /dev/null @@ -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) - ) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index 3ae2939d0..5789ac3eb 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -132,9 +132,6 @@ class Settings private constructor( get() = trackingProtectionOnboardingCount < trackingProtectionOnboardingMaximumCount && !trackingProtectionOnboardingShownThisSession - val shouldAutoBounceQuickActionSheet: Boolean - get() = autoBounceQuickActionSheetCount < autoBounceMaximumCount - val shouldShowSecurityPinWarningSync: Boolean get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount @@ -225,12 +222,6 @@ class Settings private constructor( else -> appContext.getString(R.string.preference_light_theme) } - @VisibleForTesting(otherwise = PRIVATE) - internal val autoBounceQuickActionSheetCount by intPreference( - appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action), - default = 0 - ) - @VisibleForTesting(otherwise = PRIVATE) internal val loginsSecureWarningSyncCount by intPreference( appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync), @@ -257,13 +248,6 @@ class Settings private constructor( ).apply() } - fun incrementAutomaticBounceQuickActionSheetCount() { - preferences.edit().putInt( - appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action), - autoBounceQuickActionSheetCount + 1 - ).apply() - } - val shouldShowSearchSuggestions by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_show_search_suggestions), default = true diff --git a/app/src/main/res/drawable/ic_app_links.xml b/app/src/main/res/drawable/ic_app_links.xml new file mode 100644 index 000000000..600177f12 --- /dev/null +++ b/app/src/main/res/drawable/ic_app_links.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_readermode_appearance.xml b/app/src/main/res/drawable/ic_readermode_appearance.xml new file mode 100644 index 000000000..76d61f9f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_readermode_appearance.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/component_quick_action_sheet.xml b/app/src/main/res/layout/component_quick_action_sheet.xml deleted file mode 100644 index 06f57e35b..000000000 --- a/app/src/main/res/layout/component_quick_action_sheet.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 918e7db44..80a9d1fe3 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -14,7 +14,7 @@ android:id="@+id/swipeRefresh" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginBottom="@dimen/toolbar_and_qab_height"> + android:layout_marginBottom="@dimen/browser_toolbar_height"> - - - - - - - - - - - -