Skip to content
This repository has been archived by the owner on Apr 17, 2021. It is now read-only.

Commit

Permalink
Issue #1664: Move toolbar UI code to ToolbarUiController.
Browse files Browse the repository at this point in the history
The NavigationOverlayFragment is starting to get unwieldly: it's 550+
lines and it's hard to figure out which code is related to the toolbar
and which code is related to the bottom navigation panel. Since they're
being split, now is an opportune time to separate their file contents.
Moving the toolbar code out:
- Reduces NavOverlayFrag to ~430 lines
- Makes the dependencies between the toolbar and bottom navigation panel
more obvious
- Makes the dependencies between the toolbar and the framework more
obvious
- Makes the shared code more obvious

No functionality was changed and no code was cleaned up: I essentially
just moved functions, updated references, and added the necessary
parameters.

This refactor isn't perfect - e.g. should the NavOverlayFragment really
always handle `onNavigationEvent`? - but is intended to make things easier
to change later.
  • Loading branch information
mcomella committed Jan 29, 2019
1 parent e0b3d58 commit 69e5a0f
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,14 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ScrollView
import android.widget.Toast
import kotlinx.android.synthetic.main.browser_overlay.*
import kotlinx.android.synthetic.main.browser_overlay_top_nav.*
import kotlinx.android.synthetic.main.pocket_video_mega_tile.*
import kotlinx.coroutines.Job
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.tv.firefox.MainActivity
import org.mozilla.tv.firefox.R
import org.mozilla.tv.firefox.ext.forEachChild
import org.mozilla.tv.firefox.ext.forceExhaustive
import org.mozilla.tv.firefox.ext.isEffectivelyVisible
import org.mozilla.tv.firefox.ext.isVisible
Expand All @@ -49,15 +45,9 @@ import org.mozilla.tv.firefox.settings.SettingsFragment
import org.mozilla.tv.firefox.telemetry.TelemetryIntegration
import org.mozilla.tv.firefox.telemetry.UrlTextInputLocation
import org.mozilla.tv.firefox.utils.ServiceLocator
import org.mozilla.tv.firefox.utils.URLs
import org.mozilla.tv.firefox.utils.ViewUtils
import org.mozilla.tv.firefox.widget.IgnoreFocusMovementMethod
import org.mozilla.tv.firefox.widget.InlineAutocompleteEditText
import java.lang.ref.WeakReference

private const val NAVIGATION_BUTTON_ENABLED_ALPHA = 1.0f
private const val NAVIGATION_BUTTON_DISABLED_ALPHA = 0.3f

private const val SHOW_UNPIN_TOAST_COUNTER_PREF = "show_upin_toast_counter"
private const val MAX_UNPIN_TOAST_COUNT = 3

Expand All @@ -84,7 +74,7 @@ enum class NavigationEvent {
}

