1
0
Fork 0

For #11866 - Update back press logic for SearchFragment

master
ekager 2020-06-23 20:20:48 -04:00 committed by Emily Kager
parent 7be56ce4fa
commit c77ddd8d26
5 changed files with 31 additions and 257 deletions

View File

@ -7,9 +7,6 @@ package org.mozilla.fenix.search
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
@ -51,7 +48,6 @@ class DefaultSearchController(
private val activity: HomeActivity, private val activity: HomeActivity,
private val store: SearchFragmentStore, private val store: SearchFragmentStore,
private val navController: NavController, private val navController: NavController,
private val viewLifecycleScope: CoroutineScope,
private val clearToolbarFocus: () -> Unit private val clearToolbarFocus: () -> Unit
) : SearchController { ) : SearchController {
@ -101,13 +97,7 @@ class DefaultSearchController(
} }
override fun handleEditingCancelled() { override fun handleEditingCancelled() {
viewLifecycleScope.launch { clearToolbarFocus()
clearToolbarFocus()
// Delay a short amount so the keyboard begins animating away. This makes exit animation
// much smoother instead of having two separate parts (keyboard hides THEN animation)
delay(KEYBOARD_ANIMATION_DELAY)
navController.popBackStack()
}
} }
override fun handleTextChanged(text: String) { override fun handleTextChanged(text: String) {
@ -199,8 +189,4 @@ class DefaultSearchController(
handleExistingSessionSelected(session) handleExistingSessionSelected(session)
} }
} }
companion object {
internal const val KEYBOARD_ANIMATION_DELAY = 5L
}
} }

View File

@ -24,7 +24,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
@ -120,7 +119,6 @@ class SearchFragment : Fragment(), UserInteractionHandler {
activity = activity, activity = activity,
store = searchStore, store = searchStore,
navController = findNavController(), navController = findNavController(),
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
clearToolbarFocus = ::clearToolbarFocus clearToolbarFocus = ::clearToolbarFocus
) )
@ -182,6 +180,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
} }
private fun clearToolbarFocus() { private fun clearToolbarFocus() {
toolbarView.view.hideKeyboard()
toolbarView.view.clearFocus() toolbarView.view.clearFocus()
} }
@ -347,14 +346,14 @@ class SearchFragment : Fragment(), UserInteractionHandler {
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
// Note: Actual navigation happens in `handleEditingCancelled` in SearchController
return when { return when {
qrFeature.onBackPressed() -> { qrFeature.onBackPressed() -> {
toolbarView.view.edit.focus() toolbarView.view.edit.focus()
view?.search_scan_button?.isChecked = false view?.search_scan_button?.isChecked = false
toolbarView.view.requestFocus() toolbarView.view.requestFocus()
true
} }
else -> true else -> false
} }
} }
@ -426,7 +425,6 @@ class SearchFragment : Fragment(), UserInteractionHandler {
} }
companion object { companion object {
private const val SHARED_TRANSITION_MS = 250L
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
} }
} }

View File

