Bug 1849073 - Part 2: Remove the History Search Dialog

fenix/120.0
Gabriel Luong 8 months ago committed by mergify[bot]
parent 08be473cbf
commit 46b27b6ad9

@ -21,7 +21,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromSettings(R.id.settingsFragment),
FromBookmarks(R.id.bookmarkFragment),
FromHistory(R.id.historyFragment),
FromHistorySearchDialog(R.id.historySearchDialogFragment),
FromHistoryMetadataGroup(R.id.historyMetadataGroupFragment),
FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
FromAbout(R.id.aboutFragment),

@ -999,8 +999,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistorySearchDialog ->
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistoryMetadataGroup ->
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->

@ -24,7 +24,6 @@ import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.library.history.HistoryFragment.DeleteConfirmationDialogFragment
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
@Suppress("TooManyFunctions")
@ -71,7 +70,6 @@ class DefaultHistoryController(
delete: (Set<History>) -> suspend (context: Context) -> Unit,
) -> Unit,
private val syncHistory: suspend () -> Unit,
private val settings: Settings,
) : HistoryController {
override fun handleOpen(item: History) {
@ -117,13 +115,10 @@ class DefaultHistoryController(
}
override fun handleSearch() {
val directions = if (settings.showUnifiedSearchFeature) {
HistoryFragmentDirections.actionGlobalSearchDialog(null)
} else {
HistoryFragmentDirections.actionGlobalHistorySearchDialog()
}
navController.navigateSafe(R.id.historyFragment, directions)
navController.navigateSafe(
R.id.historyFragment,
HistoryFragmentDirections.actionGlobalSearchDialog(sessionId = null),
)
}
override fun handleDeleteTimeRange() {

@ -156,7 +156,6 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
deleteSnackbar = ::deleteSnackbar,
onTimeFrameDeleted = ::onTimeFrameDeleted,
syncHistory = ::syncHistory,
settings = requireContext().components.settings,
)
historyInteractor = DefaultHistoryInteractor(
historyController,

@ -1,47 +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.library.history
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.HomeActivity
/**
* An interface that handles the view manipulation of the History Search, triggered by the Interactor
*/
interface HistorySearchController {
fun handleEditingCancelled()
fun handleTextChanged(text: String)
fun handleUrlTapped(url: String, flags: LoadUrlFlags = LoadUrlFlags.none())
}
class HistorySearchDialogController(
private val activity: HomeActivity,
private val fragmentStore: HistorySearchFragmentStore,
private val clearToolbarFocus: () -> Unit,
) : HistorySearchController {
override fun handleEditingCancelled() {
clearToolbarFocus()
}
override fun handleTextChanged(text: String) {
fragmentStore.dispatch(HistorySearchFragmentAction.UpdateQuery(text))
}
override fun handleUrlTapped(url: String, flags: LoadUrlFlags) {
History.searchResultTapped.record(NoExtras())
clearToolbarFocus()
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = true,
from = BrowserDirection.FromHistorySearchDialog,
flags = flags,
)
}
}

@ -1,314 +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.library.history
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.speech.RecognizerIntent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.InputMethodManager
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM
import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID
import androidx.constraintlayout.widget.ConstraintProperties.TOP
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.databinding.FragmentHistorySearchDialogBinding
import org.mozilla.fenix.databinding.SearchSuggestionsHintBinding
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.library.history.awesomebar.AwesomeBarView
import org.mozilla.fenix.library.history.toolbar.ToolbarView
import org.mozilla.fenix.settings.SupportUtils
@Suppress("TooManyFunctions", "LargeClass")
class HistorySearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private var _binding: FragmentHistorySearchDialogBinding? = null
private val binding get() = _binding!!
private lateinit var interactor: HistorySearchDialogInteractor
private lateinit var store: HistorySearchFragmentStore
private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarView: AwesomeBarView
private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
private var voiceSearchButtonAlreadyAdded = false
private var dialogHandledAction = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.SearchDialogStyle)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
this@HistorySearchDialogFragment.onBackPressed()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentHistorySearchDialogBinding.inflate(inflater, container, false)
val activity = requireActivity() as HomeActivity
store = HistorySearchFragmentStore(
createInitialHistorySearchFragmentState(),
)
interactor = HistorySearchDialogInteractor(
HistorySearchDialogController(
activity = activity,
fragmentStore = store,
clearToolbarFocus = {
dialogHandledAction = true
toolbarView.view.hideKeyboard()
toolbarView.view.clearFocus()
},
),
)
toolbarView = ToolbarView(
context = requireContext(),
interactor = interactor,
isPrivate = false,
view = binding.toolbar,
)
val awesomeBar = binding.awesomeBar
awesomeBarView = AwesomeBarView(
activity,
interactor,
awesomeBar,
)
awesomeBarView.view.setOnEditSuggestionListener(toolbarView.view::setSearchTerms)
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupConstraints(view)
binding.searchWrapper.setOnTouchListener { _, _ ->
dismissAllowingStateLoss()
true
}
val stubListener = ViewStub.OnInflateListener { _, inflated ->
val searchSuggestionHintBinding = SearchSuggestionsHintBinding.bind(inflated)
searchSuggestionHintBinding.learnMore.setOnClickListener {
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
SupportUtils.SumoTopic.SEARCH_SUGGESTION,
),
newTab = true,
from = BrowserDirection.FromHistorySearchDialog,
)
}
searchSuggestionHintBinding.allow.setOnClickListener {
inflated.visibility = View.GONE
requireContext().settings().also {
it.shouldShowSearchSuggestionsInPrivate = true
it.showSearchSuggestionsInPrivateOnboardingFinished = true
}
}
searchSuggestionHintBinding.dismiss.setOnClickListener {
inflated.visibility = View.GONE
requireContext().settings().also {
it.shouldShowSearchSuggestionsInPrivate = false
it.showSearchSuggestionsInPrivateOnboardingFinished = true
}
}
searchSuggestionHintBinding.text.text =
getString(R.string.search_suggestions_onboarding_text, getString(R.string.app_name))
searchSuggestionHintBinding.title.text =
getString(R.string.search_suggestions_onboarding_title)
}
binding.searchSuggestionsHintDivider.isVisible = false
binding.searchSuggestionsHint.isVisible = false
binding.searchSuggestionsHint.setOnInflateListener((stubListener))
if (view.context.settings().accessibilityServicesEnabled) {
updateAccessibilityTraversalOrder()
}
addVoiceSearchButton()
observeAwesomeBarState()
consumeFrom(store) {
toolbarView.update(it)
awesomeBarView.update(it)
}
}
private fun observeAwesomeBarState() = consumeFlow(store) { flow ->
flow.map { state -> state.query.isNotBlank() }
.distinctUntilChanged()
.collect { shouldShowAwesomebar ->
binding.awesomeBar.visibility = if (shouldShowAwesomebar) {
View.VISIBLE
} else {
View.INVISIBLE
}
}
}
private fun updateAccessibilityTraversalOrder() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
viewLifecycleOwner.lifecycleScope.launch {
binding.searchWrapper.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
}
}
override fun onPause() {
super.onPause()
view?.hideKeyboard()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/*
* This way of dismissing the keyboard is needed to smoothly dismiss the keyboard while the dialog
* is also dismissing.
*/
private fun hideDeviceKeyboard() {
// If the interactor/controller has handled a search event itself, it will hide the keyboard.
if (!dialogHandledAction) {
val imm =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY)
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
hideDeviceKeyboard()
}
override fun onBackPressed(): Boolean {
view?.hideKeyboard()
dismissAllowingStateLoss()
return true
}
private fun setupConstraints(view: View) {
if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) {
ConstraintSet().apply {
clone(binding.searchWrapper)
clear(binding.toolbar.id, TOP)
connect(binding.toolbar.id, BOTTOM, PARENT_ID, BOTTOM)
clear(binding.pillWrapper.id, BOTTOM)
connect(binding.pillWrapper.id, BOTTOM, binding.toolbar.id, TOP)
clear(binding.awesomeBar.id, TOP)
clear(binding.awesomeBar.id, BOTTOM)
connect(binding.awesomeBar.id, TOP, binding.searchSuggestionsHint.id, BOTTOM)
connect(binding.awesomeBar.id, BOTTOM, binding.pillWrapper.id, TOP)
clear(binding.searchSuggestionsHint.id, TOP)
clear(binding.searchSuggestionsHint.id, BOTTOM)
connect(binding.searchSuggestionsHint.id, TOP, PARENT_ID, TOP)
connect(binding.searchSuggestionsHint.id, BOTTOM, binding.searchHintBottomBarrier.id, TOP)
applyTo(binding.searchWrapper)
}
}
}
private val startVoiceSearchForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also {
toolbarView.view.edit.updateUrl(url = it, shouldHighlight = true)
interactor.onTextChanged(it)
toolbarView.view.edit.focus()
}
}
}
private fun addVoiceSearchButton() {
val shouldShowVoiceSearch = isSpeechAvailable() &&
requireContext().settings().shouldShowVoiceSearch
if (voiceSearchButtonAlreadyAdded || !shouldShowVoiceSearch) return
toolbarView.view.addEditActionEnd(
BrowserToolbar.Button(
imageDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
contentDescription = requireContext().getString(R.string.voice_search_content_description),
visible = { true },
listener = ::launchVoiceSearch,
),
)
voiceSearchButtonAlreadyAdded = true
}
private fun launchVoiceSearch() {
// Note if a user disables speech while the app is on the search fragment
// the voice button will still be available and *will* cause a crash if tapped,
// since the `visible` call is only checked on create. In order to avoid extra complexity
// around such a small edge case, we make the button have no functionality in this case.
if (!isSpeechAvailable()) { return }
speechIntent.apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_PROMPT, requireContext().getString(R.string.voice_search_explainer))
}
startVoiceSearchForResult.launch(speechIntent)
}
private fun isSpeechAvailable(): Boolean = speechIntent.resolveActivity(requireContext().packageManager) != null
}