@Suppress("LargeClass") // TODO remove this. See https://github.com/mozilla-mobile/firefox-tv/issues/1187
class NavigationOverlayFragment : Fragment(), View.OnClickListener {
class NavigationOverlayFragment : Fragment() {
companion object {
const val FRAGMENT_TAG = "overlay"
}
Expand Down Expand Up @@ -128,8 +118,6 @@ class NavigationOverlayFragment : Fragment(), View.OnClickListener {
private var currFocus: View? = null
get() = activity?.currentFocus

private var hasUserChangedURLSinceEditTextFocused = false

private lateinit var serviceLocator: ServiceLocator
private lateinit var toolbarViewModel: ToolbarViewModel
private lateinit var pinnedTileViewModel: PinnedTileViewModel
Expand All @@ -156,13 +144,14 @@ class NavigationOverlayFragment : Fragment(), View.OnClickListener {
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
topNavContainer.forEachChild {
it.nextFocusDownId = navUrlInput.id
if (it.isFocusable) it.setOnClickListener(this)
}
ToolbarUiController(
toolbarViewModel,
::exitFirefox,
{ currFocus },
::updateFocusableViews,
onNavigationEvent
).onCreateView(view, viewLifecycleOwner, fragmentManager!!)

setupUrlInput()
initToolbar()
initMegaTile()
initPinnedTiles()

Expand Down Expand Up @@ -193,92 +182,6 @@ class NavigationOverlayFragment : Fragment(), View.OnClickListener {
updateFocusableViews()
}

private fun setupUrlInput() = with(navUrlInput) {
setOnCommitListener {
val userInput = text.toString()
if (userInput == URLs.APP_URL_HOME) {
// If the input points to home, we short circuit and hide the keyboard, returning
// the user to the home screen
this.hideKeyboard()
return@setOnCommitListener
}

if (userInput.isNotEmpty()) {
val cachedAutocompleteResult = lastAutocompleteResult // setText clears the reference so we cache it here.
setText(cachedAutocompleteResult.text)
onNavigationEvent.invoke(NavigationEvent.LOAD_URL, userInput, cachedAutocompleteResult)
}
}
this.movementMethod = IgnoreFocusMovementMethod()
val autocompleteProvider = ShippedDomainsProvider().apply {
initialize(
context = context
)
}
setOnFilterListener { searchText, view ->
val result = autocompleteProvider.getAutocompleteSuggestion(searchText)
if (result != null)
view?.onAutocomplete(InlineAutocompleteEditText.AutocompleteResult(result.text, result.source, result.totalItems))
}

setOnUserInputListener { hasUserChangedURLSinceEditTextFocused = true }
setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hasUserChangedURLSinceEditTextFocused = false }
}

private fun initToolbar() {
fun updateOverlayButtonState(isEnabled: Boolean, overlayButton: ImageButton) {
overlayButton.isEnabled = isEnabled
overlayButton.isFocusable = isEnabled
overlayButton.alpha =
if (isEnabled) NAVIGATION_BUTTON_ENABLED_ALPHA else NAVIGATION_BUTTON_DISABLED_ALPHA
}

toolbarViewModel.state.observe(viewLifecycleOwner, Observer {
if (it == null) return@Observer
val focusedView = currFocus
updateOverlayButtonState(it.backEnabled, navButtonBack)
updateOverlayButtonState(it.forwardEnabled, navButtonForward)
updateOverlayButtonState(it.pinEnabled, pinButton)
updateOverlayButtonState(it.refreshEnabled, navButtonReload)
updateOverlayButtonState(it.desktopModeEnabled, desktopModeButton)

updateFocusableViews(focusedView)

pinButton.isChecked = it.pinChecked
desktopModeButton.isChecked = it.desktopModeChecked
turboButton.isChecked = it.turboChecked

if (!hasUserChangedURLSinceEditTextFocused) {
// The url can get updated in the background, e.g. if a loading page is redirected. We
// don't want a url update to interrupt the user typing so we don't update the url from
// the background if the user has already updated the url themselves.
//
// We revert this state when the view is unfocused: it ensures the URL is usually accurate
// (for security reasons) and it's simple compared to other options which keep more state.
//
// One problem this solution has is that if the URL is updated in the background rapidly,
// sometimes key events will be dropped, but I don't think there's much we can do about this:
// we can't determine if the keyboard is up or not and focus isn't a good indicator because
// we can focus the EditText without opening the soft keyboard and the user won't even know
// these are inaccurate!
navUrlInput.setText(it.urlBarText)
}
})

toolbarViewModel.events.observe(viewLifecycleOwner, Observer {
it?.consume {
when (it) {
is ToolbarViewModel.Action.ShowTopToast -> ViewUtils.showCenteredTopToast(context, it.textId)
is ToolbarViewModel.Action.ShowBottomToast -> ViewUtils.showCenteredBottomToast(context, it.textId)
is ToolbarViewModel.Action.SetOverlayVisible -> serviceLocator.screenController
.showNavigationOverlay(fragmentManager, it.visible)
ToolbarViewModel.Action.ExitFirefox -> exitFirefox()
}.forceExhaustive
true
}
})
}

private fun exitFirefox() {
activity!!.moveTaskToBack(true)
}
Expand Down Expand Up @@ -311,7 +214,10 @@ class NavigationOverlayFragment : Fragment(), View.OnClickListener {
}

private fun initMegaTile() {
pocketVideoMegaTileView.setOnClickListener(this)
pocketVideoMegaTileView.setOnClickListener { view ->
val event = NavigationEvent.fromViewClick(view.id) ?: return@setOnClickListener
onNavigationEvent.invoke(event, null, null)
}

pocketViewModel.state.observe(viewLifecycleOwner, Observer { state ->
when (state) {
Expand Down Expand Up @@ -386,23 +292,6 @@ class NavigationOverlayFragment : Fragment(), View.OnClickListener {
}
}

override fun onClick(view: View?) {
val event = NavigationEvent.fromViewClick(view?.id)
?: return

when (event) {
NavigationEvent.BACK -> toolbarViewModel.backButtonClicked()
NavigationEvent.FORWARD -> toolbarViewModel.forwardButtonClicked()
NavigationEvent.RELOAD -> toolbarViewModel.reloadButtonClicked()
NavigationEvent.PIN_ACTION -> toolbarViewModel.pinButtonClicked()
NavigationEvent.TURBO -> toolbarViewModel.turboButtonClicked()
NavigationEvent.DESKTOP_MODE -> toolbarViewModel.desktopModeButtonClicked()
NavigationEvent.EXIT_FIREFOX -> toolbarViewModel.exitFirefoxButtonClicked()
else -> Unit // Nothing to do.
}
onNavigationEvent.invoke(event, null, null)
}

override fun onCreateContextMenu(menu: ContextMenu?, v: View?, menuInfo: ContextMenu.ContextMenuInfo?) {
activity?.menuInflater?.inflate(R.menu.menu_context_hometile, menu)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* 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.tv.firefox.navigationoverlay

import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.Observer
import android.support.v4.app.FragmentManager
import android.view.View
import android.widget.ImageButton
import kotlinx.android.synthetic.main.browser_overlay.view.*
import kotlinx.android.synthetic.main.browser_overlay_top_nav.view.*
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.support.ktx.android.view.forEach
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.tv.firefox.ext.forceExhaustive
import org.mozilla.tv.firefox.ext.serviceLocator
import org.mozilla.tv.firefox.utils.URLs
import org.mozilla.tv.firefox.utils.ViewUtils
import org.mozilla.tv.firefox.widget.IgnoreFocusMovementMethod
import org.mozilla.tv.firefox.widget.InlineAutocompleteEditText

private const val NAVIGATION_BUTTON_ENABLED_ALPHA = 1.0f
private const val NAVIGATION_BUTTON_DISABLED_ALPHA = 0.3f

/**
* An encapsulation of the toolbar to set up and respond to UI operations.
*/
class ToolbarUiController(
private val toolbarViewModel: ToolbarViewModel,
private val exitFirefox: () -> Unit,
private val getCurrentFocus: () -> View?,
private val updateFocusableViews: (View?) -> Unit,
private val onNavigationEvent: (NavigationEvent, String?, InlineAutocompleteEditText.AutocompleteResult?) -> Unit
) {

private var hasUserChangedURLSinceEditTextFocused = false

fun onCreateView(layout: View, viewLifecycleOwner: LifecycleOwner, fragmentManager: FragmentManager) {
val toolbarClickListener = ToolbarOnClickListener()
layout.topNavContainer.forEach {
it.nextFocusDownId = layout.navUrlInput.id
if (it.isFocusable) it.setOnClickListener(toolbarClickListener)
}

setupUrlInput(layout)
initToolbar(layout, viewLifecycleOwner, fragmentManager)
}

private fun setupUrlInput(layout: View) = with(layout.navUrlInput) {
setOnCommitListener {
val userInput = text.toString()
if (userInput == URLs.APP_URL_HOME) {
// If the input points to home, we short circuit and hide the keyboard, returning
// the user to the home screen
this.hideKeyboard()
return@setOnCommitListener
}

if (userInput.isNotEmpty()) {
val cachedAutocompleteResult = lastAutocompleteResult // setText clears the reference so we cache it here.
setText(cachedAutocompleteResult.text)
onNavigationEvent.invoke(NavigationEvent.LOAD_URL, userInput, cachedAutocompleteResult)
}
}
this.movementMethod = IgnoreFocusMovementMethod()
val autocompleteProvider = ShippedDomainsProvider().apply {
initialize(
context = context
)
}
setOnFilterListener { searchText, view ->
val result = autocompleteProvider.getAutocompleteSuggestion(searchText)
if (result != null)
view?.onAutocomplete(InlineAutocompleteEditText.AutocompleteResult(result.text, result.source, result.totalItems))
}

setOnUserInputListener { hasUserChangedURLSinceEditTextFocused = true }
setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) hasUserChangedURLSinceEditTextFocused = false }
}

private fun initToolbar(layout: View, viewLifecycleOwner: LifecycleOwner, fragmentManager: FragmentManager) {
fun updateOverlayButtonState(isEnabled: Boolean, overlayButton: ImageButton) {
overlayButton.isEnabled = isEnabled
overlayButton.isFocusable = isEnabled
overlayButton.alpha =
if (isEnabled) NAVIGATION_BUTTON_ENABLED_ALPHA else NAVIGATION_BUTTON_DISABLED_ALPHA
}

val context = layout.context
val serviceLocator = context.serviceLocator

toolbarViewModel.state.observe(viewLifecycleOwner, Observer {
if (it == null) return@Observer
val focusedView = getCurrentFocus()
updateOverlayButtonState(it.backEnabled, layout.navButtonBack)
updateOverlayButtonState(it.forwardEnabled, layout.navButtonForward)
updateOverlayButtonState(it.pinEnabled, layout.pinButton)
updateOverlayButtonState(it.refreshEnabled, layout.navButtonReload)
updateOverlayButtonState(it.desktopModeEnabled, layout.desktopModeButton)

updateFocusableViews(focusedView)

layout.pinButton.isChecked = it.pinChecked
layout.desktopModeButton.isChecked = it.desktopModeChecked
layout.turboButton.isChecked = it.turboChecked

if (!hasUserChangedURLSinceEditTextFocused) {
// The url can get updated in the background, e.g. if a loading page is redirected. We
// don't want a url update to interrupt the user typing so we don't update the url from
// the background if the user has already updated the url themselves.
//
// We revert this state when the view is unfocused: it ensures the URL is usually accurate
// (for security reasons) and it's simple compared to other options which keep more state.
//
// One problem this solution has is that if the URL is updated in the background rapidly,
// sometimes key events will be dropped, but I don't think there's much we can do about this:
// we can't determine if the keyboard is up or not and focus isn't a good indicator because
// we can focus the EditText without opening the soft keyboard and the user won't even know
// these are inaccurate!
layout.navUrlInput.setText(it.urlBarText)
}
})

toolbarViewModel.events.observe(viewLifecycleOwner, Observer {
it?.consume {
when (it) {
is ToolbarViewModel.Action.ShowTopToast -> ViewUtils.showCenteredTopToast(context, it.textId)
is ToolbarViewModel.Action.ShowBottomToast -> ViewUtils.showCenteredBottomToast(context, it.textId)
is ToolbarViewModel.Action.SetOverlayVisible -> serviceLocator.screenController
.showNavigationOverlay(fragmentManager, it.visible)
ToolbarViewModel.Action.ExitFirefox -> exitFirefox()
}.forceExhaustive
true
}
})
}

private inner class ToolbarOnClickListener : View.OnClickListener {
override fun onClick(view: View?) {
val event = NavigationEvent.fromViewClick(view?.id)
?: return

when (event) {
NavigationEvent.BACK -> toolbarViewModel.backButtonClicked()
NavigationEvent.FORWARD -> toolbarViewModel.forwardButtonClicked()
NavigationEvent.RELOAD -> toolbarViewModel.reloadButtonClicked()
NavigationEvent.PIN_ACTION -> toolbarViewModel.pinButtonClicked()
NavigationEvent.TURBO -> toolbarViewModel.turboButtonClicked()
NavigationEvent.DESKTOP_MODE -> toolbarViewModel.desktopModeButtonClicked()
NavigationEvent.EXIT_FIREFOX -> toolbarViewModel.exitFirefoxButtonClicked()
else -> Unit // Nothing to do.
}
onNavigationEvent.invoke(event, null, null)
}
}
}

0 comments on commit 69e5a0f

Please sign in to comment.