commit c8c2e970b9c0f93146c018dc540ebbae29913fac parent 78b9021fd998c706175abb05a22a2b55e931d93f Author: John Oberhauser <j.git-global@obez.io> Date: Tue, 16 Dec 2025 16:04:17 +0000 Bug 2004410: Part 3 - Adding the Privacy Notice Banner to the homepage r=android-reviewers,gmalekpour,twhite Differential Revision: https://phabricator.services.mozilla.com/D275850 Diffstat:
10 files changed, 174 insertions(+), 19 deletions(-)
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 @@ -126,6 +126,7 @@ import org.mozilla.fenix.home.sessioncontrol.SessionControlControllerCallback import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.store.HomeToolbarStoreBuilder import org.mozilla.fenix.home.store.HomepageState +import org.mozilla.fenix.home.termsofuse.DefaultPrivacyNoticeBannerController import org.mozilla.fenix.home.toolbar.DefaultToolbarController import org.mozilla.fenix.home.toolbar.FenixHomeToolbar import org.mozilla.fenix.home.toolbar.HomeNavigationBar @@ -164,6 +165,11 @@ import org.mozilla.fenix.snackbar.FenixSnackbarDelegate import org.mozilla.fenix.snackbar.SnackbarBinding import org.mozilla.fenix.tabstray.Page import org.mozilla.fenix.tabstray.TabsTrayAccessPoint +import org.mozilla.fenix.termsofuse.store.DefaultPrivacyNoticeBannerRepository +import org.mozilla.fenix.termsofuse.store.PrivacyNoticeBannerAction +import org.mozilla.fenix.termsofuse.store.PrivacyNoticeBannerMiddleware +import org.mozilla.fenix.termsofuse.store.PrivacyNoticeBannerState +import org.mozilla.fenix.termsofuse.store.PrivacyNoticeBannerStore import org.mozilla.fenix.termsofuse.store.Surface import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.utils.allowUndo @@ -223,6 +229,14 @@ class HomeFragment : Fragment() { private val store: BrowserStore get() = requireComponents.core.store + private val privacyNoticeBannerRepository by lazy { + DefaultPrivacyNoticeBannerRepository( + settings = requireComponents.settings, + ) + } + + private lateinit var privacyNoticeBannerStore: PrivacyNoticeBannerStore + private var _sessionControlController: SessionControlController? = null private val sessionControlController: SessionControlController get() = _sessionControlController!! @@ -461,6 +475,17 @@ class HomeFragment : Fragment() { view = binding.root, ) + privacyNoticeBannerStore = PrivacyNoticeBannerStore( + initialState = PrivacyNoticeBannerState( + visible = privacyNoticeBannerRepository.shouldShowPrivacyNoticeBanner(), + ), + middleware = listOf( + PrivacyNoticeBannerMiddleware( + repository = privacyNoticeBannerRepository, + ), + ), + ) + _sessionControlController = DefaultSessionControlController( activityRef = WeakReference(activity), settings = components.settings, @@ -574,6 +599,9 @@ class HomeFragment : Fragment() { marsUseCases = components.useCases.marsUseCases, viewLifecycleScope = viewLifecycleOwner.lifecycleScope, ), + privacyNoticeBannerController = DefaultPrivacyNoticeBannerController( + privacyNoticeBannerStore = privacyNoticeBannerStore, + ), ) nullableToolbarView = buildToolbar(activity) @@ -1000,6 +1028,9 @@ class HomeFragment : Fragment() { } } val keyboardState by keyboardAsState() + val privacyNoticeBannerState = privacyNoticeBannerStore.flow().collectAsState( + initial = privacyNoticeBannerStore.state, + ) LaunchedEffect(isInPortrait, keyboardState) { updateLayoutParams<ViewGroup.MarginLayoutParams> { @@ -1015,6 +1046,7 @@ class HomeFragment : Fragment() { MiddleSearchHomepage( state = HomepageState.build( appState = appState.value, + privacyNoticeBannerState = privacyNoticeBannerState.value, settings = settings, browsingModeManager = browsingModeManager, ), @@ -1032,6 +1064,7 @@ class HomeFragment : Fragment() { Homepage( state = HomepageState.build( appState = appState.value, + privacyNoticeBannerState = privacyNoticeBannerState.value, settings = settings, browsingModeManager = browsingModeManager, ), @@ -1279,6 +1312,12 @@ class HomeFragment : Fragment() { requireComponents.useCases.sessionUseCases.updateLastAccess() } + override fun onStop() { + super.onStop() + + privacyNoticeBannerStore.dispatch(PrivacyNoticeBannerAction.OnFragmentStopped) + } + private fun subscribeToTabCollections(): Observer<List<TabCollection>> { return Observer<List<TabCollection>> { requireComponents.core.tabCollectionStorage.cachedTabCollections = it diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/fake/FakeHomepagePreview.kt @@ -50,6 +50,8 @@ import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor import org.mozilla.fenix.home.search.HomeSearchInteractor import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor import org.mozilla.fenix.home.store.NimbusMessageState +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerInteractor +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerInteractorNoOp import org.mozilla.fenix.home.topsites.interactor.TopSiteInteractor import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.wallpapers.WallpaperState @@ -74,7 +76,8 @@ internal object FakeHomepagePreview { RecentVisitsInteractor by recentVisitsInteractor, HomeSearchInteractor by homeSearchInteractor, CollectionInteractor by collectionInteractor, - PocketStoriesInteractor by storiesInteractor { + PocketStoriesInteractor by storiesInteractor, + PrivacyNoticeBannerInteractor by PrivacyNoticeBannerInteractorNoOp { override fun reportSessionMetrics(state: AppState) { /* no op */ } override fun onPasteAndGo(clipboardText: String) { /* no op */ } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/interactor/HomepageInteractor.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/interactor/HomepageInteractor.kt @@ -16,6 +16,7 @@ import org.mozilla.fenix.home.sessioncontrol.MessageCardInteractor import org.mozilla.fenix.home.sessioncontrol.SetupChecklistInteractor import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor import org.mozilla.fenix.home.sessioncontrol.WallpaperInteractor +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerInteractor import org.mozilla.fenix.home.toolbar.ToolbarInteractor import org.mozilla.fenix.home.topsites.interactor.TopSiteInteractor import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor @@ -30,6 +31,7 @@ interface HomepageInteractor : ToolbarInteractor, HomeSearchInteractor, MessageCardInteractor, + PrivacyNoticeBannerInteractor, RecentTabInteractor, RecentSyncedTabInteractor, BookmarksInteractor, 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 @@ -26,6 +26,7 @@ import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGrou import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController import org.mozilla.fenix.home.search.HomeSearchController +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerController import org.mozilla.fenix.home.toolbar.ToolbarController import org.mozilla.fenix.home.topsites.controller.TopSiteController import org.mozilla.fenix.search.toolbar.SearchSelectorController @@ -185,6 +186,7 @@ class SessionControlInteractor( private val toolbarController: ToolbarController, private val homeSearchController: HomeSearchController, private val topSiteController: TopSiteController, + private val privacyNoticeBannerController: PrivacyNoticeBannerController, ) : HomepageInteractor { override fun onCollectionAddTabTapped(collection: TabCollection) { @@ -400,4 +402,20 @@ class SessionControlInteractor( override fun onMenuItemTapped(item: SearchSelectorMenu.Item) { searchSelectorController.handleMenuItemTapped(item) } + + override fun onPrivacyNoticeBannerCloseClicked() { + privacyNoticeBannerController.onBannerCloseClicked() + } + + override fun onPrivacyNoticeBannerPrivacyNoticeClicked() { + privacyNoticeBannerController.onBannerPrivacyNoticeClicked() + } + + override fun onPrivacyNoticeBannerLearnMoreClicked() { + privacyNoticeBannerController.onBannerLearnMoreClicked() + } + + override fun onPrivacyNoticeBannerDisplayed() { + privacyNoticeBannerController.onBannerDisplayed() + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/HomepageState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/HomepageState.kt @@ -29,6 +29,7 @@ import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.topsites.TopSiteColors import org.mozilla.fenix.home.ui.getAttr import org.mozilla.fenix.search.SearchDialogFragment +import org.mozilla.fenix.termsofuse.store.PrivacyNoticeBannerState import org.mozilla.fenix.utils.Settings /** @@ -67,6 +68,7 @@ internal sealed class HomepageState { /** * State corresponding with the homepage in normal browsing mode. * + * @property shouldShowPrivacyNoticeBanner If the privacy notice banner should show. * @property nimbusMessage Optional message to display. * @property topSites List of [TopSite] to display. * @property recentTabs List of [RecentTab] to display. @@ -95,6 +97,7 @@ internal sealed class HomepageState { * @property bottomPadding Amount of padding to display at the bottom of the homepage. */ internal data class Normal( + val shouldShowPrivacyNoticeBanner: Boolean, val nimbusMessage: NimbusMessageState?, val topSites: List<TopSite>, val recentTabs: List<RecentTab>, @@ -148,12 +151,14 @@ internal sealed class HomepageState { * Builds a new [HomepageState] from the current [AppState] and [Settings]. * * @param appState State to build the [HomepageState] from. + * @param privacyNoticeBannerState State of the privacy notice banner. * @param browsingModeManager Manager holding current state of whether the browser is in private mode or not. * @param settings [Settings] corresponding to how the homepage should be displayed. */ @Composable internal fun build( appState: AppState, + privacyNoticeBannerState: PrivacyNoticeBannerState, browsingModeManager: BrowsingModeManager, settings: Settings, ): HomepageState { @@ -165,6 +170,7 @@ internal sealed class HomepageState { } else { buildNormalState( appState = appState, + privacyNoticeBannerState = privacyNoticeBannerState, browsingModeManager = browsingModeManager, settings = settings, ) @@ -201,17 +207,20 @@ internal sealed class HomepageState { * Builds a new [HomepageState.Normal] from the current [AppState] and [Settings]. * * @param appState State to build the [HomepageState.Normal] from. + * @param privacyNoticeBannerState State of the privacy notice banner. * @param browsingModeManager Manager holding current state of whether the browser is in private mode or not. * @param settings [Settings] corresponding to how the homepage should be displayed. */ @Composable private fun buildNormalState( appState: AppState, + privacyNoticeBannerState: PrivacyNoticeBannerState, browsingModeManager: BrowsingModeManager, settings: Settings, ) = with(appState) { Normal( - nimbusMessage = NimbusMessageState.build(appState), + shouldShowPrivacyNoticeBanner = privacyNoticeBannerState.visible, + nimbusMessage = NimbusMessageState.build(appState, privacyNoticeBannerState), topSites = topSites, recentTabs = recentTabs, syncedTab = when (recentSyncedTabState) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/NimbusMessageState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/NimbusMessageState.kt @@ -9,6 +9,7 @@ import mozilla.components.service.nimbus.messaging.Message import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.compose.MessageCardState import org.mozilla.fenix.messaging.FenixMessageSurfaceId +import org.mozilla.fenix.termsofuse.store.PrivacyNoticeBannerState /** * State representing the text and formatting for a nimbus message card displayed on the homepage. @@ -27,14 +28,22 @@ data class NimbusMessageState(val cardState: MessageCardState, val message: Mess * Builds a new [NimbusMessageState] from the current [AppState]. * * @param appState State to build the [NimbusMessageState] from. + * @param privacyNoticeBannerState State of the privacy notice banner. If the privacy + * notice banner is visible, we should not show the nimbus message banner. */ @Composable - internal fun build(appState: AppState) = with(appState) { - messaging.messageToShow[FenixMessageSurfaceId.HOMESCREEN]?.let { + internal fun build( + appState: AppState, + privacyNoticeBannerState: PrivacyNoticeBannerState, + ): NimbusMessageState? { + if (privacyNoticeBannerState.visible) { + return null + } + return appState.messaging.messageToShow[FenixMessageSurfaceId.HOMESCREEN]?.let { NimbusMessageState( cardState = MessageCardState.build( message = it, - wallpaperState = wallpaperState, + wallpaperState = appState.wallpaperState, ), message = it, ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/Homepage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/Homepage.kt @@ -68,6 +68,8 @@ import org.mozilla.fenix.home.setup.ui.SetupChecklist import org.mozilla.fenix.home.store.HeaderState import org.mozilla.fenix.home.store.HomepageState import org.mozilla.fenix.home.store.NimbusMessageState +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBanner +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerInteractor import org.mozilla.fenix.home.topsites.TopSiteColors import org.mozilla.fenix.home.topsites.TopSites import org.mozilla.fenix.home.topsites.interactor.TopSiteInteractor @@ -117,6 +119,17 @@ internal fun Homepage( .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, ) { + if (state is HomepageState.Normal) { + Spacer(modifier = Modifier.height(12.dp)) + + BannerCardSection( + shouldShowPrivacyNoticeBanner = state.shouldShowPrivacyNoticeBanner, + nimbusMessage = state.nimbusMessage, + privacyNoticeBannerInteractor = interactor, + messageCardInteractor = interactor, + ) + } + if (state.headerState.showHeader) { HomepageHeader( wordmarkTextColor = state.headerState.wordmarkTextColor, @@ -138,13 +151,6 @@ internal fun Homepage( } is HomepageState.Normal -> { - nimbusMessage?.let { - NimbusMessageCardSection( - nimbusMessage = nimbusMessage, - interactor = interactor, - ) - } - if (showTopSites) { TopSitesSection( topSites = topSites, @@ -251,16 +257,24 @@ private fun MaybeAddSetupChecklist( } @Composable -private fun NimbusMessageCardSection( - nimbusMessage: NimbusMessageState, - interactor: MessageCardInteractor, +private fun BannerCardSection( + shouldShowPrivacyNoticeBanner: Boolean, + nimbusMessage: NimbusMessageState?, + privacyNoticeBannerInteractor: PrivacyNoticeBannerInteractor, + messageCardInteractor: MessageCardInteractor, ) { - with(nimbusMessage) { + if (shouldShowPrivacyNoticeBanner) { + PrivacyNoticeBanner( + modifier = Modifier.padding(horizontal = 16.dp), + interactor = privacyNoticeBannerInteractor, + ) + } + nimbusMessage?.apply { MessageCard( messageCardState = cardState, modifier = Modifier.padding(horizontal = 16.dp), - onClick = { interactor.onMessageClicked(message) }, - onCloseButtonClick = { interactor.onMessageClosedClicked(message) }, + onClick = { messageCardInteractor.onMessageClicked(message) }, + onCloseButtonClick = { messageCardInteractor.onMessageClosedClicked(message) }, ) } } @@ -462,7 +476,8 @@ private fun HomepagePreview() { Surface { Homepage( state = HomepageState.Normal( - nimbusMessage = FakeHomepagePreview.nimbusMessageState(), + shouldShowPrivacyNoticeBanner = false, + nimbusMessage = null, topSites = FakeHomepagePreview.topSites(), recentTabs = FakeHomepagePreview.recentTabs(), syncedTab = FakeHomepagePreview.recentSyncedTab(), @@ -507,11 +522,63 @@ private fun HomepagePreview() { @Composable @PreviewLightDark +private fun HomepageBannerPreview() { + FirefoxTheme { + Surface { + Homepage( + state = HomepageState.Normal( + shouldShowPrivacyNoticeBanner = true, + nimbusMessage = null, + topSites = FakeHomepagePreview.topSites(), + recentTabs = FakeHomepagePreview.recentTabs(), + syncedTab = FakeHomepagePreview.recentSyncedTab(), + bookmarks = FakeHomepagePreview.bookmarks(), + recentlyVisited = FakeHomepagePreview.recentHistory(), + collectionsState = FakeHomepagePreview.collectionsPlaceholder(), + pocketState = FakeHomepagePreview.pocketState(), + showTopSites = true, + showRecentTabs = true, + showRecentSyncedTab = true, + showBookmarks = true, + showRecentlyVisited = true, + showPocketStories = true, + showCollections = true, + headerState = HeaderState( + showHeader = true, + wordmarkTextColor = null, + privateBrowsingButtonColor = colorResource( + getAttr( + iconsR.attr.mozac_ic_private_mode_circle_fill_icon_color, + ), + ), + ), + searchBarVisible = true, + searchBarEnabled = false, + firstFrameDrawn = true, + setupChecklistState = null, + topSiteColors = TopSiteColors.colors(), + cardBackgroundColor = WallpaperState.default.cardBackgroundColor, + buttonTextColor = WallpaperState.default.buttonTextColor, + buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor, + isSearchInProgress = false, + bottomPadding = 68, + ), + interactor = FakeHomepagePreview.homepageInteractor, + onTopSitesItemBound = {}, + modifier = Modifier.fillMaxSize(), + ) + } + } +} + +@Composable +@PreviewLightDark private fun HomepagePreviewCollections() { FirefoxTheme { Surface { Homepage( state = HomepageState.Normal( + shouldShowPrivacyNoticeBanner = false, nimbusMessage = null, topSites = FakeHomepagePreview.topSites(), recentTabs = FakeHomepagePreview.recentTabs(), @@ -562,6 +629,7 @@ private fun MinimalHomepagePreview() { Surface { Homepage( state = HomepageState.Normal( + shouldShowPrivacyNoticeBanner = false, nimbusMessage = null, topSites = FakeHomepagePreview.topSites(), recentTabs = FakeHomepagePreview.recentTabs(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/MiddleSearchHomepage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/MiddleSearchHomepage.kt @@ -163,6 +163,7 @@ private fun MiddleSearchHomepagePreview() { FirefoxTheme { MiddleSearchHomepage( HomepageState.Normal( + shouldShowPrivacyNoticeBanner = false, nimbusMessage = null, topSites = FakeHomepagePreview.topSites(), recentTabs = FakeHomepagePreview.recentTabs(), diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -27,6 +27,7 @@ import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController import org.mozilla.fenix.home.search.HomeSearchController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerController import org.mozilla.fenix.home.toolbar.ToolbarController import org.mozilla.fenix.home.topsites.controller.TopSiteController import org.mozilla.fenix.search.toolbar.SearchSelectorController @@ -43,6 +44,7 @@ class SessionControlInteractorTest { private val toolbarController: ToolbarController = mockk(relaxed = true) private val homeSearchController: HomeSearchController = mockk(relaxed = true) private val topSiteController: TopSiteController = mockk(relaxed = true) + private val privacyNoticeBannerController: PrivacyNoticeBannerController = mockk(relaxed = true) // Note: the recent visits tests are handled in [RecentVisitsInteractorTest] and [RecentVisitsControllerTest] private val recentVisitsController: RecentVisitsController = mockk(relaxed = true) @@ -63,6 +65,7 @@ class SessionControlInteractorTest { toolbarController, homeSearchController, topSiteController, + privacyNoticeBannerController, ) } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/recentvisits/interactor/RecentVisitsInteractorTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/recentvisits/interactor/RecentVisitsInteractorTest.kt @@ -22,6 +22,7 @@ import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController import org.mozilla.fenix.home.search.HomeSearchController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor +import org.mozilla.fenix.home.termsofuse.PrivacyNoticeBannerController import org.mozilla.fenix.home.toolbar.ToolbarController import org.mozilla.fenix.home.topsites.controller.TopSiteController import org.mozilla.fenix.search.toolbar.SearchSelectorController @@ -39,6 +40,7 @@ class RecentVisitsInteractorTest { private val toolbarController: ToolbarController = mockk(relaxed = true) private val homeSearchController: HomeSearchController = mockk(relaxed = true) private val topSiteController: TopSiteController = mockk(relaxed = true) + private val privacyNoticeBannerController: PrivacyNoticeBannerController = mockk(relaxed = true) private lateinit var interactor: SessionControlInteractor @@ -56,6 +58,7 @@ class RecentVisitsInteractorTest { toolbarController, homeSearchController, topSiteController, + privacyNoticeBannerController, ) }