@ -1,30 +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.library.history
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
import org.mozilla.fenix.library.history.awesomebar.AwesomeBarInteractor
import org.mozilla.fenix.library.history.toolbar.ToolbarInteractor
/**
* Interactor for the history search
* Provides implementations for the AwesomeBarView and ToolbarView
*/
class HistorySearchDialogInteractor(
private val historySearchController: HistorySearchDialogController,
) : AwesomeBarInteractor, ToolbarInteractor {
override fun onEditingCanceled() {
historySearchController.handleEditingCancelled()
}
override fun onTextChanged(text: String) {
historySearchController.handleTextChanged(text)
}
override fun onUrlTapped(url: String, flags: LoadUrlFlags) {
historySearchController.handleUrlTapped(url, flags)
}
}

@ -1,53 +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.library.history
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* The [Store] for holding the [HistorySearchFragmentState] and applying [HistorySearchFragmentAction]s.
*/
class HistorySearchFragmentStore(
initialState: HistorySearchFragmentState,
) : Store<HistorySearchFragmentState, HistorySearchFragmentAction>(
initialState,
::historySearchStateReducer,
)
/**
* The state for the History Search Screen
*
* @property query The current search query string
*/
data class HistorySearchFragmentState(
val query: String,
) : State
fun createInitialHistorySearchFragmentState(): HistorySearchFragmentState {
return HistorySearchFragmentState(query = "")
}
/**
* Actions to dispatch through the [HistorySearchFragmentStore] to modify [HistorySearchFragmentState]
* through the reducer.
*/
sealed class HistorySearchFragmentAction : Action {
data class UpdateQuery(val query: String) : HistorySearchFragmentAction()
}
/**
* The [HistorySearchFragmentState] Reducer.
*/
private fun historySearchStateReducer(
state: HistorySearchFragmentState,
action: HistorySearchFragmentAction,
): HistorySearchFragmentState {
return when (action) {
is HistorySearchFragmentAction.UpdateQuery ->
state.copy(query = action.query)
}
}

