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.browser
import androidx.lifecycle.Observer
import android.content.Context
import android.os.Bundle
import androidx.annotation.UiThread
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.mozilla.focus.R
import org.mozilla.focus.architecture.FirefoxViewModelProviders
import org.mozilla.focus.browser.URLs.APP_STARTUP_HOME
import org.mozilla.focus.ext.getNavigationOverlay
import org.mozilla.focus.ext.isVisibleAndNonNull
import org.mozilla.focus.ext.toUri
import org.mozilla.focus.home.BundledTilesManager
import org.mozilla.focus.home.CustomTilesManager
import org.mozilla.focus.iwebview.IWebView
import org.mozilla.focus.iwebview.IWebViewLifecycleFragment
import org.mozilla.focus.session.NullSession
import org.mozilla.focus.session.Session
import org.mozilla.focus.session.SessionCallbackProxy
import org.mozilla.focus.session.SessionManager
import org.mozilla.focus.telemetry.AppStartupTimeMeasurement
import org.mozilla.focus.telemetry.LoadTimeObserver
import org.mozilla.focus.toolbar.ToolbarEvent
import org.mozilla.focus.toolbar.ToolbarStateProvider
import org.mozilla.focus.utils.ToastManager
private const val ARGUMENT_SESSION_UUID = "sessionUUID"
private val URLS_BLOCKED_FROM_USERS = setOf(
APP_STARTUP_HOME.toString()
)
/** An interface expected to be implemented by the Activities that create a BrowserFragment. */
interface BrowserFragmentCallbacks : HomeTileLongClickListener {
@UiThread // performs a fragment transaction.
fun setNavigationOverlayIsVisible(isVisible: Boolean, isOverlayOnStartup: Boolean = false)
fun onNonTextInputUrlEntered(urlStr: String)
fun onUrlUpdate(url: String?)
fun onSessionLoadingUpdate(isLoading: Boolean)
fun onSessionProgressUpdate(progress: Int)
}
interface HomeTileLongClickListener {
fun onHomeTileLongClick(unpinTile: () -> Unit)
}
/**
* Fragment for displaying the browser UI.
*/
class BrowserFragment : IWebViewLifecycleFragment() {
companion object {
const val FRAGMENT_TAG = "browser"
@JvmStatic
fun createForSession(session: Session) = BrowserFragment().apply {
arguments = Bundle().apply { putString(ARGUMENT_SESSION_UUID, session.uuid) }
}
}
// IWebViewLifecycleFragment expects a value for these properties before onViewCreated. We use a getter
// for the properties that reference session because it is lateinit.
override lateinit var session: Session
override val initialUrl get() = session.url.value
override lateinit var iWebViewCallback: IWebView.Callback
internal val callbacks: BrowserFragmentCallbacks? get() = activity as BrowserFragmentCallbacks?
val toolbarStateProvider = BrowserToolbarStateProvider()
private val viewModel: BrowserViewModel
get() = FirefoxViewModelProviders.of(this)[BrowserViewModel::class.java]
/**
* The current URL.
*
* Use this instead of the WebView's URL which can return null, return a null URL, or return
* data: URLs (for error pages).
*/
var url: String? = null
private set(value) {
field = value
// We prevent users from typing this URL in loadUrl but this will still be called for
// the initial URL set in the Session.
if (url == APP_STARTUP_HOME.toString()) {
callbacks?.setNavigationOverlayIsVisible(true, isOverlayOnStartup = true)
}
callbacks?.onUrlUpdate(url) // This should be called last so app state is up-to-date.
}
// If the URL is startup home, the home screen should always be visible. For defensiveness, we
// also check this condition. It's probably not necessary (it was originally added when the startup
// url was the empty string which I was concerned the WebView could pass to us while loading).
private val isStartupHomepageVisible: Boolean
get() = url == APP_STARTUP_HOME.toString() && fragmentManager.getNavigationOverlay().isVisibleAndNonNull
private val sessionManager = SessionManager.getInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
session = initSession()
webView?.setBlockingEnabled(session.isBlockingEnabled)
iWebViewCallback = SessionCallbackProxy(session, FullscreenCallbacks(this, viewModel))
LoadTimeObserver.addObservers(session, this)
}
override fun onResume() {
super.onResume()
AppStartupTimeMeasurement.fragmentOnResume()
}
private fun initSession(): Session {
val sessionUUID = arguments?.getString(ARGUMENT_SESSION_UUID)
?: throw IllegalAccessError("No session exists")
val session = if (sessionManager.hasSessionWithUUID(sessionUUID))
sessionManager.getSessionByUUID(sessionUUID)
else
NullSession()
session.url.observe(this, Observer { url -> this@BrowserFragment.url = url })
session.loading.observe(this, SessionLoadingObserver())
session.progress.observe(this, Observer { it?.let { callbacks?.onSessionProgressUpdate(it) } })
return session
}
fun onToolbarEvent(event: ToolbarEvent, value: String?) {
val context = context!!
when (event) {
ToolbarEvent.BACK -> if (webView?.canGoBack() ?: false) webView?.goBack()
ToolbarEvent.FORWARD -> if (webView?.canGoForward() ?: false) webView?.goForward()
ToolbarEvent.TURBO -> {
when (value) {
ToolbarEvent.VAL_CHECKED -> {
ToastManager.showToast(R.string.turbo_mode_enabled_toast, context)
}
ToolbarEvent.VAL_UNCHECKED -> {
ToastManager.showToast(R.string.turbo_mode_disabled_toast, context)
}
}
webView?.reload()
}
ToolbarEvent.RELOAD -> webView?.reload()
ToolbarEvent.SETTINGS -> Unit // No Settings in BrowserFragment
ToolbarEvent.PIN_ACTION -> this@BrowserFragment.url?.let { url -> onPinToolbarEvent(context, url, value) }
ToolbarEvent.HOME -> if (!fragmentManager.getNavigationOverlay().isVisibleAndNonNull) {
callbacks?.setNavigationOverlayIsVisible(true)
}
ToolbarEvent.LOAD_URL -> throw IllegalStateException("Expected $event to be handled sooner")
}
Unit
}
private fun onPinToolbarEvent(context: Context, url: String, value: String?) {
when (value) {
ToolbarEvent.VAL_CHECKED -> {
CustomTilesManager.getInstance(context).pinSite(context, url,
webView?.takeScreenshot())
fragmentManager.getNavigationOverlay()?.refreshTilesForInsertion()
ToastManager.showPinnedToast(context)
}
ToolbarEvent.VAL_UNCHECKED -> {
url.toUri()?.let {
val tileId = BundledTilesManager.getInstance(context).unpinSite(context, it)
?: CustomTilesManager.getInstance(context).unpinSite(context, url)
// tileId should never be null, unless, for some reason we don't
// have a reference to the tile/the tile isn't a Bundled or Custom tile
if (tileId != null && !tileId.isEmpty()) {
fragmentManager.getNavigationOverlay()?.removePinnedSiteFromTiles(tileId)
ToastManager.showUnpinnedToast(context)
}
}
}
else -> throw IllegalArgumentException("Unexpected value for PIN_ACTION: " + value)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val layout = inflater.inflate(R.layout.fragment_browser, container, false)
viewModel.isWebViewVisible.observe(viewLifecycleOwner, Observer {
webView?.setVisibility(if (it!!) View.VISIBLE else View.GONE)
})
return layout
}
fun onNavigationOverlayVisibilityChange(isVisible: Boolean) {
viewModel.onNavigationOverlayVisibilityChange(isVisible)
}
fun loadUrl(url: String) {
// Intents can trigger loadUrl, and we need to make sure the navigation overlay is always hidden.
callbacks?.setNavigationOverlayIsVisible(false)
val webView = webView
if (webView != null && !TextUtils.isEmpty(url) && !URLS_BLOCKED_FROM_USERS.contains(url)) {
webView.loadUrl(url)
}
}
inner class BrowserToolbarStateProvider : ToolbarStateProvider {
override fun isBackEnabled() = webView?.canGoBack() ?: false
override fun isForwardEnabled() = webView?.canGoForward() ?: false
override fun isStartupHomepageVisible() = isStartupHomepageVisible
override fun getCurrentUrl() = url
override fun isURLPinned() = url.toUri()?.let {
// TODO: #569 fix CustomTilesManager to use Uri too
CustomTilesManager.getInstance(context!!).isURLPinned(it.toString()) ||
BundledTilesManager.getInstance(context!!).isURLPinned(it) } ?: false
}
private inner class SessionLoadingObserver : Observer<Boolean> {
override fun onChanged(isLoading: Boolean?) {
if (isLoading == null) { return }
callbacks?.onSessionLoadingUpdate(isLoading)
val uri = url?.toUri() ?: return
val webView = webView ?: return
WebCompat.onSessionLoadingChanged(isLoading, uri, webView)
}
}
}