Skip to content

For 2085047 - Drop BrowsingModeManager in favor of AppState - #409

Open
Mugurell wants to merge 25 commits into
mozilla-firefox:autolandfrom
Mugurell:browsing_mode_to_appstate
Open

Mugurell wants to merge 25 commits into
mozilla-firefox:autolandfrom
Mugurell:browsing_mode_to_appstate

Conversation

@Mugurell

@Mugurell Mugurell commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Dropped BrowsingModeManager in favor of AppState for regular tabs.
And used content.private from BrowserState for custom tabs as they can be used at the same time with the regular tabs (in multiwindow) and could need a different browsing mode than AppState.mode value.

The approach taken in this PR, edgecases and other potential approaches are documented here.


Lando: link

⚠️ This pull request has 5 warnings.
🚫 This pull request has 3 blockers.

…their session first

Custom tab activities resolved their browsing mode from the PRIVATE_BROWSING_MODE
intent extra, else from the last known mode. AuthCustomTabActivity (Firefox
account sign-in) is started without that extra, with a private session when the
last known mode is private. Once Home switched to normal, restarting or
recreating the sign-in tab resolved it to normal: its toolbar and menu turned
normal and screenshots were allowed on a private session.

Custom tabs now resolve their mode from their session, else the intent extra,
else the last known mode, through resolveBrowsingMode().
Home has no bound session and is unchanged.

To test:
1. Switch Home to private and start a Firefox account sign-in from settings.
2. Go back to Home through recents and switch it to normal.
3. Return to the sign-in tab through recents, then toggle the system dark theme.
The sign-in tab stays private throughout: theme, menu, screenshots blocked.
…n a private tab

"Open in private tab" on a top site only updated AppState.mode. The persisted
last known mode, the secure window flag and the activity theme were only
updated a moment later, when the browser fragment selected the new private tab.
Switching through the activity's BrowsingModeManager applies all of them at
once, as every other private-open flow does.

To test:
1. In normal mode, long-press a top site on the homepage and pick "Open in private tab".
2. The app switches to the private theme and the site loads in one new private tab.
3. Repeat from the shortcuts screen; same result.
…en opening a recently closed tab

The dispatch set AppState.mode to the value it already had, so it changed nothing.
…ollow the custom tab in front

WebExtension popups and extension-created tabs ask the app whether browsing is
private, and Fenix currently answers with AppState.mode.
AppState.mode can't mean both "the main browser's mode" and "the mode of the
window in focus" as these can differ.
The main browser's UI would remain using AppState.mode.
Custom tab's UI would remain using that tab's content.private mode.
Features like web extensions - for which the framework outlives the current
custom tab - would need a new way of knowing what mode to use.

Previously using AppState.mode mostly worked for custom tabs only as a side
effect: the main browser and every custom tab wrote their mode to AppState.mode
when created or restarted, so it holds the mode of the last activity started,
not of the one in front. In split screen, focus moves without a restart.
A private custom tab can then open its extension popups in a normal session,
and the main browser's extensions can open private tabs. Custom tabs also need
to stop writing AppState.mode so they no longer change the main browser's mode.

AppState now records the custom tab the user focused last, cleared when the
main browser gains focus, and the check uses that custom tab's privacy if it
still exists, else AppState.mode. Focus moving to another app keeps the last
Fenix window, so extension background scripts follow it. The custom tab answer
no longer depends on AppState.mode. The main browser side is fixed once custom
tabs stop writing AppState.mode.

To test:
1. Enable custom tab extensions, install an extension with a browser action
that is not allowed in private browsing, and keep the main browser in
normal mode.
2. Open a private custom tab (with "Open links in a private tab" enabled) and
open the extension from the custom tab menu: no popup is shown.
3. With the private custom tab open, show the main browser next to it in split
screen, then tap the custom tab and open the extension from its menu: no
popup is shown. Before this change, the popup opened in a normal session.
4. Allow the extension in private browsing and repeat: the popup opens in a
private session.
…heme

The Compose theme read AppState.mode, which holds the mode of the activity
created or restarted last. A custom tab next to Home in split screen could
then get Home's theme, or Home the custom tab's. Private windows now carry
an isPrivateTheme theme attribute and the Compose theme reads it from its
context, so Compose always matches the window's View theme.
…ivacy

The private tabs lock in a custom tab checked AppState.mode, which a custom
tab shown next to a regular tab can share with that regular tab. The lock
now checks the privacy of the tab shown in the window, and the unlock screen
recognizes a custom tab from its navigation origin.

