1
0
Fork 0
fenix/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt

360 lines
14 KiB
Kotlin
Raw Normal View History

/* 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.search
import android.Manifest
import android.content.Context
import android.content.DialogInterface
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.ITALIC
import android.os.Bundle
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
2019-08-08 00:41:52 +02:00
import androidx.transition.TransitionInflater
2019-03-25 22:13:22 +01:00
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.*
import kotlinx.android.synthetic.main.search_suggestions_onboarding.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.concept.storage.HistoryStorage
2019-04-19 23:12:42 +02:00
import mozilla.components.feature.qr.QrFeature
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
2019-04-19 23:12:42 +02:00
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getSpannable
2019-11-25 21:36:47 +01:00
import org.mozilla.fenix.ext.hideToolbar
2019-02-20 17:58:42 +01:00
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.awesomebar.AwesomeBarView
import org.mozilla.fenix.search.toolbar.ToolbarView
import org.mozilla.fenix.settings.SupportUtils
@Suppress("TooManyFunctions", "LargeClass")
class SearchFragment : Fragment(), UserInteractionHandler {
private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarView: AwesomeBarView
2019-04-19 23:12:42 +02:00
private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
private var permissionDidUpdate = false
private lateinit var searchStore: SearchFragmentStore
private lateinit var searchInteractor: SearchInteractor
2019-01-24 21:50:30 +01:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2019-08-08 00:41:52 +02:00
postponeEnterTransition()
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
.setDuration(
SHARED_TRANSITION_MS
)
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
}
override fun onCreateView(
2019-01-30 17:36:14 +01:00
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val session = arguments
?.let(SearchFragmentArgs.Companion::fromBundle)
?.let { it.sessionId }
?.let(requireComponents.core.sessionManager::findSessionById)
val pastedText = arguments
?.let(SearchFragmentArgs.Companion::fromBundle)
?.let { it.pastedText }
val searchAccessPoint = arguments
?.let(SearchFragmentArgs.Companion::fromBundle)?.searchAccessPoint
val view = inflater.inflate(R.layout.fragment_search, container, false)
2019-09-14 20:10:11 +02:00
val url = session?.url.orEmpty()
val currentSearchEngine = SearchEngineSource.Default(
Adds custom search engines (#6551) * For #5577 - Adds button to add a new search engine * For #5577 - Adds custom engine store * For #5577 - Creates a custom SearchEngineProvider * For #5577 - Gives the ability to delete search engines * For #5577 - Adds the UI to add a custom search engine * For #5577 - Adds form to create a custom search engine * For #5577 - Adds the ability to add a custom search engine * For #5577 - Adds the ability to delete custom search engines * For #5577 - Selects the first element on the add custom search engine screen * For #5577 - Prevents adding a search engine that already exists * For #5577 - Styles the add search engine preference * For #5577 - Makes the name check case-insensitive * For #5577 - Fix bug where home screen doesnt see new search engines * For #5577 - Moves Search URL validation to its own type * For #5577 - Fixes linting errors * For #5577 - Adds the ability to edit a custom search engine * For #5577 - Allows the user to edit a serach engine even when it is the last item in the list * For #5577 - Adds an undo snackbar when deleting a search engine * For #5577 - Moves all of the strings to be translated * For #5577 - Fixes bug when deleting your default search engine * For #5577 - Puts adding search engines behind a feature flag * For #5577 - Navigate to custom search engine SUMO article when tapping learn more * For #5577 - Fixes nits * For #5577 - Uses concept-fetch to validate search string * For #5577 - Adds string resources for the cannot reach error state
2019-11-20 01:30:56 +01:00
requireComponents.search.provider.getDefaultEngine(requireContext())
)
2019-02-20 17:58:42 +01:00
val showSearchSuggestions =
if ((activity as HomeActivity).browsingModeManager.mode.isPrivate) {
requireContext().settings().shouldShowSearchSuggestions &&
requireContext().settings().shouldShowSearchSuggestionsInPrivate
} else {
requireContext().settings().shouldShowSearchSuggestions
}
searchStore = StoreProvider.get(this) {
SearchFragmentStore(
SearchFragmentState(
query = url,
searchEngineSource = currentSearchEngine,
defaultEngineSource = currentSearchEngine,
showSearchSuggestions = showSearchSuggestions,
showSearchSuggestionsHint = false,
showSearchShortcuts = requireContext().settings().shouldShowSearchShortcuts && url.isEmpty(),
showClipboardSuggestions = requireContext().settings().shouldShowClipboardSuggestions,
showHistorySuggestions = requireContext().settings().shouldShowHistorySuggestions,
showBookmarkSuggestions = requireContext().settings().shouldShowBookmarkSuggestions,
session = session,
pastedText = pastedText,
searchAccessPoint = searchAccessPoint
)
)
}
2019-03-25 22:13:22 +01:00
val searchController = DefaultSearchController(
activity as HomeActivity,
searchStore,
findNavController()
)
searchInteractor = SearchInteractor(
searchController
)
awesomeBarView = AwesomeBarView(view.search_layout, searchInteractor)
toolbarView = ToolbarView(
view.toolbar_component_wrapper,
searchInteractor,
historyStorageProvider(),
2019-08-07 22:02:08 +02:00
(activity as HomeActivity).browsingModeManager.mode.isPrivate
)
val urlView = toolbarView.view
.findViewById<InlineAutocompleteEditText>(R.id.mozac_browser_toolbar_edit_url_view)
urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
2019-08-08 00:41:52 +02:00
startPostponedEnterTransition()
return view
}
@ExperimentalCoroutinesApi
@SuppressWarnings("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
search_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE
2019-04-19 23:12:42 +02:00
qrFeature.set(
QrFeature(
requireContext(),
2019-09-26 20:04:42 +02:00
fragmentManager = parentFragmentManager,
2019-04-19 23:12:42 +02:00
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
},
onScanResult = { result ->
search_scan_button.isChecked = false
activity?.let {
AlertDialog.Builder(it).apply {
val spannable = resources.getSpannable(
R.string.qr_scanner_confirmation_dialog_message,
listOf(
getString(R.string.app_name) to listOf(StyleSpan(BOLD)),
result to listOf(StyleSpan(ITALIC))
)
)
setMessage(spannable)
setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ ->
requireComponents.analytics.metrics.track(Event.QRScannerNavigationDenied)
dialog.cancel()
}
setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ ->
requireComponents.analytics.metrics.track(Event.QRScannerNavigationAllowed)
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = result,
newTab = searchStore.state.session == null,
from = BrowserDirection.FromSearch
)
dialog.dismiss()
}
create()
}.show()
requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed)
}
2019-04-19 23:12:42 +02:00
}),
owner = this,
view = view
)
view.search_scan_button.setOnClickListener {
toolbarView.view.clearFocus()
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
2019-04-19 23:12:42 +02:00
qrFeature.get()?.scan(R.id.container)
}
view.back_button.setOnClickListener {
findNavController().navigateUp()
}
val stubListener = ViewStub.OnInflateListener { _, inflated ->
inflated.learn_more.setOnClickListener {
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
SupportUtils.SumoTopic.SEARCH_SUGGESTION
),
newTab = searchStore.state.session == null,
from = BrowserDirection.FromSearch
)
}
inflated.allow.setOnClickListener {
inflated.visibility = View.GONE
context?.settings()?.shouldShowSearchSuggestionsInPrivate = true
context?.settings()?.showSearchSuggestionsInPrivateOnboardingFinished = true
requireComponents.analytics.metrics.track(Event.PrivateBrowsingShowSearchSuggestions)
}
inflated.dismiss.setOnClickListener {
inflated.visibility = View.GONE
context?.settings()?.shouldShowSearchSuggestionsInPrivate = false
context?.settings()?.showSearchSuggestionsInPrivateOnboardingFinished = true
}
inflated.text.text =
getString(R.string.search_suggestions_onboarding_text, getString(R.string.app_name))
inflated.title.text =
getString(R.string.search_suggestions_onboarding_title)
}
view.search_suggestions_onboarding.setOnInflateListener((stubListener))
2019-01-31 00:51:49 +01:00
view.toolbar_wrapper.clipToOutline = false
2019-03-25 22:13:22 +01:00
fill_link_from_clipboard.setOnClickListener {
(activity as HomeActivity)
.openToBrowserAndLoad(
2019-09-18 20:23:32 +02:00
searchTermOrURL = requireContext().components.clipboardHandler.url ?: "",
newTab = searchStore.state.session == null,
from = BrowserDirection.FromSearch
)
}
consumeFrom(searchStore) {
awesomeBarView.update(it)
toolbarView.update(it)
updateSearchWithLabel(it)
2019-09-18 20:23:32 +02:00
updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url)
updateSearchSuggestionsHintVisibility(it)
}
startPostponedEnterTransition()
}
override fun onResume() {
super.onResume()
// The user has the option to go to 'Shortcuts' -> 'Search engine settings' to modify the default search engine.
// When returning from that settings screen we need to update it to account for any changes.
2019-08-08 00:41:52 +02:00
val currentDefaultEngine =
Adds custom search engines (#6551) * For #5577 - Adds button to add a new search engine * For #5577 - Adds custom engine store * For #5577 - Creates a custom SearchEngineProvider * For #5577 - Gives the ability to delete search engines * For #5577 - Adds the UI to add a custom search engine * For #5577 - Adds form to create a custom search engine * For #5577 - Adds the ability to add a custom search engine * For #5577 - Adds the ability to delete custom search engines * For #5577 - Selects the first element on the add custom search engine screen * For #5577 - Prevents adding a search engine that already exists * For #5577 - Styles the add search engine preference * For #5577 - Makes the name check case-insensitive * For #5577 - Fix bug where home screen doesnt see new search engines * For #5577 - Moves Search URL validation to its own type * For #5577 - Fixes linting errors * For #5577 - Adds the ability to edit a custom search engine * For #5577 - Allows the user to edit a serach engine even when it is the last item in the list * For #5577 - Adds an undo snackbar when deleting a search engine * For #5577 - Moves all of the strings to be translated * For #5577 - Fixes bug when deleting your default search engine * For #5577 - Puts adding search engines behind a feature flag * For #5577 - Navigate to custom search engine SUMO article when tapping learn more * For #5577 - Fixes nits * For #5577 - Uses concept-fetch to validate search string * For #5577 - Adds string resources for the cannot reach error state
2019-11-20 01:30:56 +01:00
requireComponents.search.provider.getDefaultEngine(requireContext())
if (searchStore.state.defaultEngineSource.searchEngine != currentDefaultEngine) {
searchStore.dispatch(
SearchFragmentAction.SelectNewDefaultSearchEngine
(currentDefaultEngine)
)
}
if (!permissionDidUpdate) {
toolbarView.view.requestFocus()
}
updateClipboardSuggestion(
searchStore.state,
requireContext().components.clipboardHandler.url
)
permissionDidUpdate = false
2019-11-25 21:36:47 +01:00
hideToolbar()
}
override fun onPause() {
super.onPause()
toolbarView.view.clearFocus()
}
override fun onBackPressed(): Boolean {
return when {
qrFeature.onBackPressed() -> {
view?.search_scan_button?.isChecked = false
toolbarView.view.requestFocus()
}
else -> awesomeBarView.isKeyboardDismissedProgrammatically
}
}
private fun updateSearchWithLabel(searchState: SearchFragmentState) {
search_with_shortcuts.visibility =
if (searchState.showSearchShortcuts) View.VISIBLE else View.GONE
}
private fun updateClipboardSuggestion(searchState: SearchFragmentState, clipboardUrl: String?) {
val visibility =
if (searchState.showClipboardSuggestions && searchState.query.isEmpty() && !clipboardUrl.isNullOrEmpty())
View.VISIBLE else View.GONE
fill_link_from_clipboard.visibility = visibility
divider_line.visibility = visibility
clipboard_url.text = clipboardUrl
}
2019-08-08 00:41:52 +02:00
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CODE_CAMERA_PERMISSIONS -> qrFeature.withFeature {
it.onPermissionsResult(permissions, grantResults)
context?.let { context: Context ->
if (context.isPermissionGranted(Manifest.permission.CAMERA)) {
permissionDidUpdate = true
} else {
view?.search_scan_button?.isChecked = false
}
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private fun historyStorageProvider(): HistoryStorage? {
return if (requireContext().settings().shouldShowHistorySuggestions) {
requireComponents.core.historyStorage
} else null
}
private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) {
view?.apply {
findViewById<View>(R.id.search_suggestions_onboarding)?.isVisible = state.showSearchSuggestionsHint
search_suggestions_onboarding_divider?.isVisible =
search_with_shortcuts.isVisible && state.showSearchSuggestionsHint
}
}
2019-04-19 23:12:42 +02:00
companion object {
2019-08-08 00:41:52 +02:00
private const val SHARED_TRANSITION_MS = 200L
2019-04-19 23:12:42 +02:00
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
}
}