@ -1,20 +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.library.history.awesomebar
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
/**
* Interface for the AwesomeBarView Interactor. This interface is implemented by objects that want
* to respond to user interaction on the AwesomebarView
*/
interface AwesomeBarInteractor {
/**
* Called whenever a suggestion containing a URL is tapped
* @param url the url the suggestion was providing
*/
fun onUrlTapped(url: String, flags: LoadUrlFlags = LoadUrlFlags.none())
}

@ -1,61 +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.library.history.awesomebar
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.awesomebar.provider.CombinedHistorySuggestionProvider
import mozilla.components.feature.session.SessionUseCases
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.Core.Companion.METADATA_SHORTCUT_SUGGESTION_LIMIT
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.library.history.HistorySearchFragmentState
/**
* View that contains and configures the BrowserAwesomeBar
*/
class AwesomeBarView(
activity: HomeActivity,
val interactor: AwesomeBarInteractor,
val view: AwesomeBarWrapper,
) {
private val combinedHistoryProvider: CombinedHistorySuggestionProvider
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(
url: String,
flags: EngineSession.LoadUrlFlags,
additionalHeaders: Map<String, String>?,
) {
interactor.onUrlTapped(url, flags)
}
}
init {
val components = activity.components
val engineForSpeculativeConnects = when (activity.browsingModeManager.mode) {
BrowsingMode.Normal -> components.core.engine
BrowsingMode.Private -> null
}
combinedHistoryProvider =
CombinedHistorySuggestionProvider(
historyStorage = components.core.historyStorage,
historyMetadataStorage = components.core.historyStorage,
loadUrlUseCase = loadUrlUseCase,
icons = components.core.icons,
engine = engineForSpeculativeConnects,
maxNumberOfSuggestions = METADATA_SHORTCUT_SUGGESTION_LIMIT,
showEditSuggestion = false,
)
view.addProviders(combinedHistoryProvider)
}
fun update(state: HistorySearchFragmentState) {
view.onInputChanged(state.query)
}
}