Testing, with the private tabs lock enabled:
- Open a private regular tab and send the app to the background. A private
custom tab then shows the unlock screen without "Leave private tabs".
- In split screen, show a private regular tab next to a normal custom tab.
In the regular tab window, switch to normal mode from the homepage and
back to private. Only the regular tab window shows the unlock screen.
- A locked private regular tab still shows the unlock screen with "Leave
private tabs".
…privacy of the tab shown

The contextual menu (a dialog showing on top of any destination) was
hidden while private browsing is locked if AppState.mode was private
but this did not account for when a regular tab is shown in split view
with a custom tab and each would have a different privacy mode.
Now we'll check the privacy of the tab currently shown and for which
to decide if to show the contextual menu.

To test ensure that locking private tabs is enabled and then:
Scenario 1:
- in a private tab long click on a link to show the contextual menu
- put the app to background
- resume it and check that the lockscreen shows but no contextual
menu.
Scenario 2:
- open in splitscreen a private custom tab and a regular tab in
normal browsing
- in the custom tab long click on a link to show the contextual menu
- in the regular tab switch between private and regular tabs
- check that private tabs get locked: the custom tab shows the lockscreen
and its contextual menu is hidden.
…privacy of the tab shown

Two site permission prompt behaviours depended on AppState.mode, which a
custom tab shown next to a regular tab can share with that regular tab:
- In private tabs the "Don't ask again" checkbox is not shown.
- While private browsing is locked, an open prompt in a private tab is
dismissed when the browser stops, so it doesn't show over the unlock
screen.
Both now check the privacy of the tab currently in focus.

Testing, on a page asking for a site permission such as location:
- In a private custom tab, the prompt has no
"Remember decision for this site" checkbox.
- In a normal custom tab, the prompt has the checkbox.
- In regular tabs, the checkbox is shown in normal mode and hidden in
private mode, as before.
- With the private tabs lock enabled and a private regular tab open, show
the prompt in a private custom tab, send the app to the background and
return. Only the unlock screen shows, without the prompt.
…ests based on the privacy of the tab shown

When private browsing can be locked, a black overlay hides a private tab's
content while an Android permission dialog is shown. Whether the tab is
private was read from AppState.mode, which a custom tab shown next to a
regular tab can share with that regular tab.
Now this will be checked based on the tab currently in focus.

Testing, with "Lock private tabs" enabled and Firefox's Android camera or
location permission revoked, on a page requesting that permission:
- In a private custom tab, with a normal regular tab open, allowing the
site prompt shows the Android dialog over a black screen.
- In a normal custom tab, with a private regular tab open, the page stays
visible behind the Android dialog.
- In regular tabs, the black screen shows in private mode and not in
normal mode, as before.
…acy of the tab shown

"View certificate" in the site security panel opened the website certificate
in a private or normal tab based on AppState.mode, which a custom tab shown
next to a regular tab can share with that regular tab. It now uses the privacy
of the tab the panel was opened for, and falls back to AppState.mode only as
a sane default though this should never be reached in real life.

Testing, on an HTTPS page, open the site security panel from the toolbar,
then "Connection is secure" and "View certificate":
- In a private custom tab, with a normal regular tab open, the certificate
opens in a private tab.
- In a normal custom tab, with a private regular tab open, it opens in a
normal tab.
- In regular tabs, it opens in a tab of the current mode, as before.
…ivacy of the tab shown

A custom tab's site security panel can open the tracking protection settings
inside the custom tab window. Their "Learn more" links opened a new tab with
the default privacy of AppState.mode, which a custom tab shown next to a
regular tab can share with that regular tab. These screens don't know which
custom tab opened them, so they now use the privacy of the custom tab
currently in focus and AppState.mode otherwise.

Testing, from a custom tab, open the site security panel from the toolbar,
then "Privacy and security settings":
- In a private custom tab, with a normal regular tab open, "Learn more"
  under Enhanced Tracking Protection opens a private tab. The same holds
  for "Learn more" in Exceptions.
- In a normal custom tab, with a private regular tab open, they open a
  normal tab.
- From Settings in regular tabs, they open a tab of the current mode, as
  before.
…vacy of the tab shown

