1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.search
7 import android.Manifest
8 import android.app.Activity
9 import android.app.Dialog
10 import android.content.Context
11 import android.content.DialogInterface
12 import android.content.Intent
13 import android.graphics.Color
14 import android.graphics.Typeface
15 import android.graphics.drawable.ColorDrawable
16 import android.os.Build
17 import android.os.Bundle
18 import android.speech.RecognizerIntent
19 import android.text.style.StyleSpan
20 import android.view.LayoutInflater
21 import android.view.View
22 import android.view.ViewGroup
23 import android.view.ViewStub
24 import android.view.WindowManager
25 import android.view.accessibility.AccessibilityEvent
26 import android.view.inputmethod.InputMethodManager
27 import androidx.appcompat.app.AlertDialog
28 import androidx.appcompat.app.AppCompatDialogFragment
29 import androidx.appcompat.content.res.AppCompatResources
30 import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM
31 import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID
32 import androidx.constraintlayout.widget.ConstraintProperties.TOP
33 import androidx.constraintlayout.widget.ConstraintSet
34 import androidx.core.net.toUri
35 import androidx.core.view.isVisible
36 import androidx.lifecycle.lifecycleScope
37 import androidx.navigation.fragment.findNavController
38 import androidx.navigation.fragment.navArgs
39 import kotlinx.coroutines.flow.collect
40 import kotlinx.coroutines.flow.map
41 import kotlinx.coroutines.launch
42 import mozilla.components.browser.toolbar.BrowserToolbar
43 import mozilla.components.concept.engine.EngineSession
44 import mozilla.components.concept.storage.HistoryStorage
45 import mozilla.components.feature.qr.QrFeature
46 import mozilla.components.lib.state.ext.consumeFlow
47 import mozilla.components.lib.state.ext.consumeFrom
48 import mozilla.components.support.base.coroutines.Dispatchers
49 import mozilla.components.support.base.feature.UserInteractionHandler
50 import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
51 import mozilla.components.support.ktx.android.content.getColorFromAttr
52 import mozilla.components.support.ktx.android.content.hasCamera
53 import mozilla.components.support.ktx.android.content.isPermissionGranted
54 import mozilla.components.support.ktx.android.content.res.getSpanned
55 import mozilla.components.support.ktx.android.net.isHttpOrHttps
56 import mozilla.components.support.ktx.android.view.hideKeyboard
57 import mozilla.components.support.ktx.kotlin.toNormalizedUrl
58 import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
59 import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
60 import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
61 import org.mozilla.fenix.BrowserDirection
62 import org.mozilla.fenix.HomeActivity
63 import org.mozilla.fenix.R
64 import org.mozilla.fenix.components.metrics.Event
65 import org.mozilla.fenix.components.toolbar.ToolbarPosition
66 import org.mozilla.fenix.databinding.FragmentSearchDialogBinding
67 import org.mozilla.fenix.databinding.SearchSuggestionsHintBinding
68 import org.mozilla.fenix.ext.components
69 import org.mozilla.fenix.ext.requireComponents
70 import org.mozilla.fenix.ext.settings
71 import org.mozilla.fenix.search.awesomebar.AwesomeBarView
72 import org.mozilla.fenix.search.awesomebar.toSearchProviderState
73 import org.mozilla.fenix.search.toolbar.ToolbarView
74 import org.mozilla.fenix.settings.SupportUtils
75 import org.mozilla.fenix.widget.VoiceSearchActivity
77 typealias SearchDialogFragmentStore = SearchFragmentStore
79 @SuppressWarnings("LargeClass", "TooManyFunctions")
80 class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
81 private var _binding: FragmentSearchDialogBinding? = null
82 private val binding get() = _binding!!
84 private var voiceSearchButtonAlreadyAdded: Boolean = false
85 private lateinit var interactor: SearchDialogInteractor
86 private lateinit var store: SearchDialogFragmentStore
87 private lateinit var toolbarView: ToolbarView
88 private lateinit var inlineAutocompleteEditText: InlineAutocompleteEditText
89 private lateinit var awesomeBarView: AwesomeBarView
91 private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
92 private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
93 private var dialogHandledAction = false
95 override fun onStart() {
98 // This will need to be handled for the update to R. We need to resize here in order to
99 // see the whole homescreen behind the search dialog.
100 @Suppress("DEPRECATION")
101 requireActivity().window.setSoftInputMode(
102 WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
105 // Refocus the toolbar editing and show keyboard if the QR fragment isn't showing
106 if (childFragmentManager.findFragmentByTag(QR_FRAGMENT_TAG) == null) {
107 toolbarView.view.edit.focus()
111 override fun onStop() {
113 // https://github.com/mozilla-mobile/fenix/issues/14279
114 // Let's reset back to the default behavior after we're done searching
115 // This will be addressed on https://github.com/mozilla-mobile/fenix/issues/17805
116 @Suppress("DEPRECATION")
117 requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
120 override fun onCreate(savedInstanceState: Bundle?) {
121 super.onCreate(savedInstanceState)
122 setStyle(STYLE_NO_TITLE, R.style.SearchDialogStyle)
125 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
126 return object : Dialog(requireContext(), this.theme) {
127 override fun onBackPressed() {
128 this@SearchDialogFragment.onBackPressed()
133 @SuppressWarnings("LongMethod")
134 override fun onCreateView(
135 inflater: LayoutInflater,
136 container: ViewGroup?,
137 savedInstanceState: Bundle?
139 val args by navArgs<SearchDialogFragmentArgs>()
140 _binding = FragmentSearchDialogBinding.inflate(inflater, container, false)
141 val activity = requireActivity() as HomeActivity
142 val isPrivate = activity.browsingModeManager.mode.isPrivate
144 requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
146 store = SearchDialogFragmentStore(
147 createInitialSearchFragmentState(
150 tabId = args.sessionId,
151 pastedText = args.pastedText,
152 searchAccessPoint = args.searchAccessPoint
156 interactor = SearchDialogInteractor(
157 SearchDialogController(
159 store = requireComponents.core.store,
160 tabsUseCases = requireComponents.useCases.tabsUseCases,
161 fragmentStore = store,
162 navController = findNavController(),
163 settings = requireContext().settings(),
164 metrics = requireComponents.analytics.metrics,
166 dialogHandledAction = true
167 dismissAllowingStateLoss()
169 clearToolbarFocus = {
170 dialogHandledAction = true
171 toolbarView.view.hideKeyboard()
172 toolbarView.view.clearFocus()
174 focusToolbar = { toolbarView.view.edit.focus() },
176 inlineAutocompleteEditText.setText("")
181 val fromHomeFragment =
182 findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment
184 toolbarView = ToolbarView(
186 requireContext().settings(),
188 historyStorageProvider(),
191 requireComponents.core.engine,
194 inlineAutocompleteEditText = it.view.findViewById(R.id.mozac_browser_toolbar_edit_url_view)
197 val awesomeBar = binding.awesomeBar
199 awesomeBarView = AwesomeBarView(
206 binding.awesomeBar.setOnTouchListener { _, _ ->
207 binding.root.hideKeyboard()
211 awesomeBarView.view.setOnEditSuggestionListener(toolbarView.view::setSearchTerms)
213 inlineAutocompleteEditText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
215 requireComponents.core.engine.speculativeCreateSession(isPrivate)
217 if (fromHomeFragment) {
218 // When displayed above home, dispatches the touch events to scrim area to the HomeFragment
219 binding.searchWrapper.background = ColorDrawable(Color.TRANSPARENT)
220 dialog?.window?.decorView?.setOnTouchListener { _, event ->
221 requireActivity().dispatchTouchEvent(event)
222 // toolbarView.view.displayMode()
230 @SuppressWarnings("LongMethod")
231 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
232 super.onViewCreated(view, savedInstanceState)
234 consumeFlow(requireComponents.core.store) { flow ->
235 flow.map { state -> state.search }
238 store.dispatch(SearchFragmentAction.UpdateSearchState(search))
242 setupConstraints(view)
244 // When displayed above browser or home screen, dismisses keyboard when touching scrim area
245 when (findNavController().previousBackStackEntry?.destination?.id) {
246 R.id.browserFragment, R.id.homeFragment -> {
247 binding.searchWrapper.setOnTouchListener { _, _ ->
248 binding.searchWrapper.hideKeyboard()
255 binding.searchEnginesShortcutButton.setOnClickListener {
256 interactor.onSearchShortcutsButtonClicked()
265 binding.qrScanButton.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE
267 binding.qrScanButton.setOnClickListener {
268 if (!requireContext().hasCamera()) { return@setOnClickListener }
270 toolbarView.view.clearFocus()
272 if (requireContext().settings().shouldShowCameraPermissionPrompt) {
273 qrFeature.get()?.scan(binding.searchWrapper.id)
275 if (requireContext().isPermissionGranted(Manifest.permission.CAMERA)) {
276 qrFeature.get()?.scan(binding.searchWrapper.id)
278 interactor.onCameraPermissionsNeeded()
281 toolbarView.view.requestFocus()
284 requireContext().settings().setCameraPermissionNeededState = false
287 binding.fillLinkFromClipboard.setOnClickListener {
288 requireComponents.analytics.metrics.track(Event.ClipboardSuggestionClicked)
289 val clipboardUrl = requireContext().components.clipboardHandler.extractURL() ?: ""
291 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
292 toolbarView.view.edit.updateUrl(clipboardUrl)
293 hideClipboardSection()
294 inlineAutocompleteEditText.setSelection(clipboardUrl.length)
297 toolbarView.view.clearFocus()
298 (activity as HomeActivity)
299 .openToBrowserAndLoad(
300 searchTermOrURL = clipboardUrl,
301 newTab = store.state.tabId == null,
302 from = BrowserDirection.FromSearchDialog
307 val stubListener = ViewStub.OnInflateListener { _, inflated ->
308 val searchSuggestionHintBinding = SearchSuggestionsHintBinding.bind(inflated)
310 searchSuggestionHintBinding.learnMore.setOnClickListener {
311 (activity as HomeActivity)
312 .openToBrowserAndLoad(
313 searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
314 SupportUtils.SumoTopic.SEARCH_SUGGESTION
316 newTab = store.state.tabId == null,
317 from = BrowserDirection.FromSearchDialog
321 searchSuggestionHintBinding.allow.setOnClickListener {
322 inflated.visibility = View.GONE
323 requireContext().settings().also {
324 it.shouldShowSearchSuggestionsInPrivate = true
325 it.showSearchSuggestionsInPrivateOnboardingFinished = true
327 store.dispatch(SearchFragmentAction.SetShowSearchSuggestions(true))
328 store.dispatch(SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(false))
331 searchSuggestionHintBinding.dismiss.setOnClickListener {
332 inflated.visibility = View.GONE
333 requireContext().settings().also {
334 it.shouldShowSearchSuggestionsInPrivate = false
335 it.showSearchSuggestionsInPrivateOnboardingFinished = true
339 searchSuggestionHintBinding.text.text =
340 getString(R.string.search_suggestions_onboarding_text, getString(R.string.app_name))
342 searchSuggestionHintBinding.title.text =
343 getString(R.string.search_suggestions_onboarding_title)
346 binding.searchSuggestionsHint.setOnInflateListener((stubListener))
347 if (view.context.settings().accessibilityServicesEnabled) {
348 updateAccessibilityTraversalOrder()
351 observeClipboardState()
352 observeAwesomeBarState()
353 observeShortcutsState()
354 observeSuggestionProvidersState()
357 updateSearchSuggestionsHintVisibility(it)
358 updateToolbarContentDescription(it.searchEngineSource)
359 toolbarView.update(it)
360 awesomeBarView.update(it)
361 addVoiceSearchButton(it)
365 private fun hideClipboardSection() {
366 binding.fillLinkFromClipboard.isVisible = false
367 binding.fillLinkDivider.isVisible = false
368 binding.pillWrapperDivider.isVisible = false
369 binding.clipboardUrl.isVisible = false
370 binding.clipboardTitle.isVisible = false
371 binding.linkIcon.isVisible = false
374 private fun observeSuggestionProvidersState() = consumeFlow(store) { flow ->
375 flow.map { state -> state.toSearchProviderState() }
377 .collect { state -> awesomeBarView.updateSuggestionProvidersVisibility(state) }
380 private fun observeShortcutsState() = consumeFlow(store) { flow ->
381 flow.ifAnyChanged { state -> arrayOf(state.areShortcutsAvailable, state.showSearchShortcuts) }
382 .collect { state -> updateSearchShortcutsIcon(state.areShortcutsAvailable, state.showSearchShortcuts) }
385 private fun observeAwesomeBarState() = consumeFlow(store) { flow ->
387 * firstUpdate is used to make sure we keep the awesomebar hidden on the first run
388 * of the searchFragmentDialog. We only turn it false after the user has changed the
389 * query as consumeFrom may run several times on fragment start due to state updates.
392 flow.map { state -> state.url != state.query && state.query.isNotBlank() || state.showSearchShortcuts }
394 .collect { shouldShowAwesomebar ->
395 binding.awesomeBar.visibility = if (shouldShowAwesomebar) {
403 private fun observeClipboardState() = consumeFlow(store) { flow ->
405 val shouldShowView = state.showClipboardSuggestions &&
406 state.query.isEmpty() &&
407 state.clipboardHasUrl && !state.showSearchShortcuts
408 Pair(shouldShowView, state.clipboardHasUrl)
411 .collect { (shouldShowView) ->
412 updateClipboardSuggestion(shouldShowView)
416 private fun updateAccessibilityTraversalOrder() {
417 val searchWrapperId = binding.searchWrapper.id
418 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
419 binding.qrScanButton.accessibilityTraversalAfter = searchWrapperId
420 binding.searchEnginesShortcutButton.accessibilityTraversalAfter = searchWrapperId
421 binding.fillLinkFromClipboard.accessibilityTraversalAfter = searchWrapperId
423 viewLifecycleOwner.lifecycleScope.launch {
424 binding.searchWrapper.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
429 override fun onResume() {
433 // We delay querying the clipboard by posting this code to the main thread message queue,
434 // because ClipboardManager will return null if the does app not have input focus yet.
435 lifecycleScope.launch(Dispatchers.Cached) {
436 val hasUrl = context?.components?.clipboardHandler?.containsURL() ?: false
437 store.dispatch(SearchFragmentAction.UpdateClipboardHasUrl(hasUrl))
442 override fun onPause() {
447 override fun onDestroyView() {
448 super.onDestroyView()
454 * This way of dismissing the keyboard is needed to smoothly dismiss the keyboard while the dialog
455 * is also dismissing. For example, when clicking a top site on home while this dialog is showing.
457 private fun hideDeviceKeyboard() {
458 // If the interactor/controller has handled a search event itself, it will hide the keyboard.
459 if (!dialogHandledAction) {
461 requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
462 imm.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY)
466 override fun onDismiss(dialog: DialogInterface) {
467 super.onDismiss(dialog)
471 override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
472 if (requestCode == VoiceSearchActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
473 intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also {
474 toolbarView.view.edit.updateUrl(url = it, shouldHighlight = true)
475 interactor.onTextChanged(it)
476 toolbarView.view.edit.focus()
481 override fun onBackPressed(): Boolean {
483 qrFeature.onBackPressed() -> {
488 // In case we're displaying search results, we wouldn't have navigated to home, and
489 // so we don't need to navigate "back to" browser fragment.
490 // See mirror of this logic in BrowserToolbarController#handleToolbarClick.
491 if (store.state.searchTerms.isBlank()) {
492 val args by navArgs<SearchDialogFragmentArgs>()
493 args.sessionId?.let {
494 findNavController().navigate(
495 SearchDialogFragmentDirections.actionGlobalBrowser(null)
500 dismissAllowingStateLoss()
506 private fun historyStorageProvider(): HistoryStorage? {
507 return if (requireContext().settings().shouldShowHistorySuggestions) {
508 requireComponents.core.historyStorage
512 @Suppress("DEPRECATION")
513 // https://github.com/mozilla-mobile/fenix/issues/19920
514 private fun createQrFeature(): QrFeature {
517 fragmentManager = childFragmentManager,
518 onNeedToRequestPermissions = { permissions ->
519 requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
521 onScanResult = { result ->
522 val normalizedUrl = result.toNormalizedUrl()
523 if (!normalizedUrl.toUri().isHttpOrHttps) {
525 AlertDialog.Builder(it).apply {
526 setMessage(R.string.qr_scanner_dialog_invalid)
527 setPositiveButton(R.string.qr_scanner_dialog_invalid_ok) { dialog: DialogInterface, _ ->
534 binding.qrScanButton.isChecked = false
536 AlertDialog.Builder(it).apply {
537 val spannable = resources.getSpanned(
538 R.string.qr_scanner_confirmation_dialog_message,
539 getString(R.string.app_name) to StyleSpan(Typeface.BOLD),
540 normalizedUrl to StyleSpan(Typeface.ITALIC)
542 setMessage(spannable)
543 setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ ->
546 setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ ->
547 (activity as? HomeActivity)?.openToBrowserAndLoad(
548 searchTermOrURL = normalizedUrl,
549 newTab = store.state.tabId == null,
550 from = BrowserDirection.FromSearchDialog,
551 flags = EngineSession.LoadUrlFlags.external()
563 @Suppress("DEPRECATION")
564 // https://github.com/mozilla-mobile/fenix/issues/19920
565 override fun onRequestPermissionsResult(
567 permissions: Array<String>,
568 grantResults: IntArray
571 REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
572 it.onPermissionsResult(permissions, grantResults)
574 requireContext().settings().setCameraPermissionNeededState = false
576 else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
580 private fun resetFocus() {
581 binding.qrScanButton.isChecked = false
582 toolbarView.view.edit.focus()
583 toolbarView.view.requestFocus()
586 private fun setupConstraints(view: View) {
587 if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) {
588 ConstraintSet().apply {
589 clone(binding.searchWrapper)
591 clear(binding.toolbar.id, TOP)
592 connect(binding.toolbar.id, BOTTOM, PARENT_ID, BOTTOM)
594 clear(binding.pillWrapper.id, BOTTOM)
595 connect(binding.pillWrapper.id, BOTTOM, binding.toolbar.id, TOP)
597 clear(binding.awesomeBar.id, TOP)
598 clear(binding.awesomeBar.id, BOTTOM)
599 connect(binding.awesomeBar.id, TOP, binding.searchSuggestionsHint.id, BOTTOM)
600 connect(binding.awesomeBar.id, BOTTOM, binding.pillWrapper.id, TOP)
602 clear(binding.searchSuggestionsHint.id, TOP)
603 clear(binding.searchSuggestionsHint.id, BOTTOM)
604 connect(binding.searchSuggestionsHint.id, TOP, PARENT_ID, TOP)
605 connect(binding.searchSuggestionsHint.id, BOTTOM, binding.searchHintBottomBarrier.id, TOP)
607 clear(binding.fillLinkFromClipboard.id, TOP)
608 connect(binding.fillLinkFromClipboard.id, BOTTOM, binding.pillWrapper.id, TOP)
610 clear(binding.fillLinkDivider.id, TOP)
611 connect(binding.fillLinkDivider.id, BOTTOM, binding.fillLinkFromClipboard.id, TOP)
613 applyTo(binding.searchWrapper)
618 private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) {
620 val showHint = state.showSearchSuggestionsHint &&
621 !state.showSearchShortcuts &&
622 state.url != state.query
624 binding.searchSuggestionsHint.isVisible = showHint
625 binding.searchSuggestionsHintDivider.isVisible = showHint
629 private fun addVoiceSearchButton(searchFragmentState: SearchFragmentState) {
630 if (voiceSearchButtonAlreadyAdded) return
631 val searchEngine = searchFragmentState.searchEngineSource.searchEngine
634 searchEngine?.id?.contains("google") == true &&
635 isSpeechAvailable() &&
636 requireContext().settings().shouldShowVoiceSearch
639 toolbarView.view.addEditAction(
640 BrowserToolbar.Button(
641 AppCompatResources.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
642 requireContext().getString(R.string.voice_search_content_description),
644 listener = ::launchVoiceSearch
647 voiceSearchButtonAlreadyAdded = true
651 @Suppress("DEPRECATION")
652 // https://github.com/mozilla-mobile/fenix/issues/19919
653 private fun launchVoiceSearch() {
654 // Note if a user disables speech while the app is on the search fragment
655 // the voice button will still be available and *will* cause a crash if tapped,
656 // since the `visible` call is only checked on create. In order to avoid extra complexity
657 // around such a small edge case, we make the button have no functionality in this case.
658 if (!isSpeechAvailable()) { return }
660 requireComponents.analytics.metrics.track(Event.VoiceSearchTapped)
662 putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
663 putExtra(RecognizerIntent.EXTRA_PROMPT, requireContext().getString(R.string.voice_search_explainer))
665 startActivityForResult(speechIntent, VoiceSearchActivity.SPEECH_REQUEST_CODE)
668 private fun isSpeechAvailable(): Boolean = speechIntent.resolveActivity(requireContext().packageManager) != null
670 private fun updateClipboardSuggestion(
671 shouldShowView: Boolean
673 binding.fillLinkFromClipboard.isVisible = shouldShowView
674 binding.fillLinkDivider.isVisible = shouldShowView
675 binding.pillWrapperDivider.isVisible =
676 !(shouldShowView && requireComponents.settings.shouldUseBottomToolbar)
677 binding.clipboardTitle.isVisible = shouldShowView
678 binding.linkIcon.isVisible = shouldShowView
680 if (shouldShowView) {
681 val contentDescription = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
682 "${binding.clipboardTitle.text}."
684 val clipboardUrl = context?.components?.clipboardHandler?.extractURL()
686 if (clipboardUrl != null && !((activity as HomeActivity).browsingModeManager.mode.isPrivate)) {
687 requireComponents.core.engine.speculativeConnect(clipboardUrl)
689 binding.clipboardUrl.text = clipboardUrl
690 binding.clipboardUrl.isVisible = shouldShowView
691 "${binding.clipboardTitle.text}, ${binding.clipboardUrl.text}."
694 binding.fillLinkFromClipboard.contentDescription = contentDescription
698 private fun updateToolbarContentDescription(source: SearchEngineSource) {
699 source.searchEngine?.let { engine ->
700 toolbarView.view.contentDescription = engine.name + ", " + inlineAutocompleteEditText.hint
703 inlineAutocompleteEditText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
706 private fun updateSearchShortcutsIcon(
707 areShortcutsAvailable: Boolean,
708 showShortcuts: Boolean
711 binding.searchEnginesShortcutButton.isVisible = areShortcutsAvailable
712 binding.searchEnginesShortcutButton.isChecked = showShortcuts
714 val color = if (showShortcuts) R.attr.contrastText else R.attr.textPrimary
715 binding.searchEnginesShortcutButton.compoundDrawables[0]?.setTint(
716 requireContext().getColorFromAttr(color)
722 private const val QR_FRAGMENT_TAG = "MOZAC_QR_FRAGMENT"
723 private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1