@ -1,106 +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.library.history.awesomebar
import android.content.Context
import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AbstractComposeView
import mozilla.components.compose.browser.awesomebar.AwesomeBar
import mozilla.components.compose.browser.awesomebar.AwesomeBarDefaults
import mozilla.components.compose.browser.awesomebar.AwesomeBarOrientation
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.ThemeManager
/**
* This wrapper wraps the `AwesomeBar()` composable and exposes it as a `View` and `concept-awesomebar`
* implementation to be integrated in the view hierarchy of [HistorySearchDialogFragment] until more parts
* of that screen have been refactored to use Jetpack Compose.
*/
class AwesomeBarWrapper @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AbstractComposeView(context, attrs, defStyleAttr), AwesomeBar {
private val providers = mutableStateOf(emptyList<AwesomeBar.SuggestionProvider>())
private val text = mutableStateOf("")
private var onEditSuggestionListener: ((String) -> Unit)? = null
private var onStopListener: (() -> Unit)? = null
@Composable
override fun Content() {
if (providers.value.isEmpty()) {
return
}
val orientation = if (context.settings().shouldUseBottomToolbar) {
AwesomeBarOrientation.BOTTOM
} else {
AwesomeBarOrientation.TOP
}
FirefoxTheme {
AwesomeBar(
text = text.value,
providers = providers.value,
orientation = orientation,
colors = AwesomeBarDefaults.colors(
background = Color.Transparent,
title = ThemeManager.resolveAttributeColor(R.attr.textPrimary),
description = ThemeManager.resolveAttributeColor(R.attr.textSecondary),
autocompleteIcon = ThemeManager.resolveAttributeColor(R.attr.textSecondary),
),
onSuggestionClicked = { suggestion ->
suggestion.onSuggestionClicked?.invoke()
onStopListener?.invoke()
},
onAutoComplete = { suggestion ->
onEditSuggestionListener?.invoke(suggestion.editSuggestion!!)
},
onScroll = { hideKeyboard() },
profiler = context.components.core.engine.profiler,
)
}
}
override fun addProviders(vararg providers: AwesomeBar.SuggestionProvider) {
val newProviders = this.providers.value.toMutableList()
newProviders.addAll(providers)
this.providers.value = newProviders
}
override fun containsProvider(provider: AwesomeBar.SuggestionProvider): Boolean {
return providers.value.any { current -> current.id == provider.id }
}
override fun onInputChanged(text: String) {
this.text.value = text
}
override fun removeAllProviders() {
providers.value = emptyList()
}
override fun removeProviders(vararg providers: AwesomeBar.SuggestionProvider) {
val newProviders = this.providers.value.toMutableList()
newProviders.removeAll(providers)
this.providers.value = newProviders
}
override fun setOnEditSuggestionListener(listener: (String) -> Unit) {
onEditSuggestionListener = listener
}
override fun setOnStopListener(listener: () -> Unit) {
onStopListener = listener
}
}