A custom tab's login prompt can open the saved logins screens inside the
custom tab window through "Manage passwords". Opening a login's site or the
sync help from there creates a tab with the default privacy of
AppState.mode, which a custom tab shown next to a regular tab can share with
that regular tab.
These screens don't know which custom tab opened them, so they now use the
privacy of the custom tab currently in focus and AppState.mode otherwise.

Testing, with a saved login for a site:
- In a private custom tab, with a normal regular tab open, focus a login
field, tap "Manage passwords" in the login prompt, open the login and tap
"Go to site". The site opens in a private tab.
- In a normal custom tab, with a private regular tab open, the same steps
open it in a normal tab.
- From Settings > Passwords in regular tabs, "Go to site" opens a tab of
the current mode, as before.
…rivacy of the tab shown

An extension can open its options page while a custom tab is in front, and
the options page then shows in the custom tab window. Going back from it
leads to the add-on's details screen, from where reporting the add-on,
opening its settings in a tab, and its website, permission and warning links
opened tabs with the privacy of AppState.mode, which a custom tab shown next
to a regular tab can share with that regular tab. These screens don't know
which custom tab opened them, so they now use the privacy of the custom
tab currently in focus and AppState.mode otherwise.

Testing, with an extension whose popup calls browser.runtime.openOptionsPage()
and "Enable Custom Tab Extension List" on in Secret Settings:
- In a private custom tab, with a normal regular tab open, open the
extension from the menu, open its options page, press Back and tap
"Report". The report page opens in a private tab. The same holds for the
details links and the permissions "Learn more".
- In a normal custom tab, with a private regular tab open, they open in a
normal tab.
- From Settings > Extensions in regular tabs, they open in a tab of the
current mode, as before.
…on the privacy of the tab shown

A custom tab's in-app share screen can open "Connect another device"
inside the custom tab window. Its "Learn About Sending Tabs…" opened the
help in a tab with the default privacy of AppState.mode, which a custom
tab shown next to a regular tab can share with that regular tab.
This screen doesn't know which custom tab opened it, so it now uses the
privacy of the custom tab currently in focus and AppState.mode otherwise.

Testing, signed in to a Firefox account with no other devices that can
receive tabs, and with "Enable Native Android Share Sheet" turned off in
Secret Settings:
- In a private custom tab, with a normal regular tab open, tap menu >
Share > "Connect another device" > "Learn About Sending Tabs…". The help
opens in a private tab.
- In a normal custom tab, with a private regular tab open, the same steps
open it in a normal tab.
- From Share in a regular tab, the same button opens a tab of the current
mode, as before.
…he privacy of the tab shown

A custom tab's translation dialog can open the translation settings
inside the custom tab window. "Learn more" on the "Download languages"
screen opened the help in a tab with the default privacy of
AppState.mode, which a custom tab shown next to a regular tab can share
with that regular tab.
This screen doesn't know which custom tab opened it, so it now uses the
privacy of the custom tab currently in focus and AppState.mode otherwise.

Testing, on a page in another language:
- In a private custom tab, with a normal regular tab open, tap menu >
"Translate page", open "Translation settings" from the dialog's options,
then "Download languages" > "Learn more". The help opens in a private
tab.
- In a normal custom tab, with a private regular tab open, the same steps
open it in a normal tab.
- From Settings > Translations in regular tabs, "Learn more" opens a tab
of the current mode, as before.
…s the last known mode

Every activity's browsing mode manager saved its mode as the last known
mode, so opening a normal custom tab while regular tabs were private made
the next launch from the launcher start in normal mode.
The mode is now saved by the regular tab window only. Custom tab
activities, including the account sign-in custom tab, still resolve their
mode from their session but don't save it.

Testing, with "Open links in a private tab" off:
- Open Firefox from the launcher and switch to private mode. Send Firefox
  to the background, open a link in a normal custom tab from another app
  and close it. Without returning to Firefox, force stop it and open it
  from the launcher: it starts on the private homepage. Before this change
  it started in normal mode.

Unchanged, to check:
- Open Firefox from the launcher, switch to normal mode, force stop it
  and open it from the launcher: it starts in normal mode. Repeat ending in
  private mode: it starts on the private homepage.
…s to the app state

Every custom tab activity published its mode as the global AppState.mode
when created or restarted, overwriting the mode of the regular tab window.
Everything shown in a custom tab now follows the custom tab's own privacy,
so custom tabs now only apply their secure window flag and leave
AppState.mode to the regular tab window.
Opening a normal custom tab no longer counts as leaving private mode, so
it no longer locks private tabs on its own.

