Revision control
Copy as Markdown
/* 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
package org.mozilla.focus.home
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_SPREAD
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_WRAP
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import kotlinx.android.synthetic.main.fragment_navigation_overlay.*
import kotlinx.android.synthetic.main.fragment_navigation_overlay.view.*
import mozilla.components.support.base.observer.Consumable
import mozilla.components.support.ktx.android.content.systemService
import org.mozilla.focus.R
import org.mozilla.focus.UrlSearcher
import org.mozilla.focus.appBarSemiOpaqueBackground
import org.mozilla.focus.browser.BrowserFragmentCallbacks
import org.mozilla.focus.browser.HomeTileGridNavigation
import org.mozilla.focus.browser.HomeTileLongClickListener
import org.mozilla.focus.ext.serviceLocator
import org.mozilla.focus.ext.updateLayoutParams
import org.mozilla.focus.telemetry.TelemetryWrapper
import org.mozilla.focus.utils.FragmentViewUiCoroutineScope
private const val KEY_IS_OVERLAY_ON_STARTUP = "isOverlayOnStartup"
/**
* An overlay that allows the user to navigate to new pages including via the home tiles. The overlay
* should be added to a fullscreen view and can be used in two modes:
* - Initial homescreen: navigation is placed below the app bar
* - Dialog: navigation is full screen, overlaid on a semi opaque overlay
*
* Note: this was originally implemented as a DialogFragment to better decouple the UI of the overlay from the
* app but there was a long delay before it could be displayed so we switched to a regular Fragment.
*/
class NavigationOverlayFragment : Fragment() {
val isOverlayOnStartup: Boolean by lazy { arguments!!.getBoolean(KEY_IS_OVERLAY_ON_STARTUP) }
private val callbacks: BrowserFragmentCallbacks? get() = activity as BrowserFragmentCallbacks?
private val viewUiCoroutineScope = FragmentViewUiCoroutineScope()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewUiCoroutineScope.onCreateView()
val context = inflater.context
val overlay = inflater.inflate(R.layout.fragment_navigation_overlay, container, false)
val isVisibleInDialogMode = arrayOf(
overlay.dismissHitTarget,
overlay.semiOpaqueBackground,
// TODO: remove this HACK: see property definition for details.
@Suppress("DEPRECATION") overlay.appBarSemiOpaqueBackground
)
isVisibleInDialogMode.forEach { it.visibility = if (isOverlayOnStartup) View.GONE else View.VISIBLE }
val isVisibleInStartupMode = arrayOf(overlay.initialHomescreenBackground)
isVisibleInStartupMode.forEach { it.visibility = if (isOverlayOnStartup) View.VISIBLE else View.GONE }
overlay.homeTiles.init(viewUiCoroutineScope)
setOverlayHeight(overlay.homeTiles)
// We forward the google search event to the default search engine: Google.
overlay.googleSearchHiddenEditText.setOnEditorActionListener(DefaultSearchOnEditorActionListener(activity as UrlSearcher))
context.serviceLocator.pinnedTileRepo.googleSearchEvents.observe(viewLifecycleOwner, GoogleSearchFocusRequestObserver())
NavigationOverlayAnimations.onCreateViewAnimateIn(overlay, isOverlayOnStartup, isBeingRestored = savedInstanceState != null) {
// We defer setting click listeners until the animation completes
// so the animation will not be interrupted.
setOnClickListeners(overlay)
}
return overlay
}
override fun onDestroyView() {
super.onDestroyView()
viewUiCoroutineScope.onDestroyView()
}
private fun setOverlayHeight(homeTiles: HomeTileGridNavigation) {
homeTiles.updateLayoutParams {
val verticalBias: Float
val heightConstraintType: Int
@IdRes val topToBottom: Int
if (isOverlayOnStartup) {
// Fill constraints from top to bottom (app bar to bottom of screen).
verticalBias = 0f
heightConstraintType = MATCH_CONSTRAINT_SPREAD
topToBottom = R.id.overlayTopAsInitialHomescreen
} else {
// Set height based on content size, expanding up from the bottom.
verticalBias = 1f
heightConstraintType = MATCH_CONSTRAINT_WRAP
topToBottom = R.id.overlayTopAsDialog
}
val params = it as ConstraintLayout.LayoutParams
params.verticalBias = verticalBias
params.matchConstraintDefaultHeight = heightConstraintType
params.topToBottom = topToBottom
}
}
private fun setOnClickListeners(overlay: View) {
val dismissHitTargets = listOf(
overlay.dismissHitTarget,
// TODO: remove this HACK: see the property definition for details.
@Suppress("DEPRECATION") overlay.appBarSemiOpaqueBackground
)
fun removeClickListeners() {
// We remove the click listeners when dismissing the overlay
// so that clicking them doesn't restart the animation.
dismissHitTargets.forEach { it.setOnClickListener(null) }
with(overlay.homeTiles) {
onTileClicked = null
homeTileLongClickListener = null
}
}
dismissHitTargets.forEach { it.setOnClickListener {
removeClickListeners()
callbacks?.setNavigationOverlayIsVisible(false)
TelemetryWrapper.dismissHomeOverlayClickEvent()
} }
with(overlay.homeTiles) {
onTileClicked = {
removeClickListeners()
callbacks?.onNonTextInputUrlEntered(it)
}
homeTileLongClickListener = object : HomeTileLongClickListener {
override fun onHomeTileLongClick(unpinTile: () -> Unit) {
callbacks?.onHomeTileLongClick(unpinTile)
}
}
}
}
fun refreshTilesForInsertion() {
homeTiles.refreshTilesForInsertion()
}
fun removePinnedSiteFromTiles(tileId: String) {
homeTiles.removePinnedSiteFromTiles(tileId)
}
private inner class GoogleSearchFocusRequestObserver : Observer<Consumable<GoogleSearchFocusRequestEvent>> {
override fun onChanged(consumable: Consumable<GoogleSearchFocusRequestEvent>?) {
consumable?.consume {
googleSearchHiddenEditText.setText("")
googleSearchHiddenEditText.requestFocus()
googleSearchHiddenEditText.showKeyboard()
true
}
}
private fun EditText.showKeyboard() {
// ViewUtils.showKeyboard doesn't seem to work.
val inputMethodManager = context?.systemService<InputMethodManager>(Context.INPUT_METHOD_SERVICE)
inputMethodManager?.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
}
}
companion object {
const val FRAGMENT_TAG = "navOverlay"
fun newInstance(isOverlayOnStartup: Boolean) = NavigationOverlayFragment().apply {
arguments = Bundle().apply {
putBoolean(KEY_IS_OVERLAY_ON_STARTUP, isOverlayOnStartup)
}
}
}
}