@ -1,124 +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.library.history.toolbar
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistorySearchFragmentState
/**
* Interface for the Toolbar Interactor. This interface is implemented by objects that want
* to respond to user interaction on the [ToolbarView]
*/
interface ToolbarInteractor {
/**
* Called when a user removes focus from the [ToolbarView]
*/
fun onEditingCanceled()
/**
* Called whenever the text inside the [ToolbarView] changes
* @param text the current text displayed by [ToolbarView]
*/
fun onTextChanged(text: String)
}
/**
* View that contains and configures the BrowserToolbar to only be used in its editing mode.
*/
@Suppress("LongParameterList")
class ToolbarView(
private val context: Context,
private val interactor: ToolbarInteractor,
private val isPrivate: Boolean,
val view: BrowserToolbar,
) {
@VisibleForTesting
internal var isInitialized = false
init {
view.apply {
editMode()
background = AppCompatResources.getDrawable(
context,
context.theme.resolveAttribute(R.attr.layer1),
)
edit.hint = context.getString(R.string.history_search_1)
edit.colors = edit.colors.copy(
text = context.getColorFromAttr(R.attr.textPrimary),
hint = context.getColorFromAttr(R.attr.textSecondary),
suggestionBackground = ContextCompat.getColor(
context,
R.color.suggestion_highlight_color,
),
clear = context.getColorFromAttr(R.attr.textPrimary),
)
edit.setUrlBackground(
AppCompatResources.getDrawable(context, R.drawable.search_url_background),
)
private = isPrivate
setOnUrlCommitListener {
hideKeyboard()
// We need to return false to not show display mode
false
}
setDefaultIcon()
setOnEditListener(
object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean {
interactor.onEditingCanceled()
// We need to return false to not show display mode
return false
}
override fun onTextChanged(text: String) {
url = text
interactor.onTextChanged(text)
}
},
)
}
}
fun update(state: HistorySearchFragmentState) {
if (!isInitialized) {
view.url = state.query
view.setSearchTerms(state.query)
// We must trigger an onTextChanged so when search terms are set when transitioning to `editMode`
// we have the most up to date text
interactor.onTextChanged(view.url.toString())
view.editMode()
isInitialized = true
}
}
private fun setDefaultIcon() {
val historySearchIcon = AppCompatResources.getDrawable(context, R.drawable.ic_history)
historySearchIcon?.let {
view.edit.setIcon(historySearchIcon, context.getString(R.string.history_search_1))
}
}
}

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/search_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/scrimBackground">
<mozilla.components.browser.toolbar.BrowserToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="@dimen/browser_toolbar_height"
android:background="@drawable/toolbar_background_top"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"
app:browserToolbarClearColor="?attr/textPrimary"
app:browserToolbarInsecureColor="?attr/textPrimary"
app:browserToolbarMenuColor="?attr/textPrimary"
app:browserToolbarProgressBarGravity="bottom"
app:browserToolbarSecureColor="?attr/textPrimary"
app:browserToolbarTrackingProtectionAndSecurityIndicatorSeparatorColor="?borderPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ViewStub
android:id="@+id/search_suggestions_hint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inflatedId="@id/search_suggestions_hint"
android:layout="@layout/search_suggestions_hint"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/search_hint_bottom_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<View
android:id="@+id/search_suggestions_hint_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?borderPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/search_suggestions_hint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/search_hint_bottom_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="awesome_bar,pill_wrapper"/>
<org.mozilla.fenix.library.history.awesomebar.AwesomeBarWrapper
android:id="@+id/awesome_bar"
android:layout_width="0dp"
android:layout_height="0dp"
android:fadingEdge="horizontal"
android:fadingEdgeLength="40dp"
android:nestedScrollingEnabled="false"
android:requiresFadingEdge="vertical"
android:background="?attr/layer1"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/pill_wrapper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_suggestions_hint" />
<ImageView
android:id="@+id/link_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/search_fragment_clipboard_item_horizontal_margin"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:visibility="gone"
app:srcCompat="@drawable/ic_link"
tools:visibility="visible" />
<View
android:id="@+id/pill_wrapper_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?borderPrimary"
app:layout_constraintBottom_toTopOf="@id/pill_wrapper" />
<View
android:id="@+id/pill_wrapper"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/layer1"
android:importantForAccessibility="no"
android:paddingStart="@dimen/search_fragment_pill_padding_start"
android:paddingTop="@dimen/search_fragment_pill_padding_vertical"
android:paddingEnd="@dimen/search_fragment_pill_padding_end"
android:paddingBottom="@dimen/search_fragment_pill_padding_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -335,20 +335,8 @@
android:name="org.mozilla.fenix.library.history.HistoryFragment"
android:label="@string/library_history"
tools:layout="@layout/fragment_history">
<action
android:id="@+id/action_global_history_search_dialog"
app:destination="@id/historySearchDialogFragment"
app:popUpTo="@id/historySearchDialogFragment"
app:popUpToInclusive="true" />
</fragment>
<dialog
android:id="@+id/historySearchDialogFragment"
android:name="org.mozilla.fenix.library.history.HistorySearchDialogFragment"
tools:layout="@layout/fragment_history_search_dialog">
</dialog>
<fragment
android:id="@+id/historyMetadataGroupFragment"
android:name="org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragment">