Testing:
- With the private tabs lock on and private tabs open in the regular tab
  window, open a normal custom tab from another app in the other half of
  split screen. The private tabs stay unlocked. Sending the app to the
  background still locks them.
- With "Open links in a private tab" on, open a link from another app and
  show the private custom tab in split screen next to the regular tab
  window in normal mode. The regular tab window stays normal.

Unchanged, to check:
- In the split screen above, with "Allow screenshots in private browsing"
  off, the custom tab keeps its private theme, menu and blocked
  screenshots. Switching the regular tab window between normal and private
  doesn't change the custom tab.
- With the private tabs lock on and a private tab open in regular tabs,
  send a private custom tab to the background and back: it shows the
  unlock screen without "Leave private tabs".
- In a normal custom tab with the calling app's colors, exit fullscreen
  video: the toolbar and system bars keep those colors.
- An installed web app opened from its launcher shortcut and a sandboxed
  custom tab ("Learn more" in the VPN prompt) open and show their pages.
…singModeChanged

The action is about to be dispatched directly instead of only by the
browsing mode manager, so its name no longer refers to the manager.
Only the regular tabs window dispatches it.

No behaviour change.
…p store

The regular tabs window saved its browsing mode as the last known mode
when its mode changed. A new AppStore middleware now saves it for every
BrowsingModeChanged action, before the new mode reaches the state.

No behaviour change.
…indow from the app store

The regular tabs window applied its secure window flag and theme when its
browsing mode manager changed mode. A new BrowsingModeBinding now observes
AppState.mode and applies them whenever the mode changes, so a mode change
only has to be dispatched. The binding runs on the immediate main
dispatcher, so the secure flag is set as soon as the mode is dispatched,
before the theme switch. The secure flag is also re-applied when the
window is created or restarted and after the mode is resolved for a new
intent, as before.

No intended behaviour.
… window into the app store

The regular tabs window kept its own browsing mode in its browsing mode
manager and published every change to AppState. Its manager now reads
AppState.mode and dispatches BrowsingModeChanged whenever the mode is set
or resolved from an intent: when the window is created (before its theme
is set up), restarted or gets a new intent, and when a shortcut or widget
opens the homepage or search. Custom tabs keep their own manager.

This approach is only temporary code since there are more than 30 files
working with a BrowsingModeManager and we'd want to migrate them in
separate commits to more easily understand the changes.
…he app store

The homepage, its navigation bar, toolbar, edge to edge background, tab
strip colors, tabs cleanup, private browsing button and top sites, and
the home intent processors read or changed the browsing mode through the
activity's browsing mode manager. They now read AppState.mode and change
it by dispatching BrowsingModeChanged, which is what that manager already
did for the regular tabs window. The intent processors resolve the mode
from the intent themselves.

No intended behaviour change for the regular tabs window. The homepage
is meant to be shown only there; bug 2084745 tracks the tabs tray being
reachable from a custom tab, from where the homepage's private browsing
button now switches the regular tabs window's mode.

To check:
- Switch between normal and private mode with the homepage's private
browsing button: the homepage, toolbar and navigation bar follow the
mode.
- Long-press a top site and choose "Open in private tab": the site opens
in a private tab and Firefox switches to private mode.
- With homepage as a new tab on, in normal mode, long-press the Firefox
launcher icon and choose "New private tab": the private homepage opens.
With it off, a private search opens with the address bar focused.
- With homepage as a new tab on, tap "Private search" on the "Search bar
with private and voice search" widget: a private homepage tab opens with
the address bar focused.
- In normal mode, open the private browsing deep link. For a debug build:
adb shell am start -a android.intent.action.VIEW -d "fenix-dev://enable_private_browsing"
Firefox switches to private mode.
- In Settings > Homepage > Wallpapers, choose the edge to edge (gradient)
wallpaper. The homepage shows it in normal mode and the private
background in private mode.
…te.mode for toolbars and search

The browser and homepage toolbars, their store builders, the search
middlewares and suggestion providers, the awesome bar, and the browser
screen read or changed the browsing mode through the activity's browsing
mode manager. They now read AppState.mode and change it by dispatching
BrowsingModeChanged, which is what that manager already did for the
regular tabs window. When leaving full screen, a custom tab now checks
its own privacy instead of its activity's manager, which held the same
value.

