summaryrefslogtreecommitdiffstats
path: root/mobile/android/fenix/app/src/main/java
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /mobile/android/fenix/app/src/main/java
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-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')
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt7
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt7
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt39
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnLongPressedListener.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OnBackLongPressedListener.kt)11
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt8
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt96
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt236
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt3
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TabPreview.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/tabstrip/TabStripFeatureFlag.kt31
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt14
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/NotificationManager.kt74
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt16
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt6
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt8
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt37
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/BrowserNavigationParams.kt18
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuAccessPoint.kt38
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/MenuDialogFragment.kt253
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ExtensionsSubmenu.kt101
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MainMenu.kt365
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuDialog.kt203
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuGroup.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt202
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/SaveSubmenu.kt163
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/ToolsSubmenu.kt184
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MenuHeader.kt16
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/MozillaAccountMenuButton.kt82
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/header/SubmenuHeader.kt94
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuDialogMiddleware.kt93
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/middleware/MenuNavigationMiddleware.kt134
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuAction.kt97
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuState.kt27
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/store/MenuStore.kt33
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt24
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt51
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt22
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt9
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/DismissibleItemBackground.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt16
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt196
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt6
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/tabs/TabTools.kt11
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/ui/DebugOverlay.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt11
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Activity.kt21
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/AppState.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Bitmap.kt18
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Context.kt6
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt55
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/View.kt109
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gecko/GeckoProvider.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt74
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt14
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeMenuView.kt17
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ToolbarView.kt3
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistHandler.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/blocklist/BlocklistMiddleware.kt12
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/BookmarksFeature.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/RecentBookmarksFeature.kt)15
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/controller/BookmarksController.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/controller/RecentBookmarksController.kt)38
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/interactor/BookmarksInteractor.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/interactor/RecentBookmarksInteractor.kt)22
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/Bookmarks.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarks.kt)66
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksHeaderViewHolder.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksHeaderViewHolder.kt)16
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksMenuItem.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksMenuItem.kt)10
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/bookmarks/view/BookmarksViewHolder.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recentbookmarks/view/RecentBookmarksViewHolder.kt)29
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/collections/CollectionViewHolder.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/intent/OpenRecentlyClosedIntentProcessor.kt30
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt25
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt20
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt12
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/topsites/PagerIndicator.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt9
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkItemMenu.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadItemMenu.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt7
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyContent.kt123
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyFooter.kt105
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyHeader.kt86
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicroSurveyScaffold.kt67
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheet.kt117
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyBottomSheetFragment.kt66
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/microsurvey/ui/MicrosurveyRequestPrompt.kt96
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingFragment.kt11
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt13
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerUtils.kt6
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt3
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt3
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/HomeSettingsFragment.kt6
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt3
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt10
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SyncDebugFragment.kt19
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountProblemFragment.kt8
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt3
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/biometric/BiometricUtils.kt110
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt8
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt8
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt121
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineMenu.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineShortcuts.kt19
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckInfoCard.kt50
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsController.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SyncedTabsInteractor.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt43
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt11
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt20
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt2
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ext/SyncedDeviceTabs.kt41
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt112
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsIntegration.kt9
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsList.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt)58
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListItem.kt18
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsListSupportedFeature.kt12
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt1
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/theme/FirefoxTheme.kt167
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/DownloadIndicator.kt12
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettings.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationSettingsFragment.kt38
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsBottomSheet.kt6
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt5
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBottomSheet.kt8
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogFragment.kt35
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogMiddleware.kt13
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationItemPreference.kt27
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreference.kt4
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationOptionsPreferenceFragment.kt14
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreference.kt31
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/automatic/AutomaticTranslationPreferenceFragment.kt44
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteDialogPreferenceFragment.kt19
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSiteListItemPreference.kt12
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreferenceFragment.kt48
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreference.kt (renamed from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitePreference.kt)26
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/preferences/nevertranslatesite/NeverTranslateSitesPreferenceFragment.kt60
-rw-r--r--mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt23
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