@ -30,7 +30,6 @@ import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class HistoryControllerTest {
@ -56,7 +55,6 @@ class HistoryControllerTest {
private val state: HistoryFragmentState = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
private val historyProvider: DefaultPagedHistoryProvider = mockk(relaxed = true)
private val settings: Settings = mockk(relaxed = true)
@Before
fun setUp() {
@ -146,7 +144,7 @@ class HistoryControllerTest {
verify {
navController.navigateSafe(
R.id.historyFragment,
HistoryFragmentDirections.actionGlobalHistorySearchDialog(),
HistoryFragmentDirections.actionGlobalSearchDialog(sessionId = null),
)
}
}
@ -269,7 +267,6 @@ class HistoryControllerTest {
invalidateOptionsMenu,
{ items, _, _ -> deleteHistoryItems.invoke(items) },
syncHistory,
settings,
)
}
}

@ -1,103 +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.library.history
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.EngineSession
import mozilla.components.support.test.robolectric.testContext
import mozilla.telemetry.glean.testing.GleanTestRule
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class HistorySearchControllerTest {
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
@MockK(relaxed = true)
private lateinit var activity: HomeActivity
@MockK(relaxed = true)
private lateinit var store: HistorySearchFragmentStore
@Before
fun setUp() {
MockKAnnotations.init(this)
}
@Test
fun `WHEN editing is cancelled THEN clearToolbarFocus is called`() = runTest {
var clearToolbarFocusInvoked = false
createController(
clearToolbarFocus = {
clearToolbarFocusInvoked = true
},
).handleEditingCancelled()
assertTrue(clearToolbarFocusInvoked)
}
@Test
fun `WHEN text changed THEN update query action is dispatched`() {
val text = "fenix"
createController().handleTextChanged(text)
verify { store.dispatch(HistorySearchFragmentAction.UpdateQuery(text)) }
}
@Test
fun `WHEN text is changed to empty THEN update query action is dispatched`() {
val text = ""
createController().handleTextChanged(text)
verify { store.dispatch(HistorySearchFragmentAction.UpdateQuery(text)) }
}
@Test
fun `WHEN url is tapped THEN openToBrowserAndLoad is called`() {
val url = "https://www.google.com/"
val flags = EngineSession.LoadUrlFlags.none()
assertNull(History.searchResultTapped.testGetValue())
createController().handleUrlTapped(url, flags)
createController().handleUrlTapped(url)
assertNotNull(History.searchResultTapped.testGetValue())
assertNull(History.searchResultTapped.testGetValue()!!.last().extra)
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = url,
newTab = true,
from = BrowserDirection.FromHistorySearchDialog,
flags = flags,
)
}
}
private fun createController(
clearToolbarFocus: () -> Unit = { },
): HistorySearchDialogController {
return HistorySearchDialogController(
activity = activity,
fragmentStore = store,
clearToolbarFocus = clearToolbarFocus,
)
}
}

