|
|
|
/* 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.browser
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.view.Gravity
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
|
|
|
import android.view.accessibility.AccessibilityManager
|
|
|
|
import androidx.annotation.CallSuper
|
|
|
|
import androidx.annotation.VisibleForTesting
|
|
|
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
|
|
import androidx.core.net.toUri
|
|
|
|
import androidx.core.view.isVisible
|
|
|
|
import androidx.fragment.app.Fragment
|
|
|
|
import androidx.fragment.app.activityViewModels
|
|
|
|
import androidx.lifecycle.ViewModelProvider
|
|
|
|
import androidx.lifecycle.lifecycleScope
|
|
|
|
import androidx.navigation.fragment.findNavController
|
|
|
|
import androidx.preference.PreferenceManager
|
|
|
|
import com.google.android.material.snackbar.Snackbar
|
|
|
|
import kotlinx.android.synthetic.main.fragment_browser.*
|
|
|
|
import kotlinx.android.synthetic.main.fragment_browser.view.*
|
|
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
|
|
import kotlinx.coroutines.Dispatchers.Main
|
|
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
import kotlinx.coroutines.flow.collect
|
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
import kotlinx.coroutines.flow.mapNotNull
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
import mozilla.appservices.places.BookmarkRoot
|
|
|
|
import mozilla.components.browser.session.Session
|
|
|
|
import mozilla.components.browser.state.action.ContentAction
|
|
|
|
import mozilla.components.browser.state.selector.findTab
|
|
|
|
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
|
|
|
|
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
|
|
|
|
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
|
|
|
|
import mozilla.components.browser.state.selector.selectedTab
|
|
|
|
import mozilla.components.browser.state.state.SessionState
|
|
|
|
import mozilla.components.browser.state.state.TabSessionState
|
|
|
|
import mozilla.components.browser.state.state.content.DownloadState
|
|
|
|
import mozilla.components.browser.state.store.BrowserStore
|
|
|
|
import mozilla.components.browser.thumbnails.BrowserThumbnails
|
|
|
|
import mozilla.components.concept.engine.prompt.ShareData
|
|
|
|
import mozilla.components.feature.accounts.FxaCapability
|
|
|
|
import mozilla.components.feature.accounts.FxaWebChannelFeature
|
|
|
|
import mozilla.components.feature.app.links.AppLinksFeature
|
|
|
|
import mozilla.components.feature.contextmenu.ContextMenuCandidate
|
|
|
|
import mozilla.components.feature.contextmenu.ContextMenuFeature
|
|
|
|
import mozilla.components.feature.downloads.DownloadsFeature
|
|
|
|
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
|
|
|
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
|
|
|
|
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature
|
|
|
|
import mozilla.components.feature.privatemode.feature.SecureWindowFeature
|
|
|
|
import mozilla.components.feature.prompts.PromptFeature
|
|
|
|
import mozilla.components.feature.prompts.share.ShareDelegate
|
|
|
|
import mozilla.components.feature.readerview.ReaderViewFeature
|
|
|
|
import mozilla.components.feature.search.SearchFeature
|
|
|
|
import mozilla.components.feature.session.FullScreenFeature
|
|
|
|
import mozilla.components.feature.session.PictureInPictureFeature
|
|
|
|
import mozilla.components.feature.session.SessionFeature
|
|
|
|
import mozilla.components.feature.session.SwipeRefreshFeature
|
|
|
|
import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
|
|
|
|
import mozilla.components.feature.sitepermissions.SitePermissions
|
|
|
|
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
|
|
|
|
import mozilla.components.lib.state.ext.consumeFlow
|
|
|
|
import mozilla.components.lib.state.ext.flowScoped
|
|
|
|
import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
|
|
|
|
import mozilla.components.support.base.feature.PermissionsFeature
|
|
|
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
|
|
|
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
|
|
|
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
|
|
|
|
import mozilla.components.support.ktx.android.view.hideKeyboard
|
|
|
|
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
|
|
|
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
|
|
|
import org.mozilla.fenix.FeatureFlags
|
|
|
|
import org.mozilla.fenix.HomeActivity
|
|
|
|
import org.mozilla.fenix.IntentReceiverActivity
|
|
|
|
import org.mozilla.fenix.NavGraphDirections
|
|
|
|
import org.mozilla.fenix.OnBackLongPressedListener
|
|
|
|
import org.mozilla.fenix.R
|
|
|
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
|
|
|
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
|
|
|
|
import org.mozilla.fenix.components.Components
|
|
|
|
import org.mozilla.fenix.components.FenixSnackbar
|
|
|
|
import org.mozilla.fenix.components.FindInPageIntegration
|
|
|
|
import org.mozilla.fenix.components.StoreProvider
|
|
|
|
import org.mozilla.fenix.components.metrics.Event
|
|
|
|
import org.mozilla.fenix.components.toolbar.BrowserFragmentState
|
|
|
|
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
|
|
|
|
import org.mozilla.fenix.components.toolbar.BrowserInteractor
|
|
|
|
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
|
|
|
|
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
|
|
|
|
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
|
|
|
|
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
|
|
|
|
import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
|
|
|
|
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
|
|
|
|
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
|
|
|
import org.mozilla.fenix.downloads.DownloadService
|
|
|
|
import org.mozilla.fenix.downloads.DynamicDownloadDialog
|
|
|
|
import org.mozilla.fenix.ext.accessibilityManager
|
|
|
|
import org.mozilla.fenix.ext.breadcrumb
|
|
|
|
import org.mozilla.fenix.ext.components
|
|
|
|
import org.mozilla.fenix.ext.enterToImmersiveMode
|
|
|
|
import org.mozilla.fenix.ext.getPreferenceKey
|
|
|
|
import org.mozilla.fenix.ext.hideToolbar
|
|
|
|
import org.mozilla.fenix.ext.metrics
|
|
|
|
import org.mozilla.fenix.ext.nav
|
|
|
|
import org.mozilla.fenix.ext.requireComponents
|
|
|
|
import org.mozilla.fenix.ext.settings
|
|
|
|
import org.mozilla.fenix.home.HomeScreenViewModel
|
|
|
|
import org.mozilla.fenix.home.SharedViewModel
|
|
|
|
import org.mozilla.fenix.theme.ThemeManager
|
|
|
|
import org.mozilla.fenix.utils.allowUndo
|
|
|
|
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
|
|
|
|
import java.lang.ref.WeakReference
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Base fragment extended by [BrowserFragment].
|
|
|
|
* This class only contains shared code focused on the main browsing content.
|
|
|
|
* UI code specific to the app or to custom tabs can be found in the subclasses.
|
|
|
|
*/
|
|
|
|
@ExperimentalCoroutinesApi
|
|
|
|
@Suppress("TooManyFunctions", "LargeClass")
|
|
|
|
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
|
|
|
|
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
|
|
|
|
|
|
|
|
private lateinit var browserFragmentStore: BrowserFragmentStore
|
|
|
|
private lateinit var browserAnimator: BrowserAnimator
|
|
|
|
private lateinit var components: Components
|
|
|
|
|
|
|
|
private var _browserInteractor: BrowserToolbarViewInteractor? = null
|
|
|
|
protected val browserInteractor: BrowserToolbarViewInteractor
|
|
|
|
get() = _browserInteractor!!
|
|
|
|
|
|
|
|
private var _browserToolbarView: BrowserToolbarView? = null
|
|
|
|
@VisibleForTesting
|
|
|
|
internal val browserToolbarView: BrowserToolbarView
|
|
|
|
get() = _browserToolbarView!!
|
|
|
|
|
|
|
|
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
|
|
|
|
protected val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
|
|
|
|
|
|
|
|
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
|
|
|
|
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
|
|
|
|
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
|
|
|
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
|
|
|
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
|
|
|
private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
|
|
|
|
private val toolbarIntegration = ViewBoundFeatureWrapper<ToolbarIntegration>()
|
|
|
|
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
|
|
|
|
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
|
|
|
|
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
|
|
|
|
private val webchannelIntegration = ViewBoundFeatureWrapper<FxaWebChannelFeature>()
|
|
|
|
private val sitePermissionWifiIntegration =
|
|
|
|
ViewBoundFeatureWrapper<SitePermissionsWifiIntegration>()
|
|
|
|
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
|
|
|
|
private var fullScreenMediaFeature =
|
|
|
|
ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
|
|
|
|
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
|
|
|
|
private var pipFeature: PictureInPictureFeature? = null
|
|
|
|
|
|
|
|
var customTabSessionId: String? = null
|
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
internal var browserInitialized: Boolean = false
|
|
|
|
private var initUIJob: Job? = null
|
|
|
|
protected var webAppToolbarShouldBeVisible = true
|
|
|
|
|
|
|
|
private val sharedViewModel: SharedViewModel by activityViewModels()
|
|
|
|
|
|
|
|
@CallSuper
|
|
|
|
override fun onCreateView(
|
|
|
|
inflater: LayoutInflater,
|
|
|
|
container: ViewGroup?,
|
|
|
|
savedInstanceState: Bundle?
|
|
|
|
): View {
|
|
|
|
customTabSessionId = requireArguments().getString(EXTRA_SESSION_ID)
|
|
|
|
|
|
|
|
// Diagnostic breadcrumb for "Display already aquired" crash:
|
|
|
|
// https://github.com/mozilla-mobile/android-components/issues/7960
|
|
|
|
breadcrumb(
|
|
|
|
message = "onCreateView()",
|
|
|
|
data = mapOf(
|
|
|
|
"customTabSessionId" to customTabSessionId.toString()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
val view = inflater.inflate(R.layout.fragment_browser, container, false)
|
|
|
|
|
|
|
|
val activity = activity as HomeActivity
|
|
|
|
components = requireComponents
|
|
|
|
|
|
|
|
if (customTabSessionId == null) {
|
|
|
|
// Once tab restoration is complete, if there are no tabs to show in the browser, go home
|
|
|
|
components.core.store.flowScoped(viewLifecycleOwner) { flow ->
|
|
|
|
flow.map { state -> state.restoreComplete }
|
|
|
|
.ifChanged()
|
|
|
|
.collect { restored ->
|
|
|
|
if (restored) {
|
|
|
|
val tabs =
|
|
|
|
components.core.store.state.getNormalOrPrivateTabs(
|
|
|
|
activity.browsingModeManager.mode.isPrivate
|
|
|
|
)
|
|
|
|
if (tabs.isEmpty()) findNavController().popBackStack(
|
|
|
|
R.id.homeFragment,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
activity.themeManager.applyStatusBarTheme(activity)
|
|
|
|
|
|
|
|
browserFragmentStore = StoreProvider.get(this) {
|
|
|
|
BrowserFragmentStore(
|
|
|
|
BrowserFragmentState()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return view
|
|
|
|
}
|
|
|
|
|
|
|
|
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
browserInitialized = initializeUI(view) != null
|
|
|
|
observeTabSelection(requireComponents.core.store)
|
|
|
|
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
private val homeViewModel: HomeScreenViewModel by activityViewModels {
|
|
|
|
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
|
|
|
}
|
|
|
|
|
|
|
|
@Suppress("ComplexMethod", "LongMethod")
|
|
|
|
@CallSuper
|
|
|
|
@VisibleForTesting
|
|
|
|
internal open fun initializeUI(view: View): Session? {
|
|
|
|
val context = requireContext()
|
|
|
|
val sessionManager = context.components.core.sessionManager
|
|
|
|
val store = context.components.core.store
|
|
|
|
val activity = requireActivity() as HomeActivity
|
|
|
|
|
|
|
|
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
|
|
|
|
|
|
|
|
browserAnimator = BrowserAnimator(
|
|
|
|
fragment = WeakReference(this),
|
|
|
|
engineView = WeakReference(engineView),
|
|
|
|
swipeRefresh = WeakReference(swipeRefresh),
|
|
|
|
viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope)
|
|
|
|
).apply {
|
|
|
|
beginAnimateInIfNecessary()
|
|
|
|
}
|
|
|
|
|
|
|
|
return getSessionById()?.also { _ ->
|
|
|
|
val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
|
|
|
|
action = Intent.ACTION_VIEW
|
|
|
|
putExtra(HomeActivity.OPEN_TO_BROWSER, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
val readerMenuController = DefaultReaderModeController(
|
|
|
|
readerViewFeature,
|
|
|
|
view.readerViewControlsBar,
|
|
|
|
isPrivate = activity.browsingModeManager.mode.isPrivate
|
|
|
|
)
|
|
|
|
val browserToolbarController = DefaultBrowserToolbarController(
|
|
|
|
activity = activity,
|
|
|
|
navController = findNavController(),
|
|
|
|
metrics = requireComponents.analytics.metrics,
|
|
|
|
readerModeController = readerMenuController,
|
|
|
|
sessionManager = requireComponents.core.sessionManager,
|
|
|
|
engineView = engineView,
|
|
|
|
homeViewModel = homeViewModel,
|
|
|
|
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
|
|
|
onTabCounterClicked = {
|
|
|
|
thumbnailsFeature.get()?.requestScreenshot()
|
|
|
|
findNavController().nav(
|
|
|
|
R.id.browserFragment,
|
|
|
|
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
|
|
|
|
)
|
|
|
|
},
|
|
|
|
onCloseTab = { closedSession ->
|
|
|
|
val tab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController
|
|
|
|
|
|
|
|
val snackbarMessage = if (tab.content.private) {
|
|
|
|
requireContext().getString(R.string.snackbar_private_tab_closed)
|
|
|
|
} else {
|
|
|
|
requireContext().getString(R.string.snackbar_tab_closed)
|
|
|
|
}
|
|
|
|
|
|
|
|
viewLifecycleOwner.lifecycleScope.allowUndo(
|
|
|
|
requireView().browserLayout,
|
|
|
|
snackbarMessage,
|
|
|
|
requireContext().getString(R.string.snackbar_deleted_undo),
|
|
|
|
{
|
|
|
|
requireComponents.useCases.tabsUseCases.undo.invoke()
|
|
|
|
},
|
|
|
|
paddedForBottomToolbar = true,
|
|
|
|
operation = { }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
val browserToolbarMenuController = DefaultBrowserToolbarMenuController(
|
|
|
|
activity = activity,
|
|
|
|
navController = findNavController(),
|
|
|
|
metrics = requireComponents.analytics.metrics,
|
|
|
|
settings = context.settings(),
|
|
|
|
readerModeController = readerMenuController,
|
|
|
|
sessionManager = requireComponents.core.sessionManager,
|
|
|
|
sessionFeature = sessionFeature,
|
|
|
|
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
|
|
|
|
swipeRefresh = swipeRefresh,
|
|
|
|
browserAnimator = browserAnimator,
|
|
|
|
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
|
|
|
openInFenixIntent = openInFenixIntent,
|
|
|
|
bookmarkTapped = { url: String, title: String ->
|
|
|
|
viewLifecycleOwner.lifecycleScope.launch {
|
|
|
|
bookmarkTapped(url, title)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
scope = viewLifecycleOwner.lifecycleScope,
|
|
|
|
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
|
|
|
|
topSitesStorage = requireComponents.core.topSitesStorage,
|
|
|
|
browserStore = store
|
|
|
|
)
|
|
|
|
|
|
|
|
_browserInteractor = BrowserInteractor(
|
|
|
|
browserToolbarController,
|
|
|
|
browserToolbarMenuController
|
|
|
|
)
|
|
|
|
|
|
|
|
_browserToolbarView = BrowserToolbarView(
|
|
|
|
container = view.browserLayout,
|
|
|
|
toolbarPosition = context.settings().toolbarPosition,
|
|
|
|
interactor = browserInteractor,
|
|
|
|
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
|
|
|
lifecycleOwner = viewLifecycleOwner
|
|
|
|
)
|
|
|
|
|
|
|
|
toolbarIntegration.set(
|
|
|
|
feature = browserToolbarView.toolbarIntegration,
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
findInPageIntegration.set(
|
|
|
|
feature = FindInPageIntegration(
|
|
|
|
store = store,
|
|
|
|
sessionId = customTabSessionId,
|
|
|
|
stub = view.stubFindInPage,
|
|
|
|
engineView = view.engineView,
|
|
|
|
toolbar = browserToolbarView.view
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
browserToolbarView.view.display.setOnSiteSecurityClickedListener {
|
|
|
|
showQuickSettingsDialog()
|
|
|
|
}
|
|
|
|
|
|
|
|
browserToolbarView.view.display.setOnTrackingProtectionClickedListener {
|
|
|
|
context.metrics.track(Event.TrackingProtectionIconPressed)
|
|
|
|
showTrackingProtectionPanel()
|
|
|
|
}
|
|
|
|
|
|
|
|
contextMenuFeature.set(
|
|
|
|
feature = ContextMenuFeature(
|
|
|
|
fragmentManager = parentFragmentManager,
|
|
|
|
store = store,
|
|
|
|
candidates = getContextMenuCandidates(context, view.browserLayout),
|
|
|
|
engineView = view.engineView,
|
|
|
|
useCases = context.components.useCases.contextMenuUseCases,
|
|
|
|
tabId = customTabSessionId
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode
|
|
|
|
secureWindowFeature.set(
|
|
|
|
feature = SecureWindowFeature(
|
|
|
|
window = requireActivity().window,
|
|
|
|
store = store,
|
|
|
|
customTabId = customTabSessionId,
|
|
|
|
isSecure = { !allowScreenshotsInPrivateMode && it.content.private }
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
fullScreenMediaFeature.set(
|
|
|
|
feature = MediaFullscreenOrientationFeature(
|
|
|
|
requireActivity(),
|
|
|
|
context.components.core.store
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
val downloadFeature = DownloadsFeature(
|
|
|
|
context.applicationContext,
|
|
|
|
store = store,
|
|
|
|
useCases = context.components.useCases.downloadUseCases,
|
|
|
|
fragmentManager = childFragmentManager,
|
|
|
|
tabId = customTabSessionId,
|
|
|
|
downloadManager = FetchDownloadManager(
|
|
|
|
context.applicationContext,
|
|
|
|
store,
|
|
|
|
DownloadService::class
|
|
|
|
),
|
|
|
|
shouldForwardToThirdParties = {
|
|
|
|
PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
|
|
|
context.getPreferenceKey(R.string.pref_key_external_download_manager), false
|
|
|
|
)
|
|
|
|
},
|
|
|
|
promptsStyling = DownloadsFeature.PromptsStyling(
|
|
|
|
gravity = Gravity.BOTTOM,
|
|
|
|
shouldWidthMatchParent = true,
|
|
|
|
positiveButtonBackgroundColor = ThemeManager.resolveAttribute(
|
|
|
|
R.attr.accent,
|
|
|
|
context
|
|
|
|
),
|
|
|
|
positiveButtonTextColor = ThemeManager.resolveAttribute(
|
|
|
|
R.attr.contrastText,
|
|
|
|
context
|
|
|
|
),
|
|
|
|
positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat()
|
|
|
|
),
|
|
|
|
onNeedToRequestPermissions = { permissions ->
|
|
|
|
requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
|
|
|
|
// If the download is just paused, don't show any in-app notification
|
|
|
|
if (downloadJobStatus == DownloadState.Status.COMPLETED ||
|
|
|
|
downloadJobStatus == DownloadState.Status.FAILED
|
|
|
|
) {
|
|
|
|
|
|
|
|
saveDownloadDialogState(
|
|
|
|
downloadState.sessionId,
|
|
|
|
downloadState,
|
|
|
|
downloadJobStatus
|
|
|
|
)
|
|
|
|
|
|
|
|
val dynamicDownloadDialog = DynamicDownloadDialog(
|
|
|
|
container = view.browserLayout,
|
|
|
|
downloadState = downloadState,
|
|
|
|
didFail = downloadJobStatus == DownloadState.Status.FAILED,
|
|
|
|
tryAgain = downloadFeature::tryAgain,
|
|
|
|
onCannotOpenFile = {
|
|
|
|
FenixSnackbar.make(
|
|
|
|
view = view.browserLayout,
|
|
|
|
duration = Snackbar.LENGTH_SHORT,
|
|
|
|
isDisplayedWithBrowserToolbar = true
|
|
|
|
)
|
|
|
|
.setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
|
|
|
|
.show()
|
|
|
|
},
|
|
|
|
view = view.viewDynamicDownloadDialog,
|
|
|
|
toolbarHeight = toolbarHeight,
|
|
|
|
onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
|
|
|
|
)
|
|
|
|
|
|
|
|
// Don't show the dialog if we aren't in the tab that started the download
|
|
|
|
if (downloadState.sessionId == sessionManager.selectedSession?.id) {
|
|
|
|
dynamicDownloadDialog.show()
|
|
|
|
browserToolbarView.expand()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resumeDownloadDialogState(
|
|
|
|
sessionManager.selectedSession?.id,
|
|
|
|
store, view, context, toolbarHeight
|
|
|
|
)
|
|
|
|
|
|
|
|
downloadsFeature.set(
|
|
|
|
downloadFeature,
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
pipFeature = PictureInPictureFeature(
|
|
|
|
store = store,
|
|
|
|
activity = requireActivity(),
|
|
|
|
crashReporting = context.components.analytics.crashReporter,
|
|
|
|
tabId = customTabSessionId
|
|
|
|
)
|
|
|
|
|
|
|
|
appLinksFeature.set(
|
|
|
|
feature = AppLinksFeature(
|
|
|
|
context,
|
|
|
|
sessionManager = sessionManager,
|
|
|
|
sessionId = customTabSessionId,
|
|
|
|
fragmentManager = parentFragmentManager,
|
|
|
|
launchInApp = { context.settings().openLinksInExternalApp },
|
|
|
|
loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
promptsFeature.set(
|
|
|
|
feature = PromptFeature(
|
|
|
|
fragment = this,
|
|
|
|
store = store,
|
|
|
|
customTabId = customTabSessionId,
|
|
|
|
fragmentManager = parentFragmentManager,
|
|
|
|
loginValidationDelegate = DefaultLoginValidationDelegate(
|
Closes #7450: Lazy storage initialization
Make sure that we actually lazily initialize our storage layers.
With this patch applied, storage layers (history, logins, bookmarks) will be initialized when first
accessed. We will no longer block GeckoEngine init, for example, on waiting for the logins storage
to initialize (which needs to access the costly securePrefStorage).
Similarly, BackgroundServices init will no longer require initialized instances of the storage
components - references to their "lazy wrappers" will suffice.
In practice, this change changes when our storage layers are initialized in the following ways.
Currently, we will initialize everything on startup. This includes loading our megazord, as well.
With this change, init path depends on if the user is signed-into FxA or not.
If user is not an FxA user:
- on startup, none of the storage layers are initialized
- history storage will be initialized once, whenever:
- first non-customTab page is loaded (access to the HistoryDelegate)
- first interaction with the awesomebar
- history UI is accessed
- bookmarks storage will be initialized once, whenever:
- something is bookmarked, or we need to figure out if something's bookmarked
- bookmarks UI is accessed
- logins storage will be initialized once, whenever:
- first page is loaded with a login/password fields that can be autofilled
- (or some other interaction by GV with the autofill/loginStorage delegates)
- logins UI is accessed
- all of these storages will be initialized if the user logs into FxA and starts syncing data
- except, if a storage is not chosen to be synced, it will not be initialized
If user is an FxA user:
- on startup, none of the storage layers are initialized
- sometime shortly after startup is complete, when a sync worker runs in the background, all storage
layers that are enabled to sync will be initialized.
This change also means that we delay loading the megazord until first access (as described above).
5 years ago
|
|
|
context.components.core.lazyPasswordsStorage
|
|
|
|
),
|
|
|
|
isSaveLoginEnabled = {
|
|
|
|
context.settings().shouldPromptToSaveLogins
|
|
|
|
},
|
|
|
|
loginExceptionStorage = context.components.core.loginExceptionStorage,
|
|
|
|
shareDelegate = object : ShareDelegate {
|
|
|
|
override fun showShareSheet(
|
|
|
|
context: Context,
|
|
|
|
shareData: ShareData,
|
|
|
|
onDismiss: () -> Unit,
|
|
|
|
onSuccess: () -> Unit
|
|
|
|
) {
|
|
|
|
val directions = NavGraphDirections.actionGlobalShareFragment(
|
|
|
|
data = arrayOf(shareData),
|
|
|
|
showPage = true,
|
|
|
|
sessionId = getSessionById()?.id
|
|
|
|
)
|
|
|
|
findNavController().navigate(directions)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onNeedToRequestPermissions = { permissions ->
|
|
|
|
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
|
|
|
|
},
|
|
|
|
loginPickerView = loginSelectBar,
|
|
|
|
onManageLogins = {
|
|
|
|
browserAnimator.captureEngineViewAndDrawStatically {
|
|
|
|
val directions =
|
|
|
|
NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
|
|
|
|
findNavController().navigate(directions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
sessionFeature.set(
|
|
|
|
feature = SessionFeature(
|
|
|
|
requireComponents.core.store,
|
|
|
|
requireComponents.useCases.sessionUseCases.goBack,
|
|
|
|
view.engineView,
|
|
|
|
customTabSessionId
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
searchFeature.set(
|
|
|
|
feature = SearchFeature(store, customTabSessionId) { request, tabId ->
|
|
|
|
val parentSession = sessionManager.findSessionById(tabId)
|
|
|
|
val useCase = if (request.isPrivate) {
|
|
|
|
requireComponents.useCases.searchUseCases.newPrivateTabSearch
|
|
|
|
} else {
|
|
|
|
requireComponents.useCases.searchUseCases.newTabSearch
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parentSession?.isCustomTabSession() == true) {
|
|
|
|
useCase.invoke(request.query)
|
|
|
|
requireActivity().startActivity(openInFenixIntent)
|
|
|
|
} else {
|
|
|
|
useCase.invoke(request.query, parentSession = parentSession)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
val accentHighContrastColor =
|
|
|
|
ThemeManager.resolveAttribute(R.attr.accentHighContrast, context)
|
|
|
|
|
|
|
|
sitePermissionsFeature.set(
|
|
|
|
feature = SitePermissionsFeature(
|
|
|
|
context = context,
|
|
|
|
storage = context.components.core.permissionStorage.permissionsStorage,
|
|
|
|
sessionManager = sessionManager,
|
|
|
|
fragmentManager = parentFragmentManager,
|
|
|
|
promptsStyling = SitePermissionsFeature.PromptsStyling(
|
|
|
|
gravity = getAppropriateLayoutGravity(),
|
|
|
|
shouldWidthMatchParent = true,
|
|
|
|
positiveButtonBackgroundColor = accentHighContrastColor,
|
|
|
|
positiveButtonTextColor = R.color.photonWhite
|
|
|
|
),
|
|
|
|
sessionId = customTabSessionId,
|
|
|
|
onNeedToRequestPermissions = { permissions ->
|
|
|
|
requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS)
|
|
|
|
},
|
|
|
|
onShouldShowRequestPermissionRationale = {
|
|
|
|
shouldShowRequestPermissionRationale(
|
|
|
|
it
|
|
|
|
)
|
|
|
|
},
|
|
|
|
customTabId = customTabSessionId,
|
|
|
|
store = store),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
sitePermissionWifiIntegration.set(
|
|
|
|
feature = SitePermissionsWifiIntegration(
|
|
|
|
settings = context.settings(),
|
|
|
|
wifiConnectionMonitor = context.components.wifiConnectionMonitor
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
context.settings().setSitePermissionSettingListener(viewLifecycleOwner) {
|
|
|
|
// If the user connects to WIFI while on the BrowserFragment, this will update the
|
|
|
|
// SitePermissionsRules (specifically autoplay) accordingly
|
|
|
|
assignSitePermissionsRules()
|
|
|
|
}
|
|
|
|
assignSitePermissionsRules()
|
|
|
|
|
|
|
|
fullScreenFeature.set(
|
|
|
|
feature = FullScreenFeature(
|
|
|
|
requireComponents.core.store,
|
|
|
|
requireComponents.useCases.sessionUseCases,
|
|
|
|
customTabSessionId,
|
|
|
|
::viewportFitChange,
|
|
|
|
::fullScreenChanged
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
expandToolbarOnNavigation(store)
|
|
|
|
|
|
|
|
store.flowScoped(viewLifecycleOwner) { flow ->
|
|
|
|
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) }
|
|
|
|
.ifChanged { tab -> tab.content.pictureInPictureEnabled }
|
|
|
|
.collect { tab -> pipModeChanged(tab) }
|
|
|
|
}
|
|
|
|
|
|
|
|
view.swipeRefresh.isEnabled =
|
|
|
|
FeatureFlags.pullToRefreshEnabled && context.settings().isPullToRefreshEnabledInBrowser
|
|
|
|
if (view.swipeRefresh.isEnabled) {
|
|
|
|
val primaryTextColor =
|
|
|
|
ThemeManager.resolveAttribute(R.attr.primaryText, context)
|
|
|
|
view.swipeRefresh.setColorSchemeColors(primaryTextColor)
|
|
|
|
swipeRefreshFeature.set(
|
|
|
|
feature = SwipeRefreshFeature(
|
|
|
|
requireComponents.core.store,
|
|
|
|
context.components.useCases.sessionUseCases.reload,
|
|
|
|
view.swipeRefresh,
|
|
|
|
customTabSessionId
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
webchannelIntegration.set(
|
|
|
|
feature = FxaWebChannelFeature(
|
|
|
|
requireContext(),
|
|
|
|
customTabSessionId,
|
|
|
|
requireComponents.core.engine,
|
|
|
|
requireComponents.core.store,
|
|
|
|
requireComponents.backgroundServices.accountManager,
|
|
|
|
requireComponents.backgroundServices.serverConfig,
|
|
|
|
setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC)
|
|
|
|
),
|
|
|
|
owner = this,
|
|
|
|
view = view
|
|
|
|
)
|
|
|
|
|
|
|
|
initializeEngineView(toolbarHeight)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
internal fun expandToolbarOnNavigation(store: BrowserStore) {
|
|
|
|
consumeFlow(store) { flow ->
|
|
|
|
flow.mapNotNull {
|
|
|
|
state -> state.findCustomTabOrSelectedTab(customTabSessionId)
|
|
|
|
}
|
|
|
|
.ifAnyChanged {
|
|
|
|
tab -> arrayOf(tab.content.url, tab.content.loadRequest)
|
|
|
|
}
|
|
|
|
.collect {
|
|
|
|
browserToolbarView.expand()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Preserves current state of the [DynamicDownloadDialog] to persist through tab changes and
|
|
|
|
* other fragments navigation.
|
|
|
|
* */
|
|
|
|
private fun saveDownloadDialogState(
|
|
|
|
sessionId: String?,
|
|
|
|
downloadState: DownloadState,
|
|
|
|
downloadJobStatus: DownloadState.Status
|
|
|
|
) {
|
|
|
|
sessionId?.let { id ->
|
|
|
|
sharedViewModel.downloadDialogState[id] = Pair(
|
|
|
|
downloadState,
|
|
|
|
downloadJobStatus == DownloadState.Status.FAILED
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Re-initializes [DynamicDownloadDialog] if the user hasn't dismissed the dialog
|
|
|
|
* before navigating away from it's original tab.
|
|
|
|
* onTryAgain it will use [ContentAction.UpdateDownloadAction] to re-enqueue the former failed
|
|
|
|
* download, because [DownloadsFeature] clears any queued downloads onStop.
|
|
|
|
* */
|
|
|
|
@VisibleForTesting
|
|
|
|
internal fun resumeDownloadDialogState(
|
|
|
|
sessionId: String?,
|
|
|
|
store: BrowserStore,
|
|
|
|
view: View,
|
|
|
|
context: Context,
|
|
|
|
toolbarHeight: Int
|
|
|
|
) {
|
|
|
|
val savedDownloadState =
|
|
|
|
sharedViewModel.downloadDialogState[sessionId]
|
|
|
|
|
|
|
|
if (savedDownloadState == null || sessionId == null) {
|
|
|
|
view.viewDynamicDownloadDialog.visibility = View.GONE
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val onTryAgain: (String) -> Unit = {
|
|
|
|
savedDownloadState.first?.let { dlState ->
|
|
|
|
store.dispatch(
|
|
|
|
ContentAction.UpdateDownloadAction(
|
|
|
|
sessionId, dlState.copy(skipConfirmation = true)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
val onCannotOpenFile = {
|
|
|
|
FenixSnackbar.make(
|
|
|
|
view = view.browserLayout,
|
|
|
|
duration = Snackbar.LENGTH_SHORT,
|
|
|
|
isDisplayedWithBrowserToolbar = true
|
|
|
|
)
|
|
|
|
.setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
|
|
|
|
.show()
|
|
|
|
}
|
|
|
|
|
|
|
|
val onDismiss: () -> Unit =
|
|
|
|
{ sharedViewModel.downloadDialogState.remove(sessionId) }
|
|
|
|
|
|
|
|
DynamicDownloadDialog(
|
|
|
|
container = view.browserLayout,
|
|
|
|
downloadState = savedDownloadState.first,
|
|
|
|
didFail = savedDownloadState.second,
|
|
|
|
tryAgain = onTryAgain,
|
|
|
|
onCannotOpenFile = onCannotOpenFile,
|
|
|
|
view = view.viewDynamicDownloadDialog,
|
|
|
|
toolbarHeight = toolbarHeight,
|
|
|
|
onDismiss = onDismiss
|
|
|
|
).show()
|
|
|
|
|
|
|
|
browserToolbarView.expand()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun initializeEngineView(toolbarHeight: Int) {
|
|
|
|
val context = requireContext()
|
|
|
|
|
|
|
|
if (context.settings().isDynamicToolbarEnabled) {
|
|
|
|
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
|
|
|
|
|
|
|
|
val behavior = when (context.settings().toolbarPosition) {
|
|
|
|
// Set engineView dynamic vertical clipping depending on the toolbar position.
|
|
|
|
ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null)
|
|
|
|
// Set scroll flags depending on if if the browser or the website is doing the scroll.
|
|
|
|
ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(
|
|
|
|
context,
|
|
|
|
null,
|
|
|
|
engineView,
|
|
|
|
browserToolbarView
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
|
|
|
|
} else {
|
|
|
|
// Ensure webpage's bottom elements are aligned to the very bottom of the engineView.
|
|
|
|
engineView.setDynamicToolbarMaxHeight(0)
|
|
|
|
|
|
|
|
// Effectively place the engineView on top of the toolbar if that is not dynamic.
|
|
|
|
if (context.settings().shouldUseBottomToolbar) {
|
|
|
|
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
|
|
|
|
browserEngine.bottomMargin =
|
|
|
|
requireContext().resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a list of context menu items [ContextMenuCandidate] for the context menu
|
|
|
|
*/
|
|
|
|
protected abstract fun getContextMenuCandidates(
|
|
|
|
context: Context,
|
|
|
|
view: View
|
|
|
|
): List<ContextMenuCandidate>
|
|
|
|
|
|
|
|
@CallSuper
|
|
|
|
override fun onStart() {
|
|
|
|
super.onStart()
|
|
|
|
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
|
|
|
|
}
|
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
internal fun observeTabSelection(store: BrowserStore) {
|
|
|
|
consumeFlow(store) { flow ->
|
|
|
|
flow.ifChanged {
|
|
|
|
it.selectedTabId
|
|
|
|
}
|
|
|
|
.mapNotNull {
|
|
|
|
it.selectedTab
|
|
|
|
}
|
|
|
|
.collect {
|
|
|
|
handleTabSelected(it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleTabSelected(selectedTab: TabSessionState) {
|
|
|
|
if (!this.isRemoving) {
|
|
|
|
updateThemeForSession(selectedTab)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (browserInitialized) {
|
|
|
|
view?.let { view ->
|
|
|
|
fullScreenChanged(false)
|
|
|
|
browserToolbarView.expand()
|
|
|
|
|
|
|
|
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
|
|
|
|
val context = requireContext()
|
|
|
|
resumeDownloadDialogState(selectedTab.id, context.components.core.store, view, context, toolbarHeight)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
view?.let { view ->
|
|
|
|
browserInitialized = initializeUI(view) != null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@CallSuper
|
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
val components = requireComponents
|
|
|
|
|
|
|
|
val preferredColorScheme = components.core.getPreferredColorScheme()
|
|
|
|
if (components.core.engine.settings.preferredColorScheme != preferredColorScheme) {
|
|
|
|
components.core.engine.settings.preferredColorScheme = preferredColorScheme
|
|
|
|
components.useCases.sessionUseCases.reload()
|
|
|
|
}
|
|
|
|
hideToolbar()
|
|
|
|
|
|
|
|
components.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)?.let {
|
|
|
|
updateThemeForSession(it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@CallSuper
|
|
|
|
override fun onPause() {
|
|
|
|
super.onPause()
|
|
|
|
if (findNavController().currentDestination?.id != R.id.searchDialogFragment) {
|
|
|
|
view?.hideKeyboard()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@CallSuper
|
|
|
|
override fun onStop() {
|
|
|
|
super.onStop()
|
|
|
|
initUIJob?.cancel()
|
|
|
|
|
|
|
|
requireComponents.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)
|
|
|
|
?.let { session ->
|
|
|
|
// If we didn't enter PiP, exit full screen on stop
|
|
|
|
if (!session.content.pictureInPictureEnabled && fullScreenFeature.onBackPressed()) {
|
|
|
|
fullScreenChanged(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@CallSuper
|
|
|
|
override fun onBackPressed(): Boolean {
|
|
|
|
return findInPageIntegration.onBackPressed() ||
|
|
|
|
fullScreenFeature.onBackPressed() ||
|
|
|
|
sessionFeature.onBackPressed() ||
|
|
|
|
removeSessionIfNeeded()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onBackLongPressed(): Boolean {
|
|
|
|
findNavController().navigate(
|
|
|
|
NavGraphDirections.actionGlobalTabHistoryDialogFragment(
|
|
|
|
activeSessionId = customTabSessionId
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Saves the external app session ID to be restored later in [onViewStateRestored].
|
|
|
|
*/
|
|
|
|
final override fun onSaveInstanceState(outState: Bundle) {
|
|
|
|
super.onSaveInstanceState(outState)
|
|
|
|
outState.putString(KEY_CUSTOM_TAB_SESSION_ID, customTabSessionId)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves the external app session ID saved by [onSaveInstanceState].
|
|
|
|
*/
|
|
|
|
final override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
|
|
|
super.onViewStateRestored(savedInstanceState)
|
|
|
|
savedInstanceState?.getString(KEY_CUSTOM_TAB_SESSION_ID)?.let {
|
|
|
|
if (requireComponents.core.sessionManager.findSessionById(it)?.customTabConfig != null) {
|
|
|
|
customTabSessionId = it
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forwards permission grant results to one of the features.
|
|
|
|
*/
|
|
|
|
final override fun onRequestPermissionsResult(
|
|
|
|
requestCode: Int,
|
|
|
|
permissions: Array<String>,
|
|
|
|
grantResults: IntArray
|
|
|
|
) {
|
|
|
|
val feature: PermissionsFeature? = when (requestCode) {
|
|
|
|
REQUEST_CODE_DOWNLOAD_PERMISSIONS -> downloadsFeature.get()
|
|
|
|
REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get()
|
|
|
|
REQUEST_CODE_APP_PERMISSIONS -> sitePermissionsFeature.get()
|
|
|
|
else -> null
|
|
|
|
}
|
|
|
|
feature?.onPermissionsResult(permissions, grantResults)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forwards activity results to the prompt feature.
|
|
|
|
*/
|
|
|
|
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
|
|
promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) }
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the session if it was opened by an ACTION_VIEW intent
|
|
|
|
* or if it has a parent session and no more history
|
|
|
|
*/
|
|
|
|
protected open fun removeSessionIfNeeded(): Boolean {
|
|
|
|
getSessionById()?.let { session ->
|
|
|
|
return if (session.source == SessionState.Source.ACTION_VIEW) {
|
|
|
|
activity?.finish()
|
|
|
|
requireComponents.useCases.tabsUseCases.removeTab(session)
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
if (session.hasParentSession) {
|
|
|
|
// The removeTab use case does not currently select a parent session, so
|
|
|
|
// we are using sessionManager.remove
|
|
|
|
requireComponents.core.sessionManager.remove(
|
|
|
|
session,
|
|
|
|
selectParentIfExists = true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
// We want to return to home if this session didn't have a parent session to select.
|
|
|
|
val goToOverview = !session.hasParentSession
|
|
|
|
!goToOverview
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
protected abstract fun navToQuickSettingsSheet(
|
|
|
|
session: Session,
|
|
|
|
sitePermissions: SitePermissions?
|
|
|
|
)
|
|
|
|
|
|
|
|
protected abstract fun navToTrackingProtectionPanel(session: Session)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
|
|
|
|
*/
|
|
|
|
protected fun getAppropriateLayoutGravity(): Int =
|
|
|
|
components.settings.toolbarPosition.androidGravity
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the site permissions rules based on user settings.
|
|
|
|
*/
|
|
|
|
private fun assignSitePermissionsRules() {
|
|
|
|
val rules = components.settings.getSitePermissionsCustomSettingsRules()
|
|
|
|
|
|
|
|
sitePermissionsFeature.withFeature {
|
|
|
|
it.sitePermissionsRules = rules
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Displays the quick settings dialog,
|
|
|
|
* which lets the user control tracking protection and site settings.
|
|
|
|
*/
|
|
|
|
private fun showQuickSettingsDialog() {
|
|
|
|
val session = getSessionById() ?: return
|
|
|
|
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
|
|
|
val sitePermissions: SitePermissions? = session.url.toUri().host?.let { host ->
|
|
|
|
val storage = requireComponents.core.permissionStorage
|
|
|
|
storage.findSitePermissionsBy(host)
|
|
|
|
}
|
|
|
|
|
|
|
|
view?.let {
|
|
|
|
navToQuickSettingsSheet(session, sitePermissions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showTrackingProtectionPanel() {
|
|
|
|
val session = getSessionById() ?: return
|
|
|
|
view?.let {
|
|
|
|
navToTrackingProtectionPanel(session)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the activity normal/private theme to match the current session.
|
|
|
|
*/
|
|
|
|
@VisibleForTesting
|
|
|
|
internal fun updateThemeForSession(session: SessionState) {
|
|
|
|
val sessionMode = BrowsingMode.fromBoolean(session.content.private)
|
|
|
|
(activity as HomeActivity).browsingModeManager.mode = sessionMode
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the current session.
|
|
|
|
*/
|
|
|
|
protected fun getSessionById(): Session? {
|
|
|
|
val sessionManager = components.core.sessionManager
|
|
|
|
val localCustomTabId = customTabSessionId
|
|
|
|
return if (localCustomTabId != null) {
|
|
|
|
sessionManager.findSessionById(localCustomTabId)
|
|
|
|
} else {
|
|
|
|
sessionManager.selectedSession
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun bookmarkTapped(sessionUrl: String, sessionTitle: String) = withContext(IO) {
|
|
|
|
val bookmarksStorage = requireComponents.core.bookmarksStorage
|
|
|
|
val existing =
|
|
|
|
bookmarksStorage.getBookmarksWithUrl(sessionUrl).firstOrNull { it.url == sessionUrl }
|
|
|
|
if (existing != null) {
|
|
|
|
// Bookmark exists, go to edit fragment
|
|
|
|
withContext(Main) {
|
|
|
|
nav(
|
|
|
|
R.id.browserFragment,
|
|
|
|
BrowserFragmentDirections.actionGlobalBookmarkEditFragment(existing.guid, true)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Save bookmark, then go to edit fragment
|
|
|
|
val guid = bookmarksStorage.addItem(
|
|
|
|
BookmarkRoot.Mobile.id,
|
|
|
|
url = sessionUrl,
|
|
|
|
title = sessionTitle,
|
|
|
|
position = null
|
|
|
|
)
|
|
|
|
|
|
|
|
withContext(Main) {
|
|
|
|
requireComponents.analytics.metrics.track(Event.AddBookmark)
|
|
|
|
|
|
|
|
view?.let { view ->
|
|
|
|
FenixSnackbar.make(
|
|
|
|
view = view.browserLayout,
|
|
|
|
duration = FenixSnackbar.LENGTH_LONG,
|
|
|
|
isDisplayedWithBrowserToolbar = true
|
|
|
|
)
|
|
|
|
.setText(getString(R.string.bookmark_saved_snackbar))
|
|
|
|
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
|
|
|
|
nav(
|
|
|
|
R.id.browserFragment,
|
|
|
|
BrowserFragmentDirections.actionGlobalBookmarkEditFragment(
|
|
|
|
guid,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.show()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onHomePressed() = pipFeature?.onHomePressed() ?: false
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Exit fullscreen mode when exiting PIP mode
|
|
|
|
*/
|
|
|
|
private fun pipModeChanged(session: SessionState) {
|
|
|
|
if (!session.content.pictureInPictureEnabled && session.content.fullScreen) {
|
|
|
|
onBackPressed()
|
|
|
|
fullScreenChanged(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final override fun onPictureInPictureModeChanged(enabled: Boolean) {
|
|
|
|
pipFeature?.onPictureInPictureModeChanged(enabled)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun viewportFitChange(layoutInDisplayCutoutMode: Int) {
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
|
|
val layoutParams = activity?.window?.attributes
|
|
|
|
layoutParams?.layoutInDisplayCutoutMode = layoutInDisplayCutoutMode
|
|
|
|
activity?.window?.attributes = layoutParams
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
internal fun fullScreenChanged(inFullScreen: Boolean) {
|
|
|
|
if (inFullScreen) {
|
|
|
|
// Close find in page bar if opened
|
|
|
|
findInPageIntegration.onBackPressed()
|
|
|
|
FenixSnackbar.make(
|
|
|
|
view = requireView().browserLayout,
|
|
|
|
duration = Snackbar.LENGTH_SHORT,
|
|
|
|
isDisplayedWithBrowserToolbar = false
|
|
|
|
)
|
|
|
|
.setText(getString(R.string.full_screen_notification))
|
|
|
|
.show()
|
|
|
|
activity?.enterToImmersiveMode()
|
|
|
|
browserToolbarView.view.isVisible = false
|
|
|
|
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
|
|
|
|
browserEngine.bottomMargin = 0
|
|
|
|
|
|
|
|
engineView.setDynamicToolbarMaxHeight(0)
|
|
|
|
browserToolbarView.expand()
|
|
|
|
// Without this, fullscreen has a margin at the top.
|
|
|
|
engineView.setVerticalClipping(0)
|
|
|
|
} else {
|
|
|
|
activity?.exitImmersiveModeIfNeeded()
|
|
|
|
(activity as? HomeActivity)?.let { activity ->
|
|
|
|
activity.themeManager.applyStatusBarTheme(activity)
|
|
|
|
}
|
|
|
|
if (webAppToolbarShouldBeVisible) {
|
|
|
|
browserToolbarView.view.isVisible = true
|
|
|
|
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
|
|
|
|
initializeEngineView(toolbarHeight)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Dereference these views when the fragment view is destroyed to prevent memory leaks
|
|
|
|
*/
|
|
|
|
override fun onDestroyView() {
|
|
|
|
super.onDestroyView()
|
|
|
|
|
|
|
|
// Diagnostic breadcrumb for "Display already aquired" crash:
|
|
|
|
// https://github.com/mozilla-mobile/android-components/issues/7960
|
|
|
|
breadcrumb(
|
|
|
|
message = "onDestroyView()"
|
|
|
|
)
|
|
|
|
|
|
|
|
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
|
|
|
|
_browserToolbarView = null
|
|
|
|
_browserInteractor = null
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onAttach(context: Context) {
|
|
|
|
super.onAttach(context)
|
|
|
|
|
|
|
|
// Diagnostic breadcrumb for "Display already aquired" crash:
|
|
|
|
// https://github.com/mozilla-mobile/android-components/issues/7960
|
|
|
|
breadcrumb(
|
|
|
|
message = "onAttach()"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDetach() {
|
|
|
|
super.onDetach()
|
|
|
|
|
|
|
|
// Diagnostic breadcrumb for "Display already aquired" crash:
|
|
|
|
// https://github.com/mozilla-mobile/android-components/issues/7960
|
|
|
|
breadcrumb(
|
|
|
|
message = "onDetach()"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
|
|
|
|
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
|
|
|
|
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
|
|
|
|
private const val REQUEST_CODE_APP_PERMISSIONS = 3
|
|
|
|
|
|
|
|
private const val LOADING_PROGRESS_COMPLETE = 100
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onAccessibilityStateChanged(enabled: Boolean) {
|
|
|
|
if (_browserToolbarView != null) {
|
|
|
|
browserToolbarView.setScrollFlags(enabled)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|