@ -59,7 +59,6 @@ class ToolbarView(
) { ) {
private var isInitialized = false private var isInitialized = false
private var hasBeenCanceled = false
init { init {
view.apply { view.apply {
@ -96,19 +95,18 @@ class ToolbarView(
) )
edit.setUrlBackground( edit.setUrlBackground(
AppCompatResources.getDrawable(context, R.drawable.search_url_background)) AppCompatResources.getDrawable(context, R.drawable.search_url_background)
)
private = isPrivate private = isPrivate
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener { setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean { override fun onCancelEditing(): Boolean {
// For some reason, this can be triggered twice on one back press. This only leads to interactor.onEditingCanceled()
// navigateUp, so let's make sure we only call it once
if (!hasBeenCanceled) interactor.onEditingCanceled()
hasBeenCanceled = true
// We need to return false to not show display mode // We need to return false to not show display mode
return false return false
} }
override fun onTextChanged(text: String) { override fun onTextChanged(text: String) {
url = text url = text
this@ToolbarView.interactor.onTextChanged(text) this@ToolbarView.interactor.onTextChanged(text)
@ -144,13 +142,15 @@ class ToolbarView(
isInitialized = true isInitialized = true
} }
val iconSize = context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size) val iconSize =
context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val scaledIcon = Bitmap.createScaledBitmap( val scaledIcon = Bitmap.createScaledBitmap(
searchState.searchEngineSource.searchEngine.icon, searchState.searchEngineSource.searchEngine.icon,
iconSize, iconSize,
iconSize, iconSize,
true) true
)
val icon = BitmapDrawable(context.resources, scaledIcon) val icon = BitmapDrawable(context.resources, scaledIcon)

View File

@ -4,7 +4,6 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import io.mockk.every import io.mockk.every
@ -32,7 +31,6 @@ import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.searchEngineManager import org.mozilla.fenix.ext.searchEngineManager
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.search.DefaultSearchController.Companion.KEYBOARD_ANIMATION_DELAY
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.whatsnew.clear import org.mozilla.fenix.whatsnew.clear
@ -49,7 +47,6 @@ class DefaultSearchControllerTest {
private val searchEngine: SearchEngine = mockk(relaxed = true) private val searchEngine: SearchEngine = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true) private val metrics: MetricController = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true) private val sessionManager: SessionManager = mockk(relaxed = true)
private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
private val clearToolbarFocus: (() -> Unit) = mockk(relaxed = true) private val clearToolbarFocus: (() -> Unit) = mockk(relaxed = true)
private lateinit var controller: DefaultSearchController private lateinit var controller: DefaultSearchController
@ -67,7 +64,6 @@ class DefaultSearchControllerTest {
activity = activity, activity = activity,
store = store, store = store,
navController = navController, navController = navController,
viewLifecycleScope = lifecycleScope,
clearToolbarFocus = clearToolbarFocus clearToolbarFocus = clearToolbarFocus
) )
@ -123,17 +119,13 @@ class DefaultSearchControllerTest {
activity = activity, activity = activity,
store = store, store = store,
navController = navController, navController = navController,
viewLifecycleScope = this,
clearToolbarFocus = clearToolbarFocus clearToolbarFocus = clearToolbarFocus
) )
controller.handleEditingCancelled() controller.handleEditingCancelled()
advanceTimeBy(KEYBOARD_ANIMATION_DELAY)
verify { verify {
clearToolbarFocus() clearToolbarFocus()
navController.popBackStack()
} }
} }

View File