@ -1,50 +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.library.history
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
class HistorySearchDialogInteractorTest {
lateinit var searchController: HistorySearchDialogController
lateinit var interactor: HistorySearchDialogInteractor
@Before
fun setup() {
searchController = mockk(relaxed = true)
interactor = HistorySearchDialogInteractor(
searchController,
)
}
@Test
fun onEditingCanceled() = runTest {
interactor.onEditingCanceled()
verify {
searchController.handleEditingCancelled()
}
}
@Test
fun onTextChanged() {
interactor.onTextChanged("test")
verify { searchController.handleTextChanged("test") }
}
@Test
fun onUrlTapped() {
interactor.onUrlTapped("test")
verify {
searchController.handleUrlTapped("test")
}
}
}

@ -1,39 +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.library.history
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
import org.mozilla.fenix.components.Components
class HistorySearchFragmentStoreTest {
@MockK(relaxed = true)
private lateinit var components: Components
@Test
fun `GIVEN createInitialHistorySearchFragmentState THEN query is empty`() {
val expected = HistorySearchFragmentState(query = "")
assertEquals(
expected,
createInitialHistorySearchFragmentState(),
)
}
@Test
fun updateQuery() = runTest {
val initialState = HistorySearchFragmentState(query = "")
val store = HistorySearchFragmentStore(initialState)
val query = "test query"
store.dispatch(HistorySearchFragmentAction.UpdateQuery(query)).join()
assertNotSame(initialState, store.state)
assertEquals(query, store.state.query)
}
}

@ -15,10 +15,12 @@ import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.doAnswer
@Ignore("Intermittent failure, see https://bugzilla.mozilla.org/show_bug.cgi?id=1849525.")
class DefaultReviewQualityCheckServiceTest {
@get:Rule

Loading…
Cancel
Save