diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
commit | 8dd16259287f58f9273002717ec4d27e97127719 (patch) | |
tree | 3863e62a53829a84037444beab3abd4ed9dfc7d0 /mobile/android/fenix/app/src/main/java | |
parent | Releasing progress-linux version 126.0.1-1~progress7.99u1. (diff) | |
download | firefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz firefox-8dd16259287f58f9273002717ec4d27e97127719.zip |
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/fenix/app/src/main/java')
151 files changed, 4535 insertions, 1227 deletions
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index aa8a5250a7..4d4ce12330 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -50,7 +50,7 @@ object FeatureFlags { /** * Enables compose on the tabs tray items. */ - val composeTabsTray = Config.channel.isNightlyOrDebug || Config.channel.isBeta + const val composeTabsTray = true /** * Enables compose on the top sites. @@ -87,4 +87,9 @@ object FeatureFlags { * Enables the menu redesign. */ const val menuRedesignEnabled = false + + /** + * Enables microsurveys. + */ + const val microsurveysEnabled = false } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 7c32c02f60..cd367fa467 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -206,10 +206,11 @@ open class FenixApplication : LocaleAwareApplication(), Provider { lazy(LazyThreadSafetyMode.NONE) { components.core.client }, ), enableEventTimestamps = FxNimbus.features.glean.value().enableEventTimestamps, + delayPingLifetimeIo = FxNimbus.features.glean.value().delayPingLifetimeIo, ) // Set the metric configuration from Nimbus. - Glean.setMetricsEnabledConfig(FxNimbus.features.glean.value().metricsEnabled) + Glean.applyServerKnobsConfig(FxNimbus.features.glean.value().metricsEnabled) Glean.initialize( applicationContext = this, @@ -870,7 +871,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { disabledAds.set(!settings.isReviewQualityCheckProductRecommendationsEnabled) } - TabStrip.enabled.set(settings.isTabletAndTabStripEnabled) + TabStrip.enabled.set(settings.isTabStripEnabled) } @VisibleForTesting @@ -991,7 +992,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { // We break them out here so they can be recorded when // `nimbus.applyPendingExperiments()` is called. CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature) - CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature) + CustomizeHome.bookmarks.set(settings.showBookmarksHomeFeature) CustomizeHome.mostVisitedSites.set(settings.showTopSitesFeature) CustomizeHome.recentlyVisited.set(settings.historyMetadataUIFeature) CustomizeHome.pocket.set(settings.showPocketRecommendationsFeature) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 1b17a97507..bf442fab37 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -115,6 +115,7 @@ import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor import org.mozilla.fenix.home.intent.OpenPasswordManagerIntentProcessor +import org.mozilla.fenix.home.intent.OpenRecentlyClosedIntentProcessor import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor @@ -203,6 +204,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { OpenBrowserIntentProcessor(this, ::getIntentSessionId), OpenSpecificTabIntentProcessor(this), OpenPasswordManagerIntentProcessor(), + OpenRecentlyClosedIntentProcessor(), ReEngagementIntentProcessor(this, settings()), ) } @@ -780,7 +782,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { private fun handleBackLongPress(): Boolean { supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { - if (it is OnBackLongPressedListener && it.onBackLongPressed()) { + if (it is OnLongPressedListener && it.onBackLongPressed()) { + return true + } + } + return false + } + + private fun handleForwardLongPress(): Boolean { + supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { + if (it is OnLongPressedListener && it.onForwardLongPressed()) { return true } } @@ -805,9 +816,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { handleBackLongPress() } } + + if (keyCode == KeyEvent.KEYCODE_FORWARD) { + event?.startTracking() + return true + } + return super.onKeyDown(keyCode, event) } + @Suppress("ReturnCount") final override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { if (shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) { backLongPressJob?.cancel() @@ -821,6 +839,20 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { return true } } + + if (keyCode == KeyEvent.KEYCODE_FORWARD) { + if (navHost.navController.hasTopDestination(TabHistoryDialogFragment.NAME)) { + // returning true avoids further processing of the KeyUp event + return true + } + + supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { + if (it is UserInteractionHandler && it.onForwardPressed()) { + return true + } + } + } + return super.onKeyUp(keyCode, event) } @@ -830,6 +862,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { if (!shouldUseCustomBackLongPress() && keyCode == KeyEvent.KEYCODE_BACK) { return handleBackLongPress() } + + if (keyCode == KeyEvent.KEYCODE_FORWARD) { + return handleForwardLongPress() + } + return super.onKeyLongPress(keyCode, event) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnLongPressedListener.kt index e47a0b71b4..711830bf72 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnLongPressedListener.kt @@ -5,9 +5,9 @@ package org.mozilla.fenix /** - * Interface for features and fragments that want to handle long presses of the system back button + * Interface for features and fragments that want to handle long presses of the system back/forward button */ -interface OnBackLongPressedListener { +interface OnLongPressedListener { /** * Called when the system back button is long pressed. @@ -18,4 +18,11 @@ interface OnBackLongPressedListener { * @return true if the event was handled */ fun onBackLongPressed(): Boolean + + /** + * Called when the system forward button is long pressed. + * + * @return true if the event was handled + */ + fun onForwardLongPressed(): Boolean } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt index 5eeeeedb3e..0198cb9653 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -40,6 +40,7 @@ import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.settings.SupportUtils.AMO_HOMEPAGE_FOR_ANDROID import org.mozilla.fenix.theme.ThemeManager /** @@ -264,11 +265,4 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) from = BrowserDirection.FromAddonsManagementFragment, ) } - - companion object { - // This is locale-less on purpose so that the content negotiation happens on the AMO side because the current - // user language might not be supported by AMO and/or the language might not be exactly what AMO is expecting - // (e.g. `en` instead of `en-US`). - private const val AMO_HOMEPAGE_FOR_ANDROID = "${BuildConfig.AMO_BASE_URL}/android/" - } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index d6e93e0043..2567021e7e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -115,25 +115,29 @@ import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.kotlin.getOrigin import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.locale.ActivityContextWrapper +import mozilla.components.support.utils.ext.isLandscape import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.GleanMetrics.Events +import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.MediaState import org.mozilla.fenix.GleanMetrics.NavigationBar import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.NavGraphDirections -import org.mozilla.fenix.OnBackLongPressedListener +import org.mozilla.fenix.OnLongPressedListener import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.browser.tabstrip.TabStrip +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.menu.MenuAccessPoint import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.components.toolbar.BrowserFragmentState import org.mozilla.fenix.components.toolbar.BrowserFragmentStore @@ -165,6 +169,7 @@ import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.hideToolbar +import org.mozilla.fenix.ext.isTablet import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.navigateWithBreadcrumb import org.mozilla.fenix.ext.registerForActivityResult @@ -173,6 +178,7 @@ import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.secure import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.tabClosedUndoMessage +import org.mozilla.fenix.ext.updateNavBarForConfigurationChange import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel @@ -201,7 +207,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler, - OnBackLongPressedListener, + OnLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener { private var _binding: FragmentBrowserBinding? = null @@ -212,7 +218,9 @@ abstract class BaseBrowserFragment : private lateinit var startForResult: ActivityResultLauncher<Intent> private var _browserToolbarInteractor: BrowserToolbarInteractor? = null - protected val browserToolbarInteractor: BrowserToolbarInteractor + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal val browserToolbarInteractor: BrowserToolbarInteractor get() = _browserToolbarInteractor!! @VisibleForTesting @@ -269,6 +277,13 @@ abstract class BaseBrowserFragment : private var currentStartDownloadDialog: StartDownloadDialog? = null + private lateinit var savedLoginsLauncher: ActivityResultLauncher<Intent> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + savedLoginsLauncher = registerForActivityResult { navigateToSavedLoginsFragment() } + } + @CallSuper override fun onCreateView( inflater: LayoutInflater, @@ -419,6 +434,7 @@ abstract class BaseBrowserFragment : }, ) val browserToolbarMenuController = DefaultBrowserToolbarMenuController( + fragment = this, store = store, activity = activity, navController = findNavController(), @@ -439,7 +455,8 @@ abstract class BaseBrowserFragment : tabCollectionStorage = requireComponents.core.tabCollectionStorage, topSitesStorage = requireComponents.core.topSitesStorage, pinnedSiteStorage = requireComponents.core.pinnedSiteStorage, - browserStore = store, + onShowPinVerification = { intent -> savedLoginsLauncher.launch(intent) }, + onBiometricAuthenticationSuccessful = { navigateToSavedLoginsFragment() }, ) _browserToolbarInteractor = DefaultBrowserToolbarInteractor( @@ -485,7 +502,11 @@ abstract class BaseBrowserFragment : }, ) - if (IncompleteRedesignToolbarFeature(context.settings()).isEnabled) { + // We don't show the navigation bar for tablets and in landscape mode. + val shouldAddNavigationBar = IncompleteRedesignToolbarFeature(context.settings()).isEnabled && + !requireContext().isLandscape() && + !isTablet() + if (shouldAddNavigationBar) { initializeNavBar( browserToolbar = browserToolbarView.view, view = view, @@ -756,6 +777,9 @@ abstract class BaseBrowserFragment : loginValidationDelegate = DefaultLoginValidationDelegate( context.components.core.lazyPasswordsStorage, ), + isLoginAutofillEnabled = { + context.settings().shouldAutofillLogins + }, isSaveLoginEnabled = { context.settings().shouldPromptToSaveLogins }, @@ -836,6 +860,7 @@ abstract class BaseBrowserFragment : feature = SessionFeature( requireComponents.core.store, requireComponents.useCases.sessionUseCases.goBack, + requireComponents.useCases.sessionUseCases.goForward, binding.engineView, customTabSessionId, ), @@ -1005,7 +1030,9 @@ abstract class BaseBrowserFragment : ) initializeEngineView( - topToolbarHeight = context.settings().getTopToolbarHeight(includeTabStrip = customTabSessionId == null), + topToolbarHeight = context.settings().getTopToolbarHeight( + includeTabStrip = customTabSessionId == null && context.isTabStripEnabled(), + ), bottomToolbarHeight = bottomToolbarHeight, ) } @@ -1221,7 +1248,7 @@ abstract class BaseBrowserFragment : topToolbarHeight = topToolbarHeight, ) } else { - val toolbarHeight = if (customTabSessionId == null && context.settings().isTabletAndTabStripEnabled) { + val toolbarHeight = if (customTabSessionId == null && context.isTabStripEnabled()) { resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) + resources.getDimensionPixelSize(R.dimen.tab_strip_height) } else { @@ -1348,7 +1375,9 @@ abstract class BaseBrowserFragment : onMenuButtonClick = { findNavController().nav( R.id.browserFragment, - BrowserFragmentDirections.actionGlobalMenuDialogFragment(), + BrowserFragmentDirections.actionGlobalMenuDialogFragment( + accesspoint = MenuAccessPoint.Browser, + ), ) }, ) @@ -1513,6 +1542,11 @@ abstract class BaseBrowserFragment : removeSessionIfNeeded() } + @CallSuper + override fun onForwardPressed(): Boolean { + return sessionFeature.onForwardPressed() + } + /** * Forwards activity results to the [ActivityResultHandler] features. */ @@ -1523,12 +1557,24 @@ abstract class BaseBrowserFragment : ).any { it.onActivityResult(requestCode, data, resultCode) } } - override fun onBackLongPressed(): Boolean { + /** + * Navigate to GlobalTabHistoryDialogFragment. + */ + private fun navigateToGlobalTabHistoryDialogFragment() { findNavController().navigate( NavGraphDirections.actionGlobalTabHistoryDialogFragment( activeSessionId = customTabSessionId, ), ) + } + + override fun onBackLongPressed(): Boolean { + navigateToGlobalTabHistoryDialogFragment() + return true + } + + override fun onForwardLongPressed(): Boolean { + navigateToGlobalTabHistoryDialogFragment() return true } @@ -1773,7 +1819,7 @@ abstract class BaseBrowserFragment : browserToolbarView.visible() initializeEngineView( topToolbarHeight = requireContext().settings().getTopToolbarHeight( - includeTabStrip = customTabSessionId == null, + includeTabStrip = customTabSessionId == null && requireContext().isTabStripEnabled(), ), bottomToolbarHeight = requireContext().settings().getBottomToolbarHeight(), ) @@ -1787,6 +1833,27 @@ abstract class BaseBrowserFragment : @CallSuper internal open fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) { toolbar.dismissMenu() + + // If the navbar feature could be visible, we should update it's state. + val shouldUpdateNavBarState = + IncompleteRedesignToolbarFeature(requireContext().settings()).isEnabled && !isTablet() + if (shouldUpdateNavBarState) { + updateNavBarForConfigurationChange( + parent = binding.browserLayout, + toolbarView = browserToolbarView.view, + bottomToolbarContainerView = _bottomToolbarContainerView?.toolbarContainerView, + reinitializeNavBar = ::reinitializeNavBar, + ) + } + } + + private fun reinitializeNavBar() { + initializeNavBar( + browserToolbar = browserToolbarView.view, + view = requireView(), + context = requireContext(), + activity = requireActivity() as HomeActivity, + ) } /* @@ -1939,4 +2006,13 @@ abstract class BaseBrowserFragment : } } } + + private fun navigateToSavedLoginsFragment() { + val navController = findNavController() + if (navController.currentDestination?.id == R.id.browserFragment) { + Logins.openLogins.record(NoExtras()) + val directions = BrowserFragmentDirections.actionLoginsListFragment() + navController.navigate(directions) + } + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 325d537220..89929ed7d1 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -34,11 +34,14 @@ import mozilla.components.feature.tabs.WindowFeature import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.utils.ext.isLandscape +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.GleanMetrics.AddressToolbar import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.Shopping import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.appstate.AppAction @@ -51,6 +54,8 @@ import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.HomeFragment +import org.mozilla.fenix.messaging.FenixMessageSurfaceId +import org.mozilla.fenix.messaging.MessagingFeature import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCookieBannerUIMode import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature @@ -72,12 +77,17 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { private val reviewQualityCheckFeature = ViewBoundFeatureWrapper<ReviewQualityCheckFeature>() private val translationsBinding = ViewBoundFeatureWrapper<TranslationsBinding>() + @VisibleForTesting + internal val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>() + private var readerModeAvailable = false private var reviewQualityCheckAvailable = false private var translationsAvailable = false private var pwaOnboardingObserver: PwaOnboardingObserver? = null + @VisibleForTesting + internal var leadingAction: BrowserToolbar.Button? = null private var forwardAction: BrowserToolbar.TwoStateButton? = null private var backAction: BrowserToolbar.TwoStateButton? = null private var refreshAction: BrowserToolbar.TwoStateButton? = null @@ -89,9 +99,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { val context = requireContext() val components = context.components - val isTabletAndTabStripEnabled = context.settings().isTabletAndTabStripEnabled - if (!isTabletAndTabStripEnabled && context.settings().isSwipeToolbarToSwitchTabsEnabled) { + if (!context.isTabStripEnabled() && context.settings().isSwipeToolbarToSwitchTabsEnabled) { binding.gestureLayout.addGestureListener( ToolbarGestureHandler( activity = requireActivity(), @@ -107,15 +116,14 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ) } - if (!IncompleteRedesignToolbarFeature(context.settings()).isEnabled) { - val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate - initLeadingAction( - context = context, - isPrivate = isPrivate, - ) - } - - updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet)) + updateBrowserToolbarLeadingAndNavigationActions( + context = context, + redesignEnabled = IncompleteRedesignToolbarFeature(context.settings()).isEnabled, + isLandscape = context.isLandscape(), + isTablet = resources.getBoolean(R.bool.tablet), + isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate, + feltPrivateBrowsingEnabled = context.settings().feltPrivateBrowsingEnabled, + ) val readerModeAction = BrowserToolbar.ToggleButton( @@ -210,6 +218,22 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ) setTranslationFragmentResultListener() + + setupMicrosurvey() + } + + @VisibleForTesting + internal fun setupMicrosurvey(isMicrosurveyEnabled: Boolean = FeatureFlags.microsurveysEnabled) { + if (requireContext().settings().isExperimentationEnabled && isMicrosurveyEnabled) { + messagingFeature.set( + feature = MessagingFeature( + appStore = requireComponents.appStore, + surface = FenixMessageSurfaceId.MICROSURVEY, + ), + owner = viewLifecycleOwner, + view = binding.root, + ) + } } private fun setTranslationFragmentResultListener() { @@ -310,7 +334,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } private fun initReloadAction(context: Context) { - if (!IncompleteRedesignToolbarFeature(context.settings()).isEnabled || refreshAction != null) { + if (!IncompleteRedesignToolbarFeature(context.settings()).isEnabled) { return } @@ -412,43 +436,138 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } // Adds a home button to BrowserToolbar or, if FeltPrivateBrowsing is enabled, a clear data button instead. - private fun initLeadingAction( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun addLeadingAction( context: Context, + feltPrivateBrowsingEnabled: Boolean, isPrivate: Boolean, ) { - val leadingAction = if (isPrivate && context.settings().feltPrivateBrowsingEnabled) { - BrowserToolbar.Button( - imageDrawable = AppCompatResources.getDrawable( - context, - R.drawable.mozac_ic_data_clearance_24, - )!!, - contentDescription = context.getString(R.string.browser_toolbar_erase), - iconTintColorResource = ThemeManager.resolveAttribute(R.attr.textPrimary, context), - listener = browserToolbarInteractor::onEraseButtonClicked, + if (leadingAction == null) { + leadingAction = if (isPrivate && feltPrivateBrowsingEnabled) { + BrowserToolbar.Button( + imageDrawable = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_data_clearance_24, + )!!, + contentDescription = context.getString(R.string.browser_toolbar_erase), + iconTintColorResource = ThemeManager.resolveAttribute(R.attr.textPrimary, context), + listener = browserToolbarInteractor::onEraseButtonClicked, + ) + } else { + BrowserToolbar.Button( + imageDrawable = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_home_24, + )!!, + contentDescription = context.getString(R.string.browser_toolbar_home), + iconTintColorResource = ThemeManager.resolveAttribute(R.attr.textPrimary, context), + listener = browserToolbarInteractor::onHomeButtonClicked, + ) + }.also { + browserToolbarView.view.addNavigationAction(it) + } + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun removeLeadingAction() { + leadingAction?.let { + browserToolbarView.view.removeNavigationAction(it) + } + leadingAction = null + } + + /** + * This code takes care of the [BrowserToolbar] leading and navigation actions. + * The older design requires a HomeButton followed by navigation buttons for tablets. + * The newer design expects NavigationButtons and a HomeButton in landscape mode for phones and in both modes + * for tablets. + */ + @VisibleForTesting + internal fun updateBrowserToolbarLeadingAndNavigationActions( + context: Context, + redesignEnabled: Boolean, + isLandscape: Boolean, + isTablet: Boolean, + isPrivate: Boolean, + feltPrivateBrowsingEnabled: Boolean, + ) { + if (redesignEnabled) { + updateAddressBarNavigationActions( + isLandscape = isLandscape, + isTablet = isTablet, + context = context, + ) + updateAddressBarLeadingAction( + redesignEnabled = true, + isLandscape = isLandscape, + isTablet = isTablet, + isPrivate = isPrivate, + feltPrivateBrowsingEnabled = feltPrivateBrowsingEnabled, + context = context, ) } else { - BrowserToolbar.Button( - imageDrawable = AppCompatResources.getDrawable( - context, - R.drawable.mozac_ic_home_24, - )!!, - contentDescription = context.getString(R.string.browser_toolbar_home), - iconTintColorResource = ThemeManager.resolveAttribute(R.attr.textPrimary, context), - listener = browserToolbarInteractor::onHomeButtonClicked, + updateAddressBarLeadingAction( + redesignEnabled = false, + isLandscape = isLandscape, + isPrivate = isPrivate, + isTablet = isTablet, + feltPrivateBrowsingEnabled = feltPrivateBrowsingEnabled, + context = context, ) + updateTabletToolbarActions(isTablet = isTablet) } + browserToolbarView.view.invalidateActions() + } - browserToolbarView.view.addNavigationAction(leadingAction) + @VisibleForTesting + internal fun updateAddressBarLeadingAction( + redesignEnabled: Boolean, + isLandscape: Boolean, + isTablet: Boolean, + isPrivate: Boolean, + feltPrivateBrowsingEnabled: Boolean, + context: Context, + ) { + if (!redesignEnabled || isLandscape || isTablet) { + addLeadingAction( + isPrivate = isPrivate, + feltPrivateBrowsingEnabled = feltPrivateBrowsingEnabled, + context = context, + ) + } else { + removeLeadingAction() + } + } + + @VisibleForTesting + internal fun updateAddressBarNavigationActions( + context: Context, + isLandscape: Boolean, + isTablet: Boolean, + ) { + if (isLandscape || isTablet) { + addNavigationActions(context) + } else { + removeNavigationActions() + } } override fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) { super.onUpdateToolbarForConfigurationChange(toolbar) - updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet)) + updateBrowserToolbarLeadingAndNavigationActions( + context = requireContext(), + redesignEnabled = IncompleteRedesignToolbarFeature(requireContext().settings()).isEnabled, + isLandscape = requireContext().isLandscape(), + isTablet = resources.getBoolean(R.bool.tablet), + isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate, + feltPrivateBrowsingEnabled = requireContext().settings().feltPrivateBrowsingEnabled, + ) } @VisibleForTesting - internal fun updateToolbarActions(isTablet: Boolean) { + internal fun updateTabletToolbarActions(isTablet: Boolean) { if (isTablet == this.isTablet) return if (isTablet) { @@ -460,8 +579,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { this.isTablet = isTablet } - @Suppress("LongMethod") - private fun addTabletActions(context: Context) { + @VisibleForTesting + internal fun addNavigationActions(context: Context) { val enableTint = ThemeManager.resolveAttribute(R.attr.textPrimary, context) val disableTint = ThemeManager.resolveAttribute(R.attr.textDisabled, context) @@ -486,11 +605,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ToolbarMenu.Item.Back(viewHistory = false), ) }, - ) - } - - backAction?.let { - browserToolbarView.view.addNavigationAction(it) + ).also { + browserToolbarView.view.addNavigationAction(it) + } } if (forwardAction == null) { @@ -514,13 +631,17 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ToolbarMenu.Item.Forward(viewHistory = false), ) }, - ) + ).also { + browserToolbarView.view.addNavigationAction(it) + } } + } - forwardAction?.let { - browserToolbarView.view.addNavigationAction(it) - } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun addTabletActions(context: Context) { + addNavigationActions(context) + val enableTint = ThemeManager.resolveAttribute(R.attr.textPrimary, context) if (refreshAction == null) { refreshAction = BrowserToolbar.TwoStateButton( primaryImage = AppCompatResources.getDrawable( @@ -552,28 +673,31 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ) } }, - ) - } - - refreshAction?.let { - browserToolbarView.view.addNavigationAction(it) + ).also { + browserToolbarView.view.addNavigationAction(it) + } } - - browserToolbarView.view.invalidateActions() } - private fun removeTabletActions() { + @VisibleForTesting + internal fun removeNavigationActions() { forwardAction?.let { browserToolbarView.view.removeNavigationAction(it) } + forwardAction = null backAction?.let { browserToolbarView.view.removeNavigationAction(it) } + backAction = null + } + + @VisibleForTesting + internal fun removeTabletActions() { + removeNavigationActions() + refreshAction?.let { browserToolbarView.view.removeNavigationAction(it) } - - browserToolbarView.view.invalidateActions() } override fun onStart() { @@ -607,6 +731,10 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { override fun onDestroyView() { super.onDestroyView() isTablet = false + leadingAction = null + forwardAction = null + backAction = null + refreshAction = null } private fun updateHistoryMetadata() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt index 80fbcd61b9..c103e22026 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt @@ -59,7 +59,7 @@ class StandardSnackbarErrorBinding( snackBar.setSnackBarTextColor( ContextCompat.getColor( activity, - R.color.fx_mobile_text_color_warning, + R.color.fx_mobile_text_color_critical, ), ) snackBar.setAction( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt index a661d1ea1c..e2d36bffed 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt @@ -10,7 +10,6 @@ import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent import android.widget.FrameLayout -import androidx.core.view.GestureDetectorCompat /** * Interface that allows intercepting and handling swipe gestures received in a [SwipeGestureLayout]. @@ -101,7 +100,7 @@ class SwipeGestureLayout @JvmOverloads constructor( } } - private val gestureDetector = GestureDetectorCompat(context, gestureListener) + private val gestureDetector = GestureDetector(context, gestureListener) private val listeners = mutableListOf<SwipeGestureListener>() private var activeListener: SwipeGestureListener? = null diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt index 42b6924955..cc855c9fa2 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt @@ -110,7 +110,9 @@ class TabPreview @JvmOverloads constructor( }, ) - removeView(binding.fakeToolbar) + if (!isToolbarAtTop) { + removeView(binding.fakeToolbar) + } } // Change view properties to avoid confusing the UI tests diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/tabstrip/TabStripFeatureFlag.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/tabstrip/TabStripFeatureFlag.kt new file mode 100644 index 0000000000..faeb5446f5 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/tabstrip/TabStripFeatureFlag.kt @@ -0,0 +1,31 @@ +/* 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.browser.tabstrip + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import org.mozilla.fenix.Config +import org.mozilla.fenix.ext.isTablet +import org.mozilla.fenix.ext.settings + +/** + * Returns true if the tab strip is enabled. + */ +fun Context.isTabStripEnabled(): Boolean = + isTabStripEligible() && settings().isTabStripEnabled + +/** + * Returns true if the the device has the prerequisites to enable the tab strip. + */ +fun Context.isTabStripEligible(): Boolean = + Config.channel.isNightlyOrDebug && isTablet() && !doesDeviceHaveHinge() + +/** + * Check if the device has a hinge sensor. + */ +private fun Context.doesDeviceHaveHinge(): Boolean = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index fcd624eec8..099b24a744 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -19,6 +19,7 @@ import mozilla.components.concept.sync.DeviceCapability import mozilla.components.concept.sync.DeviceConfig import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.feature.accounts.push.CloseTabsFeature import mozilla.components.feature.accounts.push.FxaPushSupportFeature import mozilla.components.feature.accounts.push.SendTabFeature import mozilla.components.feature.syncedtabs.SyncedTabsAutocompleteProvider @@ -84,7 +85,12 @@ class BackgroundServices( // NB: flipping this flag back and worth is currently not well supported and may need hand-holding. // Consult with the android-components peers before changing. // See https://github.com/mozilla/application-services/issues/1308 - capabilities = setOf(DeviceCapability.SEND_TAB), + capabilities = buildSet { + add(DeviceCapability.SEND_TAB) + if (context.settings().enableCloseSyncedTabs) { + add(DeviceCapability.CLOSE_TABS) + } + }, // Enable encryption for account state on supported API levels (23+). // Just on Nightly and local builds for now. @@ -194,6 +200,12 @@ class BackgroundServices( notificationManager.showReceivedTabs(context, device, tabs) } + if (context.settings().enableCloseSyncedTabs) { + CloseTabsFeature(context.components.core.store, accountManager) { _, remotelyClosedUrls -> + notificationManager.showSyncedTabsClosed(context, remotelyClosedUrls.size) + }.observe() + } + SyncedTabsIntegration(context, accountManager).launch() syncStoreSupport = SyncStoreSupport(syncStore, lazyOf(accountManager)).also { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt index 0f6f107df1..35c18e98ea 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -202,7 +202,7 @@ class Components(private val context: Context) { collections = core.tabCollectionStorage.cachedTabCollections, expandedCollections = emptySet(), topSites = core.topSitesStorage.cachedTopSites.sort(), - recentBookmarks = emptyList(), + bookmarks = emptyList(), showCollectionPlaceholder = settings.showCollectionsPlaceholderOnHome, // Provide an initial state for recent tabs to prevent re-rendering on the home screen. // This will otherwise cause a visual jump as the section gets rendered from no state diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/NotificationManager.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/NotificationManager.kt index 426019b8eb..9ed2abdcae 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/NotificationManager.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/NotificationManager.kt @@ -17,11 +17,15 @@ import android.os.Build.VERSION.SDK_INT import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService +import androidx.core.os.bundleOf import mozilla.components.concept.sync.Device import mozilla.components.concept.sync.TabData +import mozilla.components.support.base.ids.SharedIdsHelper import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R +import org.mozilla.fenix.home.intent.OpenRecentlyClosedIntentProcessor import org.mozilla.fenix.utils.IntentUtils /** @@ -29,6 +33,9 @@ import org.mozilla.fenix.utils.IntentUtils */ class NotificationManager(private val context: Context) { companion object { + const val TABS_CLOSED_TAG = "TabsClosed" + const val TOTAL_TABS_CLOSED_EXTRA = "org.mozilla.fenix.TOTAL_TABS_CLOSED_EXTRA" + const val TABS_CLOSED_NOTIFICATION_TAG = "org.mozilla.fenix.TABS_CLOSED_NOTIFICATION_TAG" const val RECEIVE_TABS_TAG = "ReceivedTabs" const val RECEIVE_TABS_CHANNEL_ID = "ReceivedTabsChannel" } @@ -51,6 +58,73 @@ class NotificationManager(private val context: Context) { private val logger = Logger("NotificationManager") + /** + * Notifies the user that one or more tabs on this device were closed from another device. + * + * @param context The Android application context. + * @param count The number of tabs that were closed. + */ + fun showSyncedTabsClosed(context: Context, count: Int) { + if (count <= 0) { + return + } + val notificationManagerCompat = NotificationManagerCompat.from(context) + val (notificationId, totalCount) = if (SDK_INT >= Build.VERSION_CODES.M) { + // On Android M (released in 2015) and later, we can retrieve + // the last notification from `allNotifications`. If one exists, + // we'll update its contents in-place with the new total number of + // closed tabs. + val notificationId = SharedIdsHelper.getIdForTag(context, TABS_CLOSED_NOTIFICATION_TAG) + val lastNotification = notificationManagerCompat.activeNotifications.find { + it.tag == TABS_CLOSED_TAG && it.id == notificationId + } + val lastTotalCount = lastNotification?.notification?.extras?.getInt(TOTAL_TABS_CLOSED_EXTRA) ?: 0 + Pair(notificationId, lastTotalCount + count) + } else { + // Pre-M doesn't have `activeNotifications`, so we'll show + // a new notification for each call to `showSyncedTabsClosed`. + val notificationId = SharedIdsHelper.getNextIdForTag(context, TABS_CLOSED_NOTIFICATION_TAG) + Pair(notificationId, count) + } + + val notification = NotificationCompat.Builder(context, RECEIVE_TABS_CHANNEL_ID).apply { + val title = context.resources.getString( + R.string.fxa_tabs_closed_notification_title, + context.resources.getString(R.string.app_name), + totalCount, + ) + setContentTitle(title) + + val text = context.resources.getString(R.string.fxa_tabs_closed_text) + setContentText(text) + + val intent = Intent(context, HomeActivity::class.java).apply { + action = OpenRecentlyClosedIntentProcessor.ACTION_OPEN_RECENTLY_CLOSED + } + val pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_UPDATE_CURRENT, + ) + setContentIntent(pendingIntent) + + val extras = bundleOf(TOTAL_TABS_CLOSED_EXTRA to totalCount) + addExtras(extras) + + setSmallIcon(R.drawable.ic_status_logo) + setWhen(System.currentTimeMillis()) + setAutoCancel(true) + setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_SOUND) + + if (SDK_INT >= Build.VERSION_CODES.M) { + setCategory(Notification.CATEGORY_STATUS) + } + }.build() + + notificationManagerCompat.notify(TABS_CLOSED_TAG, notificationId, notification) + } + fun showReceivedTabs(context: Context, device: Device?, tabs: List<TabData>) { // In the future, experiment with displaying multiple tabs from the same device as as Notification Groups. // For now, a single notification per tab received will suffice. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt index dbdeb831ed..f813ad33bd 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt @@ -17,9 +17,9 @@ import org.mozilla.fenix.browser.StandardSnackbarError import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.shopping.ShoppingState +import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recenttabs.RecentTab @@ -54,7 +54,7 @@ sealed class AppAction : Action { val collections: List<TabCollection>, val showCollectionPlaceholder: Boolean, val recentTabs: List<RecentTab>, - val recentBookmarks: List<RecentBookmark>, + val bookmarks: List<Bookmark>, val recentHistory: List<RecentlyVisitedItem>, val recentSyncedTabState: RecentSyncedTabState, ) : @@ -68,8 +68,16 @@ sealed class AppAction : Action { data class TopSitesChange(val topSites: List<TopSite>) : AppAction() data class RecentTabsChange(val recentTabs: List<RecentTab>) : AppAction() data class RemoveRecentTab(val recentTab: RecentTab) : AppAction() - data class RecentBookmarksChange(val recentBookmarks: List<RecentBookmark>) : AppAction() - data class RemoveRecentBookmark(val recentBookmark: RecentBookmark) : AppAction() + + /** + * The list of bookmarks displayed on the home screen has changed. + */ + data class BookmarksChange(val bookmarks: List<Bookmark>) : AppAction() + + /** + * A bookmark has been removed from the home screen. + */ + data class RemoveBookmark(val bookmark: Bookmark) : AppAction() data class RecentHistoryChange(val recentHistory: List<RecentlyVisitedItem>) : AppAction() data class RemoveRecentHistoryHighlight(val highlightUrl: String) : AppAction() data class DisbandSearchGroupAction(val searchTerm: String) : AppAction() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt index e118dca121..dc3a1b368a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt @@ -16,9 +16,9 @@ import org.mozilla.fenix.browser.StandardSnackbarError import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.shopping.ShoppingState import org.mozilla.fenix.home.HomeFragment +import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem @@ -45,7 +45,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState * @property showCollectionPlaceholder If true, shows a placeholder when there are no collections. * @property recentTabs The list of recent [RecentTab] in the [HomeFragment]. * @property recentSyncedTabState The [RecentSyncedTabState] in the [HomeFragment]. - * @property recentBookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment]. + * @property bookmarks The list of recently saved [BookmarkNode]s to show on the [HomeFragment]. * @property recentHistory The list of [RecentlyVisitedItem]s. * @property pocketStories The list of currently shown [PocketRecommendedStory]s. * @property pocketStoriesCategories All [PocketRecommendedStory] categories. @@ -75,7 +75,7 @@ data class AppState( val showCollectionPlaceholder: Boolean = false, val recentTabs: List<RecentTab> = emptyList(), val recentSyncedTabState: RecentSyncedTabState = RecentSyncedTabState.None, - val recentBookmarks: List<RecentBookmark> = emptyList(), + val bookmarks: List<Bookmark> = emptyList(), val recentHistory: List<RecentlyVisitedItem> = emptyList(), val pocketStories: List<PocketStory> = emptyList(), val pocketStoriesCategories: List<PocketRecommendedStoriesCategory> = emptyList(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt index a614689982..1092de97f0 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt @@ -43,7 +43,7 @@ internal object AppStoreReducer { collections = action.collections, mode = action.mode, topSites = action.topSites, - recentBookmarks = action.recentBookmarks, + bookmarks = action.bookmarks, recentTabs = action.recentTabs, recentHistory = action.recentHistory, recentSyncedTabState = action.recentSyncedTabState, @@ -81,9 +81,9 @@ internal object AppStoreReducer { recentSyncedTabState = action.state, ) } - is AppAction.RecentBookmarksChange -> state.copy(recentBookmarks = action.recentBookmarks) - is AppAction.RemoveRecentBookmark -> { - state.copy(recentBookmarks = state.recentBookmarks.filterNot { it.url == action.recentBookmark.url }) + is AppAction.BookmarksChange -> state.copy(bookmarks = action.bookmarks) + is AppAction.RemoveBookmark -> { + state.copy(bookmarks = state.bookmarks.filterNot { it.url == action.bookmark.url }) } is AppAction.RecentHistoryChange -> state.copy( recentHistory = action.recentHistory, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt index f8ea604c34..986b4ba3b9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt @@ -9,7 +9,7 @@ import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.uniffi.PlacesApiException import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.concept.storage.HistoryStorage -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark +import org.mozilla.fenix.home.bookmarks.Bookmark import java.util.concurrent.TimeUnit /** @@ -20,12 +20,17 @@ class BookmarksUseCase( historyStorage: HistoryStorage, ) { + /** + * Use case for adding a new bookmark. + * + * @param storage [BookmarksStorage] used to add and retrieve bookmark data. + */ class AddBookmarksUseCase internal constructor(private val storage: BookmarksStorage) { /** * Adds a new bookmark with the provided [url] and [title]. * - * @return The result if the operation was executed or not. A bookmark may not be added if + * @return The guid of the newly added bookmark or null. A bookmark may not be added if * one with the identical [url] already exists. */ @WorkerThread @@ -34,21 +39,22 @@ class BookmarksUseCase( title: String, position: UInt? = null, parentGuid: String? = null, - ): Boolean { + ): String? { return try { val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == url } == null - if (canAdd) { + return if (canAdd) { storage.addItem( parentGuid ?: BookmarkRoot.Mobile.id, url = url, title = title, position = position, ) + } else { + null } - canAdd } catch (e: PlacesApiException.UrlParseFailed) { - false + null } } } @@ -68,27 +74,26 @@ class BookmarksUseCase( * Retrieves a list of recently added bookmarks, if any, up to maximum. * * @param count The number of recent bookmarks to return. - * @param maxAgeInMs The maximum age (ms) of a recently added bookmark to return. - * @return a list of [RecentBookmark] that were added no older than specify by [maxAgeInMs], - * if any, up to a number specified by [count]. + * @param previewImageMaxAgeMs The maximum age (ms) to search history for preview image URLs. + * @return a list of [Bookmark]s if any, up to a number specified by [count]. */ @WorkerThread suspend operator fun invoke( count: Int = DEFAULT_BOOKMARKS_TO_RETRIEVE, - maxAgeInMs: Long = TimeUnit.DAYS.toMillis(DEFAULT_BOOKMARKS_DAYS_AGE_TO_RETRIEVE), - ): List<RecentBookmark> { + previewImageMaxAgeMs: Long = TimeUnit.DAYS.toMillis(DEFAULT_BOOKMARKS_LENGTH_DAYS_PREVIEW_IMAGE_SEARCH), + ): List<Bookmark> { val currentTime = System.currentTimeMillis() // Fetch visit information within the time range of now and the specified maximum age. val history = historyStorage?.getDetailedVisits( - start = currentTime - maxAgeInMs, + start = currentTime - previewImageMaxAgeMs, end = currentTime, ) return bookmarksStorage - .getRecentBookmarks(count, maxAgeInMs) + .getRecentBookmarks(count) .map { bookmark -> - RecentBookmark( + Bookmark( title = bookmark.title, url = bookmark.url, previewImageUrl = history?.find { bookmark.url == it.url }?.previewImageUrl, @@ -107,9 +112,9 @@ class BookmarksUseCase( companion object { // Number of recent bookmarks to retrieve. - const val DEFAULT_BOOKMARKS_TO_RETRIEVE = 4 + const val DEFAULT_BOOKMARKS_TO_RETRIEVE = 8 // The maximum age in days of a recent bookmarks to retrieve. - const val DEFAULT_BOOKMARKS_DAYS_AGE_TO_RETRIEVE = 10L + const val DEFAULT_BOOKMARKS_LENGTH_DAYS_PREVIEW_IMAGE_SEARCH = 10L } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/BrowserNavigationParams.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/BrowserNavigationParams.kt new file mode 100644 index 0000000000..8de758f3d7 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/BrowserNavigationParams.kt @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu + +import org.mozilla.fenix.settings.SupportUtils.SumoTopic + +/** + * Browser navigation parameters of the URL or [SumoTopic] to be loaded. + * + * @property url The URL to be loaded. + * @property sumoTopic The [SumoTopic] to be loaded. + */ +data class BrowserNavigationParams( + val url: String? = null, + val sumoTopic: SumoTopic? = null, +) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuAccessPoint.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuAccessPoint.kt new file mode 100644 index 0000000000..f1d8b3a326 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuAccessPoint.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu + +import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint + +/** + * The origin access points that was used to navigate to the Menu dialog. + */ +enum class MenuAccessPoint { + /** + * Menu was accessed from the browser. + */ + Browser, + + /** + * Menu was accessed from an external app (e.g. custom tab). + */ + External, + + /** + * Menu was accessed from the home screen. + */ + Home, +} + +/** + * Returns the [FenixFxAEntryPoint] equivalent from the given [MenuAccessPoint]. + */ +internal fun MenuAccessPoint.toFenixFxAEntryPoint(): FenixFxAEntryPoint { + return when (this) { + MenuAccessPoint.Browser -> FenixFxAEntryPoint.BrowserToolbar + MenuAccessPoint.External -> FenixFxAEntryPoint.Unknown + MenuAccessPoint.Home -> FenixFxAEntryPoint.HomeMenu + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt index 05890cd71d..c0d9d6f11d 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt @@ -9,25 +9,42 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.lib.state.ext.observeAsState +import mozilla.components.service.fxa.manager.AccountState.NotAuthenticated import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.components.accounts.AccountState -import org.mozilla.fenix.components.lazyStore -import org.mozilla.fenix.components.menu.compose.MenuDialog +import org.mozilla.fenix.components.components +import org.mozilla.fenix.components.menu.compose.EXTENSIONS_MENU_ROUTE +import org.mozilla.fenix.components.menu.compose.ExtensionsSubmenu +import org.mozilla.fenix.components.menu.compose.MAIN_MENU_ROUTE +import org.mozilla.fenix.components.menu.compose.MainMenu import org.mozilla.fenix.components.menu.compose.MenuDialogBottomSheet +import org.mozilla.fenix.components.menu.compose.SAVE_MENU_ROUTE +import org.mozilla.fenix.components.menu.compose.SaveSubmenu +import org.mozilla.fenix.components.menu.compose.TOOLS_MENU_ROUTE +import org.mozilla.fenix.components.menu.compose.ToolsSubmenu +import org.mozilla.fenix.components.menu.middleware.MenuDialogMiddleware import org.mozilla.fenix.components.menu.middleware.MenuNavigationMiddleware +import org.mozilla.fenix.components.menu.store.BrowserMenuState import org.mozilla.fenix.components.menu.store.MenuAction import org.mozilla.fenix.components.menu.store.MenuState import org.mozilla.fenix.components.menu.store.MenuStore import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.settings.SupportUtils -import org.mozilla.fenix.settings.SupportUtils.SumoTopic import org.mozilla.fenix.theme.FirefoxTheme /** @@ -35,18 +52,8 @@ import org.mozilla.fenix.theme.FirefoxTheme */ class MenuDialogFragment : BottomSheetDialogFragment() { - private val store by lazyStore { viewModelScope -> - MenuStore( - initialState = MenuState(), - middleware = listOf( - MenuNavigationMiddleware( - navController = findNavController(), - openSumoTopic = ::openSumoTopic, - scope = viewModelScope, - ), - ), - ) - } + private val args by navArgs<MenuDialogFragmentArgs>() + private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = super.onCreateDialog(savedInstanceState).apply { @@ -59,6 +66,7 @@ class MenuDialogFragment : BottomSheetDialogFragment() { } } + @Suppress("LongMethod") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -69,42 +77,193 @@ class MenuDialogFragment : BottomSheetDialogFragment() { setContent { FirefoxTheme { MenuDialogBottomSheet(onRequestDismiss = {}) { - MenuDialog( - account = null, - accountState = AccountState.NO_ACCOUNT, - onSignInButtonClick = {}, - onHelpButtonClick = { - store.dispatch(MenuAction.Navigate.Help) - }, - onSettingsButtonClick = { - store.dispatch(MenuAction.Navigate.Settings) - }, - onBookmarksMenuClick = { - store.dispatch(MenuAction.Navigate.Bookmarks) - }, - onHistoryMenuClick = { - store.dispatch(MenuAction.Navigate.History) - }, - onDownloadsMenuClick = { - store.dispatch(MenuAction.Navigate.Downloads) - }, - onPasswordsMenuClick = { - store.dispatch(MenuAction.Navigate.Passwords) - }, - ) + val browserStore = components.core.store + val syncStore = components.backgroundServices.syncStore + val bookmarksStorage = components.core.bookmarksStorage + val addBookmarkUseCase = components.useCases.bookmarksUseCases.addBookmark + val printContentUseCase = components.useCases.sessionUseCases.printContent + val saveToPdfUseCase = components.useCases.sessionUseCases.saveToPdf + val selectedTab = browserStore.state.selectedTab + + val navHostController = rememberNavController() + val coroutineScope = rememberCoroutineScope() + val store = remember { + MenuStore( + initialState = MenuState( + browserMenuState = if (selectedTab != null) { + BrowserMenuState(selectedTab = selectedTab) + } else { + null + }, + ), + middleware = listOf( + MenuDialogMiddleware( + bookmarksStorage = bookmarksStorage, + addBookmarkUseCase = addBookmarkUseCase, + scope = coroutineScope, + ), + MenuNavigationMiddleware( + navController = findNavController(), + navHostController = navHostController, + browsingModeManager = browsingModeManager, + openToBrowser = ::openToBrowser, + scope = coroutineScope, + ), + ), + ) + } + + val account by syncStore.observeAsState(initialValue = null) { state -> state.account } + val accountState by syncStore.observeAsState(initialValue = NotAuthenticated) { state -> + state.accountState + } + val isBookmarked by store.observeAsState(initialValue = false) { state -> + state.browserMenuState != null && state.browserMenuState.bookmarkState.isBookmarked + } + + NavHost( + navController = navHostController, + startDestination = MAIN_MENU_ROUTE, + ) { + composable(route = MAIN_MENU_ROUTE) { + MainMenu( + accessPoint = args.accesspoint, + account = account, + accountState = accountState, + isPrivate = browsingModeManager.mode.isPrivate, + onMozillaAccountButtonClick = { + store.dispatch( + MenuAction.Navigate.MozillaAccount( + accountState = accountState, + accesspoint = args.accesspoint, + ), + ) + }, + onHelpButtonClick = { + store.dispatch(MenuAction.Navigate.Help) + }, + onSettingsButtonClick = { + store.dispatch(MenuAction.Navigate.Settings) + }, + onNewTabMenuClick = { + store.dispatch(MenuAction.Navigate.NewTab) + }, + onNewPrivateTabMenuClick = { + store.dispatch(MenuAction.Navigate.NewPrivateTab) + }, + onSwitchToDesktopSiteMenuClick = {}, + onFindInPageMenuClick = {}, + onToolsMenuClick = { + store.dispatch(MenuAction.Navigate.Tools) + }, + onSaveMenuClick = { + store.dispatch(MenuAction.Navigate.Save) + }, + onExtensionsMenuClick = { + store.dispatch(MenuAction.Navigate.Extensions) + }, + onBookmarksMenuClick = { + store.dispatch(MenuAction.Navigate.Bookmarks) + }, + onHistoryMenuClick = { + store.dispatch(MenuAction.Navigate.History) + }, + onDownloadsMenuClick = { + store.dispatch(MenuAction.Navigate.Downloads) + }, + onPasswordsMenuClick = { + store.dispatch(MenuAction.Navigate.Passwords) + }, + onCustomizeHomepageMenuClick = { + store.dispatch(MenuAction.Navigate.CustomizeHomepage) + }, + onNewInFirefoxMenuClick = { + store.dispatch(MenuAction.Navigate.ReleaseNotes) + }, + ) + } + + composable(route = TOOLS_MENU_ROUTE) { + ToolsSubmenu( + isReaderViewActive = false, + isTranslated = false, + onBackButtonClick = { + store.dispatch(MenuAction.Navigate.Back) + }, + onReaderViewMenuClick = {}, + onTranslatePageMenuClick = { + selectedTab?.let { + store.dispatch(MenuAction.Navigate.Translate) + } + }, + onPrintMenuClick = { + printContentUseCase() + dismiss() + }, + onShareMenuClick = { + selectedTab?.let { + store.dispatch(MenuAction.Navigate.Share) + } + }, + onOpenInAppMenuClick = {}, + ) + } + + composable(route = SAVE_MENU_ROUTE) { + SaveSubmenu( + isBookmarked = isBookmarked, + onBackButtonClick = { + store.dispatch(MenuAction.Navigate.Back) + }, + onBookmarkPageMenuClick = { + store.dispatch(MenuAction.AddBookmark) + }, + onEditBookmarkButtonClick = { + store.dispatch(MenuAction.Navigate.EditBookmark) + }, + onAddToShortcutsMenuClick = {}, + onAddToHomeScreenMenuClick = {}, + onSaveToCollectionMenuClick = {}, + onSaveAsPDFMenuClick = { + saveToPdfUseCase() + dismiss() + }, + ) + } + + composable(route = EXTENSIONS_MENU_ROUTE) { + ExtensionsSubmenu( + onBackButtonClick = { + store.dispatch(MenuAction.Navigate.Back) + }, + onManageExtensionsMenuClick = { + store.dispatch(MenuAction.Navigate.ManageExtensions) + }, + onDiscoverMoreExtensionsMenuClick = { + store.dispatch(MenuAction.Navigate.DiscoverMoreExtensions) + }, + ) + } + } } } } } - private fun openSumoTopic(topic: SumoTopic) = runIfFragmentIsAttached { - (activity as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = SupportUtils.getSumoURLForTopic( + private fun openToBrowser(params: BrowserNavigationParams) = runIfFragmentIsAttached { + val url = params.url ?: params.sumoTopic?.let { + SupportUtils.getSumoURLForTopic( context = requireContext(), - topic = topic, - ), - newTab = true, - from = BrowserDirection.FromMenuDialogFragment, - ) + topic = it, + ) + } + + url?.let { + (activity as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = url, + newTab = true, + from = BrowserDirection.FromMenuDialogFragment, + ) + } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsSubmenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsSubmenu.kt new file mode 100644 index 0000000000..2bfffb7103 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsSubmenu.kt @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.components.menu.compose.header.SubmenuHeader +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.list.TextListItem +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +internal const val EXTENSIONS_MENU_ROUTE = "extensions_menu" + +@Composable +internal fun ExtensionsSubmenu( + onBackButtonClick: () -> Unit, + onManageExtensionsMenuClick: () -> Unit, + onDiscoverMoreExtensionsMenuClick: () -> Unit, +) { + Column { + SubmenuHeader( + header = stringResource(id = R.string.browser_menu_extensions), + onClick = onBackButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = Modifier + .padding( + start = 16.dp, + top = 12.dp, + end = 16.dp, + bottom = 32.dp, + ), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + MenuGroup { + MenuItem( + label = stringResource(id = R.string.browser_menu_manage_extensions), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_extension_cog_24), + onClick = onManageExtensionsMenuClick, + ) + } + + MenuGroup { + TextListItem( + label = stringResource(id = R.string.browser_menu_discover_more_extensions), + onClick = onDiscoverMoreExtensionsMenuClick, + iconPainter = painterResource(R.drawable.mozac_ic_external_link_24), + iconTint = FirefoxTheme.colors.iconSecondary, + ) + } + } + } +} + +@LightDarkPreview +@Composable +private fun ExtensionsSubmenuPreview() { + FirefoxTheme { + Column( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3), + ) { + ExtensionsSubmenu( + onBackButtonClick = {}, + onManageExtensionsMenuClick = {}, + onDiscoverMoreExtensionsMenuClick = {}, + ) + } + } +} + +@LightDarkPreview +@Composable +private fun ExtensionsSubmenuPrivatePreview() { + FirefoxTheme(theme = Theme.Private) { + Column( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3), + ) { + ExtensionsSubmenu( + onBackButtonClick = {}, + onManageExtensionsMenuClick = {}, + onDiscoverMoreExtensionsMenuClick = {}, + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt new file mode 100644 index 0000000000..695fbfcbca --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt @@ -0,0 +1,365 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import mozilla.components.service.fxa.manager.AccountState +import mozilla.components.service.fxa.manager.AccountState.NotAuthenticated +import mozilla.components.service.fxa.store.Account +import org.mozilla.fenix.R +import org.mozilla.fenix.components.menu.MenuAccessPoint +import org.mozilla.fenix.components.menu.compose.header.MenuHeader +import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +internal const val MAIN_MENU_ROUTE = "main_menu" + +/** + * Wrapper column containing the main menu items. + * + * @param accessPoint The [MenuAccessPoint] that was used to navigate to the menu dialog. + * @param account [Account] information available for a synced account. + * @param accountState The [AccountState] of a Mozilla account. + * @param isPrivate Whether or not the browsing mode is in private mode. + * @param onMozillaAccountButtonClick Invoked when the user clicks on Mozilla account button. + * @param onHelpButtonClick Invoked when the user clicks on the help button. + * @param onSettingsButtonClick Invoked when the user clicks on the settings button. + * @param onNewTabMenuClick Invoked when the user clicks on the new tab menu item. + * @param onNewPrivateTabMenuClick Invoked when the user clicks on the new private tab menu item. + * @param onSwitchToDesktopSiteMenuClick Invoked when the user clicks on the switch to desktop site + * menu toggle. + * @param onFindInPageMenuClick Invoked when the user clicks on the find in page menu item. + * @param onToolsMenuClick Invoked when the user clicks on the tools menu item. + * @param onSaveMenuClick Invoked when the user clicks on the save menu item. + * @param onExtensionsMenuClick Invoked when the user clicks on the extensions menu item. + * @param onBookmarksMenuClick Invoked when the user clicks on the bookmarks menu item. + * @param onHistoryMenuClick Invoked when the user clicks on the history menu item. + * @param onDownloadsMenuClick Invoked when the user clicks on the downloads menu item. + * @param onPasswordsMenuClick Invoked when the user clicks on the passwords menu item. + * @param onCustomizeHomepageMenuClick Invoked when the user clicks on the customize + * homepage menu item. + * @param onNewInFirefoxMenuClick Invoked when the user clicks on the release note menu item. + */ +@Suppress("LongParameterList") +@Composable +internal fun MainMenu( + accessPoint: MenuAccessPoint, + account: Account?, + accountState: AccountState, + isPrivate: Boolean, + onMozillaAccountButtonClick: () -> Unit, + onHelpButtonClick: () -> Unit, + onSettingsButtonClick: () -> Unit, + onNewTabMenuClick: () -> Unit, + onNewPrivateTabMenuClick: () -> Unit, + onSwitchToDesktopSiteMenuClick: () -> Unit, + onFindInPageMenuClick: () -> Unit, + onToolsMenuClick: () -> Unit, + onSaveMenuClick: () -> Unit, + onExtensionsMenuClick: () -> Unit, + onBookmarksMenuClick: () -> Unit, + onHistoryMenuClick: () -> Unit, + onDownloadsMenuClick: () -> Unit, + onPasswordsMenuClick: () -> Unit, + onCustomizeHomepageMenuClick: () -> Unit, + onNewInFirefoxMenuClick: () -> Unit, +) { + Column { + MenuHeader( + account = account, + accountState = accountState, + onMozillaAccountButtonClick = onMozillaAccountButtonClick, + onHelpButtonClick = onHelpButtonClick, + onSettingsButtonClick = onSettingsButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = Modifier + .padding( + start = 16.dp, + top = 12.dp, + end = 16.dp, + bottom = 32.dp, + ), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + NewTabsMenuGroup( + accessPoint = accessPoint, + isPrivate = isPrivate, + onNewTabMenuClick = onNewTabMenuClick, + onNewPrivateTabMenuClick = onNewPrivateTabMenuClick, + ) + + ToolsAndActionsMenuGroup( + accessPoint = accessPoint, + onSwitchToDesktopSiteMenuClick = onSwitchToDesktopSiteMenuClick, + onFindInPageMenuClick = onFindInPageMenuClick, + onToolsMenuClick = onToolsMenuClick, + onSaveMenuClick = onSaveMenuClick, + onExtensionsMenuClick = onExtensionsMenuClick, + ) + + LibraryMenuGroup( + onBookmarksMenuClick = onBookmarksMenuClick, + onHistoryMenuClick = onHistoryMenuClick, + onDownloadsMenuClick = onDownloadsMenuClick, + onPasswordsMenuClick = onPasswordsMenuClick, + ) + + if (accessPoint == MenuAccessPoint.Home) { + HomepageMenuGroup( + onCustomizeHomepageMenuClick = onCustomizeHomepageMenuClick, + onNewInFirefoxMenuClick = onNewInFirefoxMenuClick, + ) + } + } + } +} + +@Composable +private fun NewTabsMenuGroup( + accessPoint: MenuAccessPoint, + isPrivate: Boolean, + onNewTabMenuClick: () -> Unit, + onNewPrivateTabMenuClick: () -> Unit, +) { + val isNewTabMenuEnabled: Boolean + val isNewPrivateTabMenuEnabled: Boolean + + when (accessPoint) { + MenuAccessPoint.Browser, + MenuAccessPoint.External, + -> { + isNewTabMenuEnabled = true + isNewPrivateTabMenuEnabled = true + } + + MenuAccessPoint.Home -> { + isNewTabMenuEnabled = isPrivate + isNewPrivateTabMenuEnabled = !isPrivate + } + } + + MenuGroup { + MenuItem( + label = stringResource(id = R.string.library_new_tab), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_plus_24), + state = if (isNewTabMenuEnabled) MenuItemState.ENABLED else MenuItemState.DISABLED, + onClick = onNewTabMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_new_private_tab), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_private_mode_circle_fill_24), + state = if (isNewPrivateTabMenuEnabled) MenuItemState.ENABLED else MenuItemState.DISABLED, + onClick = onNewPrivateTabMenuClick, + ) + } +} + +@Composable +private fun ToolsAndActionsMenuGroup( + accessPoint: MenuAccessPoint, + onSwitchToDesktopSiteMenuClick: () -> Unit, + onFindInPageMenuClick: () -> Unit, + onToolsMenuClick: () -> Unit, + onSaveMenuClick: () -> Unit, + onExtensionsMenuClick: () -> Unit, +) { + MenuGroup { + if (accessPoint == MenuAccessPoint.Browser) { + MenuItem( + label = stringResource(id = R.string.browser_menu_switch_to_desktop_site), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_device_desktop_24), + onClick = onSwitchToDesktopSiteMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_find_in_page_2), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_search_24), + onClick = onFindInPageMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_tools), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_tool_24), + onClick = onToolsMenuClick, + afterIconPainter = painterResource(id = R.drawable.mozac_ic_chevron_right_24), + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_save), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_save_24), + onClick = onSaveMenuClick, + afterIconPainter = painterResource(id = R.drawable.mozac_ic_chevron_right_24), + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + } + + MenuItem( + label = stringResource(id = R.string.browser_menu_extensions), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_extension_24), + onClick = onExtensionsMenuClick, + afterIconPainter = painterResource(id = R.drawable.mozac_ic_chevron_right_24), + ) + } +} + +@Composable +private fun LibraryMenuGroup( + onBookmarksMenuClick: () -> Unit, + onHistoryMenuClick: () -> Unit, + onDownloadsMenuClick: () -> Unit, + onPasswordsMenuClick: () -> Unit, +) { + MenuGroup { + MenuItem( + label = stringResource(id = R.string.library_bookmarks), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_bookmark_tray_fill_24), + onClick = onBookmarksMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.library_history), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_history_24), + onClick = onHistoryMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.library_downloads), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_download_24), + onClick = onDownloadsMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_passwords), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_login_24), + onClick = onPasswordsMenuClick, + ) + } +} + +@Composable +private fun HomepageMenuGroup( + onCustomizeHomepageMenuClick: () -> Unit, + onNewInFirefoxMenuClick: () -> Unit, +) { + MenuGroup { + MenuItem( + label = stringResource(id = R.string.browser_menu_customize_home_1), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_grid_add_24), + onClick = onCustomizeHomepageMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource( + id = R.string.browser_menu_new_in_firefox, + stringResource(id = R.string.app_name), + ), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_whats_new_24), + onClick = onNewInFirefoxMenuClick, + ) + } +} + +@LightDarkPreview +@Composable +private fun MenuDialogPreview() { + FirefoxTheme { + Column( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer3), + ) { + MainMenu( + accessPoint = MenuAccessPoint.Home, + account = null, + accountState = NotAuthenticated, + isPrivate = false, + onMozillaAccountButtonClick = {}, + onHelpButtonClick = {}, + onSettingsButtonClick = {}, + onNewTabMenuClick = {}, + onNewPrivateTabMenuClick = {}, + onSwitchToDesktopSiteMenuClick = {}, + onFindInPageMenuClick = {}, + onToolsMenuClick = {}, + onSaveMenuClick = {}, + onExtensionsMenuClick = {}, + onBookmarksMenuClick = {}, + onHistoryMenuClick = {}, + onDownloadsMenuClick = {}, + onPasswordsMenuClick = {}, + onCustomizeHomepageMenuClick = {}, + onNewInFirefoxMenuClick = {}, + ) + } + } +} + +@Preview +@Composable +private fun MenuDialogPrivatePreview() { + FirefoxTheme(theme = Theme.Private) { + Column( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer3), + ) { + MainMenu( + accessPoint = MenuAccessPoint.Home, + account = null, + accountState = NotAuthenticated, + isPrivate = false, + onMozillaAccountButtonClick = {}, + onHelpButtonClick = {}, + onSettingsButtonClick = {}, + onNewTabMenuClick = {}, + onNewPrivateTabMenuClick = {}, + onSwitchToDesktopSiteMenuClick = {}, + onFindInPageMenuClick = {}, + onToolsMenuClick = {}, + onSaveMenuClick = {}, + onExtensionsMenuClick = {}, + onBookmarksMenuClick = {}, + onHistoryMenuClick = {}, + onDownloadsMenuClick = {}, + onPasswordsMenuClick = {}, + onCustomizeHomepageMenuClick = {}, + onNewInFirefoxMenuClick = {}, + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuDialog.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuDialog.kt deleted file mode 100644 index a9747fce84..0000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuDialog.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.components.menu.compose - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import mozilla.components.service.fxa.store.Account -import org.mozilla.fenix.R -import org.mozilla.fenix.components.accounts.AccountState -import org.mozilla.fenix.components.accounts.AccountState.NO_ACCOUNT -import org.mozilla.fenix.components.menu.compose.header.MenuHeader -import org.mozilla.fenix.compose.Divider -import org.mozilla.fenix.compose.annotation.LightDarkPreview -import org.mozilla.fenix.compose.list.IconListItem -import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme - -/** - * The menu bottom sheet dialog. - * - * @param account [Account] information available for a synced account. - * @param accountState The [AccountState] of a synced account. - * @param onSignInButtonClick Invoked when the user clicks on the "Sign in" button. - * @param onHelpButtonClick Invoked when the user clicks on the help button. - * @param onSettingsButtonClick Invoked when the user clicks on the settings button. - * @param onBookmarksMenuClick Invoked when the user clicks on the bookmarks menu item. - * @param onHistoryMenuClick Invoked when the user clicks on the history menu item. - * @param onDownloadsMenuClick Invoked when the user clicks on the downloads menu item. - * @param onPasswordsMenuClick Invoked when the user clicks on the passwords menu item. - */ -@Suppress("LongParameterList") -@Composable -fun MenuDialog( - account: Account?, - accountState: AccountState, - onSignInButtonClick: () -> Unit, - onHelpButtonClick: () -> Unit, - onSettingsButtonClick: () -> Unit, - onBookmarksMenuClick: () -> Unit, - onHistoryMenuClick: () -> Unit, - onDownloadsMenuClick: () -> Unit, - onPasswordsMenuClick: () -> Unit, -) { - Column { - MenuHeader( - account = account, - accountState = accountState, - onSignInButtonClick = onSignInButtonClick, - onHelpButtonClick = onHelpButtonClick, - onSettingsButtonClick = onSettingsButtonClick, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - MainMenu( - onBookmarksMenuClick = onBookmarksMenuClick, - onHistoryMenuClick = onHistoryMenuClick, - onDownloadsMenuClick = onDownloadsMenuClick, - onPasswordsMenuClick = onPasswordsMenuClick, - ) - } -} - -/** - * Wrapper column containing the main menu items. - */ -@Composable -private fun MainMenu( - onBookmarksMenuClick: () -> Unit, - onHistoryMenuClick: () -> Unit, - onDownloadsMenuClick: () -> Unit, - onPasswordsMenuClick: () -> Unit, -) { - Column( - modifier = Modifier - .padding( - start = 16.dp, - top = 12.dp, - end = 16.dp, - bottom = 32.dp, - ), - verticalArrangement = Arrangement.spacedBy(32.dp), - ) { - MenuGroup { - IconListItem( - label = stringResource(id = R.string.library_new_tab), - beforeIconPainter = painterResource(id = R.drawable.mozac_ic_plus_24), - ) - - Divider(color = FirefoxTheme.colors.borderSecondary) - - IconListItem( - label = stringResource(id = R.string.browser_menu_new_private_tab), - beforeIconPainter = painterResource(id = R.drawable.mozac_ic_private_mode_circle_fill_24), - ) - } - - LibraryMenuGroup( - onBookmarksMenuClick = onBookmarksMenuClick, - onHistoryMenuClick = onHistoryMenuClick, - onDownloadsMenuClick = onDownloadsMenuClick, - onPasswordsMenuClick = onPasswordsMenuClick, - ) - } -} - -@Composable -private fun LibraryMenuGroup( - onBookmarksMenuClick: () -> Unit, - onHistoryMenuClick: () -> Unit, - onDownloadsMenuClick: () -> Unit, - onPasswordsMenuClick: () -> Unit, -) { - MenuGroup { - IconListItem( - label = stringResource(id = R.string.library_bookmarks), - onClick = onBookmarksMenuClick, - beforeIconPainter = painterResource(id = R.drawable.mozac_ic_bookmark_tray_fill_24), - ) - - Divider(color = FirefoxTheme.colors.borderSecondary) - - IconListItem( - label = stringResource(id = R.string.library_history), - onClick = onHistoryMenuClick, - beforeIconPainter = painterResource(id = R.drawable.mozac_ic_history_24), - ) - - Divider(color = FirefoxTheme.colors.borderSecondary) - - IconListItem( - label = stringResource(id = R.string.library_downloads), - onClick = onDownloadsMenuClick, - beforeIconPainter = painterResource(id = R.drawable.mozac_ic_download_24), - ) - - Divider(color = FirefoxTheme.colors.borderSecondary) - - IconListItem( - label = stringResource(id = R.string.browser_menu_passwords), - onClick = onPasswordsMenuClick, - beforeIconPainter = painterResource(id = R.drawable.mozac_ic_login_24), - ) - } -} - -@LightDarkPreview -@Composable -private fun MenuDialogPreview() { - FirefoxTheme { - Column( - modifier = Modifier - .background(color = FirefoxTheme.colors.layer3), - ) { - MenuDialog( - account = null, - accountState = NO_ACCOUNT, - onSignInButtonClick = {}, - onHelpButtonClick = {}, - onSettingsButtonClick = {}, - onBookmarksMenuClick = {}, - onHistoryMenuClick = {}, - onDownloadsMenuClick = {}, - onPasswordsMenuClick = {}, - ) - } - } -} - -@Preview -@Composable -private fun MenuDialogPrivatePreview() { - FirefoxTheme(theme = Theme.Private) { - Column( - modifier = Modifier - .background(color = FirefoxTheme.colors.layer3), - ) { - MenuDialog( - account = null, - accountState = NO_ACCOUNT, - onSignInButtonClick = {}, - onHelpButtonClick = {}, - onSettingsButtonClick = {}, - onBookmarksMenuClick = {}, - onHistoryMenuClick = {}, - onDownloadsMenuClick = {}, - onPasswordsMenuClick = {}, - ) - } - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuGroup.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuGroup.kt index 1b2fb0ab2f..811ab69db9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuGroup.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuGroup.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.compose.Divider import org.mozilla.fenix.compose.annotation.LightDarkPreview -import org.mozilla.fenix.compose.list.IconListItem import org.mozilla.fenix.theme.FirefoxTheme private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(12.dp) @@ -60,14 +59,14 @@ private fun MenuGroupPreview() { .padding(16.dp), ) { MenuGroup { - IconListItem( + MenuItem( label = stringResource(id = R.string.browser_menu_add_to_homescreen), beforeIconPainter = painterResource(id = R.drawable.mozac_ic_plus_24), ) Divider(color = FirefoxTheme.colors.borderSecondary) - IconListItem( + MenuItem( label = stringResource(id = R.string.browser_menu_add_to_homescreen), beforeIconPainter = painterResource(id = R.drawable.mozac_ic_plus_24), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt new file mode 100644 index 0000000000..41f64670a2 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.list.IconListItem +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * An [IconListItem] wrapper for menu items in a [MenuGroup] with an optional icon at the end. + * + * @param label The label in the menu item. + * @param beforeIconPainter [Painter] used to display an [Icon] before the list item. + * @param beforeIconDescription Content description of the icon. + * @param description An optional description text below the label. + * @param state The state of the menu item to display. + * @param onClick Invoked when the user clicks on the item. + * @param afterIconPainter [Painter] used to display an [IconButton] after the list item. + * @param afterIconDescription Content description of the icon. + */ +@Composable +internal fun MenuItem( + label: String, + beforeIconPainter: Painter, + beforeIconDescription: String? = null, + description: String? = null, + state: MenuItemState = MenuItemState.ENABLED, + onClick: (() -> Unit)? = null, + afterIconPainter: Painter? = null, + afterIconDescription: String? = null, +) { + val labelTextColor = getLabelTextColor(state = state) + val iconTint = getIconTint(state = state) + val enabled = state != MenuItemState.DISABLED + + IconListItem( + label = label, + labelTextColor = labelTextColor, + description = description, + enabled = enabled, + onClick = onClick, + beforeIconPainter = beforeIconPainter, + beforeIconDescription = beforeIconDescription, + beforeIconTint = iconTint, + afterIconPainter = afterIconPainter, + afterIconDescription = afterIconDescription, + afterIconTint = iconTint, + ) +} + +/** + * An [IconListItem] wrapper for menu items in a [MenuGroup] with an optional text button at the end. + * + * @param label The label in the menu item. + * @param beforeIconPainter [Painter] used to display an [Icon] before the list item. + * @param beforeIconDescription Content description of the icon. + * @param description An optional description text below the label. + * @param state The state of the menu item to display. + * @param onClick Invoked when the user clicks on the item. + * @param afterButtonText The button text to be displayed after the list item. + * @param afterButtonTextColor [Color] to apply to [afterButtonText]. + * @param onAfterButtonClick Called when the user clicks on the text button. + */ +@Composable +internal fun MenuItem( + label: String, + beforeIconPainter: Painter, + beforeIconDescription: String? = null, + description: String? = null, + state: MenuItemState = MenuItemState.ENABLED, + onClick: (() -> Unit)? = null, + afterButtonText: String? = null, + afterButtonTextColor: Color = FirefoxTheme.colors.actionPrimary, + onAfterButtonClick: (() -> Unit)? = null, +) { + val labelTextColor = getLabelTextColor(state = state) + val iconTint = getIconTint(state = state) + val enabled = state != MenuItemState.DISABLED + + IconListItem( + label = label, + labelTextColor = labelTextColor, + description = description, + enabled = enabled, + onClick = onClick, + beforeIconPainter = beforeIconPainter, + beforeIconDescription = beforeIconDescription, + beforeIconTint = iconTint, + afterButtonText = afterButtonText, + afterButtonTextColor = afterButtonTextColor, + onAfterButtonClick = onAfterButtonClick, + ) +} + +/** + * Enum containing all the supported state for the menu item. + */ +enum class MenuItemState { + /** + * The menu item is enabled. + */ + ENABLED, + + /** + * The menu item is disabled and is not clickable. + */ + DISABLED, + + /** + * The menu item is highlighted to indicate the feature behind the menu item is active. + */ + ACTIVE, + + /** + * The menu item is highlighted to indicate the feature behind the menu item is destructive. + */ + WARNING, +} + +@Composable +private fun getLabelTextColor(state: MenuItemState): Color { + return when (state) { + MenuItemState.ACTIVE -> FirefoxTheme.colors.textAccent + MenuItemState.WARNING -> FirefoxTheme.colors.textCritical + else -> FirefoxTheme.colors.textPrimary + } +} + +@Composable +private fun getIconTint(state: MenuItemState): Color { + return when (state) { + MenuItemState.ACTIVE -> FirefoxTheme.colors.iconAccentViolet + MenuItemState.WARNING -> FirefoxTheme.colors.iconCritical + else -> FirefoxTheme.colors.iconSecondary + } +} + +@LightDarkPreview +@Composable +private fun MenuItemPreview() { + FirefoxTheme { + Column( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer3) + .padding(16.dp), + ) { + MenuGroup { + for (state in MenuItemState.entries) { + MenuItem( + label = stringResource(id = R.string.browser_menu_translations), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_translate_24), + state = state, + onClick = {}, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + } + + for (state in MenuItemState.entries) { + MenuItem( + label = stringResource(id = R.string.browser_menu_extensions), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_extension_24), + state = state, + onClick = {}, + afterIconPainter = painterResource(id = R.drawable.mozac_ic_chevron_right_24), + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + } + + for (state in MenuItemState.entries) { + MenuItem( + label = stringResource(id = R.string.library_bookmarks), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_bookmark_tray_fill_24), + state = state, + onClick = {}, + afterButtonText = stringResource(id = R.string.browser_menu_edit), + onAfterButtonClick = {}, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + } + } + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/SaveSubmenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/SaveSubmenu.kt new file mode 100644 index 0000000000..9ceda75cea --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/SaveSubmenu.kt @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.components.menu.compose.header.SubmenuHeader +import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +internal const val SAVE_MENU_ROUTE = "save_menu" + +@Suppress("LongParameterList") +@Composable +internal fun SaveSubmenu( + isBookmarked: Boolean, + onBackButtonClick: () -> Unit, + onBookmarkPageMenuClick: () -> Unit, + onEditBookmarkButtonClick: () -> Unit, + onAddToShortcutsMenuClick: () -> Unit, + onAddToHomeScreenMenuClick: () -> Unit, + onSaveToCollectionMenuClick: () -> Unit, + onSaveAsPDFMenuClick: () -> Unit, +) { + Column { + SubmenuHeader( + header = stringResource(id = R.string.browser_menu_save), + onClick = onBackButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = Modifier + .padding( + start = 16.dp, + top = 12.dp, + end = 16.dp, + bottom = 32.dp, + ), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + MenuGroup { + BookmarkMenuItem( + isBookmarked = isBookmarked, + onBookmarkPageMenuClick = onBookmarkPageMenuClick, + onEditBookmarkButtonClick = onEditBookmarkButtonClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_add_to_shortcuts), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_pin_24), + onClick = onAddToShortcutsMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_add_to_homescreen_2), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_add_to_homescreen_24), + onClick = onAddToHomeScreenMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_save_to_collection), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_collection_24), + onClick = onSaveToCollectionMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_save_as_pdf), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_save_file_24), + onClick = onSaveAsPDFMenuClick, + ) + } + } + } +} + +@Composable +private fun BookmarkMenuItem( + isBookmarked: Boolean, + onBookmarkPageMenuClick: () -> Unit, + onEditBookmarkButtonClick: () -> Unit, +) { + if (isBookmarked) { + MenuItem( + label = stringResource(id = R.string.browser_menu_edit_bookmark), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_bookmark_fill_24), + state = MenuItemState.ACTIVE, + onClick = onEditBookmarkButtonClick, + ) + } else { + MenuItem( + label = stringResource(id = R.string.browser_menu_bookmark_this_page), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_bookmark_24), + onClick = onBookmarkPageMenuClick, + ) + } +} + +@LightDarkPreview +@Composable +private fun SaveSubmenuPreview() { + FirefoxTheme { + Column( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3), + ) { + SaveSubmenu( + isBookmarked = false, + onBackButtonClick = {}, + onBookmarkPageMenuClick = {}, + onEditBookmarkButtonClick = {}, + onAddToShortcutsMenuClick = {}, + onAddToHomeScreenMenuClick = {}, + onSaveToCollectionMenuClick = {}, + onSaveAsPDFMenuClick = {}, + ) + } + } +} + +@Preview +@Composable +private fun SaveSubmenuPrivatePreview() { + FirefoxTheme(theme = Theme.Private) { + Column( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3), + ) { + SaveSubmenu( + isBookmarked = false, + onBackButtonClick = {}, + onBookmarkPageMenuClick = {}, + onEditBookmarkButtonClick = {}, + onAddToShortcutsMenuClick = {}, + onAddToHomeScreenMenuClick = {}, + onSaveToCollectionMenuClick = {}, + onSaveAsPDFMenuClick = {}, + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ToolsSubmenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ToolsSubmenu.kt new file mode 100644 index 0000000000..7f80d5c3e9 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ToolsSubmenu.kt @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.components.menu.compose.header.SubmenuHeader +import org.mozilla.fenix.compose.Divider +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +internal const val TOOLS_MENU_ROUTE = "tools_menu" + +@Suppress("LongParameterList") +@Composable +internal fun ToolsSubmenu( + isReaderViewActive: Boolean, + isTranslated: Boolean, + onBackButtonClick: () -> Unit, + onReaderViewMenuClick: () -> Unit, + onTranslatePageMenuClick: () -> Unit, + onPrintMenuClick: () -> Unit, + onShareMenuClick: () -> Unit, + onOpenInAppMenuClick: () -> Unit, +) { + Column { + SubmenuHeader( + header = stringResource(id = R.string.browser_menu_tools), + onClick = onBackButtonClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = Modifier + .padding( + start = 16.dp, + top = 12.dp, + end = 16.dp, + bottom = 32.dp, + ), + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + MenuGroup { + ReaderViewMenuItem( + isReaderViewActive = isReaderViewActive, + onClick = onReaderViewMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + TranslationMenuItem( + isTranslated = isTranslated, + onClick = onTranslatePageMenuClick, + ) + } + + MenuGroup { + MenuItem( + label = stringResource(id = R.string.browser_menu_print), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_print_24), + onClick = onPrintMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_share_2), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_share_android_24), + onClick = onShareMenuClick, + ) + + Divider(color = FirefoxTheme.colors.borderSecondary) + + MenuItem( + label = stringResource(id = R.string.browser_menu_open_app_link), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_more_grid_24), + onClick = onOpenInAppMenuClick, + ) + } + } + } +} + +@Composable +private fun ReaderViewMenuItem( + isReaderViewActive: Boolean, + onClick: () -> Unit, +) { + if (isReaderViewActive) { + MenuItem( + label = stringResource(id = R.string.browser_menu_turn_off_reader_view), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_reader_view_fill_24), + state = MenuItemState.ACTIVE, + onClick = onClick, + ) + } else { + MenuItem( + label = stringResource(id = R.string.browser_menu_turn_on_reader_view), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_reader_view_24), + onClick = onClick, + ) + } +} + +@Composable +private fun TranslationMenuItem( + isTranslated: Boolean, + onClick: () -> Unit, +) { + if (isTranslated) { + MenuItem( + label = stringResource( + id = R.string.browser_menu_translated_to, + stringResource(id = R.string.app_name), + ), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_translate_24), + state = MenuItemState.ACTIVE, + onClick = onClick, + ) + } else { + MenuItem( + label = stringResource(id = R.string.browser_menu_translate_page), + beforeIconPainter = painterResource(id = R.drawable.mozac_ic_translate_24), + onClick = onClick, + ) + } +} + +@LightDarkPreview +@Composable +private fun ToolsSubmenuPreview() { + FirefoxTheme { + Column( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3), + ) { + ToolsSubmenu( + isReaderViewActive = false, + isTranslated = false, + onBackButtonClick = {}, + onReaderViewMenuClick = {}, + onTranslatePageMenuClick = {}, + onPrintMenuClick = {}, + onShareMenuClick = {}, + onOpenInAppMenuClick = {}, + ) + } + } +} + +@Preview +@Composable +private fun ToolsSubmenuPrivatePreview() { + FirefoxTheme(theme = Theme.Private) { + Column( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3), + ) { + ToolsSubmenu( + isReaderViewActive = false, + isTranslated = false, + onBackButtonClick = {}, + onReaderViewMenuClick = {}, + onTranslatePageMenuClick = {}, + onPrintMenuClick = {}, + onShareMenuClick = {}, + onOpenInAppMenuClick = {}, + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MenuHeader.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MenuHeader.kt index 1c31f8b87f..a9391696d9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MenuHeader.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MenuHeader.kt @@ -20,10 +20,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import mozilla.components.service.fxa.manager.AccountState +import mozilla.components.service.fxa.manager.AccountState.NotAuthenticated import mozilla.components.service.fxa.store.Account import org.mozilla.fenix.R -import org.mozilla.fenix.components.accounts.AccountState -import org.mozilla.fenix.components.accounts.AccountState.NO_ACCOUNT import org.mozilla.fenix.compose.Divider import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.theme.FirefoxTheme @@ -33,7 +33,7 @@ import org.mozilla.fenix.theme.Theme internal fun MenuHeader( account: Account?, accountState: AccountState, - onSignInButtonClick: () -> Unit, + onMozillaAccountButtonClick: () -> Unit, onHelpButtonClick: () -> Unit, onSettingsButtonClick: () -> Unit, ) { @@ -46,7 +46,7 @@ internal fun MenuHeader( MozillaAccountMenuButton( account = account, accountState = accountState, - onSignInButtonClick = onSignInButtonClick, + onClick = onMozillaAccountButtonClick, modifier = Modifier.weight(1f), ) @@ -86,8 +86,8 @@ private fun MenuHeaderPreview() { ) { MenuHeader( account = null, - accountState = NO_ACCOUNT, - onSignInButtonClick = {}, + accountState = NotAuthenticated, + onMozillaAccountButtonClick = {}, onHelpButtonClick = {}, onSettingsButtonClick = {}, ) @@ -105,8 +105,8 @@ private fun MenuHeaderPrivatePreview() { ) { MenuHeader( account = null, - accountState = NO_ACCOUNT, - onSignInButtonClick = {}, + accountState = NotAuthenticated, + onMozillaAccountButtonClick = {}, onHelpButtonClick = {}, onSettingsButtonClick = {}, ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MozillaAccountMenuButton.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MozillaAccountMenuButton.kt index 9cf00e248a..4e8663c81a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MozillaAccountMenuButton.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MozillaAccountMenuButton.kt @@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -24,24 +26,28 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import mozilla.components.service.fxa.manager.AccountState +import mozilla.components.service.fxa.manager.AccountState.Authenticated +import mozilla.components.service.fxa.manager.AccountState.Authenticating +import mozilla.components.service.fxa.manager.AccountState.AuthenticationProblem +import mozilla.components.service.fxa.manager.AccountState.NotAuthenticated import mozilla.components.service.fxa.store.Account import org.mozilla.fenix.R -import org.mozilla.fenix.components.accounts.AccountState -import org.mozilla.fenix.components.accounts.AccountState.AUTHENTICATED -import org.mozilla.fenix.components.accounts.AccountState.NEEDS_REAUTHENTICATION -import org.mozilla.fenix.components.accounts.AccountState.NO_ACCOUNT +import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.Theme private val BUTTON_HEIGHT = 56.dp private val BUTTON_SHAPE = RoundedCornerShape(size = 8.dp) +private val ICON_SHAPE = RoundedCornerShape(size = 24.dp) +private val AVATAR_SIZE = 24.dp @Composable internal fun MozillaAccountMenuButton( account: Account?, accountState: AccountState, - onSignInButtonClick: () -> Unit, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -51,13 +57,13 @@ internal fun MozillaAccountMenuButton( shape = BUTTON_SHAPE, ) .clip(shape = BUTTON_SHAPE) - .clickable { onSignInButtonClick() } + .clickable { onClick() } .defaultMinSize(minHeight = BUTTON_HEIGHT), verticalAlignment = Alignment.CenterVertically, ) { Spacer(modifier = Modifier.width(4.dp)) - AvatarIcon() + AvatarIcon(account) Column( modifier = Modifier @@ -65,7 +71,7 @@ internal fun MozillaAccountMenuButton( .weight(1f), ) { when (accountState) { - NO_ACCOUNT -> { + NotAuthenticated -> { Text( text = stringResource(id = R.string.browser_menu_sign_in), color = FirefoxTheme.colors.textSecondary, @@ -81,7 +87,7 @@ internal fun MozillaAccountMenuButton( ) } - NEEDS_REAUTHENTICATION -> { + AuthenticationProblem -> { Text( text = stringResource(id = R.string.browser_menu_sign_back_in_to_sync), color = FirefoxTheme.colors.textSecondary, @@ -91,13 +97,13 @@ internal fun MozillaAccountMenuButton( Text( text = stringResource(id = R.string.browser_menu_syncing_paused_caption), - color = FirefoxTheme.colors.textWarning, + color = FirefoxTheme.colors.textCritical, maxLines = 2, style = FirefoxTheme.typography.caption, ) } - AUTHENTICATED -> { + Authenticated -> { Text( text = account?.displayName ?: account?.email ?: stringResource(id = R.string.browser_menu_account_settings), @@ -106,14 +112,16 @@ internal fun MozillaAccountMenuButton( style = FirefoxTheme.typography.headline7, ) } + + is Authenticating -> Unit } } - if (accountState == NEEDS_REAUTHENTICATION) { + if (accountState == AuthenticationProblem) { Icon( painter = painterResource(R.drawable.mozac_ic_warning_fill_24), contentDescription = null, - tint = FirefoxTheme.colors.iconWarning, + tint = FirefoxTheme.colors.iconCritical, ) Spacer(modifier = Modifier.width(8.dp)) @@ -122,14 +130,14 @@ internal fun MozillaAccountMenuButton( } @Composable -private fun AvatarIcon() { +private fun FallbackAvatarIcon() { Icon( painter = painterResource(id = R.drawable.mozac_ic_avatar_circle_24), contentDescription = null, modifier = Modifier .background( color = FirefoxTheme.colors.layer2, - shape = RoundedCornerShape(size = 24.dp), + shape = ICON_SHAPE, ) .padding(all = 4.dp), tint = FirefoxTheme.colors.iconSecondary, @@ -137,6 +145,30 @@ private fun AvatarIcon() { } @Composable +private fun AvatarIcon(account: Account?) { + val avatarUrl = account?.avatar?.url + + if (avatarUrl != null) { + Image( + url = avatarUrl, + modifier = Modifier + .background( + color = FirefoxTheme.colors.layer2, + shape = ICON_SHAPE, + ) + .padding(all = 4.dp) + .size(AVATAR_SIZE) + .clip(CircleShape), + targetSize = AVATAR_SIZE, + placeholder = { FallbackAvatarIcon() }, + fallback = { FallbackAvatarIcon() }, + ) + } else { + FallbackAvatarIcon() + } +} + +@Composable private fun MenuHeaderPreviewContent() { Column( modifier = Modifier @@ -146,14 +178,14 @@ private fun MenuHeaderPreviewContent() { ) { MozillaAccountMenuButton( account = null, - accountState = NO_ACCOUNT, - onSignInButtonClick = {}, + accountState = NotAuthenticated, + onClick = {}, ) MozillaAccountMenuButton( account = null, - accountState = NEEDS_REAUTHENTICATION, - onSignInButtonClick = {}, + accountState = AuthenticationProblem, + onClick = {}, ) MozillaAccountMenuButton( @@ -165,8 +197,8 @@ private fun MenuHeaderPreviewContent() { currentDeviceId = null, sessionToken = null, ), - accountState = AUTHENTICATED, - onSignInButtonClick = {}, + accountState = Authenticated, + onClick = {}, ) MozillaAccountMenuButton( @@ -178,8 +210,8 @@ private fun MenuHeaderPreviewContent() { currentDeviceId = null, sessionToken = null, ), - accountState = AUTHENTICATED, - onSignInButtonClick = {}, + accountState = Authenticated, + onClick = {}, ) MozillaAccountMenuButton( @@ -191,8 +223,8 @@ private fun MenuHeaderPreviewContent() { currentDeviceId = null, sessionToken = null, ), - accountState = AUTHENTICATED, - onSignInButtonClick = {}, + accountState = Authenticated, + onClick = {}, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/SubmenuHeader.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/SubmenuHeader.kt new file mode 100644 index 0000000000..6b47ea17db --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/SubmenuHeader.kt @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.compose.header + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +@Composable +internal fun SubmenuHeader( + header: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .padding(start = 4.dp, end = 16.dp) + .defaultMinSize(minHeight = 56.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + onClick = { onClick() }, + ) { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_back_24), + contentDescription = null, + tint = FirefoxTheme.colors.iconSecondary, + ) + } + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = header, + modifier = Modifier + .weight(1f) + .semantics { heading() }, + color = FirefoxTheme.colors.textSecondary, + style = FirefoxTheme.typography.headline7, + ) + } +} + +@LightDarkPreview +@Composable +private fun SubmenuHeaderPreview() { + FirefoxTheme { + Column( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer3), + ) { + SubmenuHeader( + header = "sub-menu header", + onClick = {}, + ) + } + } +} + +@Preview +@Composable +private fun SubmenuMenuHeaderPrivatePreview() { + FirefoxTheme(theme = Theme.Private) { + Column( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer3), + ) { + SubmenuHeader( + header = "sub-menu header", + onClick = {}, + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuDialogMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuDialogMiddleware.kt new file mode 100644 index 0000000000..16df4ed576 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuDialogMiddleware.kt @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.menu.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.browser.state.ext.getUrl +import mozilla.components.concept.storage.BookmarksStorage +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.lib.state.Store +import org.mozilla.fenix.components.bookmarks.BookmarksUseCase +import org.mozilla.fenix.components.menu.store.BookmarkState +import org.mozilla.fenix.components.menu.store.MenuAction +import org.mozilla.fenix.components.menu.store.MenuState + +/** + * [Middleware] implementation for handling [MenuAction] and managing the [MenuState] for the menu + * dialog. + * + * @param bookmarksStorage An instance of the [BookmarksStorage] used + * to query matching bookmarks. + * @param addBookmarkUseCase The [BookmarksUseCase.AddBookmarksUseCase] for adding the + * selected tab as a bookmark. + * @param scope [CoroutineScope] used to launch coroutines. + */ +class MenuDialogMiddleware( + private val bookmarksStorage: BookmarksStorage, + private val addBookmarkUseCase: BookmarksUseCase.AddBookmarksUseCase, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO), +) : Middleware<MenuState, MenuAction> { + + override fun invoke( + context: MiddlewareContext<MenuState, MenuAction>, + next: (MenuAction) -> Unit, + action: MenuAction, + ) { + when (action) { + is MenuAction.InitAction -> initialize(context.store) + is MenuAction.AddBookmark -> addBookmark(context.store) + else -> Unit + } + + next(action) + } + + private fun initialize( + store: Store<MenuState, MenuAction>, + ) = scope.launch { + val url = store.state.browserMenuState?.selectedTab?.content?.url ?: return@launch + val bookmark = + bookmarksStorage.getBookmarksWithUrl(url).firstOrNull { it.url == url } ?: return@launch + + store.dispatch( + MenuAction.UpdateBookmarkState( + bookmarkState = BookmarkState( + guid = bookmark.guid, + isBookmarked = true, + ), + ), + ) + } + + private fun addBookmark( + store: Store<MenuState, MenuAction>, + ) = scope.launch { + val browserMenuState = store.state.browserMenuState ?: return@launch + + if (browserMenuState.bookmarkState.isBookmarked) { + return@launch + } + + val selectedTab = browserMenuState.selectedTab + val url = selectedTab.getUrl() ?: return@launch + + val guid = addBookmarkUseCase( + url = url, + title = selectedTab.content.title, + ) + + store.dispatch( + MenuAction.UpdateBookmarkState( + BookmarkState( + guid = guid, + isBookmarked = true, + ), + ), + ) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuNavigationMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuNavigationMiddleware.kt index 7c0a03d5ab..21a361bd0b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuNavigationMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuNavigationMiddleware.kt @@ -5,18 +5,35 @@ package org.mozilla.fenix.components.menu.middleware import androidx.navigation.NavController +import androidx.navigation.NavHostController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.appservices.places.BookmarkRoot +import mozilla.components.browser.state.ext.getUrl +import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.service.fxa.manager.AccountState.Authenticated +import mozilla.components.service.fxa.manager.AccountState.Authenticating +import mozilla.components.service.fxa.manager.AccountState.AuthenticationProblem +import mozilla.components.service.fxa.manager.AccountState.NotAuthenticated import org.mozilla.fenix.R +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.menu.BrowserNavigationParams import org.mozilla.fenix.components.menu.MenuDialogFragmentDirections +import org.mozilla.fenix.components.menu.compose.EXTENSIONS_MENU_ROUTE +import org.mozilla.fenix.components.menu.compose.SAVE_MENU_ROUTE +import org.mozilla.fenix.components.menu.compose.TOOLS_MENU_ROUTE import org.mozilla.fenix.components.menu.store.MenuAction import org.mozilla.fenix.components.menu.store.MenuState import org.mozilla.fenix.components.menu.store.MenuStore +import org.mozilla.fenix.components.menu.toFenixFxAEntryPoint import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.settings.SupportUtils.AMO_HOMEPAGE_FOR_ANDROID import org.mozilla.fenix.settings.SupportUtils.SumoTopic /** @@ -24,25 +41,62 @@ import org.mozilla.fenix.settings.SupportUtils.SumoTopic * dispatched to the [MenuStore]. * * @param navController [NavController] used for navigation. - * @param openSumoTopic Callback to open the provided [SumoTopic] in a new browser tab. + * @param navHostController [NavHostController] used for Compose navigation. + * @param browsingModeManager [BrowsingModeManager] used for setting the browsing mode. + * @param openToBrowser Callback to open the provided [BrowserNavigationParams] + * in a new browser tab. * @param scope [CoroutineScope] used to launch coroutines. */ class MenuNavigationMiddleware( private val navController: NavController, - private val openSumoTopic: (topic: SumoTopic) -> Unit, + private val navHostController: NavHostController, + private val browsingModeManager: BrowsingModeManager, + private val openToBrowser: (params: BrowserNavigationParams) -> Unit, private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : Middleware<MenuState, MenuAction> { + @Suppress("CyclomaticComplexMethod", "LongMethod") override fun invoke( context: MiddlewareContext<MenuState, MenuAction>, next: (MenuAction) -> Unit, action: MenuAction, ) { + // Get the current state before further processing of the chain of actions. + // This is to ensure that any navigation action will be using correct + // state properties before they are modified due to other actions being + // dispatched and processes. + val currentState = context.state + next(action) scope.launch { when (action) { - is MenuAction.Navigate.Help -> openSumoTopic(SumoTopic.HELP) + is MenuAction.Navigate.MozillaAccount -> { + when (action.accountState) { + Authenticated -> navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalAccountSettingsFragment(), + ) + + AuthenticationProblem -> navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalAccountProblemFragment( + entrypoint = action.accesspoint.toFenixFxAEntryPoint(), + ), + ) + + is Authenticating, NotAuthenticated -> navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalTurnOnSync( + entrypoint = action.accesspoint.toFenixFxAEntryPoint(), + ), + ) + } + } + + is MenuAction.Navigate.Help -> openToBrowser( + BrowserNavigationParams(sumoTopic = SumoTopic.HELP), + ) is MenuAction.Navigate.Settings -> navController.nav( R.id.menuDialogFragment, @@ -69,8 +123,82 @@ class MenuNavigationMiddleware( MenuDialogFragmentDirections.actionGlobalSavedLoginsAuthFragment(), ) + is MenuAction.Navigate.CustomizeHomepage -> navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalHomeSettingsFragment(), + ) + + is MenuAction.Navigate.ReleaseNotes -> openToBrowser( + BrowserNavigationParams(url = SupportUtils.WHATS_NEW_URL), + ) + + is MenuAction.Navigate.Tools -> navHostController.navigate(route = TOOLS_MENU_ROUTE) + + is MenuAction.Navigate.Save -> navHostController.navigate(route = SAVE_MENU_ROUTE) + + is MenuAction.Navigate.Extensions -> navHostController.navigate(route = EXTENSIONS_MENU_ROUTE) + + is MenuAction.Navigate.Back -> navHostController.popBackStack() + + is MenuAction.Navigate.EditBookmark -> { + currentState.browserMenuState?.bookmarkState?.guid?.let { guidToEdit -> + navController.nav( + R.id.menuDialogFragment, + BrowserFragmentDirections.actionGlobalBookmarkEditFragment( + guidToEdit = guidToEdit, + requiresSnackbarPaddingForToolbar = true, + ), + ) + } + } + + is MenuAction.Navigate.Translate -> navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionMenuDialogFragmentToTranslationsDialogFragment(), + ) + + is MenuAction.Navigate.Share -> { + currentState.browserMenuState?.selectedTab?.let { selectedTab -> + navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalShareFragment( + sessionId = selectedTab.id, + data = arrayOf( + ShareData( + url = selectedTab.getUrl(), + title = selectedTab.content.title, + ), + ), + showPage = true, + ), + ) + } + } + + is MenuAction.Navigate.ManageExtensions -> navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalAddonsManagementFragment(), + ) + + is MenuAction.Navigate.DiscoverMoreExtensions -> openToBrowser( + BrowserNavigationParams(url = AMO_HOMEPAGE_FOR_ANDROID), + ) + + is MenuAction.Navigate.NewTab -> openNewTab(isPrivate = false) + + is MenuAction.Navigate.NewPrivateTab -> openNewTab(isPrivate = true) + else -> Unit } } } + + private fun openNewTab(isPrivate: Boolean) { + browsingModeManager.mode = BrowsingMode.fromBoolean(isPrivate) + + navController.nav( + R.id.menuDialogFragment, + MenuDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true), + ) + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuAction.kt index 1400deeae1..021bb63687 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuAction.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuAction.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix.components.menu.store import mozilla.components.lib.state.Action +import mozilla.components.service.fxa.manager.AccountState +import org.mozilla.fenix.components.menu.MenuAccessPoint /** * Actions to dispatch through the [MenuStore] to modify the [MenuState]. @@ -12,11 +14,24 @@ import mozilla.components.lib.state.Action sealed class MenuAction : Action { /** - * Updates whether or not the current selected tab is bookmarked. + * [MenuAction] dispatched to indicate that the store is initialized and + * ready to use. This action is dispatched automatically before any other + * action is processed. Its main purpose is to trigger initialization logic + * in middlewares. + */ + data object InitAction : MenuAction() + + /** + * [MenuAction] dispatched when a bookmark is to be added. + */ + data object AddBookmark : MenuAction() + + /** + * [MenuAction] dispatched when a bookmark state is updated. * - * @property isBookmarked Whether or not the current selected is bookmarked. + * @property bookmarkState The new [BookmarkState] to be updated. */ - data class UpdateBookmarked(val isBookmarked: Boolean) : MenuAction() + data class UpdateBookmarkState(val bookmarkState: BookmarkState) : MenuAction() /** * [MenuAction] dispatched when a navigation event occurs for a specific destination. @@ -24,6 +39,17 @@ sealed class MenuAction : Action { sealed class Navigate : MenuAction() { /** + * [Navigate] action dispatched when navigating to Mozilla account. + * + * @property accountState The [AccountState] of a Mozilla account. + * @property accesspoint The access point that was used to navigate to the menu. + */ + data class MozillaAccount( + val accountState: AccountState, + val accesspoint: MenuAccessPoint, + ) : Navigate() + + /** * [Navigate] action dispatched when navigating to the help SUMO article. */ data object Help : Navigate() @@ -52,5 +78,70 @@ sealed class MenuAction : Action { * [Navigate] action dispatched when navigating to passwords. */ data object Passwords : Navigate() + + /** + * [Navigate] action dispatched when navigating to customize homepage. + */ + data object CustomizeHomepage : Navigate() + + /** + * [Navigate] action dispatched when navigating to release notes. + */ + data object ReleaseNotes : Navigate() + + /** + * [Navigate] action dispatched when navigating to the tools submenu. + */ + data object Tools : Navigate() + + /** + * [Navigate] action dispatched when navigating to the save submenu. + */ + data object Save : Navigate() + + /** + * [Navigate] action dispatched when navigating to the extensions submenu. + */ + data object Extensions : Navigate() + + /** + * [Navigate] action dispatched when a back navigation event occurs. + */ + data object Back : Navigate() + + /** + * [Navigate] action dispatched when navigating to edit the existing bookmark. + */ + data object EditBookmark : Navigate() + + /** + * [Navigate] action dispatched when navigating to translations dialog. + */ + data object Translate : Navigate() + + /** + * [Navigate] action dispatched when navigating to the share sheet. + */ + data object Share : Navigate() + + /** + * [Navigate] action dispatched when navigating to the extensions manager. + */ + data object ManageExtensions : Navigate() + + /** + * [Navigate] action dispatched when navigating to the AMO page. + */ + data object DiscoverMoreExtensions : Navigate() + + /** + * [Navigate] action dispatched when navigating to the new tab. + */ + data object NewTab : Navigate() + + /** + * [Navigate] action dispatched when navigating to the new private tab. + */ + data object NewPrivateTab : Navigate() } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuState.kt index 0f5b7a61e3..7635eebaae 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuState.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuState.kt @@ -4,13 +4,36 @@ package org.mozilla.fenix.components.menu.store +import mozilla.components.browser.state.state.TabSessionState import mozilla.components.lib.state.State /** * Value type that represents the state of the menu. * - * @property isBookmarked Whether or not the current selected tab is bookmarked. + * @property browserMenuState The [BrowserMenuState] of the current browser session if any. */ data class MenuState( - val isBookmarked: Boolean = false, + val browserMenuState: BrowserMenuState? = null, ) : State + +/** + * Value type that represents the state of the browser menu. + * + * @property selectedTab The current selected [TabSessionState]. + * @property bookmarkState The [BookmarkState] of the selected tab. + */ +data class BrowserMenuState( + val selectedTab: TabSessionState, + val bookmarkState: BookmarkState = BookmarkState(), +) + +/** + * Value type that represents the bookmark state of a tab. + * + * @property guid The id of the bookmark. + * @property isBookmarked Whether or not the selected tab is bookmarked. + */ +data class BookmarkState( + val guid: String? = null, + val isBookmarked: Boolean = false, +) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuStore.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuStore.kt index 14cca54e61..bc2c5b9037 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuStore.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuStore.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.components.menu.store +import androidx.annotation.VisibleForTesting import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Store @@ -13,16 +14,32 @@ import mozilla.components.lib.state.Store class MenuStore( initialState: MenuState, middleware: List<Middleware<MenuState, MenuAction>> = listOf(), -) : - Store<MenuState, MenuAction>( - initialState = initialState, - reducer = ::reducer, - middleware = middleware, - ) +) : Store<MenuState, MenuAction>( + initialState = initialState, + reducer = ::reducer, + middleware = middleware, +) { + init { + dispatch(MenuAction.InitAction) + } +} private fun reducer(state: MenuState, action: MenuAction): MenuState { return when (action) { - is MenuAction.UpdateBookmarked -> state.copy(isBookmarked = action.isBookmarked) - is MenuAction.Navigate -> state + is MenuAction.InitAction, + is MenuAction.AddBookmark, + is MenuAction.Navigate, + -> state + + is MenuAction.UpdateBookmarkState -> state.copyWithBrowserMenuState { + it.copy(bookmarkState = action.bookmarkState) + } } } + +@VisibleForTesting +internal inline fun MenuState.copyWithBrowserMenuState( + crossinline update: (BrowserMenuState) -> BrowserMenuState, +): MenuState { + return this.copy(browserMenuState = this.browserMenuState?.let { update(it) }) +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index 63cec2f8d7..78e3a7c44c 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.components.toolbar import androidx.navigation.NavController import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.ext.getUrl import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.getNormalOrPrivateTabs @@ -233,18 +234,20 @@ class DefaultBrowserToolbarController( override fun handleTranslationsButtonClick() { Translations.action.record(Translations.ActionExtra("main_flow_toolbar")) val directions = - BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment( - sessionId = currentSession?.id, - ) + BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment() navController.navigateSafe(R.id.browserFragment, directions) } override fun onShareActionClicked() { + val sessionId = currentSession?.id + val url = sessionId?.let { + store.state.findTab(it)?.getUrl() + } val directions = NavGraphDirections.actionGlobalShareFragment( - sessionId = currentSession?.id, + sessionId = sessionId, data = arrayOf( ShareData( - url = getProperUrl(currentSession), + url = url, title = currentSession?.content?.title, ), ), @@ -253,17 +256,6 @@ class DefaultBrowserToolbarController( navController.navigate(directions) } - private fun getProperUrl(currentSession: SessionState?): String? { - return currentSession?.id?.let { - val currentTab = store.state.findTab(it) - if (currentTab?.readerState?.active == true) { - currentTab.readerState.activeUrl - } else { - currentSession.content.url - } - } - } - companion object { internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu" } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt index 56d65453fe..a214718ba1 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.navigation.NavController import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CoroutineScope @@ -16,10 +17,10 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.ext.getUrl import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab -import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.concept.engine.prompt.ShareData @@ -51,6 +52,7 @@ import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.openSetDefaultBrowserOption +import org.mozilla.fenix.settings.biometric.bindBiometricsCredentialsPromptOrShowWarning import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.utils.Settings @@ -63,6 +65,7 @@ interface BrowserToolbarMenuController { @Suppress("LargeClass", "ForbiddenComment", "LongParameterList") class DefaultBrowserToolbarMenuController( + private val fragment: Fragment, private val store: BrowserStore, private val activity: HomeActivity, private val navController: NavController, @@ -79,7 +82,8 @@ class DefaultBrowserToolbarMenuController( private val tabCollectionStorage: TabCollectionStorage, private val topSitesStorage: DefaultTopSitesStorage, private val pinnedSiteStorage: PinnedSiteStorage, - private val browserStore: BrowserStore, + private val onShowPinVerification: (Intent) -> Unit, + private val onBiometricAuthenticationSuccessful: () -> Unit, ) : BrowserToolbarMenuController { private val currentSession @@ -214,11 +218,15 @@ class DefaultBrowserToolbarMenuController( } } is ToolbarMenu.Item.Share -> { + val sessionId = currentSession?.id + val url = sessionId?.let { + store.state.findTab(it)?.getUrl() + } val directions = NavGraphDirections.actionGlobalShareFragment( - sessionId = currentSession?.id, + sessionId = sessionId, data = arrayOf( ShareData( - url = getProperUrl(currentSession), + url = url, title = currentSession?.content?.title, ), ), @@ -257,9 +265,9 @@ class DefaultBrowserToolbarMenuController( } } is ToolbarMenu.Item.OpenInRegularTab -> { - currentSession?.let { session -> - getProperUrl(session)?.let { url -> - tabsUseCases.migratePrivateTabUseCase.invoke(session.id, url) + currentSession?.id?.let { sessionId -> + store.state.findTab(sessionId)?.getUrl()?.let { url -> + tabsUseCases.migratePrivateTabUseCase.invoke(sessionId, url) } } } @@ -350,7 +358,7 @@ class DefaultBrowserToolbarMenuController( } is ToolbarMenu.Item.Bookmark -> { store.state.selectedTab?.let { - getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) } + it.getUrl()?.let { url -> bookmarkTapped(url, it.content.title) } } } is ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically { @@ -365,7 +373,15 @@ class DefaultBrowserToolbarMenuController( BrowserFragmentDirections.actionGlobalHistoryFragment(), ) } - + is ToolbarMenu.Item.Passwords -> browserAnimator.captureEngineViewAndDrawStatically { + fragment.view?.let { view -> + bindBiometricsCredentialsPromptOrShowWarning( + view = view, + onShowPinVerification = onShowPinVerification, + onAuthSuccess = onBiometricAuthenticationSuccessful, + ) + } + } is ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, @@ -411,25 +427,12 @@ class DefaultBrowserToolbarMenuController( ToolbarMenu.Item.Translate -> { Translations.action.record(Translations.ActionExtra("main_flow_browser")) val directions = - BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment( - sessionId = currentSession?.id, - ) + BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment() navController.navigateSafe(R.id.browserFragment, directions) } } } - private fun getProperUrl(currentSession: SessionState?): String? { - return currentSession?.id?.let { - val currentTab = browserStore.state.findTab(it) - if (currentTab?.readerState?.active == true) { - currentTab.readerState.activeUrl - } else { - currentSession.content.url - } - } - } - @Suppress("ComplexMethod", "LongMethod") private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { when (item) { @@ -494,6 +497,8 @@ class DefaultBrowserToolbarMenuController( Events.browserMenuAction.record(Events.BrowserMenuActionExtra("bookmarks")) is ToolbarMenu.Item.History -> Events.browserMenuAction.record(Events.BrowserMenuActionExtra("history")) + is ToolbarMenu.Item.Passwords -> + Events.browserMenuAction.record(Events.BrowserMenuActionExtra("passwords")) is ToolbarMenu.Item.Downloads -> Events.browserMenuAction.record(Events.BrowserMenuActionExtra("downloads")) is ToolbarMenu.Item.NewTab -> diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index 2c93538953..199bfb3bcc 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.components.toolbar import android.content.Context import android.graphics.Color +import android.net.Uri import android.view.HapticFeedbackConstants import android.view.LayoutInflater import android.view.View @@ -29,6 +30,7 @@ import mozilla.components.concept.toolbar.ScrollableToolbar import mozilla.components.support.ktx.util.URLStringUtils import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior import org.mozilla.fenix.R +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration import org.mozilla.fenix.customtabs.CustomTabToolbarMenu @@ -43,7 +45,7 @@ import mozilla.components.ui.widgets.behavior.ViewPosition as MozacToolbarPositi @SuppressWarnings("LargeClass", "LongParameterList") class BrowserToolbarView( - context: Context, + private val context: Context, container: ViewGroup, private val settings: Settings, private val interactor: BrowserToolbarInteractor, @@ -70,6 +72,8 @@ class BrowserToolbarView( private val tabStripView: ComposeView by lazy { layout.findViewById(R.id.tabStripView) } + private val isNavBarEnabled = IncompleteRedesignToolbarFeature(context.settings()).isEnabled + val toolbarIntegration: ToolbarIntegration val menuToolbar: ToolbarMenu @@ -101,9 +105,11 @@ class BrowserToolbarView( true } + view.isNavBarEnabled = isNavBarEnabled + with(context) { val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported() - val searchUrlBackground = if (IncompleteRedesignToolbarFeature(context.settings()).isEnabled) { + val searchUrlBackground = if (isNavBarEnabled) { R.drawable.search_url_background } else { R.drawable.search_old_url_background @@ -150,7 +156,13 @@ class BrowserToolbarView( ThemeManager.resolveAttribute(R.attr.borderToolbarDivider, context), ) - display.urlFormatter = { url -> URLStringUtils.toDisplayUrl(url) } + display.urlFormatter = { url -> + if (isNavBarEnabled) { + Uri.parse(url.toString()).host ?: url + } else { + URLStringUtils.toDisplayUrl(url) + } + } display.colors = display.colors.copy( text = primaryTextColor, @@ -211,8 +223,6 @@ class BrowserToolbarView( isPrivate = customTabSession.content.private, ) } else { - val isNavBarEnabled = IncompleteRedesignToolbarFeature(context.settings()).isEnabled - DefaultToolbarIntegration( this, view, @@ -322,5 +332,5 @@ class BrowserToolbarView( } private fun shouldShowTabStrip() = - customTabSession == null && settings.isTabletAndTabStripEnabled + customTabSession == null && context.isTabStripEnabled() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index 6e28294734..0af1430417 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -232,6 +232,14 @@ open class DefaultToolbarMenu( onItemTapped.invoke(ToolbarMenu.Item.Downloads) } + private val passwordsItem = BrowserMenuImageText( + context.getString(R.string.preferences_sync_logins_2), + R.drawable.mozac_ic_login_24, + primaryTextColor(), + ) { + onItemTapped.invoke(ToolbarMenu.Item.Passwords) + } + private val extensionsItem = WebExtensionPlaceholderMenuItem( id = WebExtensionPlaceholderMenuItem.MAIN_EXTENSIONS_MENU_ID, ) @@ -409,6 +417,7 @@ open class DefaultToolbarMenu( bookmarksItem, historyItem, downloadsItem, + passwordsItem, extensionsItem, syncMenuItem(), BrowserMenuDivider(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index 179d30db5c..ebffb04909 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -48,6 +48,11 @@ interface ToolbarMenu { object CustomizeReaderView : Item() object Bookmarks : Item() object History : Item() + + /** + * The Passwords menu item + */ + object Passwords : Item() object Downloads : Item() object NewTab : Item() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/DismissibleItemBackground.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/DismissibleItemBackground.kt index a4ad6da918..98448f626b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/DismissibleItemBackground.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/DismissibleItemBackground.kt @@ -59,7 +59,7 @@ fun DismissibleItemBackground( Alignment.CenterStart }, ), - tint = FirefoxTheme.colors.iconWarning, + tint = FirefoxTheme.colors.iconCritical, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt index 2cc4421ac7..ad65c37d17 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt @@ -232,7 +232,7 @@ fun DestructiveButton( text: String, modifier: Modifier = Modifier.fillMaxWidth(), enabled: Boolean = true, - textColor: Color = FirefoxTheme.colors.textWarningButton, + textColor: Color = FirefoxTheme.colors.textCriticalButton, backgroundColor: Color = FirefoxTheme.colors.actionSecondary, icon: Painter? = null, iconModifier: Modifier = Modifier, @@ -246,7 +246,7 @@ fun DestructiveButton( enabled = enabled, icon = icon, iconModifier = iconModifier, - tint = FirefoxTheme.colors.iconWarningButton, + tint = FirefoxTheme.colors.iconCriticalButton, onClick = onClick, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt index 018b6722ba..0d09bc6a22 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt @@ -5,7 +5,7 @@ package org.mozilla.fenix.compose.button import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -20,6 +20,8 @@ import java.util.Locale * @param text The button text to be displayed. * @param onClick Invoked when the user clicks on the button. * @param modifier [Modifier] Used to shape and position the underlying [androidx.compose.material.TextButton]. + * @param enabled Controls the enabled state of the button. When `false`, this button will not + * be clickable. * @param textColor [Color] to apply to the button text. * @param upperCaseText If the button text should be in uppercase letters. */ @@ -28,12 +30,14 @@ fun TextButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, + enabled: Boolean = true, textColor: Color = FirefoxTheme.colors.textAccent, upperCaseText: Boolean = true, ) { androidx.compose.material.TextButton( onClick = onClick, modifier = modifier, + enabled = enabled, ) { Text( text = if (upperCaseText) { @@ -41,7 +45,7 @@ fun TextButton( } else { text }, - color = textColor, + color = if (enabled) textColor else FirefoxTheme.colors.textDisabled, style = FirefoxTheme.typography.button, maxLines = 1, ) @@ -52,11 +56,17 @@ fun TextButton( @LightDarkPreview private fun TextButtonPreview() { FirefoxTheme { - Box(Modifier.background(FirefoxTheme.colors.layer1)) { + Column(Modifier.background(FirefoxTheme.colors.layer1)) { TextButton( text = "label", onClick = {}, ) + + TextButton( + text = "disabled", + onClick = {}, + enabled = false, + ) } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt index 0a8e88b1ea..48c6e903b2 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role @@ -36,6 +37,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.button.RadioButton +import org.mozilla.fenix.compose.button.TextButton import org.mozilla.fenix.theme.FirefoxTheme private val LIST_ITEM_HEIGHT = 56.dp @@ -43,8 +45,8 @@ private val LIST_ITEM_HEIGHT = 56.dp private val ICON_SIZE = 24.dp /** - * List item used to display a label with an optional description text and - * an optional [IconButton] at the end. + * List item used to display a label with an optional description text and an optional + * [IconButton] or [Icon] at the end. * * @param label The label in the list item. * @param modifier [Modifier] to be applied to the layout. @@ -52,9 +54,11 @@ private val ICON_SIZE = 24.dp * @param description An optional description text below the label. * @param maxDescriptionLines An optional maximum number of lines for the description text to span. * @param onClick Called when the user clicks on the item. - * @param iconPainter [Painter] used to display an [IconButton] after the list item. + * @param iconPainter [Painter] used to display an icon after the list item. * @param iconDescription Content description of the icon. - * @param onIconClick Called when the user clicks on the icon. + * @param iconTint Tint applied to [iconPainter]. + * @param onIconClick Called when the user clicks on the icon. An [IconButton] will be + * displayed if this is provided. Otherwise, an [Icon] will be displayed. */ @Composable fun TextListItem( @@ -66,6 +70,7 @@ fun TextListItem( onClick: (() -> Unit)? = null, iconPainter: Painter? = null, iconDescription: String? = null, + iconTint: Color = FirefoxTheme.colors.iconPrimary, onIconClick: (() -> Unit)? = null, ) { ListItem( @@ -87,9 +92,16 @@ fun TextListItem( Icon( painter = iconPainter, contentDescription = iconDescription, - tint = FirefoxTheme.colors.iconPrimary, + tint = iconTint, ) } + } else if (iconPainter != null) { + Icon( + painter = iconPainter, + contentDescription = iconDescription, + modifier = Modifier.padding(end = 16.dp), + tint = iconTint, + ) } } } @@ -160,54 +172,135 @@ fun FaviconListItem( /** * List item used to display a label and an icon at the beginning with an optional description - * text and an optional [IconButton] at the end. + * text and an optional [IconButton] or [Icon] at the end. * * @param label The label in the list item. + * @param labelTextColor [Color] to be applied to the label. * @param description An optional description text below the label. + * @param enabled Controls the enabled state of the list item. When `false`, the list item will not + * be clickable. * @param onClick Called when the user clicks on the item. * @param beforeIconPainter [Painter] used to display an [Icon] before the list item. * @param beforeIconDescription Content description of the icon. - * @param afterIconPainter [Painter] used to display an [IconButton] after the list item. + * @param beforeIconTint Tint applied to [beforeIconPainter]. + * @param afterIconPainter [Painter] used to display an icon after the list item. * @param afterIconDescription Content description of the icon. - * @param onAfterIconClick Called when the user clicks on the icon. + * @param afterIconTint Tint applied to [afterIconPainter]. + * @param onAfterIconClick Called when the user clicks on the icon. An [IconButton] will be + * displayed if this is provided. Otherwise, an [Icon] will be displayed. */ @Composable fun IconListItem( label: String, + labelTextColor: Color = FirefoxTheme.colors.textPrimary, description: String? = null, + enabled: Boolean = true, onClick: (() -> Unit)? = null, beforeIconPainter: Painter, beforeIconDescription: String? = null, + beforeIconTint: Color = FirefoxTheme.colors.iconPrimary, afterIconPainter: Painter? = null, afterIconDescription: String? = null, + afterIconTint: Color = FirefoxTheme.colors.iconPrimary, onAfterIconClick: (() -> Unit)? = null, ) { ListItem( label = label, + labelTextColor = labelTextColor, description = description, + enabled = enabled, onClick = onClick, beforeListAction = { Icon( painter = beforeIconPainter, contentDescription = beforeIconDescription, modifier = Modifier.padding(horizontal = 16.dp), - tint = FirefoxTheme.colors.iconPrimary, + tint = if (enabled) beforeIconTint else FirefoxTheme.colors.iconDisabled, ) }, afterListAction = { + val tint = if (enabled) afterIconTint else FirefoxTheme.colors.iconDisabled + if (afterIconPainter != null && onAfterIconClick != null) { IconButton( onClick = onAfterIconClick, modifier = Modifier .padding(end = 16.dp) .size(ICON_SIZE), + enabled = enabled, ) { Icon( painter = afterIconPainter, contentDescription = afterIconDescription, - tint = FirefoxTheme.colors.iconPrimary, + tint = tint, ) } + } else if (afterIconPainter != null) { + Icon( + painter = afterIconPainter, + contentDescription = afterIconDescription, + modifier = Modifier.padding(end = 16.dp), + tint = tint, + ) + } + }, + ) +} + +/** + * List item used to display a label and an icon at the beginning with an optional description + * text and an optional [TextButton] at the end. + * + * @param label The label in the list item. + * @param labelTextColor [Color] to be applied to the label. + * @param description An optional description text below the label. + * @param enabled Controls the enabled state of the list item. When `false`, the list item will not + * be clickable. + * @param onClick Called when the user clicks on the item. + * @param beforeIconPainter [Painter] used to display an [Icon] before the list item. + * @param beforeIconDescription Content description of the icon. + * @param beforeIconTint Tint applied to [beforeIconPainter]. + * @param afterButtonText The button text to be displayed after the list item. + * @param afterButtonTextColor [Color] to apply to [afterButtonText]. + * @param onAfterButtonClick Called when the user clicks on the text button. + */ +@Composable +fun IconListItem( + label: String, + labelTextColor: Color = FirefoxTheme.colors.textPrimary, + description: String? = null, + enabled: Boolean = true, + onClick: (() -> Unit)? = null, + beforeIconPainter: Painter, + beforeIconDescription: String? = null, + beforeIconTint: Color = FirefoxTheme.colors.iconPrimary, + afterButtonText: String? = null, + afterButtonTextColor: Color = FirefoxTheme.colors.actionPrimary, + onAfterButtonClick: (() -> Unit)? = null, +) { + ListItem( + label = label, + labelTextColor = labelTextColor, + description = description, + enabled = enabled, + onClick = onClick, + beforeListAction = { + Icon( + painter = beforeIconPainter, + contentDescription = beforeIconDescription, + modifier = Modifier.padding(horizontal = 16.dp), + tint = if (enabled) beforeIconTint else FirefoxTheme.colors.iconDisabled, + ) + }, + afterListAction = { + if (afterButtonText != null && onAfterButtonClick != null) { + TextButton( + text = afterButtonText, + onClick = onAfterButtonClick, + enabled = enabled, + textColor = afterButtonTextColor, + upperCaseText = false, + ) } }, ) @@ -270,9 +363,12 @@ fun RadioButtonListItem( * * @param label The label in the list item. * @param modifier [Modifier] to be applied to the layout. + * @param labelTextColor [Color] to be applied to the label. * @param maxLabelLines An optional maximum number of lines for the label text to span. * @param description An optional description text below the label. * @param maxDescriptionLines An optional maximum number of lines for the description text to span. + * @param enabled Controls the enabled state of the list item. When `false`, the list item will not + * be clickable. * @param onClick Called when the user clicks on the item. * @param beforeListAction Optional Composable for adding UI before the list item. * @param afterListAction Optional Composable for adding UI to the end of the list item. @@ -281,16 +377,18 @@ fun RadioButtonListItem( private fun ListItem( label: String, modifier: Modifier = Modifier, + labelTextColor: Color = FirefoxTheme.colors.textPrimary, maxLabelLines: Int = 1, description: String? = null, maxDescriptionLines: Int = 1, + enabled: Boolean = true, onClick: (() -> Unit)? = null, beforeListAction: @Composable RowScope.() -> Unit = {}, afterListAction: @Composable RowScope.() -> Unit = {}, ) { Row( modifier = when (onClick != null) { - true -> Modifier.clickable { onClick() } + true -> Modifier.clickable(enabled = enabled) { onClick() } false -> Modifier }.then( Modifier.defaultMinSize(minHeight = LIST_ITEM_HEIGHT), @@ -306,7 +404,7 @@ private fun ListItem( ) { Text( text = label, - color = FirefoxTheme.colors.textPrimary, + color = if (enabled) labelTextColor else FirefoxTheme.colors.textDisabled, style = FirefoxTheme.typography.subtitle1, maxLines = maxLabelLines, ) @@ -314,7 +412,7 @@ private fun ListItem( description?.let { Text( text = description, - color = FirefoxTheme.colors.textSecondary, + color = if (enabled) FirefoxTheme.colors.textSecondary else FirefoxTheme.colors.textDisabled, style = FirefoxTheme.typography.body2, maxLines = maxDescriptionLines, ) @@ -352,13 +450,21 @@ private fun TextListItemWithDescriptionPreview() { @Preview(name = "TextListItem with a right icon", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TextListItemWithIconPreview() { FirefoxTheme { - Box(Modifier.background(FirefoxTheme.colors.layer1)) { + Column(Modifier.background(FirefoxTheme.colors.layer1)) { TextListItem( - label = "Label + right icon", - iconPainter = painterResource(R.drawable.ic_menu), + label = "Label + right icon button", + onClick = {}, + iconPainter = painterResource(R.drawable.mozac_ic_folder_24), iconDescription = "click me", onIconClick = { println("icon click") }, ) + + TextListItem( + label = "Label + right icon", + onClick = {}, + iconPainter = painterResource(R.drawable.mozac_ic_folder_24), + iconDescription = "click me", + ) } } } @@ -367,11 +473,40 @@ private fun TextListItemWithIconPreview() { @Preview(name = "IconListItem", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun IconListItemPreview() { FirefoxTheme { - Box(Modifier.background(FirefoxTheme.colors.layer1)) { + Column(Modifier.background(FirefoxTheme.colors.layer1)) { + IconListItem( + label = "Left icon list item", + onClick = {}, + beforeIconPainter = painterResource(R.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + ) + IconListItem( label = "Left icon list item", - beforeIconPainter = painterResource(R.drawable.ic_folder_icon), + labelTextColor = FirefoxTheme.colors.textAccent, + onClick = {}, + beforeIconPainter = painterResource(R.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + beforeIconTint = FirefoxTheme.colors.iconAccentViolet, + ) + + IconListItem( + label = "Left icon list item + right icon", + onClick = {}, + beforeIconPainter = painterResource(R.drawable.mozac_ic_folder_24), beforeIconDescription = "click me", + afterIconPainter = painterResource(R.drawable.mozac_ic_chevron_right_24), + afterIconDescription = null, + ) + + IconListItem( + label = "Left icon list item + right icon (disabled)", + enabled = false, + onClick = {}, + beforeIconPainter = painterResource(R.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + afterIconPainter = painterResource(R.drawable.mozac_ic_chevron_right_24), + afterIconDescription = null, ) } } @@ -379,20 +514,29 @@ private fun IconListItemPreview() { @Composable @Preview( - name = "IconListItem with an interactable right icon", + name = "IconListItem with after list action", uiMode = Configuration.UI_MODE_NIGHT_YES, ) -private fun IconListItemWithRightIconPreview() { +private fun IconListItemWithAfterListActionPreview() { FirefoxTheme { - Box(Modifier.background(FirefoxTheme.colors.layer1)) { + Column(Modifier.background(FirefoxTheme.colors.layer1)) { IconListItem( - label = "Left icon list item + right icon", - beforeIconPainter = painterResource(R.drawable.ic_folder_icon), + label = "IconListItem + right icon + clicks", + beforeIconPainter = painterResource(R.drawable.mozac_ic_folder_24), beforeIconDescription = null, - afterIconPainter = painterResource(R.drawable.ic_menu), + afterIconPainter = painterResource(R.drawable.mozac_ic_ellipsis_vertical_24), afterIconDescription = "click me", onAfterIconClick = { println("icon click") }, ) + + IconListItem( + label = "IconListItem + text button", + onClick = { println("list item click") }, + beforeIconPainter = painterResource(R.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + afterButtonText = "Edit", + onAfterButtonClick = { println("text button click") }, + ) } } } @@ -410,14 +554,14 @@ private fun FaviconListItemPreview() { description = "Description text", onClick = { println("list item click") }, url = "", - iconPainter = painterResource(R.drawable.ic_menu), + iconPainter = painterResource(R.drawable.mozac_ic_ellipsis_vertical_24), onIconClick = { println("icon click") }, ) FaviconListItem( label = "Favicon + painter", description = "Description text", - faviconPainter = painterResource(id = R.drawable.ic_tab_collection), + faviconPainter = painterResource(id = R.drawable.mozac_ic_collection_24), onClick = { println("list item click") }, url = "", ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt index 802457d4f8..3b40e3278a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt @@ -60,7 +60,7 @@ fun DismissedTabBackground( .alpha( if (dismissDirection == DismissDirection.StartToEnd || dismissDirection == null) 1f else 0f, ), - tint = FirefoxTheme.colors.iconWarning, + tint = FirefoxTheme.colors.iconCritical, ) Icon( @@ -72,7 +72,7 @@ fun DismissedTabBackground( .alpha( if (dismissDirection == DismissDirection.EndToStart || dismissDirection == null) 1f else 0f, ), - tint = FirefoxTheme.colors.iconWarning, + tint = FirefoxTheme.colors.iconCritical, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 0f1fff66bd..6c3abb3b0c 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -34,6 +34,7 @@ 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.menu.MenuAccessPoint import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarPosition @@ -130,8 +131,9 @@ class ExternalAppBrowserFragment : BaseBrowserFragment() { onMenuButtonClick = { nav( R.id.externalAppBrowserFragment, - ExternalAppBrowserFragmentDirections - .actionGlobalMenuDialogFragment(), + ExternalAppBrowserFragmentDirections.actionGlobalMenuDialogFragment( + accesspoint = MenuAccessPoint.External, + ), ) }, ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/tabs/TabTools.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/tabs/TabTools.kt index 5fa97b7c03..54ca60feaf 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/tabs/TabTools.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/tabs/TabTools.kt @@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults @@ -116,7 +118,8 @@ private fun TabToolsContent( Column( modifier = Modifier .fillMaxSize() - .padding(all = 16.dp), + .padding(all = 16.dp) + .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp), ) { TabCounter( @@ -152,7 +155,7 @@ private fun TabCounter( Spacer(modifier = Modifier.height(16.dp)) TabCountRow( - tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_normal), + tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_active), count = activeTabCount, ) @@ -254,10 +257,10 @@ private fun TabCreationTool( textColor = FirefoxTheme.colors.textPrimary, backgroundColor = Color.Transparent, cursorColor = FirefoxTheme.colors.borderFormDefault, - errorCursorColor = FirefoxTheme.colors.borderWarning, + errorCursorColor = FirefoxTheme.colors.borderCritical, focusedIndicatorColor = FirefoxTheme.colors.borderPrimary, unfocusedIndicatorColor = FirefoxTheme.colors.borderPrimary, - errorIndicatorColor = FirefoxTheme.colors.borderWarning, + errorIndicatorColor = FirefoxTheme.colors.borderCritical, ), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/ui/DebugOverlay.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/ui/DebugOverlay.kt index fa1959cd79..92e41355c0 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/ui/DebugOverlay.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/ui/DebugOverlay.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController @@ -88,6 +89,7 @@ fun DebugOverlay( onClick = { onDrawerOpen() }, + contentDescription = stringResource(R.string.debug_drawer_fab_content_description), ) // ModalDrawer utilizes a Surface, which blocks ALL clicks behind it, preventing the app diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt index 6ed2c57fb5..96d07d5276 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt @@ -17,7 +17,6 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.annotation.VisibleForTesting import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.viewbinding.ViewBinding import mozilla.components.concept.base.crash.Breadcrumb @@ -124,10 +123,7 @@ abstract class StartDownloadDialog( parent?.children ?.filterNot { it.id == R.id.startDownloadDialogContainer } ?.forEach { - ViewCompat.setImportantForAccessibility( - it, - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES, - ) + it.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES) } } @@ -136,10 +132,7 @@ abstract class StartDownloadDialog( parent?.children ?.filterNot { it.id == R.id.startDownloadDialogContainer } ?.forEach { - ViewCompat.setImportantForAccessibility( - it, - ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, - ) + it.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt index bb927596a4..d0423728d4 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt @@ -146,6 +146,27 @@ fun Activity.openSetDefaultBrowserOption( } } +/** + * Checks if the app can prompt the user to set it as the default browser. + * + * From Android 10, a new method to prompt the user to set default apps has been introduced. + * This method checks if the app can prompt the user to set it as the default browser + * based on the Android version and the availability of the ROLE_BROWSER. + */ +fun Activity.isDefaultBrowserPromptSupported(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + getSystemService(RoleManager::class.java).also { + if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld( + RoleManager.ROLE_BROWSER, + ) + ) { + return true + } + } + } + return false +} + @RequiresApi(Build.VERSION_CODES.N) private fun Activity.navigateToDefaultBrowserAppsSettings( from: BrowserDirection, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt index d0491be7f0..db191ed0d1 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt @@ -170,7 +170,7 @@ internal fun getFilteredSponsoredStories( fun AppState.filterState(blocklistHandler: BlocklistHandler): AppState = with(blocklistHandler) { copy( - recentBookmarks = recentBookmarks.filteredByBlocklist(), + bookmarks = bookmarks.filteredByBlocklist(), recentTabs = recentTabs.filteredByBlocklist().filterContile(), recentHistory = recentHistory.filteredByBlocklist().filterContile(), recentSyncedTabState = recentSyncedTabState.filteredByBlocklist().filterContile(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Bitmap.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Bitmap.kt index 02f63839f6..5e02e68873 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Bitmap.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Bitmap.kt @@ -8,6 +8,7 @@ import android.graphics.Bitmap import android.graphics.Matrix import android.view.View import android.widget.ImageView +import androidx.annotation.VisibleForTesting /** * This will scale the received [Bitmap] to the size of the [view]. It retains the bitmap's @@ -33,8 +34,8 @@ fun Bitmap.scaleToBottomOfView(view: ImageView) { oldRight: Int, oldBottom: Int, ) { - val viewWidth: Float = view.width.toFloat() - val viewHeight: Float = view.height.toFloat() + val viewWidth = view.width.toFloat() + val viewHeight = view.safeHeight().toFloat() val bitmapWidth = bitmap.width val bitmapHeight = bitmap.height val widthScale = viewWidth / bitmapWidth @@ -52,3 +53,16 @@ fun Bitmap.scaleToBottomOfView(view: ImageView) { }, ) } + +/** + * If the keyboard is open we must factor in the height for the correct view height. + */ +@VisibleForTesting +internal fun View.safeHeight(): Int { + val keyboardHeight = getKeyboardHeight() + return if (keyboardHeight > 0) { + keyboardHeight.plus(height) + } else { + height + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt index 11f37595d8..a4432ba37a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt @@ -145,3 +145,9 @@ fun Context.tabClosedUndoMessage(private: Boolean): String = } else { getString(R.string.snackbar_tab_closed) } + +/** + * Returns true if the device is a tablet + */ +fun Context.isTablet(): Boolean = + resources.getBoolean(R.bool.tablet) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt index 0bbc0ee729..db7f36cc40 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt @@ -6,6 +6,8 @@ package org.mozilla.fenix.ext import android.app.Activity import android.content.Intent +import android.view.View +import android.view.ViewGroup import android.view.WindowManager import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher @@ -18,9 +20,12 @@ import androidx.navigation.NavDirections import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.support.utils.ext.isLandscape import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.Components +import org.mozilla.fenix.components.toolbar.ToolbarPosition +import org.mozilla.fenix.components.toolbar.navbar.ToolbarContainerView /** * Get the requireComponents of this application. @@ -149,3 +154,53 @@ fun Fragment.registerForActivityResult( } } } + +/** + * Checks whether the current fragment is running on a tablet. + */ +fun Fragment.isTablet(): Boolean { + return resources.getBoolean(R.bool.tablet) +} + +/** + * + * Manages the state of the NavBar upon an orientation change. + * + * @param parent The top level [ViewGroup] of the fragment, which will be hosting toolbar/navbar container. + * @param toolbarView [View] responsible for showing the AddressBar. + * @param bottomToolbarContainerView The [ToolbarContainerView] hosting the NavBar. + * @param reinitializeNavBar lambda for re-initializing the NavBar inside the host [Fragment]. + */ +fun Fragment.updateNavBarForConfigurationChange( + parent: ViewGroup, + toolbarView: View, + bottomToolbarContainerView: ToolbarContainerView?, + reinitializeNavBar: () -> Unit, +) { + if (requireContext().isLandscape()) { + // In landscape mode we want to remove the navigation bar. + parent.removeView(bottomToolbarContainerView) + + // If address bar was positioned at bottom and we have removed the toolbar container, we are adding address bar + // back. + val isToolbarAtBottom = requireComponents.settings.toolbarPosition == ToolbarPosition.BOTTOM + + // Toolbar already having a parent is an edge case, but it could happen if configurationChange is called after + // onCreateView with the same orientation. Caught it on a foldable emulator while going from single screen + // portrait mode to landscape table, back and forth. + val hasParent = toolbarView.parent != null + if (isToolbarAtBottom && !hasParent) { + parent.addView(toolbarView) + } + } else { + // Already having a bottomContainer after switching back to portrait mode will happen when address bar is + // positioned at bottom and also as an edge case if configurationChange is called after onCreateView with the + // same orientation. Caught it on a foldable emulator while going from single screen portrait mode to landscape + // table, back and forth. + bottomToolbarContainerView?.let { + parent.removeView(it) + } + + reinitializeNavBar() + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/View.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/View.kt index 3c5976e428..9f2d2e0035 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/View.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/View.kt @@ -2,18 +2,16 @@ * 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.ext import android.graphics.Rect import android.os.Build import android.view.TouchDelegate import android.view.View -import android.view.accessibility.AccessibilityNodeInfo import androidx.annotation.Dimension import androidx.annotation.Dimension.Companion.DP import androidx.annotation.VisibleForTesting +import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.utils.ext.bottom @@ -55,97 +53,11 @@ fun View.removeTouchDelegate() { } /** - * Sets the new a11y parent. - */ -fun View.setNewAccessibilityParent(newParent: View) { - this.accessibilityDelegate = object : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - info.setParent(newParent) - } - } -} - -/** - * Updates the a11y collection item info for an item in a list. - */ -fun View.updateAccessibilityCollectionItemInfo( - rowIndex: Int, - columnIndex: Int, - isSelected: Boolean, - rowSpan: Int = 1, - columnSpan: Int = 1, -) { - this.accessibilityDelegate = object : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - info.collectionItemInfo = - AccessibilityNodeInfo.CollectionItemInfo( - rowIndex, - rowSpan, - columnIndex, - columnSpan, - false, - isSelected, - ) - } else { - @Suppress("DEPRECATION") - AccessibilityNodeInfo.CollectionItemInfo.obtain( - rowIndex, - rowSpan, - columnIndex, - columnSpan, - false, - isSelected, - ) - } - } - } -} - -/** - * Updates the a11y collection info for a list. - */ -fun View.updateAccessibilityCollectionInfo( - rowCount: Int, - columnCount: Int, -) { - this.accessibilityDelegate = object : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo( - host: View, - info: AccessibilityNodeInfo, - ) { - super.onInitializeAccessibilityNodeInfo(host, info) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - info.collectionInfo = AccessibilityNodeInfo.CollectionInfo( - rowCount, - columnCount, - false, - ) - } else { - @Suppress("DEPRECATION") - info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( - rowCount, - columnCount, - false, - ) - } - } - } -} - -/** * Fills a [Rect] with data about a view's location in the screen. * - * @see View.getLocationOnScreen - * @see View.getRectWithViewLocation for a version of this that is relative to a window + * @see android.view.View.getLocationOnScreen + * @see mozilla.components.support.ktx.android.view.getRectWithViewLocation for a version of this + * that is relative to a window */ fun View.getRectWithScreenLocation(): Rect { val locationOnScreen = IntArray(2).apply { getLocationOnScreen(this) } @@ -196,8 +108,10 @@ internal fun View.getWindowVisibleDisplayFrame(): Rect = with(Rect()) { this } -@VisibleForTesting -internal fun View.getKeyboardHeight(): Int { +/** + * Calculates the height of the onscreen keyboard. + */ +fun View.getKeyboardHeight(): Int { val windowRect = getWindowVisibleDisplayFrame() val statusBarHeight = windowRect.top var keyboardHeight = rootView.height - (windowRect.height() + statusBarHeight) @@ -207,10 +121,3 @@ internal fun View.getKeyboardHeight(): Int { return keyboardHeight } - -/** - * The assumed minimum height of the keyboard. - */ -@VisibleForTesting -@Dimension(unit = DP) -internal const val MINIMUM_KEYBOARD_HEIGHT = 100 diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt index b0837f80cc..b25a5dbc4a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt @@ -93,7 +93,10 @@ object GeckoProvider { isCreditCardAutofillEnabled = { context.settings().shouldAutofillCreditCardDetails }, isAddressAutofillEnabled = { context.settings().shouldAutofillAddressDetails }, ), - GeckoLoginStorageDelegate(loginStorage), + GeckoLoginStorageDelegate( + loginStorage = loginStorage, + isLoginAutofillEnabled = { context.settings().shouldAutofillLogins }, + ), ) return geckoRuntime diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 75c837a62b..ac8de36e7b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.home import android.annotation.SuppressLint +import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.drawable.ColorDrawable @@ -13,6 +14,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -80,10 +82,12 @@ import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.utils.ext.isLandscape import mozilla.components.ui.colors.PhotonColors import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.HomeScreen import org.mozilla.fenix.GleanMetrics.Homepage +import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.NavigationBar import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcutCfr import org.mozilla.fenix.HomeActivity @@ -93,10 +97,12 @@ import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.browser.BrowserAnimator import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.tabstrip.TabStrip +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.menu.MenuAccessPoint import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.navbar.BottomToolbarContainerView @@ -107,16 +113,19 @@ import org.mozilla.fenix.databinding.FragmentHomeBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.containsQueryParameters import org.mozilla.fenix.ext.hideToolbar +import org.mozilla.fenix.ext.isTablet import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.ext.registerForActivityResult import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.scaleToBottomOfView import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.tabClosedUndoMessage +import org.mozilla.fenix.ext.updateNavBarForConfigurationChange +import org.mozilla.fenix.home.bookmarks.BookmarksFeature +import org.mozilla.fenix.home.bookmarks.controller.DefaultBookmarksController import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.privatebrowsing.controller.DefaultPrivateBrowsingController -import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature -import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabFeature import org.mozilla.fenix.home.recentsyncedtabs.controller.DefaultRecentSyncedTabController import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature @@ -132,6 +141,7 @@ import org.mozilla.fenix.home.toolbar.SearchSelectorBinding import org.mozilla.fenix.home.toolbar.SearchSelectorMenuBinding import org.mozilla.fenix.home.topsites.DefaultTopSitesView import org.mozilla.fenix.messaging.DefaultMessageController +import org.mozilla.fenix.messaging.FenixMessageSurfaceId import org.mozilla.fenix.messaging.MessagingFeature import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks @@ -233,12 +243,14 @@ class HomeFragment : Fragment() { private val messagingFeature = ViewBoundFeatureWrapper<MessagingFeature>() private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>() private val recentSyncedTabFeature = ViewBoundFeatureWrapper<RecentSyncedTabFeature>() - private val recentBookmarksFeature = ViewBoundFeatureWrapper<RecentBookmarksFeature>() + private val bookmarksFeature = ViewBoundFeatureWrapper<BookmarksFeature>() private val historyMetadataFeature = ViewBoundFeatureWrapper<RecentVisitsFeature>() private val searchSelectorBinding = ViewBoundFeatureWrapper<SearchSelectorBinding>() private val searchSelectorMenuBinding = ViewBoundFeatureWrapper<SearchSelectorMenuBinding>() private val navbarIntegration = ViewBoundFeatureWrapper<NavbarIntegration>() + private lateinit var savedLoginsLauncher: ActivityResultLauncher<Intent> + override fun onCreate(savedInstanceState: Bundle?) { // DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL! val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime() @@ -246,6 +258,7 @@ class HomeFragment : Fragment() { super.onCreate(savedInstanceState) bundleArgs = args.toBundle() + savedLoginsLauncher = registerForActivityResult { navigateToSavedLoginsFragment() } // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL! requireComponents.core.engine.profiler?.addMarker( @@ -301,6 +314,7 @@ class HomeFragment : Fragment() { messagingFeature.set( feature = MessagingFeature( appStore = requireComponents.appStore, + surface = FenixMessageSurfaceId.HOMESCREEN, ), owner = viewLifecycleOwner, view = binding.root, @@ -347,9 +361,9 @@ class HomeFragment : Fragment() { ) } - if (requireContext().settings().showRecentBookmarksFeature) { - recentBookmarksFeature.set( - feature = RecentBookmarksFeature( + if (requireContext().settings().showBookmarksHomeFeature) { + bookmarksFeature.set( + feature = BookmarksFeature( appStore = components.appStore, bookmarksUseCase = run { requireContext().components.useCases.bookmarksUseCases @@ -409,7 +423,7 @@ class HomeFragment : Fragment() { accessPoint = TabsTrayAccessPoint.HomeRecentSyncedTab, appStore = components.appStore, ), - recentBookmarksController = DefaultRecentBookmarksController( + bookmarksController = DefaultBookmarksController( activity = activity, navController = findNavController(), appStore = components.appStore, @@ -451,7 +465,11 @@ class HomeFragment : Fragment() { searchEngine = components.core.store.state.search.selectedOrDefaultSearchEngine, ) - if (IncompleteRedesignToolbarFeature(requireContext().settings()).isEnabled) { + // We don't show the navigation bar for tablets and in landscape mode. + val shouldAddNavigationBar = IncompleteRedesignToolbarFeature(requireContext().settings()).isEnabled && + !requireContext().isLandscape() && + !isTablet() + if (shouldAddNavigationBar) { initializeNavBar(activity = activity) } @@ -478,11 +496,27 @@ class HomeFragment : Fragment() { return binding.root } + private fun reinitializeNavBar() { + initializeNavBar(activity = requireActivity() as HomeActivity) + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) homeMenuView?.dismissMenu() + // If the navbar feature could be visible, we should update it's state. + val shouldUpdateNavBarState = + IncompleteRedesignToolbarFeature(requireContext().settings()).isEnabled && !isTablet() + if (shouldUpdateNavBarState) { + updateNavBarForConfigurationChange( + parent = binding.homeLayout, + toolbarView = binding.toolbarLayout, + bottomToolbarContainerView = _bottomToolbarContainerView?.toolbarContainerView, + reinitializeNavBar = ::reinitializeNavBar, + ) + } + val currentWallpaperName = requireContext().settings().currentWallpaperName applyWallpaper( wallpaperName = currentWallpaperName, @@ -511,7 +545,10 @@ class HomeFragment : Fragment() { lifecycleOwner = viewLifecycleOwner, homeActivity = activity, navController = findNavController(), + homeFragment = this, menuButton = WeakReference(menuButton), + onShowPinVerification = { intent -> savedLoginsLauncher.launch(intent) }, + onBiometricAuthenticationSuccessful = ::navigateToSavedLoginsFragment, ).also { it.build() } _bottomToolbarContainerView = BottomToolbarContainerView( @@ -559,7 +596,9 @@ class HomeFragment : Fragment() { onMenuButtonClick = { findNavController().nav( findNavController().currentDestination?.id, - HomeFragmentDirections.actionGlobalMenuDialogFragment(), + HomeFragmentDirections.actionGlobalMenuDialogFragment( + accesspoint = MenuAccessPoint.Home, + ), ) }, ) @@ -682,7 +721,10 @@ class HomeFragment : Fragment() { lifecycleOwner = viewLifecycleOwner, homeActivity = activity as HomeActivity, navController = findNavController(), + homeFragment = this, menuButton = WeakReference(binding.menuButton), + onShowPinVerification = { intent -> savedLoginsLauncher.launch(intent) }, + onBiometricAuthenticationSuccessful = { navigateToSavedLoginsFragment() }, ).also { it.build() } tabCounterView = TabCounterView( @@ -693,7 +735,7 @@ class HomeFragment : Fragment() { ) toolbarView?.build() - if (requireContext().settings().isTabletAndTabStripEnabled) { + if (requireContext().isTabStripEnabled()) { initTabStrip() } @@ -1224,6 +1266,18 @@ class HomeFragment : Fragment() { } } + /** + * Called when authentication succeeds. + */ + private fun navigateToSavedLoginsFragment() { + val navController = findNavController() + if (navController.currentDestination?.id == R.id.homeFragment) { + Logins.openLogins.record(NoExtras()) + val directions = HomeFragmentDirections.actionLoginsListFragment() + navController.navigate(directions) + } + } + companion object { // Used to set homeViewModel.sessionToDelete when all tabs of a browsing mode are closed const val ALL_NORMAL_TABS = "all_normal" diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt index f2f7ef7522..93b9518b5a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt @@ -49,6 +49,11 @@ class HomeMenu( object Bookmarks : Item() object History : Item() object Downloads : Item() + + /** + * The Passwords menu item + */ + object Passwords : Item() object Extensions : Item() data class SyncAccount(val accountState: AccountState) : Item() @@ -142,6 +147,14 @@ class HomeMenu( onItemTapped.invoke(Item.Downloads) } + val passwordsItem = BrowserMenuImageText( + context.getString(R.string.preferences_sync_logins_2), + R.drawable.mozac_ic_login_24, + primaryTextColor, + ) { + onItemTapped.invoke(Item.Passwords) + } + val extensionsItem = BrowserMenuImageText( context.getString(R.string.browser_menu_extensions), R.drawable.ic_addons_extensions, @@ -217,6 +230,7 @@ class HomeMenu( bookmarksItem, historyItem, downloadsItem, + passwordsItem, extensionsItem, syncSignInMenuItem, accountAuthItem, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt index 7a94a7837a..611946a237 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.home import android.content.Context +import android.content.Intent import android.view.View import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.Companion.PRIVATE @@ -29,6 +30,7 @@ import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.settings import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.settings.biometric.bindBiometricsCredentialsPromptOrShowWarning import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.whatsnew.WhatsNew @@ -43,18 +45,26 @@ import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics * @param lifecycleOwner [LifecycleOwner] for the view. * @param homeActivity [HomeActivity] used to open URLs in a new tab. * @param navController [NavController] used for navigation. + * @param homeFragment [HomeFragment] used to attach the biometric prompt. * @param menuButton The [MenuButton] that will be used to create a menu when the button is * clicked. * @param fxaEntrypoint The source entry point to FxA. + * @param onShowPinVerification Callback for registering the pin verification result. + * @param onBiometricAuthenticationSuccessful Callback for displaying the next screen after a + * successful biometric authentication. */ +@Suppress("LongParameterList") class HomeMenuView( private val view: View, private val context: Context, private val lifecycleOwner: LifecycleOwner, private val homeActivity: HomeActivity, private val navController: NavController, + private val homeFragment: HomeFragment, private val menuButton: WeakReference<MenuButton>, private val fxaEntrypoint: FxAEntryPoint = FenixFxAEntryPoint.HomeMenu, + private val onShowPinVerification: (Intent) -> Unit, + private val onBiometricAuthenticationSuccessful: () -> Unit, ) { /** @@ -166,6 +176,13 @@ class HomeMenuView( HomeFragmentDirections.actionGlobalDownloadsFragment(), ) } + HomeMenu.Item.Passwords -> { + bindBiometricsCredentialsPromptOrShowWarning( + view = view, + onShowPinVerification = onShowPinVerification, + onAuthSuccess = onBiometricAuthenticationSuccessful, + ) + } HomeMenu.Item.Help -> { HomeMenuMetrics.helpTapped.record(NoExtras()) homeActivity.openToBrowserAndLoad( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ToolbarView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ToolbarView.kt index 43319eb061..c8802868e8 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ToolbarView.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ToolbarView.kt @@ -19,6 +19,7 @@ import androidx.core.view.updateLayoutParams import mozilla.components.browser.state.search.SearchEngine import mozilla.components.support.ktx.android.content.res.resolveAttribute import org.mozilla.fenix.R +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.databinding.FragmentHomeBinding @@ -106,7 +107,7 @@ class ToolbarView( gravity = Gravity.TOP } - val isTabletAndTabStripEnabled = context.settings().isTabletAndTabStripEnabled + val isTabletAndTabStripEnabled = context.isTabStripEnabled() ConstraintSet().apply { clone(binding.toolbarLayout) clear(binding.bottomBar.id, ConstraintSet.BOTTOM) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistHandler.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistHandler.kt index 041eb95836..80bad8fc76 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistHandler.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistHandler.kt @@ -8,7 +8,7 @@ import android.net.Uri import androidx.annotation.VisibleForTesting import mozilla.components.support.ktx.kotlin.sha1 import org.mozilla.fenix.ext.containsQueryParameters -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark +import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem @@ -37,7 +37,7 @@ class BlocklistHandler(private val settings: Settings) { * in a scope. */ @JvmName("filterRecentBookmark") - fun List<RecentBookmark>.filteredByBlocklist(): List<RecentBookmark> = + fun List<Bookmark>.filteredByBlocklist(): List<Bookmark> = settings.homescreenBlocklist.let { blocklist -> filterNot { it.url?.let { url -> blocklistContainsUrl(blocklist, url) } ?: false diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt index 9c8928c41d..1b65af86fa 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt @@ -41,7 +41,7 @@ class BlocklistMiddleware( when (action) { is AppAction.Change -> { action.copy( - recentBookmarks = action.recentBookmarks.filteredByBlocklist(), + bookmarks = action.bookmarks.filteredByBlocklist(), recentTabs = action.recentTabs.filteredByBlocklist().filterContile(), recentHistory = action.recentHistory.filteredByBlocklist().filterContile(), recentSyncedTabState = action.recentSyncedTabState.filteredByBlocklist().filterContile(), @@ -52,9 +52,9 @@ class BlocklistMiddleware( recentTabs = action.recentTabs.filteredByBlocklist().filterContile(), ) } - is AppAction.RecentBookmarksChange -> { + is AppAction.BookmarksChange -> { action.copy( - recentBookmarks = action.recentBookmarks.filteredByBlocklist(), + bookmarks = action.bookmarks.filteredByBlocklist(), ) } is AppAction.RecentHistoryChange -> { @@ -73,8 +73,8 @@ class BlocklistMiddleware( action } } - is AppAction.RemoveRecentBookmark -> { - action.recentBookmark.url?.let { url -> + is AppAction.RemoveBookmark -> { + action.bookmark.url?.let { url -> addUrlToBlocklist(url) state.toActionFilteringAllState(this) } ?: action @@ -99,7 +99,7 @@ class BlocklistMiddleware( with(blocklistHandler) { AppAction.Change( recentTabs = recentTabs.filteredByBlocklist().filterContile(), - recentBookmarks = recentBookmarks.filteredByBlocklist(), + bookmarks = bookmarks.filteredByBlocklist(), recentHistory = recentHistory.filteredByBlocklist().filterContile(), topSites = topSites, mode = mode, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/BookmarksFeature.kt index 3e7dc9d6c5..79af88ea7e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/BookmarksFeature.kt @@ -2,7 +2,7 @@ * 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.home.recentbookmarks +package org.mozilla.fenix.home.bookmarks import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -17,16 +17,15 @@ import org.mozilla.fenix.components.bookmarks.BookmarksUseCase import org.mozilla.fenix.home.HomeFragment /** - * View-bound feature that retrieves a list of recently added [BookmarkNode]s and dispatches + * View-bound feature that retrieves a list of [BookmarkNode]s and dispatches * updates to the [AppStore]. * * @param appStore the [AppStore] that holds the state of the [HomeFragment]. - * @param bookmarksUseCase the [BookmarksUseCase] for retrieving the list of recently saved - * bookmarks from storage. + * @param bookmarksUseCase the [BookmarksUseCase] for retrieving the list of bookmarks from storage. * @param scope the [CoroutineScope] used to fetch the bookmarks list * @param ioDispatcher the [CoroutineDispatcher] for performing read/write operations. */ -class RecentBookmarksFeature( +class BookmarksFeature( private val appStore: AppStore, private val bookmarksUseCase: BookmarksUseCase, private val scope: CoroutineScope, @@ -37,7 +36,7 @@ class RecentBookmarksFeature( override fun start() { job = scope.launch(ioDispatcher) { val bookmarks = bookmarksUseCase.retrieveRecentBookmarks() - appStore.dispatch(AppAction.RecentBookmarksChange(bookmarks)) + appStore.dispatch(AppAction.BookmarksChange(bookmarks)) } } @@ -47,13 +46,13 @@ class RecentBookmarksFeature( } /** - * A bookmark that was recently added. + * The simple metadata of a bookmark. * * @property title The title of the bookmark. * @property url The url of the bookmark. * @property previewImageUrl A preview image of the page (a.k.a. the hero image), if available. */ -data class RecentBookmark( +data class Bookmark( val title: String? = null, val url: String? = null, val previewImageUrl: String? = null, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/controller/RecentBookmarksController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/controller/BookmarksController.kt index a3591d9b08..6487efc1b3 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/controller/RecentBookmarksController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/controller/BookmarksController.kt @@ -2,7 +2,7 @@ * 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.home.recentbookmarks.controller +package org.mozilla.fenix.home.bookmarks.controller import androidx.navigation.NavController import mozilla.appservices.places.BookmarkRoot @@ -11,49 +11,49 @@ import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.ALLOW_JAVASCRIPT_URL import mozilla.components.feature.tabs.TabsUseCases import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.GleanMetrics.RecentBookmarks +import org.mozilla.fenix.GleanMetrics.HomeBookmarks import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.home.HomeFragmentDirections -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark -import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor +import org.mozilla.fenix.home.bookmarks.Bookmark +import org.mozilla.fenix.home.bookmarks.interactor.BookmarksInteractor /** - * An interface that handles the view manipulation of the recently saved bookmarks on the + * An interface that handles the view manipulation of the bookmarks on the * Home screen. */ -interface RecentBookmarksController { +interface BookmarksController { /** - * @see [RecentBookmarksInteractor.onRecentBookmarkClicked] + * @see [BookmarksInteractor.onBookmarkClicked] */ - fun handleBookmarkClicked(bookmark: RecentBookmark) + fun handleBookmarkClicked(bookmark: Bookmark) /** - * @see [RecentBookmarksInteractor.onShowAllBookmarksClicked] + * @see [BookmarksInteractor.onShowAllBookmarksClicked] */ fun handleShowAllBookmarksClicked() /** - * @see [RecentBookmarksInteractor.onRecentBookmarkRemoved] + * @see [BookmarksInteractor.onBookmarkRemoved] */ - fun handleBookmarkRemoved(bookmark: RecentBookmark) + fun handleBookmarkRemoved(bookmark: Bookmark) } /** - * The default implementation of [RecentBookmarksController]. + * The default implementation of [BookmarksController]. */ -class DefaultRecentBookmarksController( +class DefaultBookmarksController( private val activity: HomeActivity, private val navController: NavController, private val appStore: AppStore, private val browserStore: BrowserStore, private val selectTabUseCase: TabsUseCases.SelectTabUseCase, -) : RecentBookmarksController { +) : BookmarksController { - override fun handleBookmarkClicked(bookmark: RecentBookmark) { + override fun handleBookmarkClicked(bookmark: Bookmark) { val existingTabForBookmark = browserStore.state.tabs.firstOrNull { it.content.url == bookmark.url } @@ -70,17 +70,17 @@ class DefaultRecentBookmarksController( navController.navigate(R.id.browserFragment) } - RecentBookmarks.bookmarkClicked.add() + HomeBookmarks.bookmarkClicked.add() } override fun handleShowAllBookmarksClicked() { - RecentBookmarks.showAllBookmarks.add() + HomeBookmarks.showAllBookmarks.add() navController.navigate( HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id), ) } - override fun handleBookmarkRemoved(bookmark: RecentBookmark) { - appStore.dispatch(AppAction.RemoveRecentBookmark(bookmark)) + override fun handleBookmarkRemoved(bookmark: Bookmark) { + appStore.dispatch(AppAction.RemoveBookmark(bookmark)) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/interactor/RecentBookmarksInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/interactor/BookmarksInteractor.kt index 810da7e14a..efe95d2979 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/interactor/RecentBookmarksInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/interactor/BookmarksInteractor.kt @@ -2,35 +2,35 @@ * 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.home.recentbookmarks.interactor +package org.mozilla.fenix.home.bookmarks.interactor -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark +import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor /** - * Interface for recently saved bookmark related actions in the [SessionControlInteractor]. + * Interface for bookmark related actions in the [SessionControlInteractor]. */ -interface RecentBookmarksInteractor { +interface BookmarksInteractor { /** - * Opens the given bookmark in a new tab. Called when an user clicks on a recently saved - * bookmark on the home screen. + * Opens the given bookmark in a new tab. Called when an user clicks on a bookmark on the home + * screen. * * @param bookmark The bookmark that will be opened. */ - fun onRecentBookmarkClicked(bookmark: RecentBookmark) + fun onBookmarkClicked(bookmark: Bookmark) /** * Navigates to bookmark list. Called when an user clicks on the "Show all" button for - * recently saved bookmarks on the home screen. + * bookmarks on the home screen. */ fun onShowAllBookmarksClicked() /** - * Removes a bookmark from the recent bookmark list. Called when a user clicks the "Remove" - * button for recently saved bookmarks on the home screen. + * Removes a bookmark from the list on the home screen. Called when a user clicks the "Remove" + * button for a bookmark on the home screen. * * @param bookmark The bookmark that has been removed. */ - fun onRecentBookmarkRemoved(bookmark: RecentBookmark) + fun onBookmarkRemoved(bookmark: Bookmark) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarks.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/Bookmarks.kt index df32d3940b..6f70c98db8 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarks.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/Bookmarks.kt @@ -2,7 +2,7 @@ * 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.home.recentbookmarks.view +package org.mozilla.fenix.home.bookmarks.view import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -49,7 +49,7 @@ import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.MenuItem import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.inComposePreview -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark +import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.theme.FirefoxTheme private val cardShape = RoundedCornerShape(8.dp) @@ -61,58 +61,58 @@ private val imageModifier = Modifier .clip(cardShape) /** - * A list of recent bookmarks. + * A list of bookmarks. * - * @param bookmarks List of [RecentBookmark]s to display. - * @param menuItems List of [RecentBookmarksMenuItem] shown when long clicking a [RecentBookmarkItem] + * @param bookmarks List of [Bookmark]s to display. + * @param menuItems List of [BookmarksMenuItem] shown when long clicking a [BookmarkItem] * @param backgroundColor The background [Color] of each bookmark. - * @param onRecentBookmarkClick Invoked when the user clicks on a recent bookmark. + * @param onBookmarkClick Invoked when the user clicks on a bookmark. */ @OptIn(ExperimentalComposeUiApi::class) @Composable -fun RecentBookmarks( - bookmarks: List<RecentBookmark>, - menuItems: List<RecentBookmarksMenuItem>, +fun Bookmarks( + bookmarks: List<Bookmark>, + menuItems: List<BookmarksMenuItem>, backgroundColor: Color, - onRecentBookmarkClick: (RecentBookmark) -> Unit = {}, + onBookmarkClick: (Bookmark) -> Unit = {}, ) { LazyRow( modifier = Modifier.semantics { testTagsAsResourceId = true - testTag = "recent.bookmarks" + testTag = "bookmarks" }, contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { items(bookmarks) { bookmark -> - RecentBookmarkItem( + BookmarkItem( bookmark = bookmark, menuItems = menuItems, backgroundColor = backgroundColor, - onRecentBookmarkClick = onRecentBookmarkClick, + onBookmarkClick = onBookmarkClick, ) } } } /** - * A recent bookmark item. + * A bookmark item. * - * @param bookmark The [RecentBookmark] to display. - * @param menuItems The list of [RecentBookmarksMenuItem] shown when long clicking on the recent bookmark item. - * @param backgroundColor The background [Color] of the recent bookmark item. - * @param onRecentBookmarkClick Invoked when the user clicks on the recent bookmark item. + * @param bookmark The [Bookmark] to display. + * @param menuItems The list of [BookmarksMenuItem] shown when long clicking on the bookmark item. + * @param backgroundColor The background [Color] of the bookmark item. + * @param onBookmarkClick Invoked when the user clicks on the bookmark item. */ @OptIn( ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ) @Composable -private fun RecentBookmarkItem( - bookmark: RecentBookmark, - menuItems: List<RecentBookmarksMenuItem>, +private fun BookmarkItem( + bookmark: Bookmark, + menuItems: List<BookmarksMenuItem>, backgroundColor: Color, - onRecentBookmarkClick: (RecentBookmark) -> Unit = {}, + onBookmarkClick: (Bookmark) -> Unit = {}, ) { var isMenuExpanded by remember { mutableStateOf(false) } @@ -121,7 +121,7 @@ private fun RecentBookmarkItem( .width(158.dp) .combinedClickable( enabled = true, - onClick = { onRecentBookmarkClick(bookmark) }, + onClick = { onBookmarkClick(bookmark) }, onLongClick = { isMenuExpanded = true }, ), shape = cardShape, @@ -133,7 +133,7 @@ private fun RecentBookmarkItem( .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp), ) { - RecentBookmarkImage(bookmark) + BookmarkImage(bookmark) Spacer(modifier = Modifier.height(8.dp)) @@ -141,7 +141,7 @@ private fun RecentBookmarkItem( text = bookmark.title ?: bookmark.url ?: "", modifier = Modifier.semantics { testTagsAsResourceId = true - testTag = "recent.bookmark.title" + testTag = "bookmark.title" }, color = FirefoxTheme.colors.textPrimary, overflow = TextOverflow.Ellipsis, @@ -155,7 +155,7 @@ private fun RecentBookmarkItem( menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(bookmark) } }, modifier = Modifier.semantics { testTagsAsResourceId = true - testTag = "recent.bookmark.menu" + testTag = "bookmark.menu" }, ) } @@ -163,7 +163,7 @@ private fun RecentBookmarkItem( } @Composable -private fun RecentBookmarkImage(bookmark: RecentBookmark) { +private fun BookmarkImage(bookmark: Bookmark) { when { !bookmark.previewImageUrl.isNullOrEmpty() -> { Image( @@ -221,26 +221,26 @@ private fun FallbackBookmarkFaviconImage( @Composable @LightDarkPreview -private fun RecentBookmarksPreview() { +private fun BookmarksPreview() { FirefoxTheme { - RecentBookmarks( + Bookmarks( bookmarks = listOf( - RecentBookmark( + Bookmark( title = "Other Bookmark Title", url = "https://www.example.com", previewImageUrl = null, ), - RecentBookmark( + Bookmark( title = "Other Bookmark Title", url = "https://www.example.com", previewImageUrl = null, ), - RecentBookmark( + Bookmark( title = "Other Bookmark Title", url = "https://www.example.com", previewImageUrl = null, ), - RecentBookmark( + Bookmark( title = "Other Bookmark Title", url = "https://www.example.com", previewImageUrl = null, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksHeaderViewHolder.kt index 210bad9623..354524b7cb 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksHeaderViewHolder.kt @@ -2,7 +2,7 @@ * 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.home.recentbookmarks.view +package org.mozilla.fenix.home.bookmarks.view import android.view.View import androidx.compose.foundation.layout.Column @@ -17,19 +17,19 @@ import androidx.lifecycle.LifecycleOwner import org.mozilla.fenix.R import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.home.HomeSectionHeader -import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor +import org.mozilla.fenix.home.bookmarks.interactor.BookmarksInteractor /** - * View holder for the recent bookmarks header and "Show all" button. + * View holder for the bookmarks header and "Show all" button. * * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. - * @param interactor [RecentBookmarksInteractor] which will have delegated to all user interactions. + * @param interactor [BookmarksInteractor] which will have delegated to all user interactions. */ -class RecentBookmarksHeaderViewHolder( +class BookmarksHeaderViewHolder( composeView: ComposeView, viewLifecycleOwner: LifecycleOwner, - private val interactor: RecentBookmarksInteractor, + private val interactor: BookmarksInteractor, ) : ComposeViewHolder(composeView, viewLifecycleOwner) { init { @@ -44,8 +44,8 @@ class RecentBookmarksHeaderViewHolder( Spacer(modifier = Modifier.height(40.dp)) HomeSectionHeader( - headerText = stringResource(R.string.recently_saved_title), - description = stringResource(R.string.recently_saved_show_all_content_description_2), + headerText = stringResource(R.string.home_bookmarks_title), + description = stringResource(R.string.home_bookmarks_show_all_content_description), onShowAllClick = { interactor.onShowAllBookmarksClicked() }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksMenuItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksMenuItem.kt index 961245ce1f..b447a71166 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksMenuItem.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksMenuItem.kt @@ -2,17 +2,17 @@ * 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.home.recentbookmarks.view +package org.mozilla.fenix.home.bookmarks.view -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark +import org.mozilla.fenix.home.bookmarks.Bookmark /** - * A menu item in the recent bookmarks dropdown menu. + * A menu item in the bookmarks dropdown menu. * * @property title The menu item title. * @property onClick Invoked when the user clicks on the menu item. */ -data class RecentBookmarksMenuItem( +data class BookmarksMenuItem( val title: String, - val onClick: (RecentBookmark) -> Unit, + val onClick: (Bookmark) -> Unit, ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksViewHolder.kt index 13912bd041..de77577701 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksViewHolder.kt @@ -2,7 +2,7 @@ * 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.home.recentbookmarks.view +package org.mozilla.fenix.home.bookmarks.view import android.view.View import androidx.compose.runtime.Composable @@ -11,21 +11,24 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.LifecycleOwner import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.service.glean.private.NoExtras +import org.mozilla.fenix.GleanMetrics.HomeBookmarks import org.mozilla.fenix.R import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.ComposeViewHolder -import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor +import org.mozilla.fenix.home.bookmarks.interactor.BookmarksInteractor import org.mozilla.fenix.wallpapers.WallpaperState -import org.mozilla.fenix.GleanMetrics.RecentBookmarks as RecentBookmarksMetrics -class RecentBookmarksViewHolder( +/** + * ViewHolder for the Bookmarks section in the HomeFragment. + */ +class BookmarksViewHolder( composeView: ComposeView, viewLifecycleOwner: LifecycleOwner, - val interactor: RecentBookmarksInteractor, + val interactor: BookmarksInteractor, ) : ComposeViewHolder(composeView, viewLifecycleOwner) { init { - RecentBookmarksMetrics.shown.record(NoExtras()) + HomeBookmarks.shown.record(NoExtras()) } companion object { @@ -34,18 +37,18 @@ class RecentBookmarksViewHolder( @Composable override fun Content() { - val recentBookmarks = components.appStore.observeAsComposableState { state -> state.recentBookmarks } + val bookmarks = components.appStore.observeAsComposableState { state -> state.bookmarks } val wallpaperState = components.appStore .observeAsComposableState { state -> state.wallpaperState }.value ?: WallpaperState.default - RecentBookmarks( - bookmarks = recentBookmarks.value ?: emptyList(), + Bookmarks( + bookmarks = bookmarks.value ?: emptyList(), backgroundColor = wallpaperState.wallpaperCardColor, - onRecentBookmarkClick = interactor::onRecentBookmarkClicked, + onBookmarkClick = interactor::onBookmarkClicked, menuItems = listOf( - RecentBookmarksMenuItem( - stringResource(id = R.string.recently_saved_menu_item_remove), - onClick = { bookmark -> interactor.onRecentBookmarkRemoved(bookmark) }, + BookmarksMenuItem( + stringResource(id = R.string.home_bookmarks_menu_item_remove), + onClick = { bookmark -> interactor.onBookmarkRemoved(bookmark) }, ), ), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt index cd70ec0612..80b783b5da 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt @@ -141,7 +141,7 @@ private fun getMenuItems( MenuItem( title = stringResource(R.string.collection_delete), - color = FirefoxTheme.colors.textWarning, + color = FirefoxTheme.colors.textCritical, ) { onDeleteCollectionTapped(collection) }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/intent/OpenRecentlyClosedIntentProcessor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/intent/OpenRecentlyClosedIntentProcessor.kt new file mode 100644 index 0000000000..441e2af7f1 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/intent/OpenRecentlyClosedIntentProcessor.kt @@ -0,0 +1,30 @@ +/* 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.home.intent + +import android.content.Intent +import androidx.navigation.NavController +import org.mozilla.fenix.NavGraphDirections +import org.mozilla.fenix.ext.nav + +/** + * Opens the "recently closed tabs" fragment when the user taps on a + * "synced tabs closed" notification. + */ +class OpenRecentlyClosedIntentProcessor : HomeIntentProcessor { + override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { + return if (intent.action == ACTION_OPEN_RECENTLY_CLOSED) { + val directions = NavGraphDirections.actionGlobalRecentlyClosed() + navController.nav(null, directions) + true + } else { + false + } + } + + companion object { + const val ACTION_OPEN_RECENTLY_CLOSED = "org.mozilla.fenix.open_recently_closed" + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index ff93260109..4b5d49e697 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -18,13 +18,13 @@ import mozilla.components.service.nimbus.messaging.Message import org.mozilla.fenix.components.Components import org.mozilla.fenix.home.BottomSpacerViewHolder import org.mozilla.fenix.home.TopPlaceholderViewHolder +import org.mozilla.fenix.home.bookmarks.view.BookmarksHeaderViewHolder +import org.mozilla.fenix.home.bookmarks.view.BookmarksViewHolder import org.mozilla.fenix.home.collections.CollectionViewHolder import org.mozilla.fenix.home.collections.TabInCollectionViewHolder import org.mozilla.fenix.home.pocket.PocketCategoriesViewHolder import org.mozilla.fenix.home.pocket.PocketRecommendationsHeaderViewHolder import org.mozilla.fenix.home.pocket.PocketStoriesViewHolder -import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarksHeaderViewHolder -import org.mozilla.fenix.home.recentbookmarks.view.RecentBookmarksViewHolder import org.mozilla.fenix.home.recentsyncedtabs.view.RecentSyncedTabViewHolder import org.mozilla.fenix.home.recenttabs.view.RecentTabViewHolder import org.mozilla.fenix.home.recenttabs.view.RecentTabsHeaderViewHolder @@ -155,8 +155,15 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { object RecentVisitsHeader : AdapterItem(RecentVisitsHeaderViewHolder.LAYOUT_ID) object RecentVisitsItems : AdapterItem(RecentlyVisitedViewHolder.LAYOUT_ID) - object RecentBookmarksHeader : AdapterItem(RecentBookmarksHeaderViewHolder.LAYOUT_ID) - object RecentBookmarks : AdapterItem(RecentBookmarksViewHolder.LAYOUT_ID) + /** + * The header for the Bookmarks section. + */ + object BookmarksHeader : AdapterItem(BookmarksHeaderViewHolder.LAYOUT_ID) + + /** + * The Bookmarks section. + */ + object Bookmarks : AdapterItem(BookmarksViewHolder.LAYOUT_ID) object PocketStoriesItem : AdapterItem(PocketStoriesViewHolder.LAYOUT_ID) object PocketCategoriesItem : AdapterItem(PocketCategoriesViewHolder.LAYOUT_ID) @@ -230,7 +237,7 @@ class SessionControlAdapter( viewLifecycleOwner = viewLifecycleOwner, interactor = interactor, ) - RecentBookmarksViewHolder.LAYOUT_ID -> return RecentBookmarksViewHolder( + BookmarksViewHolder.LAYOUT_ID -> return BookmarksViewHolder( composeView = ComposeView(parent.context), viewLifecycleOwner = viewLifecycleOwner, interactor = interactor, @@ -255,7 +262,7 @@ class SessionControlAdapter( viewLifecycleOwner = viewLifecycleOwner, interactor = interactor, ) - RecentBookmarksHeaderViewHolder.LAYOUT_ID -> return RecentBookmarksHeaderViewHolder( + BookmarksHeaderViewHolder.LAYOUT_ID -> return BookmarksHeaderViewHolder( composeView = ComposeView(parent.context), viewLifecycleOwner = viewLifecycleOwner, interactor = interactor, @@ -314,8 +321,8 @@ class SessionControlAdapter( is CustomizeHomeButtonViewHolder, is RecentlyVisitedViewHolder, is RecentVisitsHeaderViewHolder, - is RecentBookmarksViewHolder, - is RecentBookmarksHeaderViewHolder, + is BookmarksViewHolder, + is BookmarksHeaderViewHolder, is RecentTabViewHolder, is RecentSyncedTabViewHolder, is RecentTabsHeaderViewHolder, @@ -394,7 +401,7 @@ class SessionControlAdapter( } is TopSitesViewHolder, is RecentlyVisitedViewHolder, - is RecentBookmarksViewHolder, + is BookmarksViewHolder, is RecentTabViewHolder, is RecentSyncedTabViewHolder, is PocketStoriesViewHolder, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index 6f429e6dfb..a5412eeb9a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -32,10 +32,10 @@ import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Collections +import org.mozilla.fenix.GleanMetrics.HomeBookmarks import org.mozilla.fenix.GleanMetrics.HomeScreen import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.Pocket -import org.mozilla.fenix.GleanMetrics.RecentBookmarks import org.mozilla.fenix.GleanMetrics.RecentTabs import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.HomeActivity @@ -551,6 +551,6 @@ class DefaultSessionControlController( RecentTabs.sectionVisible.set(true) } - RecentBookmarks.recentBookmarksCount.set(state.recentBookmarks.size.toLong()) + HomeBookmarks.bookmarksCount.set(state.bookmarks.size.toLong()) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 8ef6086cfe..f5d214fd92 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -11,14 +11,14 @@ import mozilla.components.service.nimbus.messaging.Message import mozilla.components.service.pocket.PocketStory import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.AppState +import org.mozilla.fenix.home.bookmarks.Bookmark +import org.mozilla.fenix.home.bookmarks.controller.BookmarksController +import org.mozilla.fenix.home.bookmarks.interactor.BookmarksInteractor import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory import org.mozilla.fenix.home.pocket.PocketStoriesController import org.mozilla.fenix.home.pocket.PocketStoriesInteractor import org.mozilla.fenix.home.privatebrowsing.controller.PrivateBrowsingController import org.mozilla.fenix.home.privatebrowsing.interactor.PrivateBrowsingInteractor -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark -import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController -import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor @@ -229,7 +229,7 @@ class SessionControlInteractor( private val controller: SessionControlController, private val recentTabController: RecentTabController, private val recentSyncedTabController: RecentSyncedTabController, - private val recentBookmarksController: RecentBookmarksController, + private val bookmarksController: BookmarksController, private val recentVisitsController: RecentVisitsController, private val pocketStoriesController: PocketStoriesController, private val privateBrowsingController: PrivateBrowsingController, @@ -242,7 +242,7 @@ class SessionControlInteractor( MessageCardInteractor, RecentTabInteractor, RecentSyncedTabInteractor, - RecentBookmarksInteractor, + BookmarksInteractor, RecentVisitsInteractor, CustomizeHomeIteractor, PocketStoriesInteractor, @@ -366,16 +366,16 @@ class SessionControlInteractor( recentSyncedTabController.handleRecentSyncedTabRemoved(tab) } - override fun onRecentBookmarkClicked(bookmark: RecentBookmark) { - recentBookmarksController.handleBookmarkClicked(bookmark) + override fun onBookmarkClicked(bookmark: Bookmark) { + bookmarksController.handleBookmarkClicked(bookmark) } override fun onShowAllBookmarksClicked() { - recentBookmarksController.handleShowAllBookmarksClicked() + bookmarksController.handleShowAllBookmarksClicked() } - override fun onRecentBookmarkRemoved(bookmark: RecentBookmark) { - recentBookmarksController.handleBookmarkRemoved(bookmark) + override fun onBookmarkRemoved(bookmark: Bookmark) { + bookmarksController.handleBookmarkRemoved(bookmark) } override fun onHistoryShowAllClicked() { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 3e7e9ddee7..902d7d8688 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -20,7 +20,7 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.shouldShowRecentSyncedTabs import org.mozilla.fenix.ext.shouldShowRecentTabs -import org.mozilla.fenix.home.recentbookmarks.RecentBookmark +import org.mozilla.fenix.home.bookmarks.Bookmark import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.messaging.FenixMessageSurfaceId import org.mozilla.fenix.onboarding.HomeCFRPresenter @@ -35,7 +35,7 @@ internal fun normalModeAdapterItems( topSites: List<TopSite>, collections: List<TabCollection>, expandedCollections: Set<Long>, - recentBookmarks: List<RecentBookmark>, + bookmarks: List<Bookmark>, showCollectionsPlaceholder: Boolean, nimbusMessageCard: Message? = null, showRecentTab: Boolean, @@ -72,10 +72,10 @@ internal fun normalModeAdapterItems( } } - if (settings.showRecentBookmarksFeature && recentBookmarks.isNotEmpty()) { + if (settings.showBookmarksHomeFeature && bookmarks.isNotEmpty()) { shouldShowCustomizeHome = true - items.add(AdapterItem.RecentBookmarksHeader) - items.add(AdapterItem.RecentBookmarks) + items.add(AdapterItem.BookmarksHeader) + items.add(AdapterItem.Bookmarks) } if (settings.historyMetadataUIFeature && recentVisits.isNotEmpty()) { @@ -137,7 +137,7 @@ private fun AppState.toAdapterList(settings: Settings): List<AdapterItem> = when topSites, collections, expandedCollections, - recentBookmarks, + bookmarks, showCollectionPlaceholder, messaging.messageToShow[FenixMessageSurfaceId.HOMESCREEN], shouldShowRecentTabs(settings), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/PagerIndicator.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/PagerIndicator.kt index 72f315d165..8046ed3dfd 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/PagerIndicator.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/PagerIndicator.kt @@ -8,8 +8,8 @@ import android.content.Context import android.util.AttributeSet import android.util.TypedValue import android.view.View +import android.view.ViewGroup.LayoutParams import android.widget.LinearLayout -import androidx.core.view.MarginLayoutParamsCompat import org.mozilla.fenix.R /** @@ -46,7 +46,7 @@ class PagerIndicator : LinearLayout { }, LayoutParams(dpToPx(DOT_SIZE_IN_DP), dpToPx(DOT_SIZE_IN_DP)).apply { if (!isLast) { - MarginLayoutParamsCompat.setMarginEnd(this, dpToPx(DOT_MARGIN)) + this.setMarginEnd(dpToPx(DOT_MARGIN)) } }, ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index 4664675890..5afe189836 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -67,9 +67,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan private lateinit var bookmarkStore: BookmarkFragmentStore private lateinit var bookmarkView: BookmarkView - private var _bookmarkInteractor: BookmarkFragmentInteractor? = null - private val bookmarkInteractor: BookmarkFragmentInteractor - get() = _bookmarkInteractor!! + private lateinit var bookmarkInteractor: BookmarkFragmentInteractor private val sharedViewModel: BookmarksSharedViewModel by activityViewModels() private val desktopFolders by lazy { DesktopFolders(requireContext(), showMobileRoot = false) } @@ -92,7 +90,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan BookmarkFragmentStore(BookmarkFragmentState(null)) } - _bookmarkInteractor = BookmarkFragmentInteractor( + bookmarkInteractor = BookmarkFragmentInteractor( bookmarksController = DefaultBookmarkController( activity = requireActivity() as HomeActivity, navController = findNavController(), @@ -191,7 +189,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan menu.findItem(R.id.delete_bookmarks_multi_select).title = SpannableString(getString(R.string.bookmark_menu_delete_button)).apply { - setTextColor(requireContext(), R.attr.textWarning) + setTextColor(requireContext(), R.attr.textCritical) } } } @@ -391,7 +389,6 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan override fun onDestroyView() { super.onDestroyView() - _bookmarkInteractor = null _binding = null } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt index fe46380044..58864f750f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt @@ -104,7 +104,7 @@ class BookmarkItemMenu( }, TextMenuCandidate( text = context.getString(R.string.bookmark_menu_delete_button), - textStyle = TextStyle(color = context.getColorFromAttr(R.attr.textWarning)), + textStyle = TextStyle(color = context.getColorFromAttr(R.attr.textCritical)), ) { onItemTapped.invoke(Item.Delete) }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt index b80b013eba..e3e8affee8 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt @@ -299,7 +299,7 @@ class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark), MenuProv ColorStateList.valueOf( ContextCompat.getColor( requireContext(), - R.color.fx_mobile_text_color_warning, + R.color.fx_mobile_text_color_critical, ), ), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt index 2fcac34ab1..34d68916b9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt @@ -165,7 +165,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan menu.findItem(R.id.delete_downloads_multi_select)?.title = SpannableString(getString(R.string.download_delete_item_1)).apply { - setTextColor(requireContext(), R.attr.textWarning) + setTextColor(requireContext(), R.attr.textCritical) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadItemMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadItemMenu.kt index e1c4dc4407..a747139d97 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadItemMenu.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadItemMenu.kt @@ -34,7 +34,7 @@ class DownloadItemMenu( TextMenuCandidate( text = context.getString(R.string.history_delete_item), textStyle = TextStyle( - color = context.getColorFromAttr(R.attr.textWarning), + color = context.getColorFromAttr(R.attr.textCritical), ), ) { onItemTapped.invoke(Item.Delete) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index fbeda874b7..5de7644db3 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -254,7 +254,7 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler, menu.findItem(R.id.share_history_multi_select)?.isVisible = true menu.findItem(R.id.delete_history_multi_select)?.title = SpannableString(getString(R.string.bookmark_menu_delete_button)).apply { - setTextColor(requireContext(), R.attr.textWarning) + setTextColor(requireContext(), R.attr.textCritical) } } else { inflater.inflate(R.menu.history_menu, menu) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt index 7ba3a9cf13..e84fbbc440 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt @@ -157,7 +157,7 @@ class HistoryMetadataGroupFragment : menu.findItem(R.id.delete_history_multi_select)?.let { deleteItem -> deleteItem.title = SpannableString(deleteItem.title).apply { - setTextColor(requireContext(), R.attr.textWarning) + setTextColor(requireContext(), R.attr.textCritical) } } } else { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt index 593a9d3678..fb9aa6bfb7 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -58,7 +58,7 @@ class RecentlyClosedFragment : inflater.inflate(R.menu.history_select_multi, menu) menu.findItem(R.id.delete_history_multi_select)?.let { deleteItem -> deleteItem.title = SpannableString(deleteItem.title) - .apply { setTextColor(requireContext(), R.attr.textWarning) } + .apply { setTextColor(requireContext(), R.attr.textCritical) } } } else { inflater.inflate(R.menu.library_menu, menu) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt index f7092f6b01..e362b435cb 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt @@ -22,4 +22,9 @@ object FenixMessageSurfaceId { * A survey dialog that is intended to be disruptive. */ const val SURVEY = "survey" + + /** + * A microsurvey UI for a specific feature. + */ + const val MICROSURVEY = "microsurvey" } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt index 9e9a5ef812..c8eb6508c7 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt @@ -4,17 +4,18 @@ package org.mozilla.fenix.messaging +import mozilla.components.service.nimbus.messaging.MessageSurfaceId import mozilla.components.support.base.feature.LifecycleAwareFeature import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction /** - * A message observer that updates the provided. + * A [LifecycleAwareFeature] which tries to evaluate if message is available for the provided [surface]. */ -class MessagingFeature(val appStore: AppStore) : LifecycleAwareFeature { +class MessagingFeature(val appStore: AppStore, val surface: MessageSurfaceId) : LifecycleAwareFeature { override fun start() { - appStore.dispatch(MessagingAction.Evaluate(FenixMessageSurfaceId.HOMESCREEN)) + appStore.dispatch(MessagingAction.Evaluate(surface)) } override fun stop() = Unit diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyContent.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyContent.kt new file mode 100644 index 0000000000..e89c5b64ed --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyContent.kt @@ -0,0 +1,123 @@ +/* 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.microsurvey.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.list.RadioButtonListItem +import org.mozilla.fenix.theme.FirefoxTheme + +private val shape = RoundedCornerShape(16.dp) +private val elevation: Dp = 5.dp + +/** + * The micro survey content UI to hold question and answer data. + * + * @param question The survey question text. + * @param answers The survey answer text options available for the question. + * @param icon The survey icon, this will represent the feature the survey is for. + * @param backgroundColor The view background color. + * @param selectedAnswer The current selected answer. Will be null until user selects an option. + * @param onSelectionChange An event that updates the [selectedAnswer]. + */ +@Composable +fun MicroSurveyContent( + question: String, + answers: List<String>, + @DrawableRes icon: Int = R.drawable.ic_print, // todo currently unknown what the default will be if any. + backgroundColor: Color = FirefoxTheme.colors.layer2, + selectedAnswer: String? = null, + onSelectionChange: (String) -> Unit, +) { + Card( + shape = shape, + backgroundColor = backgroundColor, + elevation = elevation, + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + ) { + Column(modifier = Modifier.wrapContentHeight()) { + Header(icon, question) + + answers.forEach { + RadioButtonListItem( + label = it, + selected = selectedAnswer == it, + onClick = { + onSelectionChange.invoke(it) + }, + ) + } + } + } +} + +@Composable +private fun Header(icon: Int, question: String) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(icon), + contentDescription = "Survey icon", // todo update to string res once a11y strings are available. + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Text( + text = question, + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline7, + ) + } +} + +/** + * Preview for [MicroSurveyContent]. + */ +@PreviewScreenSizes +@LightDarkPreview +@Composable +fun MicroSurveyContentPreview() { + FirefoxTheme { + MicroSurveyContent( + question = "How satisfied are you with printing in Firefox?", + icon = R.drawable.ic_print, + answers = listOf( + stringResource(id = R.string.likert_scale_option_1), + stringResource(id = R.string.likert_scale_option_2), + stringResource(id = R.string.likert_scale_option_3), + stringResource(id = R.string.likert_scale_option_4), + stringResource(id = R.string.likert_scale_option_5), + ), + onSelectionChange = {}, + ) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyFooter.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyFooter.kt new file mode 100644 index 0000000000..e9dd23c97f --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyFooter.kt @@ -0,0 +1,105 @@ +/* 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.microsurvey.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.LinkText +import org.mozilla.fenix.compose.LinkTextState +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * The footer UI used for micro-survey. + * + * @param isSubmitted Whether the user has "Submitted" the survey or not. + * @param isContentAnswerSelected Whether the user clicked on one of the answers or not. + * @param onLinkClick Invoked when the link is clicked. + * @param onButtonClick Invoked when the "Submit"/"Close" button is clicked. + */ +@Composable +fun MicroSurveyFooter( + isSubmitted: Boolean, + isContentAnswerSelected: Boolean, + onLinkClick: () -> Unit, + onButtonClick: () -> Unit, +) { + val buttonText = if (isSubmitted) { + stringResource(id = R.string.micro_survey_close_button_label) + } else { + stringResource(id = R.string.micro_survey_submit_button_label) + } + val buttonColor = if (isContentAnswerSelected) { + FirefoxTheme.colors.actionPrimary + } else { + FirefoxTheme.colors.actionPrimaryDisabled + } + + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxWidth(), + ) { + LinkText( + text = stringResource(id = R.string.about_privacy_notice), + linkTextStates = listOf( + LinkTextState( + text = stringResource(id = R.string.micro_survey_privacy_notice), + url = "", + onClick = { + onLinkClick() + }, + ), + ), + style = FirefoxTheme.typography.caption, + linkTextDecoration = TextDecoration.Underline, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = { onButtonClick() }, + enabled = isContentAnswerSelected, + shape = RoundedCornerShape(size = 4.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = buttonColor, + ), + contentPadding = PaddingValues(16.dp, 12.dp), + ) { + Text( + text = buttonText, + color = FirefoxTheme.colors.textActionPrimary, + style = FirefoxTheme.typography.button, + ) + } + } +} + +@PreviewScreenSizes +@LightDarkPreview +@Composable +private fun ReviewQualityCheckFooterPreview() { + FirefoxTheme { + MicroSurveyFooter( + isSubmitted = false, + isContentAnswerSelected = false, + onLinkClick = {}, + onButtonClick = {}, + ) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyHeader.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyHeader.kt new file mode 100644 index 0000000000..77fda94dc2 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyHeader.kt @@ -0,0 +1,86 @@ +/* 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.microsurvey.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * The header UI used for micro-survey. + * + * @param title The text that will be visible on the header. + * @param onCloseButtonClick Invoked when the close button is clicked. + */ +@Composable +fun MicroSurveyHeader( + title: String, + onCloseButtonClick: () -> Unit, +) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Image( + painter = painterResource(R.drawable.ic_firefox), + contentDescription = null, // todo update to string res once a11y strings are available. + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = title, + style = FirefoxTheme.typography.headline7, + color = FirefoxTheme.colors.textPrimary, + modifier = Modifier.weight(1f), + ) + + IconButton(onClick = onCloseButtonClick) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, // todo update to string res once a11y strings are available. + tint = FirefoxTheme.colors.iconPrimary, + modifier = Modifier.size(20.dp), + ) + } + } +} + +@PreviewScreenSizes +@LightDarkPreview +@Composable +private fun MicroSurveyHeaderPreview() { + FirefoxTheme { + Box( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer1) + .padding(16.dp), + ) { + MicroSurveyHeader(stringResource(R.string.micro_survey_survey_header)) {} + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyScaffold.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyScaffold.kt new file mode 100644 index 0000000000..20294a0158 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyScaffold.kt @@ -0,0 +1,67 @@ +/* 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.microsurvey.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * A scaffold for micro-survey UI that implements the basic layout structure with + * [content]. + * + * @param content The content of micro-survey. + */ +@Composable +fun MicroSurveyScaffold( + content: @Composable () -> Unit, +) { + var isOpen by remember { mutableStateOf(false) } + val cardShape = if (isOpen) { + RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + } else { + RectangleShape + } + val height = if (isOpen) { + 600.dp + } else { + 200.dp + } + + Card( + shape = cardShape, + backgroundColor = FirefoxTheme.colors.actionQuarternary, + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { isOpen = !isOpen }, + ), + ) { + Column(modifier = Modifier.height(height)) { + content() + } + } +} + +@LightDarkPreview +@Composable +private fun MicroSurveyScaffoldPreview() { + FirefoxTheme { + MicroSurveyScaffold {} + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheet.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheet.kt new file mode 100644 index 0000000000..eaf2f63081 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheet.kt @@ -0,0 +1,117 @@ +/* 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.microsurvey.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.BottomSheetHandle +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme + +private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f +private val bottomSheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + +/** + * The microsurvey bottom sheet. + * + * @param question The question text. + * @param answers The answer text options available for the given [question]. + * @param icon The icon that represents the feature for the given [question]. + */ +@Composable +fun MicrosurveyBottomSheet( + question: String, + answers: List<String>, + @DrawableRes icon: Int = R.drawable.ic_print, // todo currently unknown if default is used FXDROID-1921. +) { + var selectedAnswer by remember { mutableStateOf<String?>(null) } + var isSubmitted by remember { mutableStateOf(false) } + + Surface( + color = FirefoxTheme.colors.layer1, + shape = bottomSheetShape, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding( + vertical = 8.dp, + horizontal = 16.dp, + ), + ) { + BottomSheetHandle( + onRequestDismiss = {}, + contentDescription = stringResource(R.string.review_quality_check_close_handle_content_description), + modifier = Modifier + .fillMaxWidth(BOTTOM_SHEET_HANDLE_WIDTH_PERCENT) + .align(Alignment.CenterHorizontally) + .semantics { traversalIndex = -1f }, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + MicroSurveyHeader(title = stringResource(id = R.string.micro_survey_survey_header)) {} + + Spacer(modifier = Modifier.height(8.dp)) + + MicroSurveyContent( + question = question, + icon = icon, + answers = answers, + selectedAnswer = selectedAnswer, + onSelectionChange = { selectedAnswer = it }, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + MicroSurveyFooter( + isSubmitted = isSubmitted, + isContentAnswerSelected = selectedAnswer != null, + onLinkClick = {}, // todo add privacy policy link and open new tab FXDROID-1876. + onButtonClick = { isSubmitted = true }, + ) + } + } +} + +@PreviewScreenSizes +@LightDarkPreview +@Composable +private fun MicroSurveyBottomSheetPreview() { + FirefoxTheme { + MicrosurveyBottomSheet( + question = "How satisfied are you with printing in Firefox?", + icon = R.drawable.ic_print, + answers = listOf( + stringResource(id = R.string.likert_scale_option_1), + stringResource(id = R.string.likert_scale_option_2), + stringResource(id = R.string.likert_scale_option_3), + stringResource(id = R.string.likert_scale_option_4), + stringResource(id = R.string.likert_scale_option_5), + ), + ) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheetFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheetFragment.kt new file mode 100644 index 0000000000..338b1f3779 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheetFragment.kt @@ -0,0 +1,66 @@ +/* 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.microsurvey.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.mozilla.fenix.R +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * todo update behaviour FXDROID-1944. + * todo pass question and icon values from messaging FXDROID-1945. + * todo add dismiss request FXDROID-1946. + */ + +/** + * A bottom sheet fragment for displaying a microsurvey. + */ +class MicrosurveyBottomSheetFragment : BottomSheetDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + super.onCreateDialog(savedInstanceState).apply { + setOnShowListener { + val bottomSheet = findViewById<View?>(R.id.design_bottom_sheet) + bottomSheet?.setBackgroundResource(android.R.color.transparent) + val behavior = BottomSheetBehavior.from(bottomSheet) + behavior.peekHeight = resources.displayMetrics.heightPixels + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = ComposeView(requireContext()).apply { + val answers = listOf( + getString(R.string.likert_scale_option_1), + getString(R.string.likert_scale_option_2), + getString(R.string.likert_scale_option_3), + getString(R.string.likert_scale_option_4), + getString(R.string.likert_scale_option_5), + ) + + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + + setContent { + FirefoxTheme { + MicrosurveyBottomSheet( + question = "How satisfied are you with printing in Firefox?", // todo get value from messaging + icon = R.drawable.ic_print, // todo get value from messaging + answers = answers, // todo get value from messaging + ) + } + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyRequestPrompt.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyRequestPrompt.kt new file mode 100644 index 0000000000..44d3ce71b2 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyRequestPrompt.kt @@ -0,0 +1,96 @@ +/* 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.microsurvey.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.compose.button.PrimaryButton +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Initial microsurvey prompt displayed to the user to request completion of feedback. + * + * @param title The prompt header title. + */ +@Composable +fun MicrosurveyRequestPrompt( + // todo this is the message title FXDROID-1966). + title: String = "Help make printing in Firefox better. It only takes a sec.", +) { + Column( + modifier = Modifier + .background(color = FirefoxTheme.colors.layer1) + .padding(all = 16.dp), + ) { + Header(title) + + Spacer(modifier = Modifier.height(8.dp)) + + PrimaryButton(text = stringResource(id = R.string.micro_survey_continue_button_label)) {} + } +} + +@Composable +private fun Header( + title: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Image( + painter = painterResource(R.drawable.ic_firefox), + contentDescription = null, // todo update to string res once a11y strings are available FXDROID-1919. + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = title, + style = FirefoxTheme.typography.headline7, + color = FirefoxTheme.colors.textPrimary, + modifier = Modifier.weight(1f), + ) + + IconButton( + onClick = {}, // todo FXDROID-1947. + modifier = Modifier.size(20.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = null, // todo update to string res once a11y strings are available FXDROID-1919. + tint = FirefoxTheme.colors.iconPrimary, + ) + } + } +} + +@PreviewScreenSizes +@LightDarkPreview +@Composable +private fun MicrosurveyRequestPromptPreview() { + FirefoxTheme { + MicrosurveyRequestPrompt() + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt index 487e46b7d6..7bf2e6a547 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt @@ -10,6 +10,7 @@ import android.content.IntentFilter import android.content.pm.ActivityInfo import android.os.Build import android.os.Bundle +import android.os.StrictMode import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -29,6 +30,7 @@ import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint import org.mozilla.fenix.compose.LinkTextState import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideToolbar +import org.mozilla.fenix.ext.isDefaultBrowserPromptSupported import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.ext.requireComponents @@ -51,7 +53,8 @@ class OnboardingFragment : Fragment() { private val pagesToDisplay by lazy { pagesToDisplay( - isNotDefaultBrowser(requireContext()), + isNotDefaultBrowser(requireContext()) && + activity?.isDefaultBrowserPromptSupported() == false, canShowNotificationPage(requireContext()), canShowAddSearchWidgetPrompt(), ) @@ -76,9 +79,11 @@ class OnboardingFragment : Fragment() { .registerReceiver(pinAppWidgetReceiver, filter) if (isNotDefaultBrowser(context) && - pagesToDisplay.none { it.type == OnboardingPageUiData.Type.DEFAULT_BROWSER } + activity?.isDefaultBrowserPromptSupported() == true ) { - promptToSetAsDefaultBrowser() + requireComponents.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { + promptToSetAsDefaultBrowser() + } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt index f08b1a6552..351860f442 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt @@ -56,9 +56,8 @@ class ProfilerStopDialogFragment : DialogFragment() { } } - override fun dismiss() { + private fun setProfilerState() { profilerViewModel.setProfilerState(requireContext().components.core.engine.profiler!!.isProfilerActive()) - super.dismiss() } @Composable @@ -111,7 +110,14 @@ class ProfilerStopDialogFragment : DialogFragment() { ) { TextButton( onClick = { - requireContext().components.core.engine.profiler?.stopProfiler({}, {}) + requireContext().components.core.engine.profiler?.stopProfiler( + onSuccess = { + setProfilerState() + }, + onError = { + setProfilerState() + }, + ) dismiss() }, ) { @@ -162,6 +168,7 @@ class ProfilerStopDialogFragment : DialogFragment() { resources.getString(message) + extra, Toast.LENGTH_LONG, ).show() + setProfilerState() dismiss() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerUtils.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerUtils.kt index 28a8211e59..5fddb6500e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerUtils.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerUtils.kt @@ -34,6 +34,7 @@ private val firefox_features = arrayOf( "java", "processcpu", "ipcmessages", + "memory", ) private val firefox_threads = arrayOf( "GeckoMain", @@ -43,7 +44,8 @@ private val firefox_threads = arrayOf( "DOM Worker", ) -private val graphics_features = arrayOf("stackwalk", "js", "cpu", "java", "processcpu", "ipcmessages") +private val graphics_features = + arrayOf("stackwalk", "js", "cpu", "java", "processcpu", "ipcmessages", "memory") private val graphics_threads = arrayOf( "GeckoMain", "Compositor", @@ -64,6 +66,7 @@ private val media_features = arrayOf( "ipcmessages", "processcpu", "java", + "memory", ) private val media_threads = arrayOf( "cubeb", "audio", "BackgroundThreadPool", "camera", "capture", "Compositor", "decoder", "GeckoMain", "gmp", @@ -81,6 +84,7 @@ private val networking_features = arrayOf( "processcpu", "bandwidth", "ipcmessages", + "memory", ) private val networking_threads = arrayOf( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt index cd0bc2f758..def5cd4999 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt @@ -17,6 +17,7 @@ import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.R +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature import org.mozilla.fenix.ext.settings @@ -139,7 +140,7 @@ class ToolbarView( }, ) - if (settings.isTabletAndTabStripEnabled) { + if (context.isTabStripEnabled()) { (layoutParams as ViewGroup.MarginLayoutParams).updateMargins( top = context.resources.getDimensionPixelSize(R.dimen.tab_strip_height), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt index 9f5d6b6d05..642e4d4160 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.R +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -53,7 +54,7 @@ class CustomizationFragment : PreferenceFragmentCompat() { bindLightTheme() bindAutoBatteryTheme() setupRadioGroups() - val tabletAndTabStripEnabled = requireContext().settings().isTabletAndTabStripEnabled + val tabletAndTabStripEnabled = requireContext().isTabStripEnabled() if (tabletAndTabStripEnabled) { val preferenceScreen: PreferenceScreen = requirePreference(R.string.pref_key_customization_preference_screen) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/HomeSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/HomeSettingsFragment.kt index 9bba8fca3d..17427cda52 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/HomeSettingsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/HomeSettingsFragment.kt @@ -84,14 +84,14 @@ class HomeSettingsFragment : PreferenceFragmentCompat() { } } - requirePreference<SwitchPreference>(R.string.pref_key_recent_bookmarks).apply { - isChecked = context.settings().showRecentBookmarksFeature + requirePreference<SwitchPreference>(R.string.pref_key_customization_bookmarks).apply { + isChecked = context.settings().showBookmarksHomeFeature onPreferenceChangeListener = object : SharedPreferenceUpdater() { override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { CustomizeHome.preferenceToggled.record( CustomizeHome.PreferenceToggledExtra( newValue as Boolean, - "recently_saved", + "bookmarks", ), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt index 5a1f3c3a10..ea82f9f13b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt @@ -16,6 +16,9 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import mozilla.components.feature.qr.QrFeature +import mozilla.components.service.fxa.manager.SCOPE_PROFILE +import mozilla.components.service.fxa.manager.SCOPE_SESSION +import mozilla.components.service.fxa.manager.SCOPE_SYNC import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.R @@ -54,6 +57,7 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler { requireContext(), pairingUrl, args.entrypoint, + setOf(SCOPE_SYNC, SCOPE_PROFILE, SCOPE_SESSION), ) val vibrator = requireContext().getSystemService<Vibrator>()!! if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt index 5101f10f28..c50cbe6a1c 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt @@ -18,6 +18,7 @@ import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R +import org.mozilla.fenix.browser.tabstrip.isTabStripEligible import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav @@ -142,7 +143,7 @@ class SecretSettingsFragment : PreferenceFragmentCompat() { private fun setupTabStripPreference() { requirePreference<SwitchPreference>(R.string.pref_key_enable_tab_strip).apply { - isVisible = Config.channel.isNightlyOrDebug && context.resources.getBoolean(R.bool.tablet) + isVisible = context.isTabStripEligible() isChecked = context.settings().isTabStripEnabled onPreferenceChangeListener = SharedPreferenceUpdater() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index d392ba8697..b6d9e1ea78 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -47,6 +47,7 @@ import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.CookieBanners import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.TrackingProtection +import org.mozilla.fenix.GleanMetrics.Translations import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint @@ -158,6 +159,10 @@ class SettingsFragment : PreferenceFragmentCompat() { updateProfilerUI(it) }, ) + + findPreference<Preference>( + getPreferenceKey(R.string.pref_key_translation), + )?.isVisible = FxNimbus.features.translations.value().globalSettingsEnabled } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -315,6 +320,11 @@ class SettingsFragment : PreferenceFragmentCompat() { SettingsFragmentDirections.actionSettingsFragmentToLocaleSettingsFragment() } + resources.getString(R.string.pref_key_translation) -> { + Translations.action.record(Translations.ActionExtra("global_settings_from_preferences")) + SettingsFragmentDirections.actionSettingsFragmentToTranslationsSettingsFragment() + } + /* Privacy and security preferences */ resources.getString(R.string.pref_key_private_browsing) -> { SettingsFragmentDirections.actionSettingsFragmentToPrivateBrowsingFragment() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index da73fcc10e..9b0c7649ce 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -39,6 +39,11 @@ object SupportUtils { const val GOOGLE_XX_URL = "https://www.google.com/webhp?client=firefox-b-m&channel=ts" const val WHATS_NEW_URL = "https://www.mozilla.org/firefox/android/notes" + // This is locale-less on purpose so that the content negotiation happens on the AMO side because the current + // user language might not be supported by AMO and/or the language might not be exactly what AMO is expecting + // (e.g. `en` instead of `en-US`). + const val AMO_HOMEPAGE_FOR_ANDROID = "${BuildConfig.AMO_BASE_URL}/android/" + enum class SumoTopic(internal val topicStr: String) { HELP("faq-android"), PRIVATE_BROWSING_MYTHS("common-myths-about-private-browsing"), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SyncDebugFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SyncDebugFragment.kt index b5a0886498..62f33c6ce3 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SyncDebugFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SyncDebugFragment.kt @@ -11,6 +11,7 @@ import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceClickListener import androidx.preference.PreferenceFragmentCompat import org.mozilla.fenix.R +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import kotlin.system.exitProcess @@ -59,6 +60,24 @@ class SyncDebugFragment : PreferenceFragmentCompat() { requirePreference<CheckBoxPreference>(R.string.pref_key_use_react_fxa).apply { onPreferenceChangeListener = SharedPreferenceUpdater() } + requirePreference<Preference>(R.string.pref_key_sync_debug_network_error).let { pref -> + pref.onPreferenceClickListener = OnPreferenceClickListener { + requireComponents.backgroundServices.accountManager.simulateNetworkError() + true + } + } + requirePreference<Preference>(R.string.pref_key_sync_debug_temporary_auth_error).let { pref -> + pref.onPreferenceClickListener = OnPreferenceClickListener { + requireComponents.backgroundServices.accountManager.simulateTemporaryAuthTokenIssue() + true + } + } + requirePreference<Preference>(R.string.pref_key_sync_debug_permanent_auth_error).let { pref -> + pref.onPreferenceClickListener = OnPreferenceClickListener { + requireComponents.backgroundServices.accountManager.simulatePermanentAuthTokenIssue() + true + } + } updateMenu() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountProblemFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountProblemFragment.kt index 9b82a91a88..6e39f105d4 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountProblemFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountProblemFragment.kt @@ -15,6 +15,8 @@ import kotlinx.coroutines.launch import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.service.fxa.manager.SCOPE_PROFILE +import mozilla.components.service.fxa.manager.SCOPE_SYNC import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.R @@ -27,7 +29,11 @@ class AccountProblemFragment : PreferenceFragmentCompat(), AccountObserver { private val args by navArgs<AccountProblemFragmentArgs>() private val signInClickListener = Preference.OnPreferenceClickListener { - requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext(), args.entrypoint) + requireComponents.services.accountsAuthFeature.beginAuthentication( + requireContext(), + args.entrypoint, + setOf(SCOPE_PROFILE, SCOPE_SYNC), + ) SyncAuth.useEmailProblem.record(NoExtras()) // TODO The sign-in web content populates session history, // so pressing "back" after signing in won't take us back into the settings screen, but rather up the diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt index ab03b079b7..a2e5f5b8e5 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt @@ -17,6 +17,8 @@ import androidx.navigation.fragment.navArgs import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.service.fxa.manager.SCOPE_PROFILE +import mozilla.components.service.fxa.manager.SCOPE_SYNC import mozilla.components.support.ktx.android.content.hasCamera import mozilla.components.support.ktx.android.content.isPermissionGranted import mozilla.components.support.ktx.android.view.hideKeyboard @@ -184,6 +186,7 @@ class TurnOnSyncFragment : Fragment(), AccountObserver { requireComponents.services.accountsAuthFeature.beginAuthentication( requireContext(), entrypoint = args.entrypoint, + setOf(SCOPE_PROFILE, SCOPE_SYNC), ) SyncAuth.useEmail.record(NoExtras()) // TODO The sign-in web content populates session history, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricUtils.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricUtils.kt new file mode 100644 index 0000000000..db83dee4a1 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricUtils.kt @@ -0,0 +1,110 @@ +/* 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.settings.biometric + +import android.app.KeyguardManager +import android.content.DialogInterface +import android.content.Intent +import android.provider.Settings +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.content.getSystemService +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.findFragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.ui.widgets.withCenterAlignedButtons +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.runIfFragmentIsAttached +import org.mozilla.fenix.ext.secure +import org.mozilla.fenix.ext.settings + +/** + * Prompts the biometric authentication before navigating to new fragment + * or displays warning dialog in case the feature is not available + */ +@Suppress("Deprecation") +fun bindBiometricsCredentialsPromptOrShowWarning( + view: View, + onShowPinVerification: (Intent) -> Unit, + onAuthSuccess: () -> Unit, + onAuthFailure: () -> Unit = {}, + doWhileAuthenticating: () -> Unit = {}, +) { + val (fragment, context) = Result.runCatching { + view.findFragment() as Fragment to view.context + }.getOrElse { return } + + val biometricPromptFeature = ViewBoundFeatureWrapper( + owner = fragment.viewLifecycleOwner, + view = view, + feature = BiometricPromptFeature( + context = context, + fragment = fragment, + onAuthSuccess = { + fragment.runIfFragmentIsAttached { + fragment.lifecycleScope.launch(Dispatchers.Main) { + onAuthSuccess() + } + } + }, + onAuthFailure = onAuthFailure, + ), + ) + // Use the BiometricPrompt first + if (BiometricPromptFeature.canUseFeature(context)) { + doWhileAuthenticating() + biometricPromptFeature.get() + ?.requestAuthentication(context.resources.getString(R.string.logins_biometric_prompt_message_2)) + return + } + + // Fallback to prompting for password with the KeyguardManager + val manager = context.getSystemService<KeyguardManager>() + if (manager?.isKeyguardSecure == true) { + val confirmDeviceCredentialIntent = manager.createConfirmDeviceCredentialIntent( + context.resources.getString(R.string.logins_biometric_prompt_message_pin), + context.resources.getString(R.string.logins_biometric_prompt_message), + ) + onShowPinVerification(confirmDeviceCredentialIntent) + } else { + // Warn that the device has not been secured + if (context.settings().shouldShowSecurityPinWarning) { + fragment.activity?.let { + showPinDialogWarning(it, onAuthSuccess) + } ?: return + } else { + onAuthSuccess() + } + } +} + +@Suppress("MaxLineLength") +private fun showPinDialogWarning( + activity: FragmentActivity, + onIgnorePinWarning: () -> Unit, +) { + AlertDialog.Builder(activity).apply { + setTitle(context.resources.getString(R.string.logins_warning_dialog_title_2)) + setMessage( + context.resources.getString(R.string.logins_warning_dialog_message_2), + ) + + setNegativeButton(context.resources.getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ -> + onIgnorePinWarning() + } + + setPositiveButton(context.resources.getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ -> + it.dismiss() + val intent = Intent(Settings.ACTION_SECURITY_SETTINGS) + context.startActivity(intent) + } + create().withCenterAlignedButtons() + }.show().secure(activity) + activity.settings().incrementSecureWarningCount() +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt index f59b6a1f87..b2501cab3f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt @@ -62,12 +62,12 @@ class CreditCardEditorView( binding.cardNumberLayout.setErrorTextColor( ColorStateList.valueOf( - binding.root.context.getColorFromAttr(R.attr.textWarning), + binding.root.context.getColorFromAttr(R.attr.textCritical), ), ) binding.nameOnCardLayout.setErrorTextColor( ColorStateList.valueOf( - binding.root.context.getColorFromAttr(R.attr.textWarning), + binding.root.context.getColorFromAttr(R.attr.textCritical), ), ) @@ -128,7 +128,7 @@ class CreditCardEditorView( binding.cardNumberLayout.error = binding.root.context.getString(R.string.credit_cards_number_validation_error_message_2) - binding.cardNumberTitle.setTextColor(binding.root.context.getColorFromAttr(R.attr.textWarning)) + binding.cardNumberTitle.setTextColor(binding.root.context.getColorFromAttr(R.attr.textCritical)) } if (binding.nameOnCardInput.text.toString().isNotBlank()) { @@ -139,7 +139,7 @@ class CreditCardEditorView( binding.nameOnCardLayout.error = binding.root.context.getString(R.string.credit_cards_name_on_card_validation_error_message_2) - binding.nameOnCardTitle.setTextColor(binding.root.context.getColorFromAttr(R.attr.textWarning)) + binding.nameOnCardTitle.setTextColor(binding.root.context.getColorFromAttr(R.attr.textCritical)) } return isValid diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt index e1cadea39c..5a85e34302 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt @@ -281,7 +281,7 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { ColorStateList.valueOf( ContextCompat.getColor( requireContext(), - R.color.fx_mobile_text_color_warning, + R.color.fx_mobile_text_color_critical, ), ), ) @@ -295,7 +295,7 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { ColorStateList.valueOf( ContextCompat.getColor( requireContext(), - R.color.fx_mobile_text_color_warning, + R.color.fx_mobile_text_color_critical, ), ), ) @@ -319,7 +319,7 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding) layout.setErrorIconTintList( ColorStateList.valueOf( - ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_warning), + ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_critical), ), ) } @@ -332,7 +332,7 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding) layout.setErrorIconTintList( ColorStateList.valueOf( - ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_warning), + ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_critical), ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt index 59b210ae36..5310536b77 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt @@ -273,7 +273,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { ColorStateList.valueOf( ContextCompat.getColor( requireContext(), - R.color.fx_mobile_text_color_warning, + R.color.fx_mobile_text_color_critical, ), ), ) @@ -290,7 +290,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding) layout.setErrorIconTintList( ColorStateList.valueOf( - ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_warning), + ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_critical), ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt index 5b7c0c2d71..f492e70bad 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt @@ -4,29 +4,16 @@ package org.mozilla.fenix.settings.logins.fragment -import android.app.KeyguardManager -import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.os.Bundle -import android.provider.Settings.ACTION_SECURITY_SETTINGS -import android.view.View import androidx.activity.result.ActivityResultLauncher -import androidx.appcompat.app.AlertDialog -import androidx.core.content.getSystemService -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import mozilla.components.feature.autofill.preference.AutofillPreference import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.glean.private.NoExtras -import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.R import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint @@ -34,24 +21,20 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.navigateWithBreadcrumb import org.mozilla.fenix.ext.registerForActivityResult import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.runIfFragmentIsAttached -import org.mozilla.fenix.ext.secure import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.SharedPreferenceUpdater import org.mozilla.fenix.settings.SyncPreferenceView -import org.mozilla.fenix.settings.biometric.BiometricPromptFeature +import org.mozilla.fenix.settings.biometric.bindBiometricsCredentialsPromptOrShowWarning import org.mozilla.fenix.settings.requirePreference @Suppress("TooManyFunctions") class SavedLoginsAuthFragment : PreferenceFragmentCompat() { - private val biometricPromptFeature = ViewBoundFeatureWrapper<BiometricPromptFeature>() - private lateinit var startForResult: ActivityResultLauncher<Intent> + private lateinit var savedLoginsFragmentLauncher: ActivityResultLauncher<Intent> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - startForResult = registerForActivityResult { + savedLoginsFragmentLauncher = registerForActivityResult { navigateToSavedLoginsFragment() } } @@ -71,32 +54,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { requirePreference<Preference>(R.string.pref_key_saved_logins).isEnabled = enabled } - private fun navigateToSavedLogins() { - runIfFragmentIsAttached { - viewLifecycleOwner.lifecycleScope.launch(Main) { - // Workaround for likely biometric library bug - // https://github.com/mozilla-mobile/fenix/issues/8438 - delay(SHORT_DELAY_MS) - navigateToSavedLoginsFragment() - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - biometricPromptFeature.set( - feature = BiometricPromptFeature( - context = requireContext(), - fragment = this, - onAuthFailure = { togglePrefsEnabledWhileAuthenticating(true) }, - onAuthSuccess = ::navigateToSavedLogins, - ), - owner = this, - view = view, - ) - } - + @Suppress("LongMethod") override fun onResume() { super.onResume() showToolbar(getString(R.string.preferences_passwords_logins_and_passwords)) @@ -146,7 +104,17 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { } requirePreference<Preference>(R.string.pref_key_saved_logins).setOnPreferenceClickListener { - verifyCredentialsOrShowSetupWarning(it.context) + view?.let { view -> + bindBiometricsCredentialsPromptOrShowWarning( + view = view, + onShowPinVerification = { intent -> + savedLoginsFragmentLauncher.launch(intent) + }, + onAuthSuccess = ::navigateToSavedLoginsFragment, + onAuthFailure = { togglePrefsEnabledWhileAuthenticating(true) }, + doWhileAuthenticating = { togglePrefsEnabledWhileAuthenticating(false) }, + ) + } true } @@ -178,60 +146,6 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { togglePrefsEnabledWhileAuthenticating(true) } - private fun verifyCredentialsOrShowSetupWarning(context: Context) { - // Use the BiometricPrompt first - if (BiometricPromptFeature.canUseFeature(context)) { - togglePrefsEnabledWhileAuthenticating(false) - biometricPromptFeature.get() - ?.requestAuthentication(getString(R.string.logins_biometric_prompt_message_2)) - return - } - - // Fallback to prompting for password with the KeyguardManager - val manager = context.getSystemService<KeyguardManager>() - if (manager?.isKeyguardSecure == true) { - showPinVerification(manager) - } else { - // Warn that the device has not been secured - if (context.settings().shouldShowSecurityPinWarning) { - showPinDialogWarning(context) - } else { - navigateToSavedLoginsFragment() - } - } - } - - private fun showPinDialogWarning(context: Context) { - AlertDialog.Builder(context).apply { - setTitle(getString(R.string.logins_warning_dialog_title_2)) - setMessage( - getString(R.string.logins_warning_dialog_message_2), - ) - - setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ -> - navigateToSavedLoginsFragment() - } - - setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ -> - it.dismiss() - val intent = Intent(ACTION_SECURITY_SETTINGS) - startActivity(intent) - } - create().withCenterAlignedButtons() - }.show().secure(activity) - context.settings().incrementSecureWarningCount() - } - - @Suppress("Deprecation") - private fun showPinVerification(manager: KeyguardManager) { - val intent = manager.createConfirmDeviceCredentialIntent( - getString(R.string.logins_biometric_prompt_message_pin), - getString(R.string.logins_biometric_prompt_message), - ) - - startForResult.launch(intent) - } - /** * Called when authentication succeeds. */ @@ -260,9 +174,4 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginExceptionsFragment() findNavController().navigate(directions) } - - companion object { - const val SHORT_DELAY_MS = 100L - const val PIN_REQUEST = 303 - } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineMenu.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineMenu.kt index d28867513b..cb1b17788f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineMenu.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineMenu.kt @@ -40,7 +40,7 @@ class SearchEngineMenu( items.add( SimpleBrowserMenuItem( context.getString(R.string.search_engine_delete), - textColorResource = ThemeManager.resolveAttribute(R.attr.textWarning, context), + textColorResource = ThemeManager.resolveAttribute(R.attr.textCritical, context), ) { onItemTapped.invoke(Item.Delete) }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineShortcuts.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineShortcuts.kt index 46ca337e42..04d4d90dde 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineShortcuts.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineShortcuts.kt @@ -198,13 +198,13 @@ private fun SearchItem( menuItems = listOf( MenuItem( stringResource(R.string.search_engine_edit), - color = FirefoxTheme.colors.textWarning, + color = FirefoxTheme.colors.textCritical, ) { onEditEngineClicked(engine) }, MenuItem( stringResource(R.string.search_engine_delete), - color = FirefoxTheme.colors.textWarning, + color = FirefoxTheme.colors.textCritical, ) { onDeleteEngineClicked(engine) }, @@ -268,7 +268,7 @@ private fun SearchEngineShortcutsPreview() { initialState = BrowserState( search = SearchState( regionSearchEngines = generateFakeEnginesList(), - disabledSearchEngineIds = listOf("8", "9"), + disabledSearchEngineIds = listOf("7", "8"), ), ), ), @@ -288,13 +288,12 @@ private fun generateFakeEnginesList(): List<SearchEngine> { generateFakeEngines("1", "Google"), generateFakeEngines("2", "Bing"), generateFakeEngines("3", "Bing"), - generateFakeEngines("4", "Amazon.com"), - generateFakeEngines("5", "DuckDuckGo"), - generateFakeEngines("6", "Qwant"), - generateFakeEngines("7", "eBay"), - generateFakeEngines("8", "Reddit"), - generateFakeEngines("9", "YouTube"), - generateFakeEngines("10", "Yandex", SearchEngine.Type.CUSTOM), + generateFakeEngines("4", "DuckDuckGo"), + generateFakeEngines("5", "Qwant"), + generateFakeEngines("6", "eBay"), + generateFakeEngines("7", "Reddit"), + generateFakeEngines("8", "YouTube"), + generateFakeEngines("9", "Yandex", SearchEngine.Type.CUSTOM), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt index 25f28887f4..3e9593851b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt @@ -16,6 +16,7 @@ import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext +import org.mozilla.experiments.nimbus.NimbusEventStore import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.R import org.mozilla.fenix.browser.StandardSnackbarError @@ -35,10 +36,12 @@ import java.io.IOException * * @param context An Application context. * @param mainScope Coroutine scope to launch coroutines. + * @param nimbusEventStore Nimbus event store for recording events. */ class SaveToPDFMiddleware( private val context: Context, private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main), + private val nimbusEventStore: NimbusEventStore = context.components.nimbus.events, ) : Middleware<BrowserState, BrowserAction> { override fun invoke( @@ -151,6 +154,7 @@ class SaveToPDFMiddleware( source = telemetrySource(isPdf), ), ) + nimbusEventStore.recordEvent("print_tapped") } else { Events.saveToPdfTapped.record( Events.SaveToPdfTappedExtra( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt index 3fbb9b8d5a..5c033066c7 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt @@ -40,19 +40,20 @@ import org.mozilla.fenix.theme.FirefoxTheme /** * Review Quality Check Info Card UI. * + * @param modifier Modifier to be applied to the card. * @param title The primary text of the info message. * @param type The [ReviewQualityCheckInfoType] of message to display. - * @param modifier Modifier to be applied to the card. * @param verticalRowAlignment An optional adjustment of how the row of text aligns. * @param description The optional secondary piece of text. * @param footer An optional piece of text with a clickable link. * @param buttonText The text to show in the optional button. */ +@Suppress("LongMethod") @Composable fun ReviewQualityCheckInfoCard( - title: String, - type: ReviewQualityCheckInfoType, modifier: Modifier = Modifier, + title: String? = null, + type: ReviewQualityCheckInfoType, verticalRowAlignment: Alignment.Vertical = Alignment.Top, description: String? = null, footer: Pair<String, LinkTextState>? = null, @@ -67,7 +68,7 @@ fun ReviewQualityCheckInfoCard( ), elevation = 0.dp, ) { - val titleContentDescription = headingResource(title) + val titleContentDescription = title?.let { headingResource(it) } Row( verticalAlignment = verticalRowAlignment, @@ -81,7 +82,10 @@ fun ReviewQualityCheckInfoCard( InfoCardIcon(iconId = R.drawable.mozac_ic_checkmark_24) } - ReviewQualityCheckInfoType.Error, + ReviewQualityCheckInfoType.Error -> { + InfoCardIcon(iconId = R.drawable.mozac_ic_critical_fill_24) + } + ReviewQualityCheckInfoType.Info, ReviewQualityCheckInfoType.AnalysisUpdate, -> { @@ -92,18 +96,22 @@ fun ReviewQualityCheckInfoCard( Spacer(modifier = Modifier.width(12.dp)) Column { - Text( - text = title, - color = FirefoxTheme.colors.textPrimary, - style = FirefoxTheme.typography.headline8, - modifier = Modifier.semantics { - heading() - contentDescription = titleContentDescription - }, - ) + title?.let { + Text( + text = it, + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline8, + modifier = Modifier.semantics { + heading() + if (titleContentDescription != null) { + contentDescription = titleContentDescription + } + }, + ) + } description?.let { - Spacer(modifier = Modifier.height(4.dp)) + title?.let { Spacer(modifier = Modifier.height(4.dp)) } Text( text = description, @@ -170,9 +178,9 @@ enum class ReviewQualityCheckInfoType { @Composable get() = when (this) { Warning -> FirefoxTheme.colors.layerWarning - Confirmation -> FirefoxTheme.colors.layerConfirmation - Error -> FirefoxTheme.colors.layerError - Info -> FirefoxTheme.colors.layerInfo + Confirmation -> FirefoxTheme.colors.layerSuccess + Error -> FirefoxTheme.colors.layerCritical + Info -> FirefoxTheme.colors.layerInformation AnalysisUpdate -> Color.Transparent } @@ -180,9 +188,9 @@ enum class ReviewQualityCheckInfoType { @Composable get() = when (this) { Warning -> FirefoxTheme.colors.actionWarning - Confirmation -> FirefoxTheme.colors.actionConfirmation - Error -> FirefoxTheme.colors.actionError - Info -> FirefoxTheme.colors.actionInfo + Confirmation -> FirefoxTheme.colors.actionSuccess + Error -> FirefoxTheme.colors.actionCritical + Info -> FirefoxTheme.colors.actionInformation AnalysisUpdate -> FirefoxTheme.colors.actionSecondary } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsController.kt index 27336f6343..58b0042c64 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsController.kt @@ -16,4 +16,9 @@ interface SyncedTabsController { * @param tab The synced [Tab] that was clicked. */ fun handleSyncedTabClicked(tab: Tab) + + /** + * Handles a click on the "close" button for a synced tab. + */ + fun handleSyncedTabClosed(deviceId: String, tab: Tab) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsInteractor.kt index 153a0f48d7..f37655bcac 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsInteractor.kt @@ -16,4 +16,9 @@ interface SyncedTabsInteractor { * @param tab The synced [Tab] that was clicked. */ fun onSyncedTabClicked(tab: Tab) + + /** + * Invoked when the user closes a synced [Tab]. + */ + fun onSyncedTabClosed(deviceId: String, tab: Tab) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt index 65909b5261..533a6454df 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt @@ -48,6 +48,8 @@ import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsList import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem import org.mozilla.fenix.theme.FirefoxTheme import mozilla.components.browser.storage.sync.Tab as SyncTab +import org.mozilla.fenix.tabstray.syncedtabs.OnTabClick as OnSyncedTabClick +import org.mozilla.fenix.tabstray.syncedtabs.OnTabCloseClick as OnSyncedTabClose /** * Top-level UI for displaying the Tabs Tray feature. @@ -75,6 +77,7 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab * @param onInactiveTabClick Invoked when the user clicks on an inactive tab. * @param onInactiveTabClose Invoked when the user clicks on an inactive tab's close button. * @param onSyncedTabClick Invoked when the user clicks on a synced tab. + * @param onSyncedTabClose Invoked when the user clicks on a synced tab's close button. * @param onSaveToCollectionClick Invoked when the user clicks on the save to collection button from * the multi select banner. * @param onShareSelectedTabsClick Invoked when the user clicks on the share button from the @@ -92,6 +95,10 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab * @param onTabAutoCloseBannerDismiss Invoked when the user clicks to dismiss the auto close banner. * @param onTabAutoCloseBannerShown Invoked when the auto close banner has been shown to the user. * @param onMove Invoked after the drag and drop gesture completed. Swaps positions of two tabs. + * @param shouldShowInactiveTabsCFR Returns whether the inactive tabs CFR should be displayed. + * @param onInactiveTabsCFRShown Invoked when the inactive tabs CFR is displayed. + * @param onInactiveTabsCFRClick Invoked when the inactive tabs CFR is clicked. + * @param onInactiveTabsCFRDismiss Invoked when the inactive tabs CFR is dismissed. */ @OptIn(ExperimentalFoundationApi::class) @Suppress("LongMethod", "LongParameterList", "ComplexMethod") @@ -116,7 +123,8 @@ fun TabsTray( onEnableInactiveTabAutoCloseClick: () -> Unit, onInactiveTabClick: (TabSessionState) -> Unit, onInactiveTabClose: (TabSessionState) -> Unit, - onSyncedTabClick: (SyncTab) -> Unit, + onSyncedTabClick: OnSyncedTabClick, + onSyncedTabClose: OnSyncedTabClose, onSaveToCollectionClick: () -> Unit, onShareSelectedTabsClick: () -> Unit, onShareAllTabsClick: () -> Unit, @@ -132,6 +140,10 @@ fun TabsTray( onTabAutoCloseBannerDismiss: () -> Unit, onTabAutoCloseBannerShown: () -> Unit, onMove: (String, String?, Boolean) -> Unit, + shouldShowInactiveTabsCFR: () -> Boolean, + onInactiveTabsCFRShown: () -> Unit, + onInactiveTabsCFRClick: () -> Unit, + onInactiveTabsCFRDismiss: () -> Unit, ) { val multiselectMode = tabsTrayStore .observeAsComposableState { state -> state.mode }.value ?: TabsTrayState.Mode.Normal @@ -210,6 +222,10 @@ fun TabsTray( onInactiveTabClick = onInactiveTabClick, onInactiveTabClose = onInactiveTabClose, onMove = onMove, + shouldShowInactiveTabsCFR = shouldShowInactiveTabsCFR, + onInactiveTabsCFRShown = onInactiveTabsCFRShown, + onInactiveTabsCFRClick = onInactiveTabsCFRClick, + onInactiveTabsCFRDismiss = onInactiveTabsCFRDismiss, ) } @@ -230,6 +246,7 @@ fun TabsTray( SyncedTabsPage( tabsTrayStore = tabsTrayStore, onTabClick = onSyncedTabClick, + onTabClose = onSyncedTabClose, ) } } @@ -258,6 +275,10 @@ private fun NormalTabsPage( onInactiveTabClick: (TabSessionState) -> Unit, onInactiveTabClose: (TabSessionState) -> Unit, onMove: (String, String?, Boolean) -> Unit, + shouldShowInactiveTabsCFR: () -> Boolean, + onInactiveTabsCFRShown: () -> Unit, + onInactiveTabsCFRClick: () -> Unit, + onInactiveTabsCFRDismiss: () -> Unit, ) { val inactiveTabsExpanded = appStore .observeAsComposableState { state -> state.inactiveTabsExpanded }.value ?: false @@ -283,6 +304,7 @@ private fun NormalTabsPage( inactiveTabs = inactiveTabs, expanded = inactiveTabsExpanded, showAutoCloseDialog = showAutoCloseDialog, + showCFR = shouldShowInactiveTabsCFR(), onHeaderClick = onInactiveTabsHeaderClick, onDeleteAllButtonClick = onDeleteAllInactiveTabsClick, onAutoCloseDismissClick = { @@ -295,6 +317,9 @@ private fun NormalTabsPage( }, onTabClick = onInactiveTabClick, onTabCloseClick = onInactiveTabClose, + onCFRShown = onInactiveTabsCFRShown, + onCFRClick = onInactiveTabsCFRClick, + onCFRDismiss = onInactiveTabsCFRDismiss, ) } } @@ -366,7 +391,8 @@ private fun PrivateTabsPage( @Composable private fun SyncedTabsPage( tabsTrayStore: TabsTrayStore, - onTabClick: (SyncTab) -> Unit, + onTabClick: OnSyncedTabClick, + onTabClose: OnSyncedTabClose, ) { val syncedTabs = tabsTrayStore .observeAsComposableState { state -> state.syncedTabs }.value ?: emptyList() @@ -374,6 +400,7 @@ private fun SyncedTabsPage( SyncedTabsList( syncedTabs = syncedTabs, onTabClick = onTabClick, + onTabCloseClick = onTabClose, ) } @@ -565,6 +592,7 @@ private fun TabsTrayPreviewRoot( onInactiveTabClick = {}, onInactiveTabClose = inactiveTabsState::remove, onSyncedTabClick = {}, + onSyncedTabClose = { _, _ -> }, onSaveToCollectionClick = {}, onShareSelectedTabsClick = {}, onShareAllTabsClick = {}, @@ -580,6 +608,10 @@ private fun TabsTrayPreviewRoot( onTabAutoCloseBannerDismiss = {}, onTabAutoCloseBannerShown = {}, onMove = { _, _, _ -> }, + shouldShowInactiveTabsCFR = { false }, + onInactiveTabsCFRShown = {}, + onInactiveTabsCFRClick = {}, + onInactiveTabsCFRDismiss = {}, ) } } @@ -607,10 +639,15 @@ private fun generateFakeSyncedTabsList(deviceCount: Int = 1): List<SyncedTabsLis ) } -private fun generateFakeSyncedTab(tabName: String, tabUrl: String): SyncedTabsListItem.Tab = +private fun generateFakeSyncedTab( + tabName: String, + tabUrl: String, + action: SyncedTabsListItem.Tab.Action = SyncedTabsListItem.Tab.Action.None, +): SyncedTabsListItem.Tab = SyncedTabsListItem.Tab( tabName.ifEmpty { tabUrl }, tabUrl, + action, SyncTab( history = listOf(TabEntry(tabName, tabUrl, null)), active = 0, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt index a48e1d622f..51e23f4b3a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -21,6 +21,7 @@ import mozilla.components.browser.storage.sync.Tab import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.feature.accounts.push.CloseTabsUseCases import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.lib.state.DelicateAction @@ -185,7 +186,8 @@ interface TabsTrayController : SyncedTabsController, InactiveTabsController, Tab * @param navigationInteractor [NavigationInteractor] used to perform navigation actions with side effects. * @param tabsUseCases Use case wrapper for interacting with tabs. * @param bookmarksUseCase Use case wrapper for interacting with bookmarks. - * @param ioDispatcher [CoroutineContext] used to handle saving tabs as bookmarks. + * @param closeSyncedTabsUseCases Use cases for closing synced tabs. + * @param ioDispatcher [CoroutineContext] used for storage and network operations. * @param collectionStorage Storage layer for interacting with collections. * @param selectTabPosition Lambda used to scroll the tabs tray to the desired position. * @param dismissTray Lambda used to dismiss/minimize the tabs tray. @@ -210,6 +212,7 @@ class DefaultTabsTrayController( private val navigationInteractor: NavigationInteractor, private val tabsUseCases: TabsUseCases, private val bookmarksUseCase: BookmarksUseCase, + private val closeSyncedTabsUseCases: CloseTabsUseCases, private val ioDispatcher: CoroutineContext, private val collectionStorage: TabCollectionStorage, private val selectTabPosition: (Int, Boolean) -> Unit, @@ -520,6 +523,12 @@ class DefaultTabsTrayController( ) } + override fun handleSyncedTabClosed(deviceId: String, tab: Tab) { + CoroutineScope(ioDispatcher).launch { + closeSyncedTabsUseCases.close(deviceId, tab.active().url) + } + } + override fun handleTabLongClick(tab: TabSessionState): Boolean { return if (tab.isNormalTab() && tabsTrayStore.state.mode.selectedTabs.isEmpty()) { Collections.longPress.record(NoExtras()) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index fded1b9494..9086708892 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -30,6 +30,7 @@ import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.feature.accounts.push.CloseTabsUseCases import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper @@ -181,6 +182,7 @@ class TabsTrayFragment : AppCompatDialogFragment() { navigationInteractor = navigationInteractor, profiler = requireComponents.core.engine.profiler, tabsUseCases = requireComponents.useCases.tabsUseCases, + closeSyncedTabsUseCases = CloseTabsUseCases(requireComponents.backgroundServices.accountManager), bookmarksUseCase = requireComponents.useCases.bookmarksUseCases, ioDispatcher = Dispatchers.IO, collectionStorage = requireComponents.core.tabCollectionStorage, @@ -275,6 +277,7 @@ class TabsTrayFragment : AppCompatDialogFragment() { onInactiveTabClick = tabsTrayInteractor::onInactiveTabClicked, onInactiveTabClose = tabsTrayInteractor::onInactiveTabClosed, onSyncedTabClick = tabsTrayInteractor::onSyncedTabClicked, + onSyncedTabClose = tabsTrayInteractor::onSyncedTabClosed, onSaveToCollectionClick = tabsTrayInteractor::onAddSelectedTabsToCollectionClicked, onShareSelectedTabsClick = tabsTrayInteractor::onShareSelectedTabs, onShareAllTabsClick = { @@ -307,6 +310,23 @@ class TabsTrayFragment : AppCompatDialogFragment() { requireContext().settings().lastCfrShownTimeInMillis = System.currentTimeMillis() }, onMove = tabsTrayInteractor::onTabsMove, + shouldShowInactiveTabsCFR = { + requireContext().settings().shouldShowInactiveTabsOnboardingPopup && + requireContext().settings().canShowCfr + }, + onInactiveTabsCFRShown = { + TabsTray.inactiveTabsCfrVisible.record(NoExtras()) + }, + onInactiveTabsCFRClick = { + requireContext().settings().shouldShowInactiveTabsOnboardingPopup = false + navigationInteractor.onTabSettingsClicked() + TabsTray.inactiveTabsCfrSettings.record(NoExtras()) + onTabsTrayDismissed() + }, + onInactiveTabsCFRDismiss = { + requireContext().settings().shouldShowInactiveTabsOnboardingPopup = false + TabsTray.inactiveTabsCfrDismissed.record(NoExtras()) + }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt index b9f8bb5473..e29aafc793 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt @@ -157,6 +157,10 @@ class DefaultTabsTrayInteractor( controller.handleSyncedTabClicked(tab) } + override fun onSyncedTabClosed(deviceId: String, tab: Tab) { + controller.handleSyncedTabClosed(deviceId, tab) + } + override fun onBackPressed(): Boolean = controller.handleBackPressed() override fun onTabClosed(tab: TabSessionState, source: String?) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt index 6980806179..945cacd5be 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt @@ -60,6 +60,7 @@ class InactiveTabViewHolder( inactiveTabs = inactiveTabs, expanded = expanded, showAutoCloseDialog = showAutoClosePrompt, + showCFR = false, // The CFR in XML is handled by [TabsTrayInactiveTabsOnboardingBinding] onHeaderClick = { interactor.onInactiveTabsHeaderClicked(!expanded) }, onDeleteAllButtonClick = interactor::onDeleteAllInactiveTabsClicked, onAutoCloseDismissClick = { @@ -73,6 +74,9 @@ class InactiveTabViewHolder( }, onTabClick = interactor::onInactiveTabClicked, onTabCloseClick = interactor::onInactiveTabClosed, + onCFRShown = {}, + onCFRClick = {}, + onCFRDismiss = {}, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt index b9d762f2f9..37ac921e0f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt @@ -92,7 +92,7 @@ class TouchCallback( val icon = recyclerView.context.getDrawableWithTint( R.drawable.ic_delete, - recyclerView.context.getColorFromAttr(R.attr.textWarning), + recyclerView.context.getColorFromAttr(R.attr.textCritical), )!! val background = AppCompatResources.getDrawable( recyclerView.context, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ext/SyncedDeviceTabs.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ext/SyncedDeviceTabs.kt index 39462fa9b0..c8654eac3f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ext/SyncedDeviceTabs.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ext/SyncedDeviceTabs.kt @@ -5,22 +5,41 @@ package org.mozilla.fenix.tabstray.ext import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.concept.sync.DeviceCapability import mozilla.components.support.ktx.kotlin.trimmed import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem +import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListSupportedFeature /** * Converts a list of [SyncedDeviceTabs] into a list of [SyncedTabsListItem]. + * + * @param features Supported [SyncedTabsListSupportedFeature]s. */ -fun List<SyncedDeviceTabs>.toComposeList(): List<SyncedTabsListItem> = asSequence().flatMap { (device, tabs) -> - val deviceTabs = if (tabs.isEmpty()) { - emptyList() - } else { - tabs.map { - val url = it.active().url - val titleText = it.active().title.ifEmpty { url.trimmed() } - SyncedTabsListItem.Tab(titleText, url, it) +fun List<SyncedDeviceTabs>.toComposeList( + features: Set<SyncedTabsListSupportedFeature> = emptySet(), +): List<SyncedTabsListItem> = + asSequence().flatMap { (device, tabs) -> + val deviceTabs = if (tabs.isEmpty()) { + emptyList() + } else { + tabs.map { + val url = it.active().url + val titleText = it.active().title.ifEmpty { url.trimmed() } + SyncedTabsListItem.Tab( + displayTitle = titleText, + displayURL = url, + action = if ( + features.contains(SyncedTabsListSupportedFeature.CLOSE_TABS) && + device.capabilities.contains(DeviceCapability.CLOSE_TABS) + ) { + SyncedTabsListItem.Tab.Action.Close(deviceId = device.id) + } else { + SyncedTabsListItem.Tab.Action.None + }, + tab = it, + ) + } } - } - sequenceOf(SyncedTabsListItem.DeviceSection(device.displayName, deviceTabs)) -}.toList() + sequenceOf(SyncedTabsListItem.DeviceSection(device.displayName, deviceTabs)) + }.toList() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt index 27ad2c1b6c..0fbe3941f8 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt @@ -6,9 +6,9 @@ package org.mozilla.fenix.tabstray.inactivetabs -import android.content.res.Configuration import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -31,14 +31,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.compose.cfr.CFRPopup +import mozilla.components.compose.cfr.CFRPopupLayout +import mozilla.components.compose.cfr.CFRPopupProperties import org.mozilla.fenix.R +import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.button.TextButton import org.mozilla.fenix.compose.list.ExpandableListHeader import org.mozilla.fenix.compose.list.FaviconListItem @@ -54,12 +59,16 @@ private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(8.dp) * @param inactiveTabs List of [TabSessionState] to display. * @param expanded Whether to show the inactive tabs section expanded or collapsed. * @param showAutoCloseDialog Whether to show the auto close inactive tabs dialog. + * @param showCFR Whether to show the CFR. * @param onHeaderClick Called when the user clicks on the inactive tabs section header. * @param onDeleteAllButtonClick Called when the user clicks on the delete all inactive tabs button. * @param onAutoCloseDismissClick Called when the user clicks on the auto close dialog's dismiss button. * @param onEnableAutoCloseClick Called when the user clicks on the auto close dialog's enable button. * @param onTabClick Called when the user clicks on a specific inactive tab. * @param onTabCloseClick Called when the user clicks on a specific inactive tab's close button. + * @param onCFRShown Invoked when the CFR is displayed. + * @param onCFRClick Invoked when the CFR is clicked. + * @param onCFRDismiss Invoked when the CFR is dismissed. */ @Composable @Suppress("LongParameterList") @@ -67,12 +76,16 @@ fun InactiveTabsList( inactiveTabs: List<TabSessionState>, expanded: Boolean, showAutoCloseDialog: Boolean, + showCFR: Boolean, onHeaderClick: (Boolean) -> Unit, onDeleteAllButtonClick: () -> Unit, onAutoCloseDismissClick: () -> Unit, onEnableAutoCloseClick: () -> Unit, onTabClick: (TabSessionState) -> Unit, onTabCloseClick: (TabSessionState) -> Unit, + onCFRShown: () -> Unit, + onCFRClick: () -> Unit, + onCFRDismiss: () -> Unit, ) { Card( modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), @@ -88,6 +101,10 @@ fun InactiveTabsList( ) { InactiveTabsHeader( expanded = expanded, + showCFR = showCFR, + onCFRShown = onCFRShown, + onCFRClick = onCFRClick, + onCFRDismiss = onCFRDismiss, onClick = { onHeaderClick(!expanded) }, onDeleteAllClick = onDeleteAllButtonClick, ) @@ -128,35 +145,84 @@ fun InactiveTabsList( } /** - * Collapsible header for the Inactive Tabs section. + * Collapsible header for the Inactive Tabs section with a CFR. * * @param expanded Whether the section is expanded. + * @param showCFR Whether to show the CFR. * @param onClick Called when the user clicks on the header. * @param onDeleteAllClick Called when the user clicks on the delete all button. + * @param onCFRShown Invoked when the CFR is displayed. + * @param onCFRClick Invoked when the CFR is clicked. + * @param onCFRDismiss Invoked when the CFR is dismissed. */ @Composable private fun InactiveTabsHeader( expanded: Boolean, + showCFR: Boolean, onClick: () -> Unit, onDeleteAllClick: () -> Unit, + onCFRShown: () -> Unit, + onCFRClick: () -> Unit, + onCFRDismiss: () -> Unit, ) { - ExpandableListHeader( - headerText = stringResource(R.string.inactive_tabs_title), - headerTextStyle = FirefoxTheme.typography.headline7, - expanded = expanded, - expandActionContentDescription = stringResource(R.string.inactive_tabs_expand_content_description), - collapseActionContentDescription = stringResource(R.string.inactive_tabs_collapse_content_description), - onClick = onClick, + CFRPopupLayout( + showCFR = showCFR, + properties = CFRPopupProperties( + popupBodyColors = listOf( + FirefoxTheme.colors.layerGradientEnd.toArgb(), + FirefoxTheme.colors.layerGradientStart.toArgb(), + ), + dismissButtonColor = FirefoxTheme.colors.iconOnColor.toArgb(), + indicatorDirection = CFRPopup.IndicatorDirection.UP, + popupVerticalOffset = (-12).dp, + dismissOnBackPress = true, + dismissOnClickOutside = false, + ), + onCFRShown = onCFRShown, + onDismiss = { onCFRDismiss() }, + text = { + FirefoxTheme { + Text( + text = stringResource(R.string.tab_tray_inactive_onboarding_message), + color = FirefoxTheme.colors.textOnColorPrimary, + style = FirefoxTheme.typography.body2, + ) + } + }, + action = { dismissCFR -> + FirefoxTheme { + Text( + text = stringResource(R.string.tab_tray_inactive_onboarding_button_text), + color = FirefoxTheme.colors.textOnColorPrimary, + modifier = Modifier.clickable { + dismissCFR() + onCFRClick() + }, + style = FirefoxTheme.typography.body2.copy( + textDecoration = TextDecoration.Underline, + ), + ) + } + }, ) { - IconButton( - onClick = onDeleteAllClick, - modifier = Modifier.padding(horizontal = 4.dp), + ExpandableListHeader( + headerText = stringResource(R.string.inactive_tabs_title), + headerTextStyle = FirefoxTheme.typography.headline7, + expanded = expanded, + expandActionContentDescription = stringResource(R.string.inactive_tabs_expand_content_description), + collapseActionContentDescription = stringResource(R.string.inactive_tabs_collapse_content_description), + onClick = onClick, ) { - Icon( - painter = painterResource(R.drawable.ic_delete), - contentDescription = stringResource(R.string.inactive_tabs_delete_all), - tint = FirefoxTheme.colors.iconPrimary, - ) + IconButton( + onClick = onDeleteAllClick, + modifier = Modifier.padding(horizontal = 4.dp), + ) { + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = stringResource(R.string.inactive_tabs_delete_all), + tint = FirefoxTheme.colors.iconPrimary, + ) + } } } } @@ -229,8 +295,7 @@ private fun InactiveTabsAutoClosePrompt( } @Composable -@Preview(name = "Auto close dialog dark", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview(name = "Auto close dialog light", uiMode = Configuration.UI_MODE_NIGHT_NO) +@LightDarkPreview private fun InactiveTabsAutoClosePromptPreview() { FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { @@ -243,8 +308,7 @@ private fun InactiveTabsAutoClosePromptPreview() { } @Composable -@Preview(name = "Full preview dark", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview(name = "Full preview light", uiMode = Configuration.UI_MODE_NIGHT_NO) +@LightDarkPreview private fun InactiveTabsListPreview() { var expanded by remember { mutableStateOf(true) } var showAutoClosePrompt by remember { mutableStateOf(true) } @@ -255,12 +319,16 @@ private fun InactiveTabsListPreview() { inactiveTabs = generateFakeInactiveTabsList(), expanded = expanded, showAutoCloseDialog = showAutoClosePrompt, + showCFR = false, onHeaderClick = { expanded = !expanded }, onDeleteAllButtonClick = {}, onAutoCloseDismissClick = { showAutoClosePrompt = !showAutoClosePrompt }, onEnableAutoCloseClick = { showAutoClosePrompt = !showAutoClosePrompt }, onTabClick = {}, onTabCloseClick = {}, + onCFRShown = {}, + onCFRClick = {}, + onCFRDismiss = {}, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt index 2bc7814e53..9dc0a2246f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt @@ -15,6 +15,7 @@ import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.ObserverRegistry +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.tabstray.FloatingActionButtonBinding import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.tabstray.TabsTrayStore @@ -91,7 +92,13 @@ class SyncedTabsIntegration( override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) { store.dispatch( TabsTrayAction.UpdateSyncedTabs( - syncedTabs.toComposeList(), + syncedTabs.toComposeList( + buildSet { + if (context.settings().enableCloseSyncedTabs) { + add(SyncedTabsListSupportedFeature.CLOSE_TABS) + } + }, + ), ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsList.kt index fe39760447..42e8799697 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsList.kt @@ -47,17 +47,29 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab private const val EXPANDED_BY_DEFAULT = true /** + * A lambda invoked when the user clicks on a synced tab in the [SyncedTabsList]. + */ +typealias OnTabClick = (tab: SyncTab) -> Unit + +/** + * A lambda invoked when the user clicks a synced tab's close button in the [SyncedTabsList]. + */ +typealias OnTabCloseClick = (deviceId: String, tab: SyncTab) -> Unit + +/** * Top-level list UI for displaying Synced Tabs in the Tabs Tray. * * @param syncedTabs The tab UI items to be displayed. * @param onTabClick The lambda for handling clicks on synced tabs. + * @param onTabCloseClick The lambda for handling clicks on a synced tab's close button. */ @SuppressWarnings("LongMethod") @OptIn(ExperimentalFoundationApi::class) @Composable fun SyncedTabsList( syncedTabs: List<SyncedTabsListItem>, - onTabClick: (SyncTab) -> Unit, + onTabClick: OnTabClick, + onTabCloseClick: OnTabCloseClick, ) { val listState = rememberLazyListState() val expandedState = @@ -86,12 +98,22 @@ fun SyncedTabsList( if (sectionExpanded) { if (syncedTabItem.tabs.isNotEmpty()) { items(syncedTabItem.tabs) { syncedTab -> - FaviconListItem( - label = syncedTab.displayTitle, - description = syncedTab.displayURL, - url = syncedTab.displayURL, - onClick = { onTabClick(syncedTab.tab) }, - ) + when (syncedTab.action) { + is SyncedTabsListItem.Tab.Action.Close -> FaviconListItem( + label = syncedTab.displayTitle, + description = syncedTab.displayURL, + url = syncedTab.displayURL, + onClick = { onTabClick(syncedTab.tab) }, + iconPainter = painterResource(R.drawable.ic_close), + onIconClick = { onTabCloseClick(syncedTab.action.deviceId, syncedTab.tab) }, + ) + is SyncedTabsListItem.Tab.Action.None -> FaviconListItem( + label = syncedTab.displayTitle, + description = syncedTab.displayURL, + url = syncedTab.displayURL, + onClick = { onTabClick(syncedTab.tab) }, + ) + } } } else { item { SyncedTabsNoTabsItem() } @@ -274,9 +296,9 @@ private fun SyncedTabsListPreview() { Box(Modifier.background(FirefoxTheme.colors.layer1)) { SyncedTabsList( syncedTabs = getFakeSyncedTabList(), - ) { - println("Tab clicked") - } + onTabClick = { println("Tab clicked") }, + onTabCloseClick = { _, _ -> println("Tab closed") }, + ) } } } @@ -294,17 +316,29 @@ internal fun getFakeSyncedTabList(): List<SyncedTabsListItem> = listOf( generateFakeTab("", "www.google.com"), ), ), - SyncedTabsListItem.DeviceSection("Device 2", emptyList()), + SyncedTabsListItem.DeviceSection( + displayName = "Device 2", + tabs = listOf( + generateFakeTab("Firefox", "www.getfirefox.org", SyncedTabsListItem.Tab.Action.Close("device2222")), + generateFakeTab("Thunderbird", "www.getthunderbird.org", SyncedTabsListItem.Tab.Action.Close("device2222")), + ), + ), + SyncedTabsListItem.DeviceSection("Device 3", emptyList()), SyncedTabsListItem.Error("Please re-authenticate"), ) /** * Helper function to create a [SyncedTabsListItem.Tab] for previewing. */ -private fun generateFakeTab(tabName: String, tabUrl: String): SyncedTabsListItem.Tab = +private fun generateFakeTab( + tabName: String, + tabUrl: String, + action: SyncedTabsListItem.Tab.Action = SyncedTabsListItem.Tab.Action.None, +): SyncedTabsListItem.Tab = SyncedTabsListItem.Tab( tabName.ifEmpty { tabUrl }, tabUrl, + action, SyncTab( history = listOf(TabEntry(tabName, tabUrl, null)), active = 0, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListItem.kt index 186d3192a4..0943187e0b 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListItem.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListItem.kt @@ -31,13 +31,29 @@ sealed class SyncedTabsListItem { * * @property displayTitle The title of the tab's web page. * @property displayURL The tab's URL up to BrowserToolbar.MAX_URI_LENGTH characters long. + * @property action The action button to show for this tab. * @property tab The underlying SyncTab object passed when the tab is clicked. */ data class Tab( val displayTitle: String, val displayURL: String, + val action: Action, val tab: SyncTab, - ) : SyncedTabsListItem() + ) : SyncedTabsListItem() { + /** An action button to show for a [Tab]. */ + sealed class Action { + /** + * An action button to close the [Tab] on the synced device. + * + * @property deviceId The ID of the device on which the [Tab] is + * currently open. + */ + data class Close(val deviceId: String) : Action() + + /** A placeholder for a [Tab] without an action button. */ + data object None : Action() + } + } /** * A placeholder for a device that has no tabs synced. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListSupportedFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListSupportedFeature.kt new file mode 100644 index 0000000000..55fac2a78f --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListSupportedFeature.kt @@ -0,0 +1,12 @@ +/* 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.tabstray.syncedtabs + +/** + * Configurable or experimental features that a [SyncedTabsList] supports. + */ +enum class SyncedTabsListSupportedFeature { + CLOSE_TABS, +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt index 58ef98f1f1..d6f96723a4 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt @@ -35,6 +35,7 @@ class SyncedTabsPageViewHolder( SyncedTabsList( syncedTabs = tabs ?: emptyList(), onTabClick = interactor::onSyncedTabClicked, + onTabCloseClick = interactor::onSyncedTabClosed, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/theme/FirefoxTheme.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/theme/FirefoxTheme.kt index 921e989a18..02cff3042f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/theme/FirefoxTheme.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/theme/FirefoxTheme.kt @@ -102,19 +102,19 @@ private val darkColorPalette = FirefoxColors( layerGradientStart = PhotonColors.Violet70, layerGradientEnd = PhotonColors.Violet40, layerWarning = PhotonColors.Yellow70A77, - layerConfirmation = PhotonColors.Green80, - layerError = PhotonColors.Pink80, - layerInfo = PhotonColors.Blue50, + layerSuccess = PhotonColors.Green80, + layerCritical = PhotonColors.Pink80, + layerInformation = PhotonColors.Blue50, layerSearch = PhotonColors.DarkGrey80, actionPrimary = PhotonColors.Violet60, actionPrimaryDisabled = PhotonColors.Violet60A50, - actionSecondary = PhotonColors.LightGrey30, + actionSecondary = PhotonColors.DarkGrey05, actionTertiary = PhotonColors.DarkGrey10, actionQuarternary = PhotonColors.DarkGrey80, actionWarning = PhotonColors.Yellow40A41, - actionConfirmation = PhotonColors.Green70, - actionError = PhotonColors.Pink70A69, - actionInfo = PhotonColors.Blue60, + actionSuccess = PhotonColors.Green70, + actionCritical = PhotonColors.Pink70A69, + actionInformation = PhotonColors.Blue60, formDefault = PhotonColors.LightGrey05, formSelected = PhotonColors.Violet40, formSurface = PhotonColors.DarkGrey05, @@ -126,15 +126,15 @@ private val darkColorPalette = FirefoxColors( textPrimary = PhotonColors.LightGrey05, textSecondary = PhotonColors.LightGrey40, textDisabled = PhotonColors.LightGrey05A40, - textWarning = PhotonColors.Red20, - textWarningButton = PhotonColors.Red70, + textCritical = PhotonColors.Red20, + textCriticalButton = PhotonColors.Red20, textAccent = PhotonColors.Violet20, textAccentDisabled = PhotonColors.Violet20A60, textOnColorPrimary = PhotonColors.LightGrey05, textOnColorSecondary = PhotonColors.LightGrey40, textActionPrimary = PhotonColors.LightGrey05, textActionPrimaryDisabled = PhotonColors.LightGrey05A40, - textActionSecondary = PhotonColors.DarkGrey90, + textActionSecondary = PhotonColors.LightGrey05, textActionTertiary = PhotonColors.LightGrey05, textActionTertiaryActive = PhotonColors.LightGrey05, iconPrimary = PhotonColors.LightGrey05, @@ -146,15 +146,15 @@ private val darkColorPalette = FirefoxColors( iconOnColorDisabled = PhotonColors.LightGrey05A40, iconNotice = PhotonColors.Blue30, iconButton = PhotonColors.LightGrey05, - iconWarning = PhotonColors.Red20, - iconWarningButton = PhotonColors.Red70, + iconCritical = PhotonColors.Red20, + iconCriticalButton = PhotonColors.Red20, iconAccentViolet = PhotonColors.Violet20, iconAccentBlue = PhotonColors.Blue20, iconAccentPink = PhotonColors.Pink20, iconAccentGreen = PhotonColors.Green20, iconAccentYellow = PhotonColors.Yellow20, iconActionPrimary = PhotonColors.LightGrey05, - iconActionSecondary = PhotonColors.DarkGrey90, + iconActionSecondary = PhotonColors.LightGrey05, iconActionTertiary = PhotonColors.LightGrey05, iconGradientStart = PhotonColors.Violet20, iconGradientEnd = PhotonColors.Blue20, @@ -164,7 +164,7 @@ private val darkColorPalette = FirefoxColors( borderFormDefault = PhotonColors.LightGrey05, borderAccent = PhotonColors.Violet40, borderDisabled = PhotonColors.LightGrey05A40, - borderWarning = PhotonColors.Red40, + borderCritical = PhotonColors.Red20, borderToolbarDivider = PhotonColors.DarkGrey60, ) @@ -182,9 +182,9 @@ private val lightColorPalette = FirefoxColors( layerGradientStart = PhotonColors.Violet70, layerGradientEnd = PhotonColors.Violet40, layerWarning = PhotonColors.Yellow20, - layerConfirmation = PhotonColors.Green20, - layerError = PhotonColors.Red10, - layerInfo = PhotonColors.Blue50A44, + layerSuccess = PhotonColors.Green20, + layerCritical = PhotonColors.Red10, + layerInformation = PhotonColors.Blue50A44, layerSearch = PhotonColors.LightGrey30, actionPrimary = PhotonColors.Ink20, actionPrimaryDisabled = PhotonColors.Ink20A50, @@ -192,9 +192,9 @@ private val lightColorPalette = FirefoxColors( actionTertiary = PhotonColors.LightGrey40, actionQuarternary = PhotonColors.LightGrey10, actionWarning = PhotonColors.Yellow60A40, - actionConfirmation = PhotonColors.Green60, - actionError = PhotonColors.Red30, - actionInfo = PhotonColors.Blue50, + actionSuccess = PhotonColors.Green60, + actionCritical = PhotonColors.Red30, + actionInformation = PhotonColors.Blue50, formDefault = PhotonColors.DarkGrey90, formSelected = PhotonColors.Ink20, formSurface = PhotonColors.LightGrey50, @@ -206,8 +206,8 @@ private val lightColorPalette = FirefoxColors( textPrimary = PhotonColors.DarkGrey90, textSecondary = PhotonColors.DarkGrey05, textDisabled = PhotonColors.DarkGrey90A40, - textWarning = PhotonColors.Red70, - textWarningButton = PhotonColors.Red70, + textCritical = PhotonColors.Red70, + textCriticalButton = PhotonColors.Red70, textAccent = PhotonColors.Violet70, textAccentDisabled = PhotonColors.Violet70A80, textOnColorPrimary = PhotonColors.LightGrey05, @@ -226,9 +226,9 @@ private val lightColorPalette = FirefoxColors( iconOnColorDisabled = PhotonColors.LightGrey05A40, iconNotice = PhotonColors.Blue30, iconButton = PhotonColors.Ink20, - iconWarning = PhotonColors.Red70, - iconWarningButton = PhotonColors.Red70, - iconAccentViolet = PhotonColors.Violet60, + iconCritical = PhotonColors.Red70, + iconCriticalButton = PhotonColors.Red70, + iconAccentViolet = PhotonColors.Violet70, iconAccentBlue = PhotonColors.Blue60, iconAccentPink = PhotonColors.Pink60, iconAccentGreen = PhotonColors.Green60, @@ -244,15 +244,16 @@ private val lightColorPalette = FirefoxColors( borderFormDefault = PhotonColors.DarkGrey90, borderAccent = PhotonColors.Ink20, borderDisabled = PhotonColors.DarkGrey90A40, - borderWarning = PhotonColors.Red70, + borderCritical = PhotonColors.Red70, borderToolbarDivider = PhotonColors.LightGrey10, ) private val privateColorPalette = darkColorPalette.copy( - layer1 = PhotonColors.Ink50, - layer2 = PhotonColors.Ink50, + layer1 = PhotonColors.Violet90, + layer2 = PhotonColors.Violet90, layer3 = PhotonColors.Ink90, layerSearch = PhotonColors.Ink90, + borderPrimary = PhotonColors.Ink05, borderSecondary = PhotonColors.Ink10, borderToolbarDivider = PhotonColors.Violet80, ) @@ -276,9 +277,9 @@ class FirefoxColors( layerGradientStart: Color, layerGradientEnd: Color, layerWarning: Color, - layerConfirmation: Color, - layerError: Color, - layerInfo: Color, + layerSuccess: Color, + layerCritical: Color, + layerInformation: Color, layerSearch: Color, actionPrimary: Color, actionPrimaryDisabled: Color, @@ -286,9 +287,9 @@ class FirefoxColors( actionTertiary: Color, actionQuarternary: Color, actionWarning: Color, - actionConfirmation: Color, - actionError: Color, - actionInfo: Color, + actionSuccess: Color, + actionCritical: Color, + actionInformation: Color, formDefault: Color, formSelected: Color, formSurface: Color, @@ -300,8 +301,8 @@ class FirefoxColors( textPrimary: Color, textSecondary: Color, textDisabled: Color, - textWarning: Color, - textWarningButton: Color, + textCritical: Color, + textCriticalButton: Color, textAccent: Color, textAccentDisabled: Color, textOnColorPrimary: Color, @@ -320,8 +321,8 @@ class FirefoxColors( iconOnColorDisabled: Color, iconNotice: Color, iconButton: Color, - iconWarning: Color, - iconWarningButton: Color, + iconCritical: Color, + iconCriticalButton: Color, iconAccentViolet: Color, iconAccentBlue: Color, iconAccentPink: Color, @@ -338,7 +339,7 @@ class FirefoxColors( borderFormDefault: Color, borderAccent: Color, borderDisabled: Color, - borderWarning: Color, + borderCritical: Color, borderToolbarDivider: Color, ) { // Layers @@ -395,15 +396,15 @@ class FirefoxColors( private set // Confirmation background - var layerConfirmation by mutableStateOf(layerConfirmation) + var layerSuccess by mutableStateOf(layerSuccess) private set // Error Background - var layerError by mutableStateOf(layerError) + var layerCritical by mutableStateOf(layerCritical) private set // Info background - var layerInfo by mutableStateOf(layerInfo) + var layerInformation by mutableStateOf(layerInformation) private set // Search @@ -437,15 +438,15 @@ class FirefoxColors( private set // Confirmation button - var actionConfirmation by mutableStateOf(actionConfirmation) + var actionSuccess by mutableStateOf(actionSuccess) private set // Error button - var actionError by mutableStateOf(actionError) + var actionCritical by mutableStateOf(actionCritical) private set // Info button - var actionInfo by mutableStateOf(actionInfo) + var actionInformation by mutableStateOf(actionInformation) private set // Checkbox default, Radio button default @@ -495,11 +496,11 @@ class FirefoxColors( private set // Warning text - var textWarning by mutableStateOf(textWarning) + var textCritical by mutableStateOf(textCritical) private set // Warning text on Secondary button - var textWarningButton by mutableStateOf(textWarningButton) + var textCriticalButton by mutableStateOf(textCriticalButton) private set // Small heading, Text link @@ -575,11 +576,11 @@ class FirefoxColors( // Icon button var iconButton by mutableStateOf(iconButton) private set - var iconWarning by mutableStateOf(iconWarning) + var iconCritical by mutableStateOf(iconCritical) private set // Warning icon on Secondary button - var iconWarningButton by mutableStateOf(iconWarningButton) + var iconCriticalButton by mutableStateOf(iconCriticalButton) private set var iconAccentViolet by mutableStateOf(iconAccentViolet) private set @@ -638,7 +639,7 @@ class FirefoxColors( private set // Form parts - var borderWarning by mutableStateOf(borderWarning) + var borderCritical by mutableStateOf(borderCritical) private set // Toolbar divider @@ -663,9 +664,9 @@ class FirefoxColors( layerGradientStart = other.layerGradientStart layerGradientEnd = other.layerGradientEnd layerWarning = other.layerWarning - layerConfirmation = other.layerConfirmation - layerError = other.layerError - layerInfo = other.layerInfo + layerSuccess = other.layerSuccess + layerCritical = other.layerCritical + layerInformation = other.layerInformation layerSearch = other.layerSearch actionPrimary = other.actionPrimary actionPrimaryDisabled = other.actionPrimaryDisabled @@ -673,9 +674,9 @@ class FirefoxColors( actionTertiary = other.actionTertiary actionQuarternary = other.actionQuarternary actionWarning = other.actionWarning - actionConfirmation = other.actionConfirmation - actionError = other.actionError - actionInfo = other.actionInfo + actionSuccess = other.actionSuccess + actionCritical = other.actionCritical + actionInformation = other.actionInformation formDefault = other.formDefault formSelected = other.formSelected formSurface = other.formSurface @@ -687,8 +688,8 @@ class FirefoxColors( textPrimary = other.textPrimary textSecondary = other.textSecondary textDisabled = other.textDisabled - textWarning = other.textWarning - textWarningButton = other.textWarningButton + textCritical = other.textCritical + textCriticalButton = other.textCriticalButton textAccent = other.textAccent textAccentDisabled = other.textAccentDisabled textOnColorPrimary = other.textOnColorPrimary @@ -707,8 +708,8 @@ class FirefoxColors( iconOnColorDisabled = other.iconOnColorDisabled iconNotice = other.iconNotice iconButton = other.iconButton - iconWarning = other.iconWarning - iconWarningButton = other.iconWarningButton + iconCritical = other.iconCritical + iconCriticalButton = other.iconCriticalButton iconAccentViolet = other.iconAccentViolet iconAccentBlue = other.iconAccentBlue iconAccentPink = other.iconAccentPink @@ -725,7 +726,7 @@ class FirefoxColors( borderFormDefault = other.borderFormDefault borderAccent = other.borderAccent borderDisabled = other.borderDisabled - borderWarning = other.borderWarning + borderCritical = other.borderCritical borderToolbarDivider = other.borderToolbarDivider } @@ -747,9 +748,9 @@ class FirefoxColors( layerGradientStart: Color = this.layerGradientStart, layerGradientEnd: Color = this.layerGradientEnd, layerWarning: Color = this.layerWarning, - layerConfirmation: Color = this.layerConfirmation, - layerError: Color = this.layerError, - layerInfo: Color = this.layerInfo, + layerSuccess: Color = this.layerSuccess, + layerCritical: Color = this.layerCritical, + layerInformation: Color = this.layerInformation, layerSearch: Color = this.layerSearch, actionPrimary: Color = this.actionPrimary, actionPrimaryDisabled: Color = this.actionPrimaryDisabled, @@ -757,9 +758,9 @@ class FirefoxColors( actionTertiary: Color = this.actionTertiary, actionQuarternary: Color = this.actionQuarternary, actionWarning: Color = this.actionWarning, - actionConfirmation: Color = this.actionConfirmation, - actionError: Color = this.actionError, - actionInfo: Color = this.actionInfo, + actionSuccess: Color = this.actionSuccess, + actionCritical: Color = this.actionCritical, + actionInformation: Color = this.actionInformation, formDefault: Color = this.formDefault, formSelected: Color = this.formSelected, formSurface: Color = this.formSurface, @@ -771,8 +772,8 @@ class FirefoxColors( textPrimary: Color = this.textPrimary, textSecondary: Color = this.textSecondary, textDisabled: Color = this.textDisabled, - textWarning: Color = this.textWarning, - textWarningButton: Color = this.textWarningButton, + textCritical: Color = this.textCritical, + textCriticalButton: Color = this.textCriticalButton, textAccent: Color = this.textAccent, textAccentDisabled: Color = this.textAccentDisabled, textOnColorPrimary: Color = this.textOnColorPrimary, @@ -791,8 +792,8 @@ class FirefoxColors( iconOnColorDisabled: Color = this.iconOnColorDisabled, iconNotice: Color = this.iconNotice, iconButton: Color = this.iconButton, - iconWarning: Color = this.iconWarning, - iconWarningButton: Color = this.iconWarningButton, + iconCritical: Color = this.iconCritical, + iconCriticalButton: Color = this.iconCriticalButton, iconAccentViolet: Color = this.iconAccentViolet, iconAccentBlue: Color = this.iconAccentBlue, iconAccentPink: Color = this.iconAccentPink, @@ -809,7 +810,7 @@ class FirefoxColors( borderFormDefault: Color = this.borderFormDefault, borderAccent: Color = this.borderAccent, borderDisabled: Color = this.borderDisabled, - borderWarning: Color = this.borderWarning, + borderWarning: Color = this.borderCritical, borderToolbarDivider: Color = this.borderToolbarDivider, ): FirefoxColors = FirefoxColors( layer1 = layer1, @@ -825,9 +826,9 @@ class FirefoxColors( layerGradientStart = layerGradientStart, layerGradientEnd = layerGradientEnd, layerWarning = layerWarning, - layerConfirmation = layerConfirmation, - layerError = layerError, - layerInfo = layerInfo, + layerSuccess = layerSuccess, + layerCritical = layerCritical, + layerInformation = layerInformation, layerSearch = layerSearch, actionPrimary = actionPrimary, actionPrimaryDisabled = actionPrimaryDisabled, @@ -835,9 +836,9 @@ class FirefoxColors( actionTertiary = actionTertiary, actionQuarternary = actionQuarternary, actionWarning = actionWarning, - actionConfirmation = actionConfirmation, - actionError = actionError, - actionInfo = actionInfo, + actionSuccess = actionSuccess, + actionCritical = actionCritical, + actionInformation = actionInformation, formDefault = formDefault, formSelected = formSelected, formSurface = formSurface, @@ -849,8 +850,8 @@ class FirefoxColors( textPrimary = textPrimary, textSecondary = textSecondary, textDisabled = textDisabled, - textWarning = textWarning, - textWarningButton = textWarningButton, + textCritical = textCritical, + textCriticalButton = textCriticalButton, textAccent = textAccent, textAccentDisabled = textAccentDisabled, textOnColorPrimary = textOnColorPrimary, @@ -869,8 +870,8 @@ class FirefoxColors( iconOnColorDisabled = iconOnColorDisabled, iconNotice = iconNotice, iconButton = iconButton, - iconWarning = iconWarning, - iconWarningButton = iconWarningButton, + iconCritical = iconCritical, + iconCriticalButton = iconCriticalButton, iconAccentViolet = iconAccentViolet, iconAccentBlue = iconAccentBlue, iconAccentPink = iconAccentPink, @@ -887,7 +888,7 @@ class FirefoxColors( borderFormDefault = borderFormDefault, borderAccent = borderAccent, borderDisabled = borderDisabled, - borderWarning = borderWarning, + borderCritical = borderWarning, borderToolbarDivider = borderToolbarDivider, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt index 952727fe85..89c9cb47dc 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt @@ -14,6 +14,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -26,7 +29,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.role import androidx.compose.ui.unit.dp import org.mozilla.fenix.R @@ -87,15 +89,15 @@ fun DownloadIndicator( modifier = modifier.then( Modifier .clearAndSetSemantics { - disabled() role = Role.Button contentDescription?.let { this.contentDescription = contentDescription } - }, + } + .wrapContentSize(), ), - enabled = false, icon = icon, iconModifier = Modifier - .rotate(rotationAnimation()), + .rotate(rotationAnimation()) + .size(ButtonDefaults.IconSize), onClick = {}, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt index d29da59cfd..9edbaef823 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt @@ -46,6 +46,7 @@ fun TranslationSettings( onNeverTranslationClicked: () -> Unit, onDownloadLanguageClicked: () -> Unit, ) { + val showHeader = showAutomaticTranslations || showNeverTranslate || showDownloads Column( modifier = Modifier .background( @@ -67,12 +68,12 @@ fun TranslationSettings( .padding(start = 72.dp, end = 16.dp), ) - if (item.type.hasDivider) { + if (item.type.hasDivider && showHeader) { Divider(Modifier.padding(top = 8.dp, bottom = 8.dp)) } } - if (showAutomaticTranslations || showNeverTranslate || showDownloads) { + if (showHeader) { item { Text( text = stringResource( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt index 5fbd7a2dc1..0859c51bd3 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt @@ -17,11 +17,9 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs import mozilla.components.browser.state.action.TranslationsAction -import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.TranslationsBrowserState import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.translate.TranslationPageSettingOperation import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.support.base.feature.UserInteractionHandler import org.mozilla.fenix.GleanMetrics.Translations @@ -36,7 +34,6 @@ import org.mozilla.fenix.theme.FirefoxTheme * A fragment displaying the Firefox Translation settings screen. */ class TranslationSettingsFragment : Fragment(), UserInteractionHandler { - private val args by navArgs<TranslationSettingsFragmentArgs>() private val browserStore: BrowserStore by lazy { requireComponents.core.store } override fun onResume() { @@ -67,7 +64,7 @@ class TranslationSettingsFragment : Fragment(), UserInteractionHandler { Translations.action.record(Translations.ActionExtra("global_site_settings")) findNavController().navigate( TranslationSettingsFragmentDirections - .actionTranslationSettingsFragmentToNeverTranslateSitePreferenceFragment(), + .actionTranslationSettingsToNeverTranslateSitePreference(), ) }, onDownloadLanguageClicked = { @@ -84,19 +81,19 @@ class TranslationSettingsFragment : Fragment(), UserInteractionHandler { /** * Set the switch item values. - * The first one is based on [TranslationPageSettings.alwaysOfferPopup]. + * The first one is based on [TranslationsBrowserState.offerTranslation]. * The second one is [DownloadLanguageFileDialog] visibility. * This pop-up will appear if the switch item is unchecked, the phone is in saving mode, and * doesn't have a WiFi connection. */ @Composable private fun getTranslationSwitchItemList(): MutableList<TranslationSwitchItem> { - val pageSettingsState = browserStore.observeAsComposableState { state -> - state.findTab(args.sessionId)?.translationsState?.pageSettings + val offerToTranslate = browserStore.observeAsComposableState { state -> + state.translationEngine.offerTranslation }.value val translationSwitchItems = mutableListOf<TranslationSwitchItem>() - pageSettingsState?.alwaysOfferPopup?.let { + offerToTranslate?.let { translationSwitchItems.add( TranslationSwitchItem( type = TranslationSettingsScreenOption.OfferToTranslate( @@ -107,10 +104,8 @@ class TranslationSettingsFragment : Fragment(), UserInteractionHandler { isEnabled = true, onStateChange = { _, checked -> browserStore.dispatch( - TranslationsAction.UpdatePageSettingAction( - tabId = args.sessionId, - operation = TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP, - setting = checked, + TranslationsAction.SetGlobalOfferTranslateSettingAction( + offerTranslation = checked, ), ) // Ensures persistence of value @@ -141,12 +136,15 @@ class TranslationSettingsFragment : Fragment(), UserInteractionHandler { } override fun onBackPressed(): Boolean { - findNavController().navigate( - TranslationSettingsFragmentDirections.actionTranslationSettingsFragmentToTranslationsDialogFragment( - sessionId = args.sessionId, - translationsDialogAccessPoint = TranslationsDialogAccessPoint.TranslationsOptions, - ), - ) - return true + return if (findNavController().previousBackStackEntry?.destination?.id == R.id.browserFragment) { + findNavController().navigate( + TranslationSettingsFragmentDirections.actionTranslationSettingsFragmentToTranslationsDialogFragment( + translationsDialogAccessPoint = TranslationsDialogAccessPoint.TranslationsOptions, + ), + ) + true + } else { + false + } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt index 8d4a74e02c..f6e27391be 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt @@ -171,6 +171,7 @@ internal fun TranslationsOptionsDialog( context: Context, showGlobalSettings: Boolean, translationPageSettings: TranslationPageSettings? = null, + offerTranslation: Boolean? = null, initialFrom: Language? = null, onStateChange: (TranslationSettingsOption, Boolean) -> Unit, onBackClicked: () -> Unit, @@ -181,6 +182,7 @@ internal fun TranslationsOptionsDialog( showGlobalSettings = showGlobalSettings, translationOptionsList = getTranslationSwitchItemList( translationPageSettings = translationPageSettings, + offerTranslation = offerTranslation, initialFrom = initialFrom, context = context, onStateChange = onStateChange, @@ -194,6 +196,7 @@ internal fun TranslationsOptionsDialog( @Composable private fun getTranslationSwitchItemList( translationPageSettings: TranslationPageSettings? = null, + offerTranslation: Boolean? = null, initialFrom: Language? = null, context: Context, onStateChange: (TranslationSettingsOption, Boolean) -> Unit, @@ -201,12 +204,11 @@ private fun getTranslationSwitchItemList( val translationSwitchItemList = mutableListOf<TranslationSwitchItem>() translationPageSettings?.let { - val alwaysOfferPopup = translationPageSettings.alwaysOfferPopup val alwaysTranslateLanguage = translationPageSettings.alwaysTranslateLanguage val neverTranslateLanguage = translationPageSettings.neverTranslateLanguage val neverTranslateSite = translationPageSettings.neverTranslateSite - alwaysOfferPopup?.let { + offerTranslation?.let { translationSwitchItemList.add( TranslationSwitchItem( type = TranslationPageSettingsOption.AlwaysOfferPopup(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt index e2d25f82dc..b053e70498 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.mapNotNull -import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TranslationsBrowserState @@ -29,7 +29,6 @@ import java.util.Locale class TranslationsDialogBinding( browserStore: BrowserStore, private val translationsDialogStore: TranslationsDialogStore, - private val sessionId: String, private val getTranslatedPageTitle: (localizedFrom: String?, localizedTo: String?) -> String, ) : AbstractBinding<BrowserState>(browserStore) { @@ -42,7 +41,7 @@ class TranslationsDialogBinding( } // Session level flows - val sessionFlow = flow.mapNotNull { state -> state.findTab(sessionId) } + val sessionFlow = flow.mapNotNull { state -> state.selectedTab } .distinctUntilChangedBy { it.translationsState } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt index 2c834aea08..727034c0d8 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt @@ -484,16 +484,18 @@ private fun TranslationErrorWarning( when (translationError) { is TranslationError.CouldNotTranslateError -> { ReviewQualityCheckInfoCard( - title = stringResource(id = R.string.translation_error_could_not_translate_warning_text), + description = stringResource(id = R.string.translation_error_could_not_translate_warning_text), type = ReviewQualityCheckInfoType.Error, + verticalRowAlignment = Alignment.CenterVertically, modifier = modifier, ) } is TranslationError.CouldNotLoadLanguagesError -> { ReviewQualityCheckInfoCard( - title = stringResource(id = R.string.translation_error_could_not_load_languages_warning_text), + description = stringResource(id = R.string.translation_error_could_not_load_languages_warning_text), type = ReviewQualityCheckInfoType.Error, + verticalRowAlignment = Alignment.CenterVertically, modifier = modifier, ) } @@ -501,7 +503,7 @@ private fun TranslationErrorWarning( is TranslationError.LanguageNotSupportedError -> { documentLangDisplayName?.let { ReviewQualityCheckInfoCard( - title = stringResource( + description = stringResource( id = R.string.translation_error_language_not_supported_warning_text, it, ), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt index 7e31dd594d..0d5909548e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf @@ -28,7 +29,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.translate.Language import mozilla.components.concept.engine.translate.TranslationError @@ -48,6 +49,9 @@ import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLang import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLanguageFileDialogType import org.mozilla.fenix.translations.preferences.downloadlanguages.DownloadLanguagesFeature +// Friction should be increased, since peek height on this dialog is to fill the screen. +private const val DIALOG_FRICTION = .65f + /** * The enum is to know what bottom sheet to open. */ @@ -78,6 +82,7 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { behavior = BottomSheetBehavior.from(bottomSheet) behavior?.peekHeight = resources.displayMetrics.heightPixels behavior?.state = BottomSheetBehavior.STATE_EXPANDED + behavior?.hideFriction = DIALOG_FRICTION } } @@ -92,7 +97,6 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { listOf( TranslationsDialogMiddleware( browserStore = browserStore, - sessionId = args.sessionId, settings = requireContext().settings(), ), ), @@ -245,7 +249,6 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { feature = TranslationsDialogBinding( browserStore = browserStore, translationsDialogStore = translationsDialogStore, - sessionId = args.sessionId, getTranslatedPageTitle = { localizedFrom, localizedTo -> requireContext().getString( R.string.translations_bottom_sheet_title_translation_completed, @@ -280,6 +283,8 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { onSettingClicked: () -> Unit, onShowDownloadLanguageFileDialog: () -> Unit, ) { + val localView = LocalView.current + TranslationsDialog( translationsDialogState = translationsDialogState, learnMoreUrl = learnMoreUrl, @@ -295,6 +300,11 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { }, onNegativeButtonClicked = { if (translationsDialogState.isTranslated) { + localView.announceForAccessibility( + requireContext().getString( + R.string.translations_bottom_sheet_restore_accessibility_announcement, + ), + ) translationsDialogStore.dispatch(TranslationsDialogAction.RestoreTranslation) } dismiss() @@ -384,12 +394,19 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { ) { val pageSettingsState = browserStore.observeAsComposableState { state -> - state.findTab(args.sessionId)?.translationsState?.pageSettings + state.selectedTab?.translationsState?.pageSettings }.value + val offerTranslation = browserStore.observeAsComposableState { state -> + state.translationEngine.offerTranslation + }.value + + val localView = LocalView.current + TranslationsOptionsDialog( context = requireContext(), translationPageSettings = pageSettingsState, + offerTranslation = offerTranslation, showGlobalSettings = showGlobalSettings, initialFrom = initialFrom, onStateChange = { type, checked -> @@ -402,15 +419,17 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { checked, ), ) + + if (checked) { + localView.announceForAccessibility(type.descriptionId?.let { getString(it) }) + } }, onBackClicked = onBackClicked, onTranslationSettingsClicked = { Translations.action.record(Translations.ActionExtra("global_settings")) findNavController().navigate( TranslationsDialogFragmentDirections - .actionTranslationsDialogFragmentToTranslationSettingsFragment( - sessionId = args.sessionId, - ), + .actionTranslationsDialogFragmentToTranslationSettingsFragment(), ) }, aboutTranslationClicked = { @@ -425,7 +444,7 @@ class TranslationsDialogFragment : BottomSheetDialogFragment() { setFragmentResult( TRANSLATION_IN_PROGRESS, bundleOf( - SESSION_ID to args.sessionId, + SESSION_ID to browserStore.state.selectedTab?.id, ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt index 20bfee0d84..cb6ac2c62d 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt @@ -5,12 +5,12 @@ package org.mozilla.fenix.translations import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.translate.TranslationOperation import mozilla.components.concept.engine.translate.TranslationPageSettingOperation import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext -import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.Settings /** @@ -18,16 +18,17 @@ import org.mozilla.fenix.utils.Settings */ class TranslationsDialogMiddleware( private val browserStore: BrowserStore, - private val sessionId: String, private val settings: Settings, ) : Middleware<TranslationsDialogState, TranslationsDialogAction> { - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod") override fun invoke( context: MiddlewareContext<TranslationsDialogState, TranslationsDialogAction>, next: (TranslationsDialogAction) -> Unit, action: TranslationsDialogAction, ) { + val sessionId = browserStore.state.selectedTab?.id ?: return + when (action) { is TranslationsDialogAction.InitTranslationsDialog -> { // If the languages are missing, we should attempt to fetch the supported languages. @@ -98,10 +99,8 @@ class TranslationsDialogMiddleware( is TranslationPageSettingsOption.AlwaysOfferPopup -> { // Ensures the translations engine has the correct value browserStore.dispatch( - TranslationsAction.UpdatePageSettingAction( - tabId = sessionId, - operation = TranslationPageSettingOperation.UPDATE_ALWAYS_OFFER_POPUP, - setting = action.checkValue, + TranslationsAction.SetGlobalOfferTranslateSettingAction( + offerTranslation = action.checkValue, ), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationItemPreference.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationItemPreference.kt index 30bfed028e..8c32bc570c 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationItemPreference.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationItemPreference.kt @@ -6,17 +6,20 @@ package org.mozilla.fenix.translations.preferences.automatic import android.os.Parcelable import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import mozilla.components.concept.engine.translate.Language +import mozilla.components.concept.engine.translate.LanguageSetting import org.mozilla.fenix.R /** * AutomaticTranslationItem that will appear on Automatic Translation screen. * - * @property displayName The text that will appear in the list. + * @property language The text that will appear in the list. * @property automaticTranslationOptionPreference The option that the user selected. */ @Parcelize data class AutomaticTranslationItemPreference( - val displayName: String, + val language: @RawValue Language, val automaticTranslationOptionPreference: AutomaticTranslationOptionPreference, ) : Parcelable @@ -65,3 +68,23 @@ sealed class AutomaticTranslationOptionPreference( ), ) : AutomaticTranslationOptionPreference(titleId = titleId, summaryId = summaryId) } + +internal fun getAutomaticTranslationOptionPreference( + languageSetting: LanguageSetting, +): AutomaticTranslationOptionPreference { + return when (languageSetting) { + LanguageSetting.ALWAYS -> AutomaticTranslationOptionPreference.AlwaysTranslate() + LanguageSetting.OFFER -> AutomaticTranslationOptionPreference.OfferToTranslate() + LanguageSetting.NEVER -> AutomaticTranslationOptionPreference.NeverTranslate() + } +} + +internal fun getLanguageSetting( + automaticTranslationItemPreference: AutomaticTranslationOptionPreference, +): LanguageSetting { + return when (automaticTranslationItemPreference) { + is AutomaticTranslationOptionPreference.AlwaysTranslate -> LanguageSetting.ALWAYS + is AutomaticTranslationOptionPreference.NeverTranslate -> LanguageSetting.NEVER + is AutomaticTranslationOptionPreference.OfferToTranslate -> LanguageSetting.OFFER + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreference.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreference.kt index bbfd3d42ba..a62e6d3a36 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreference.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreference.kt @@ -21,10 +21,12 @@ import org.mozilla.fenix.theme.FirefoxTheme * Firefox Automatic Translation Options preference screen. * * @param selectedOption Selected option that will come from the translations engine. + * @param onItemClick Invoked when the user clicks on a [AutomaticTranslationOptionPreference] from the list. */ @Composable fun AutomaticTranslationOptionsPreference( selectedOption: AutomaticTranslationOptionPreference, + onItemClick: (AutomaticTranslationOptionPreference) -> Unit, ) { val optionsList = arrayListOf( AutomaticTranslationOptionPreference.OfferToTranslate(), @@ -50,6 +52,7 @@ fun AutomaticTranslationOptionsPreference( maxDescriptionLines = Int.MAX_VALUE, onClick = { selected.value = item + onItemClick(item) }, ) } @@ -63,6 +66,7 @@ private fun AutomaticTranslationOptionsPreview() { FirefoxTheme { AutomaticTranslationOptionsPreference( selectedOption = AutomaticTranslationOptionPreference.AlwaysTranslate(), + onItemClick = {}, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreferenceFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreferenceFragment.kt index b144227312..ad2dea0072 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreferenceFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreferenceFragment.kt @@ -11,6 +11,9 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs +import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.theme.FirefoxTheme @@ -19,10 +22,11 @@ import org.mozilla.fenix.theme.FirefoxTheme */ class AutomaticTranslationOptionsPreferenceFragment : Fragment() { private val args by navArgs<AutomaticTranslationOptionsPreferenceFragmentArgs>() + private val browserStore: BrowserStore by lazy { requireComponents.core.store } override fun onResume() { super.onResume() - showToolbar(args.selectedTranslationOptionPreference.displayName) + args.selectedTranslationOptionPreference.language.localizedDisplayName?.let { showToolbar(it) } } override fun onCreateView( @@ -34,6 +38,14 @@ class AutomaticTranslationOptionsPreferenceFragment : Fragment() { FirefoxTheme { AutomaticTranslationOptionsPreference( selectedOption = args.selectedTranslationOptionPreference.automaticTranslationOptionPreference, + onItemClick = { + browserStore.dispatch( + TranslationsAction.UpdateLanguageSettingsAction( + languageCode = args.selectedTranslationOptionPreference.language.code, + setting = getLanguageSetting(it), + ), + ) + }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreference.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreference.kt index 4ce45c4e2b..76183742c3 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreference.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreference.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import mozilla.components.concept.engine.translate.Language import org.mozilla.fenix.R import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.list.TextListItem @@ -58,16 +59,18 @@ fun AutomaticTranslationPreference( ) { description = stringResource(item.automaticTranslationOptionPreference.titleId) } - TextListItem( - label = item.displayName, - description = description, - modifier = Modifier - .fillMaxWidth() - .padding(start = 56.dp), - onClick = { - onItemClick(item) - }, - ) + item.language.localizedDisplayName?.let { + TextListItem( + label = it, + description = description, + modifier = Modifier + .fillMaxWidth() + .padding(start = 56.dp), + onClick = { + onItemClick(item) + }, + ) + } } } } @@ -78,25 +81,25 @@ internal fun getAutomaticTranslationListPreferences(): List<AutomaticTranslation return mutableListOf<AutomaticTranslationItemPreference>().apply { add( AutomaticTranslationItemPreference( - displayName = Locale.ENGLISH.displayLanguage, + language = Language(Locale.ENGLISH.toLanguageTag(), Locale.ENGLISH.displayLanguage), automaticTranslationOptionPreference = AutomaticTranslationOptionPreference.AlwaysTranslate(), ), ) add( AutomaticTranslationItemPreference( - displayName = Locale.FRENCH.displayLanguage, + language = Language(Locale.FRANCE.toLanguageTag(), Locale.FRANCE.displayLanguage), automaticTranslationOptionPreference = AutomaticTranslationOptionPreference.OfferToTranslate(), ), ) add( AutomaticTranslationItemPreference( - displayName = Locale.GERMAN.displayLanguage, + language = Language(Locale.GERMAN.toLanguageTag(), Locale.GERMAN.displayLanguage), automaticTranslationOptionPreference = AutomaticTranslationOptionPreference.NeverTranslate(), ), ) add( AutomaticTranslationItemPreference( - displayName = Locale.ITALIAN.displayLanguage, + language = Language(Locale.ITALIAN.toLanguageTag(), Locale.ITALIAN.displayLanguage), automaticTranslationOptionPreference = AutomaticTranslationOptionPreference.AlwaysTranslate(), ), ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreferenceFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreferenceFragment.kt index 9830a17156..c2b07f98bb 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreferenceFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreferenceFragment.kt @@ -11,7 +11,13 @@ import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.translate.LanguageSetting +import mozilla.components.concept.engine.translate.TranslationSupport +import mozilla.components.concept.engine.translate.findLanguage +import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.R +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.theme.FirefoxTheme @@ -19,6 +25,8 @@ import org.mozilla.fenix.theme.FirefoxTheme * A fragment displaying the Firefox Automatic Translation list screen. */ class AutomaticTranslationPreferenceFragment : Fragment() { + private val browserStore: BrowserStore by lazy { requireComponents.core.store } + override fun onResume() { super.onResume() showToolbar(getString(R.string.automatic_translation_toolbar_title_preference)) @@ -31,8 +39,18 @@ class AutomaticTranslationPreferenceFragment : Fragment() { ): View = ComposeView(requireContext()).apply { setContent { FirefoxTheme { + val languageSettings = browserStore.observeAsComposableState { state -> + state.translationEngine.languageSettings + }.value + val translationSupport = browserStore.observeAsComposableState { state -> + state.translationEngine.supportedLanguages + }.value + AutomaticTranslationPreference( - automaticTranslationListPreferences = getAutomaticTranslationListPreferences(), + automaticTranslationListPreferences = getAutomaticTranslationListPreferences( + languageSettings = languageSettings, + translationSupport = translationSupport, + ), onItemClick = { findNavController().navigate( AutomaticTranslationPreferenceFragmentDirections @@ -45,4 +63,28 @@ class AutomaticTranslationPreferenceFragment : Fragment() { } } } + + private fun getAutomaticTranslationListPreferences( + languageSettings: Map<String, LanguageSetting>? = null, + translationSupport: TranslationSupport? = null, + ): List<AutomaticTranslationItemPreference> { + val automaticTranslationListPreferences = + mutableListOf<AutomaticTranslationItemPreference>() + + if (translationSupport != null && languageSettings != null) { + languageSettings.forEach { entry -> + translationSupport.findLanguage(entry.key)?.let { + automaticTranslationListPreferences.add( + AutomaticTranslationItemPreference( + language = it, + automaticTranslationOptionPreference = getAutomaticTranslationOptionPreference( + entry.value, + ), + ), + ) + } + } + } + return automaticTranslationListPreferences + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteDialogPreferenceFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteDialogPreferenceFragment.kt index 20204b2afb..42caba39a5 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteDialogPreferenceFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteDialogPreferenceFragment.kt @@ -13,6 +13,9 @@ import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.DialogFragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import mozilla.components.browser.state.action.TranslationsAction +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.theme.FirefoxTheme /** @@ -21,6 +24,7 @@ import org.mozilla.fenix.theme.FirefoxTheme class NeverTranslateSiteDialogPreferenceFragment : DialogFragment() { private val args by navArgs<NeverTranslateSiteDialogPreferenceFragmentArgs>() + private val browserStore: BrowserStore by lazy { requireComponents.core.store } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = super.onCreateDialog(savedInstanceState).apply { @@ -37,9 +41,18 @@ class NeverTranslateSiteDialogPreferenceFragment : DialogFragment() { setContent { FirefoxTheme { NeverTranslateSiteDialogPreference( - websiteUrl = args.websiteUrl, - onConfirmDelete = { findNavController().popBackStack() }, - onCancel = { findNavController().popBackStack() }, + websiteUrl = args.neverTranslateSiteUrl, + onConfirmDelete = { + browserStore.dispatch( + TranslationsAction.RemoveNeverTranslateSiteAction( + origin = args.neverTranslateSiteUrl, + ), + ) + findNavController().popBackStack() + }, + onCancel = { + findNavController().popBackStack() + }, ) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteListItemPreference.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteListItemPreference.kt deleted file mode 100644 index 6baf2868ef..0000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteListItemPreference.kt +++ /dev/null @@ -1,12 +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.translations.preferences.nevertranslatesite - -/** - * NeverTranslateSiteListItemPreference that will appear on [NeverTranslateSitePreferenceFragment] screens. - * - * @property websiteUrl The text that will appear on the item list. - */ -data class NeverTranslateSiteListItemPreference(val websiteUrl: String) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreferenceFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreferenceFragment.kt deleted file mode 100644 index 473d397b86..0000000000 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreferenceFragment.kt +++ /dev/null @@ -1,48 +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.translations.preferences.nevertranslatesite - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.showToolbar -import org.mozilla.fenix.theme.FirefoxTheme - -/** - * A fragment displaying never translate site items list. - */ -class NeverTranslateSitePreferenceFragment : Fragment() { - override fun onResume() { - super.onResume() - showToolbar(getString(R.string.never_translate_site_toolbar_title_preference)) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View = ComposeView(requireContext()).apply { - setContent { - FirefoxTheme { - NeverTranslateSitePreference( - neverTranslateSiteListPreferences = getNeverTranslateListItemsPreference(), - onItemClick = { - findNavController().navigate( - NeverTranslateSitePreferenceFragmentDirections - .actionNeverTranslateSitePreferenceFragmentToNeverTranslateSiteDialogPreferenceFragment( - it.websiteUrl, - ), - ) - }, - ) - } - } - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreference.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreference.kt index e8cf6c1a44..02215ea1fa 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreference.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreference.kt @@ -28,13 +28,13 @@ import org.mozilla.fenix.theme.FirefoxTheme /** * Never Translate Site preference screen. * - * @param neverTranslateSiteListPreferences List of [NeverTranslateSiteListItemPreference]s to display. + * @param neverTranslateSitesListPreferences List of site urls to display. * @param onItemClick Invoked when the user clicks on the a item from the list. */ @Composable -fun NeverTranslateSitePreference( - neverTranslateSiteListPreferences: List<NeverTranslateSiteListItemPreference>, - onItemClick: (NeverTranslateSiteListItemPreference) -> Unit, +fun NeverTranslateSitesPreference( + neverTranslateSitesListPreferences: List<String>, + onItemClick: (String) -> Unit, ) { Column( modifier = Modifier @@ -53,13 +53,13 @@ fun NeverTranslateSitePreference( ) LazyColumn { - items(neverTranslateSiteListPreferences) { item: NeverTranslateSiteListItemPreference -> + items(neverTranslateSitesListPreferences) { item: String -> val itemContentDescription = stringResource( id = R.string.never_translate_site_item_list_content_description_preference, - item.websiteUrl, + item, ) TextListItem( - label = item.websiteUrl, + label = item, modifier = Modifier .padding( start = 56.dp, @@ -78,12 +78,10 @@ fun NeverTranslateSitePreference( } @Composable -internal fun getNeverTranslateListItemsPreference(): List<NeverTranslateSiteListItemPreference> { - return mutableListOf<NeverTranslateSiteListItemPreference>().apply { +internal fun getNeverTranslateSitesList(): List<String> { + return mutableListOf<String>().apply { add( - NeverTranslateSiteListItemPreference( - websiteUrl = "mozilla.org", - ), + "mozilla.org", ) } } @@ -92,8 +90,8 @@ internal fun getNeverTranslateListItemsPreference(): List<NeverTranslateSiteList @LightDarkPreview private fun NeverTranslateSitePreferencePreview() { FirefoxTheme { - NeverTranslateSitePreference( - neverTranslateSiteListPreferences = getNeverTranslateListItemsPreference(), + NeverTranslateSitesPreference( + neverTranslateSitesListPreferences = getNeverTranslateSitesList(), ) {} } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreferenceFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreferenceFragment.kt new file mode 100644 index 0000000000..429c89f18f --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreferenceFragment.kt @@ -0,0 +1,60 @@ +/* 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.translations.preferences.nevertranslatesite + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.state.ext.observeAsComposableState +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * A fragment displaying never translate site items list. + */ +class NeverTranslateSitesPreferenceFragment : Fragment() { + + private val browserStore: BrowserStore by lazy { requireComponents.core.store } + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.never_translate_site_toolbar_title_preference)) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = ComposeView(requireContext()).apply { + setContent { + FirefoxTheme { + val neverTranslateSites = browserStore.observeAsComposableState { state -> + state.translationEngine.neverTranslateSites + }.value + + neverTranslateSites?.let { neverTranslateSitesList -> + NeverTranslateSitesPreference( + neverTranslateSitesListPreferences = neverTranslateSitesList, + onItemClick = { + findNavController().navigate( + NeverTranslateSitesPreferenceFragmentDirections + .actionNeverTranslateSitePreferenceToNeverTranslateSiteDialogPreference( + neverTranslateSiteUrl = it, + ), + ) + }, + ) + } + } + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index ebcf83e21f..5262cad451 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -35,6 +35,7 @@ import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.browser.tabstrip.isTabStripEnabled import org.mozilla.fenix.components.metrics.MozillaProductDetector import org.mozilla.fenix.components.settings.counterPreference import org.mozilla.fenix.components.settings.featureFlagPreference @@ -862,9 +863,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { return touchExplorationIsEnabled || switchServiceIsEnabled } - private val isTablet: Boolean - get() = appContext.resources.getBoolean(R.bool.tablet) - /** * Indicates if the user has enabled the tab strip feature. */ @@ -873,9 +871,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false, ) - val isTabletAndTabStripEnabled: Boolean - get() = isTablet && isTabStripEnabled - var lastKnownMode: BrowsingMode = BrowsingMode.Normal get() { val lastKnownModeWasPrivate = preferences.getBoolean( @@ -944,7 +939,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { ) val toolbarPosition: ToolbarPosition - get() = if (isTabletAndTabStripEnabled) { + get() = if (appContext.isTabStripEnabled()) { ToolbarPosition.TOP } else if (shouldUseBottomToolbar) { ToolbarPosition.BOTTOM @@ -1594,9 +1589,9 @@ class Settings(private val appContext: Context) : PreferencesHolder { /** * Indicates if the recent saved bookmarks functionality should be visible. */ - var showRecentBookmarksFeature by lazyFeatureFlagPreference( - appContext.getPreferenceKey(R.string.pref_key_recent_bookmarks), - default = { homescreenSections[HomeScreenSection.RECENTLY_SAVED] == true }, + var showBookmarksHomeFeature by lazyFeatureFlagPreference( + appContext.getPreferenceKey(R.string.pref_key_customization_bookmarks), + default = { homescreenSections[HomeScreenSection.BOOKMARKS] == true }, featureFlag = true, ) @@ -2005,6 +2000,12 @@ class Settings(private val appContext: Context) : PreferencesHolder { ) /** + * Indicates if the feature to close synced tabs is enabled. + */ + val enableCloseSyncedTabs: Boolean + get() = FxNimbus.features.remoteTabManagement.value().closeTabsEnabled + + /** * Returns the height of the bottom toolbar. * * The bottom toolbar can consist of a navigation bar, @@ -2033,7 +2034,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { val isToolbarAtTop = toolbarPosition == ToolbarPosition.TOP val toolbarHeight = appContext.resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) - return if (isToolbarAtTop && includeTabStrip && isTabletAndTabStripEnabled) { + return if (isToolbarAtTop && includeTabStrip) { toolbarHeight + appContext.resources.getDimensionPixelSize(R.dimen.tab_strip_height) } else if (isToolbarAtTop) { toolbarHeight |