tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit b9f7e1c09e030fbfd28360a0e674fd2d3299e488
parent 07e4f06bf778367c47f61c5e03fef2a57f19e98c
Author: Mugurell <Mugurell@users.noreply.github.com>
Date:   Tue, 28 Oct 2025 18:43:53 +0000

Bug 1994005 - Handle the commit and back buttons for the edit toolbar r=android-reviewers,Roger

When the user presses "Go" in the toolbar we will navigate to the inline
autocomplete suggestion if available or the current query.
Handling the back press in the OS navigation bar is only possible on API33+
which is already supported in the application through Compose BackHandlers.
On lower Android versions the framework does not inform that about the gesture
something that is requested at google in
https://issuetracker.google.com/issues/241705563.

Differential Revision: https://phabricator.services.mozilla.com/D268897

Diffstat:
Mmobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt | 4----
Mmobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.kt | 2--
Mmobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/store/BrowserToolbarAction.kt | 6------
Mmobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/store/BrowserToolbarStore.kt | 2--
Mmobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/InlineAutocompleteTextField.kt | 40+++++++++++++++++++++++++---------------
Mmobile/android/android-components/components/compose/browser-toolbar/src/test/java/mozilla/components/compose/browser/toolbar/ui/InlineAutocompleteTextFieldTest.kt | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/BrowserToolbarSearchMiddleware.kt | 13-------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/search/BrowserToolbarSearchMiddlewareTest.kt | 50--------------------------------------------------
8 files changed, 108 insertions(+), 92 deletions(-)

diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserEditToolbar.kt @@ -55,8 +55,6 @@ private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(90.dp) * the edit toolbar. * @param onUrlEdit Will be called when the URL value changes. An updated text value comes as a * parameter of the callback. - * @param onUrlEditAborted Will be called when the user has aborted editing the URL. - * This callback works only up until Android API 33. * @param onUrlCommitted Will be called when the user has finished editing and wants to initiate * loading the entered URL. The committed text value comes as a parameter of the callback. * @param onInteraction Callback for handling [BrowserToolbarEvent]s on user interactions. @@ -72,7 +70,6 @@ fun BrowserEditToolbar( editActionsStart: List<Action> = emptyList(), editActionsEnd: List<Action> = emptyList(), onUrlEdit: (BrowserToolbarQuery) -> Unit = {}, - onUrlEditAborted: () -> Unit = {}, onUrlCommitted: (String) -> Unit = {}, onInteraction: (BrowserToolbarEvent) -> Unit, ) { @@ -105,7 +102,6 @@ fun BrowserEditToolbar( modifier = Modifier.weight(1f), onUrlEdit = onUrlEdit, onUrlCommitted = onUrlCommitted, - onUrlEditAborted = onUrlEditAborted, ) ActionContainer( diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/BrowserToolbar.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import mozilla.components.compose.base.theme.AcornTheme import mozilla.components.compose.browser.toolbar.concept.PageOrigin -import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchAborted import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchQueryUpdated import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction.CommitUrl import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent @@ -49,7 +48,6 @@ fun BrowserToolbar( editActionsEnd = uiState.editState.editActionsEnd, onUrlCommitted = { text -> store.dispatch(CommitUrl(text)) }, onUrlEdit = { store.dispatch(SearchQueryUpdated(it)) }, - onUrlEditAborted = { store.dispatch(SearchAborted) }, onInteraction = { store.dispatch(it) }, ) } else { diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/store/BrowserToolbarAction.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/store/BrowserToolbarAction.kt @@ -136,12 +136,6 @@ sealed class BrowserEditToolbarAction : BrowserToolbarAction { data class PrivateModeUpdated(val inPrivateMode: Boolean) : BrowserEditToolbarAction() /** - * Indicates that the user has aborted editing the URL/text. - * This callback works only up until Android API 33. - */ - data object SearchAborted : BrowserEditToolbarAction() - - /** * Indicates that a new autocomplete suggestion is available or that the previous one is not valid anymore. * * @property autocompletedSuggestion The new autocomplete suggestion. `null` if none is available. diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/store/BrowserToolbarStore.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/store/BrowserToolbarStore.kt @@ -11,7 +11,6 @@ import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAct import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.PageActionsStartUpdated import mozilla.components.compose.browser.toolbar.store.BrowserDisplayToolbarAction.PageOriginUpdated import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.AutocompleteSuggestionUpdated -import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchAborted import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent import mozilla.components.compose.browser.toolbar.ui.BrowserToolbarQuery import mozilla.components.lib.state.Middleware @@ -143,7 +142,6 @@ private fun reduce(state: BrowserToolbarState, action: BrowserToolbarAction): Br is EnvironmentRehydrated, is EnvironmentCleared, - is SearchAborted, is BrowserToolbarEvent, -> { // no-op diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/InlineAutocompleteTextField.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/main/java/mozilla/components/compose/browser/toolbar/ui/InlineAutocompleteTextField.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -74,7 +75,6 @@ private const val TEXT_HIGHLIGHT_COLOR = "#5C592ACB" * @param modifier The [Modifier] to be applied to this text field. * @param onUrlEdit Callback invoked when the user types or deletes text, providing [BrowserToolbarQuery] * with information about the previous and the new query. - * @param onUrlEditAborted A callback for when an edit is aborted. * @param onUrlCommitted A callback for when the user commits the text via an IME action like "Go". */ @OptIn(ExperimentalComposeUiApi::class) // for InterceptPlatformTextInput @@ -88,7 +88,6 @@ internal fun InlineAutocompleteTextField( usePrivateModeQueries: Boolean, modifier: Modifier = Modifier, onUrlEdit: (BrowserToolbarQuery) -> Unit = {}, - onUrlEditAborted: () -> Unit = {}, onUrlCommitted: (String) -> Unit = {}, ) { var textFieldValue by remember { mutableStateOf(TextFieldValue("")) } @@ -156,8 +155,8 @@ internal fun InlineAutocompleteTextField( onValueChange = { newValue -> // Remove suggestion if cursor placement changed val onlySelectionChanged = textFieldValue.text == newValue.text && - textFieldValue.composition == newValue.composition && - textFieldValue.annotatedString == newValue.annotatedString + textFieldValue.composition == newValue.composition && + textFieldValue.annotatedString == newValue.annotatedString if (onlySelectionChanged) { currentSuggestion = null textFieldValue = newValue @@ -169,8 +168,8 @@ internal fun InlineAutocompleteTextField( val originalText = textFieldValue.text val newText = newValue.text val isBackspaceHidingSuggestion = originalText.length == newText.length + 1 && - originalText.startsWith(newText) && - currentSuggestion?.text?.startsWith(originalText) == true + originalText.startsWith(newText) && + currentSuggestion?.text?.startsWith(originalText) == true if (isBackspaceHidingSuggestion) { currentSuggestion = null } else { @@ -206,6 +205,17 @@ internal fun InlineAutocompleteTextField( imeAction = ImeAction.Go, autoCorrectEnabled = !usePrivateModeQueries, ), + keyboardActions = KeyboardActions( + onGo = { + keyboardController?.hide() + onUrlCommitted( + when (currentSuggestion?.text?.isNotEmpty()) { + true -> currentSuggestion?.text.orEmpty() + else -> textFieldValue.text + }, + ) + }, + ), singleLine = true, visualTransformation = suggestionVisualTransformation, onTextLayout = { layoutResult -> @@ -251,15 +261,15 @@ internal fun InlineAutocompleteTextField( LayoutDirection.Rtl -> Alignment.CenterEnd }, ) { - if (textFieldValue.text.isEmpty()) { - Text( - text = hint, - style = TextStyle( - fontSize = TEXT_SIZE.sp, - color = AcornTheme.colors.textSecondary, - ), - ) - } + if (textFieldValue.text.isEmpty()) { + Text( + text = hint, + style = TextStyle( + fontSize = TEXT_SIZE.sp, + color = AcornTheme.colors.textSecondary, + ), + ) + } innerTextField() } }, diff --git a/mobile/android/android-components/components/compose/browser-toolbar/src/test/java/mozilla/components/compose/browser/toolbar/ui/InlineAutocompleteTextFieldTest.kt b/mobile/android/android-components/components/compose/browser-toolbar/src/test/java/mozilla/components/compose/browser/toolbar/ui/InlineAutocompleteTextFieldTest.kt @@ -5,6 +5,9 @@ package mozilla.components.compose.browser.toolbar.ui import android.view.inputmethod.EditorInfo +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertTextEquals @@ -12,6 +15,7 @@ import androidx.compose.ui.test.click import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextReplacement import androidx.compose.ui.test.performTouchInput @@ -185,4 +189,83 @@ class InlineAutocompleteTextFieldTest { assertTrue(editorInfo.imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING != 0) } + + @Test + fun `GIVEN a query but no suggestion WHEN the IME action button is tapped THEN hide the IME and inform callbacks of the query accepted`() { + val userQuery = "mozilla" + val keyboardController: SoftwareKeyboardController = mock() + val urlCommitedCallback: (String) -> Unit = mock() + composeTestRule.setContent { + CompositionLocalProvider(LocalSoftwareKeyboardController provides keyboardController) { + InlineAutocompleteTextField( + query = userQuery, + hint = "test", + suggestion = null, // No suggestion + showQueryAsPreselected = false, + usePrivateModeQueries = false, + onUrlCommitted = urlCommitedCallback, + ) + } + } + + composeTestRule.onNodeWithTag(ADDRESSBAR_SEARCH_BOX).performImeAction() + + verify(keyboardController).hide() + verify(urlCommitedCallback).invoke(userQuery) + } + + @Test + fun `GIVEN a query and suggestion WHEN the IME action button is tapped THEN hide the IME and inform callbacks of the suggestion accepted`() { + val userQuery = "wiki" + val suggestion = AutocompleteResult( + input = "wiki", + text = "wikipedia.org", + url = "https://wikipedia.org", + source = "test", + totalItems = 1, + ) + val keyboardController: SoftwareKeyboardController = mock() + val urlCommitedCallback: (String) -> Unit = mock() + composeTestRule.setContent { + CompositionLocalProvider(LocalSoftwareKeyboardController provides keyboardController) { + InlineAutocompleteTextField( + query = userQuery, + hint = "test", + suggestion = suggestion, + showQueryAsPreselected = false, + usePrivateModeQueries = false, + onUrlCommitted = urlCommitedCallback, + ) + } + } + + composeTestRule.onNodeWithTag(ADDRESSBAR_SEARCH_BOX).performImeAction() + + verify(keyboardController).hide() + verify(urlCommitedCallback).invoke(suggestion.text) + } + + @Test + fun `GIVEN no query and no suggestion WHEN the IME action button is tapped THEN hide keyboard and inform callbacks`() { + val userQuery = "" + val keyboardController: SoftwareKeyboardController = mock() + val urlCommitedCallback: (String) -> Unit = mock() + composeTestRule.setContent { + CompositionLocalProvider(LocalSoftwareKeyboardController provides keyboardController) { + InlineAutocompleteTextField( + query = userQuery, + hint = "test", + suggestion = null, // No suggestion + showQueryAsPreselected = false, + usePrivateModeQueries = false, + onUrlCommitted = urlCommitedCallback, + ) + } + } + + composeTestRule.onNodeWithTag(ADDRESSBAR_SEARCH_BOX).performImeAction() + + verify(keyboardController).hide() + verify(urlCommitedCallback).invoke(userQuery) + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/BrowserToolbarSearchMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/search/BrowserToolbarSearchMiddleware.kt @@ -6,7 +6,6 @@ package org.mozilla.fenix.search import android.content.Intent import android.content.res.Resources -import android.os.Build import android.speech.RecognizerIntent import androidx.annotation.VisibleForTesting import androidx.core.graphics.drawable.toDrawable @@ -38,7 +37,6 @@ import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.HintUpdated -import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchAborted import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchActionsEndUpdated import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchActionsStartUpdated import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchQueryUpdated @@ -206,17 +204,6 @@ class BrowserToolbarSearchMiddleware( observeVoiceInputJob?.cancel() } - is SearchAborted -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - val sourceTabId = appStore.state.searchState.sourceTabId - appStore.dispatch(SearchEnded) - browserStore.dispatch(EngagementFinished(abandoned = true)) - if (sourceTabId != null) { - environment?.navController?.navigate(R.id.browserFragment) - } - } - } - is SearchSelectorClicked -> { Toolbar.buttonTapped.record( Toolbar.ButtonTappedExtra( diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/search/BrowserToolbarSearchMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/search/BrowserToolbarSearchMiddlewareTest.kt @@ -34,7 +34,6 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton import mozilla.components.compose.browser.toolbar.concept.Action.ActionButtonRes import mozilla.components.compose.browser.toolbar.concept.Action.SearchSelectorAction -import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchAborted import mozilla.components.compose.browser.toolbar.store.BrowserEditToolbarAction.SearchQueryUpdated import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction.CommitUrl import mozilla.components.compose.browser.toolbar.store.BrowserToolbarAction.EnterEditMode @@ -107,7 +106,6 @@ import org.mozilla.fenix.telemetry.SOURCE_ADDRESS_BAR import org.mozilla.fenix.utils.Settings import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf -import org.robolectric.annotation.Config import mozilla.components.browser.toolbar.R as toolbarR import mozilla.components.feature.qr.R as qrR import mozilla.components.ui.icons.R as iconsR @@ -731,54 +729,6 @@ class BrowserToolbarSearchMiddlewareTest { } @Test - @Config(sdk = [33]) - fun `GIVEN on Android 33+ WHEN the search is aborted THEN don't exit search mode`() { - val appStore: AppStore = mockk(relaxed = true) { - every { state.searchState } returns AppSearchState.EMPTY - } - val browserStore: BrowserStore = mockk(relaxed = true) - val (_, store) = buildMiddlewareAndAddToStore(appStore, browserStore) - - store.dispatch(SearchAborted) - - verify(exactly = 0) { appStore.dispatch(SearchEnded) } - verify(exactly = 0) { browserStore.dispatch(EngagementFinished(abandoned = true)) } - } - - @Test - @Config(sdk = [32]) - fun `GIVEN on Android 32- WHEN the search is aborted THEN sync this in application and browser state`() { - val appStore: AppStore = mockk(relaxed = true) { - every { state.searchState } returns AppSearchState.EMPTY - } - val browserStore: BrowserStore = mockk(relaxed = true) - val (_, store) = buildMiddlewareAndAddToStore(appStore, browserStore) - - store.dispatch(SearchAborted) - - verify { appStore.dispatch(SearchEnded) } - verify { browserStore.dispatch(EngagementFinished(abandoned = true)) } - } - - @Test - @Config(sdk = [32]) - fun `GIVEN on Android 32- and search was started from a tab WHEN the search is aborted THEN sync this data and navigate back to the tab that started search`() { - val appStore: AppStore = mockk(relaxed = true) { - every { state.searchState } returns AppSearchState.EMPTY.copy( - sourceTabId = "test", - ) - } - val browserStore: BrowserStore = mockk(relaxed = true) - val (_, store) = buildMiddlewareAndAddToStore(appStore, browserStore) - - store.dispatch(SearchAborted) - - verify { appStore.dispatch(SearchEnded) } - verify { browserStore.dispatch(EngagementFinished(abandoned = true)) } - verify { navController.navigate(R.id.browserFragment) } - } - - @Test fun `WHEN the search engine is added by the application THEN do not load URL`() { val captorMiddleware = CaptureActionsMiddleware<AppState, AppAction>() val appStore = AppStore(middlewares = listOf(captorMiddleware))