@ -4,249 +4,84 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore.PREF_FILE_SEARCH_ENGINES
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.searchEngineManager
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class SearchInteractorTest { class SearchInteractorTest {
private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true) lateinit var searchController: DefaultSearchController
private val clearToolbarFocus = { } lateinit var interactor: SearchInteractor
@Before
fun setup() {
searchController = mockk(relaxed = true)
interactor = SearchInteractor(
searchController
)
}
@Test @Test
fun onUrlCommitted() { fun onUrlCommitted() {
val context: HomeActivity = mockk(relaxed = true)
val store: SearchFragmentStore = mockk()
val state: SearchFragmentState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every { state.searchAccessPoint } returns searchAccessPoint
every {
context.getSharedPreferences(
PREF_FILE_SEARCH_ENGINES,
Context.MODE_PRIVATE
)
} returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onUrlCommitted("test") interactor.onUrlCommitted("test")
verify { verify {
context.openToBrowserAndLoad( searchController.handleUrlCommitted("test")
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine
)
} }
} }
@Test @Test
fun onEditingCanceled() = runBlockingTest { fun onEditingCanceled() = runBlockingTest {
val navController: NavController = mockk(relaxed = true)
val store: SearchFragmentStore = mockk(relaxed = true)
every { store.state } returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
navController,
this,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onEditingCanceled() interactor.onEditingCanceled()
advanceTimeBy(DefaultSearchController.KEYBOARD_ANIMATION_DELAY)
verify { verify {
clearToolbarFocus() searchController.handleEditingCancelled()
navController.popBackStack()
} }
} }
@Test @Test
fun onTextChanged() { fun onTextChanged() {
val store: SearchFragmentStore = mockk(relaxed = true)
val context: HomeActivity = mockk(relaxed = true)
every { store.state } returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
interactor.onTextChanged("test") interactor.onTextChanged("test")
verify { store.dispatch(SearchFragmentAction.UpdateQuery("test")) } verify { searchController.handleTextChanged("test") }
verify { store.dispatch(SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(false)) }
} }
@Test @Test
fun onUrlTapped() { fun onUrlTapped() {
val context: HomeActivity = mockk()
val store: SearchFragmentStore = mockk()
val state: SearchFragmentState = mockk()
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
every { context.metrics } returns mockk(relaxed = true)
every { context.openToBrowserAndLoad(any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every {
context.getSharedPreferences(
PREF_FILE_SEARCH_ENGINES,
Context.MODE_PRIVATE
)
} returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onUrlTapped("test") interactor.onUrlTapped("test")
verify { verify {
context.openToBrowserAndLoad( searchController.handleUrlTapped("test")
"test",
true,
BrowserDirection.FromSearch
)
} }
} }
@Test @Test
fun onSearchTermsTapped() { fun onSearchTermsTapped() {
val context: HomeActivity = mockk(relaxed = true)
val store: SearchFragmentStore = mockk()
val state: SearchFragmentState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk(relaxed = true))
val searchAccessPoint: Event.PerformedSearch.SearchAccessPoint = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
every { state.searchAccessPoint } returns searchAccessPoint
every {
context.getSharedPreferences(
PREF_FILE_SEARCH_ENGINES,
Context.MODE_PRIVATE
)
} returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onSearchTermsTapped("test") interactor.onSearchTermsTapped("test")
verify { verify {
context.openToBrowserAndLoad( searchController.handleSearchTermsTapped("test")
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine,
forceSearch = true
)
} }
} }
@Test @Test
fun onSearchShortcutEngineSelected() { fun onSearchShortcutEngineSelected() {
val context: HomeActivity = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
val store: SearchFragmentStore = mockk(relaxed = true)
val state: SearchFragmentState = mockk(relaxed = true)
every { store.state } returns state
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk(),
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
val searchEngine: SearchEngine = mockk(relaxed = true) val searchEngine: SearchEngine = mockk(relaxed = true)
interactor.onSearchShortcutEngineSelected(searchEngine) interactor.onSearchShortcutEngineSelected(searchEngine)
verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) } verify { searchController.handleSearchShortcutEngineSelected(searchEngine) }
} }
@Test @Test
fun onSearchShortcutsButtonClicked() { fun onSearchShortcutsButtonClicked() {
val searchController: SearchController = mockk(relaxed = true)
val interactor = SearchInteractor(searchController)
interactor.onSearchShortcutsButtonClicked() interactor.onSearchShortcutsButtonClicked()
verify { searchController.handleSearchShortcutsButtonClicked() } verify { searchController.handleSearchShortcutsButtonClicked() }
@ -254,58 +89,21 @@ class SearchInteractorTest {
@Test @Test
fun onClickSearchEngineSettings() { fun onClickSearchEngineSettings() {
val navController: NavController = mockk()
val store: SearchFragmentStore = mockk()
every { store.state } returns mockk(relaxed = true)
every { navController.currentDestination?.id } returns R.id.searchFragment
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
navController,
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
every { navController.navigate(any() as NavDirections) } just Runs
interactor.onClickSearchEngineSettings() interactor.onClickSearchEngineSettings()
verify { verify {
navController.navigateSafe( searchController.handleClickSearchEngineSettings()
R.id.searchFragment,
SearchFragmentDirections.actionGlobalSearchEngineFragment()
)
} }
} }
@Test @Test
fun onExistingSessionSelected() { fun onExistingSessionSelected() {
val navController: NavController = mockk(relaxed = true)
val context: HomeActivity = mockk(relaxed = true)
val applicationContext: FenixApplication = mockk(relaxed = true)
every { context.applicationContext } returns applicationContext
val store: SearchFragmentStore = mockk()
every { context.openToBrowser(any(), any()) } just Runs
every { store.state } returns mockk(relaxed = true)
val searchController: SearchController = DefaultSearchController(
context,
store,
navController,
lifecycleScope,
clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
val session = Session("http://mozilla.org", false) val session = Session("http://mozilla.org", false)
interactor.onExistingSessionSelected(session) interactor.onExistingSessionSelected(session)
verify { verify {
context.openToBrowser(BrowserDirection.FromSearch) searchController.handleExistingSessionSelected(session)
} }
} }
} }