No intended behaviour change. To check:
- Long-press the toolbar's tab counter and choose "New private tab", then
"New tab": Firefox switches to private mode and back, each time opening
a tab or the search of that mode.
- With different default search engines for normal and private browsing
and search suggestions in private browsing allowed, type a query in
private mode: suggestions and results use the private engine.
- In History, search and open a result in each browsing mode: it opens
in a tab of that mode. Searching bookmarks still works.
- Select a private tab from the tabs tray while in normal mode: Firefox
switches to private mode.
…te.mode for the remaining screens

The tabs tray, history, history groups, the private tabs unlock screen,
the private browsing settings, the homepage tab binding and the VPN
unavailable dialog read or changed the browsing mode through the
activity's browsing mode manager. They now read AppState.mode and change
it by dispatching BrowsingModeChanged, which is what that manager already
did for the regular tabs window. The VPN unavailable dialog can be shown
in a custom tab, so it opens the tabs tray page of the focused tab's
privacy, the same value its activity's manager held.

No intended behaviour change for the regular tabs window. These screens
are meant to be shown only there; bug 2084745 tracks the tabs tray being
reachable from a custom tab, from where its actions and the history
screens now change or follow the regular tabs window's mode.

To check:
- In the tabs tray, select a private tab while in normal mode, and open a
new tab from the private tabs page: Firefox switches to private mode.
- In History, select items and choose "Open in private tab": they open in
private tabs and Firefox switches to private mode. The same from a
history group.
- With the private tabs lock on and a private tab shown, send Firefox to
the background and return, cancel authentication, then tap "Leave
private tabs": Firefox switches to normal mode.
- With the private tabs lock off, in private mode, toggle "Allow
screenshots in private browsing": it applies right away.
- With homepage as a new tab on, close all tabs of the current mode from
the tabs tray and leave it: a homepage tab of that mode is added.
Nothing outside the activities reads a browsing mode manager any more.
The regular tabs window resolves its mode from the intent or the last
known mode when it is created, restarted or gets a new intent, and
dispatches it as AppState.mode. A custom tab resolves its mode session
first, then from the intent, then from the last known mode, when it is
created or restarted, without publishing it, and keeps it only for its
own theme, secure window flag and accessibility announcement.
BrowsingModeManager and its implementations are removed.
BrowsingMode was moved to its own file.

No intended behaviour change.

To check, with "Allow screenshots in private browsing" off:
- With "Open links in a private tab" on, open a link from another app
and show the private custom tab in split screen next to the regular tabs
window in normal mode: the custom tab has the private theme and blocks
screenshots, the regular tabs window stays normal and allows them.
- Switch the regular tabs window between normal and private mode: its
theme and screenshot blocking follow, the custom tab doesn't change.
- With the private custom tab next to the normal regular tabs window and
Settings > Customize set to follow the device theme, switch the system
between light and dark theme: both windows are recreated and the custom
tab stays private.
- Switch to private mode, force stop Firefox and open it from the
launcher: it starts on the private homepage.
- Sign in to a Mozilla account from Settings: the sign-in custom tab
opens and completes as before.
@github-actions
github-actions Bot changed the base branch from main to autoland October 22, 2026 16:37
@github-actions

Copy link
Copy Markdown
Contributor

Warning

The target branch has been automatically set to autoland.

Please Edit this PR if this wasn't your intention.

@lando-web
lando-web Bot requested a review from a team October 22, 2026 16:37
@ncalexan
ncalexan self-requested a review October 22, 2026 20:12
@ncalexan

Copy link
Copy Markdown
Contributor

The plan is for Pacific R-n-B to review this next Wednesday, October 28. Marking myself as a reviewer here as well. We'd prefer to review in Phab, when that's healthy again.

@MatthewTighe MatthewTighe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some early feedback, will be coming back to this

searchTermOrURL = url.toString(),
newTab = true,
private =
requireComponents.appStore.state.isForegroundTabOrCustomTabPrivate(requireComponents.core.store.state),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a little surprising to me to need to extract all this state just to send it to this Usecase. Both of these Stores are dependencies in the Usecase object hierarchy. Why can't we depend on them to handle business logic like this correctly, and instead need to wire this logic of state comprehension in through all these various points?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!
The AppStore is indeed already available in FenixBrowserUseCases but the BrowserStore from which to read the current tab is not. Could be if we'd want it..
AppState.isForegroundTabOrCustomTabPrivate(BrowserState) is the sole API proposed to support this refactoring so we'd need both the states to use it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants