/* 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.Intent import android.content.pm.ActivityInfo import android.os.Bundle import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.RadioButton import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.component_search.* 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.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.intent.IntentProcessor import mozilla.components.feature.prompts.PromptFeature import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SwipeRefreshFeature import mozilla.components.feature.session.ThumbnailsFeature import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.kotlin.toUri import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ThemeManager import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.toolbar.BrowserInteractor import org.mozilla.fenix.components.toolbar.BrowserState import org.mozilla.fenix.components.toolbar.BrowserStore import org.mozilla.fenix.components.toolbar.BrowserToolbarView import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController import org.mozilla.fenix.components.toolbar.QuickActionSheetAction import org.mozilla.fenix.components.toolbar.QuickActionSheetState import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.customtabs.CustomTabsIntegration import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.enterToImmersiveMode import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.mvi.getManagedEmitter import org.mozilla.fenix.quickactionsheet.DefaultQuickActionSheetController import org.mozilla.fenix.quickactionsheet.QuickActionSheetView import org.mozilla.fenix.utils.Settings import java.net.MalformedURLException import java.net.URL @SuppressWarnings("TooManyFunctions", "LargeClass") class BrowserFragment : Fragment(), BackHandler { private lateinit var browserStore: BrowserStore private lateinit var browserInteractor: BrowserInteractor private lateinit var browserToolbarView: BrowserToolbarView private lateinit var quickActionSheetView: QuickActionSheetView private var tabCollectionObserver: Observer>? = null private var sessionObserver: Session.Observer? = null private var sessionManagerObserver: SessionManager.Observer? = null private val sessionFeature = ViewBoundFeatureWrapper() private val contextMenuFeature = ViewBoundFeatureWrapper() private val downloadsFeature = ViewBoundFeatureWrapper() private val appLinksFeature = ViewBoundFeatureWrapper() private val promptsFeature = ViewBoundFeatureWrapper() private val findInPageIntegration = ViewBoundFeatureWrapper() private val toolbarIntegration = ViewBoundFeatureWrapper() private val readerViewFeature = ViewBoundFeatureWrapper() private val sitePermissionsFeature = ViewBoundFeatureWrapper() private val fullScreenFeature = ViewBoundFeatureWrapper() private val thumbnailsFeature = ViewBoundFeatureWrapper() private val swipeRefreshFeature = ViewBoundFeatureWrapper() private val customTabsIntegration = ViewBoundFeatureWrapper() private var findBookmarkJob: Job? = null var customTabSessionId: String? = null /* override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Disabled while awaiting a better solution to #3209 postponeEnterTransition() sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move).setDuration( SHARED_TRANSITION_MS ) } */ @SuppressWarnings("ComplexMethod") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { require(arguments != null) customTabSessionId = arguments?.getString(IntentProcessor.ACTIVE_SESSION_ID) val view = inflater.inflate(R.layout.fragment_browser, container, false) view.browserLayout.transitionName = "$TAB_ITEM_TRANSITION_NAME${getSessionById()?.id}" startPostponedEnterTransition() val activity = activity as HomeActivity ThemeManager.applyStatusBarTheme(activity.window, activity.themeManager, activity) val appLink = requireComponents.useCases.appLinksUseCases.appLinkRedirect browserStore = StoreProvider.get(this) { BrowserStore( BrowserState( quickActionSheetState = QuickActionSheetState( readable = getSessionById()?.readerable ?: false, bookmarked = false, readerActive = getSessionById()?.readerMode ?: false, bounceNeeded = false, isAppLink = getSessionById()?.let { appLink.invoke(it.url).hasExternalApp() } ?: false ) ) ) } return view } @Suppress("LongMethod", "ComplexMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val sessionManager = requireComponents.core.sessionManager getSessionById()?.let { session -> val viewModel = activity!!.run { ViewModelProviders.of(this).get(CreateCollectionViewModel::class.java) } browserInteractor = BrowserInteractor( context = context!!, store = browserStore, browserToolbarController = DefaultBrowserToolbarController( context!!, findNavController(), findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, nestedScrollQuickActionView = nestedScrollQuickAction, engineView = engineView, currentSession = session, viewModel = viewModel ), quickActionSheetController = DefaultQuickActionSheetController( context = context!!, navController = findNavController(), currentSession = session, appLinksUseCases = requireComponents.useCases.appLinksUseCases, bookmarkTapped = { lifecycleScope.launch { bookmarkTapped(it) } } ), readerModeController = DefaultReaderModeController(readerViewFeature), currentSession = session ) browserToolbarView = BrowserToolbarView( container = view.browserLayout, interactor = browserInteractor, currentSession = session ) toolbarIntegration.set( feature = browserToolbarView.toolbarIntegration, owner = this, view = view ) findInPageIntegration.set( feature = FindInPageIntegration( sessionManager = requireComponents.core.sessionManager, sessionId = customTabSessionId, view = view.findInPageView, engineView = view.engineView, toolbar = toolbar ), owner = this, view = view ) quickActionSheetView = QuickActionSheetView(view.nestedScrollQuickAction, browserInteractor) browserToolbarView.view.setOnSiteSecurityClickedListener { showQuickSettingsDialog() } customTabSessionId?.let { customTabSessionId -> customTabsIntegration.set( feature = CustomTabsIntegration( requireContext(), requireComponents.core.sessionManager, toolbar, customTabSessionId, activity, view.nestedScrollQuickAction, view.swipeRefresh, onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) } ), owner = this, view = view) } consumeFrom(browserStore) { quickActionSheetView.update(it) browserToolbarView.update(it) } } contextMenuFeature.set( feature = ContextMenuFeature( requireFragmentManager(), sessionManager, FenixContextMenuCandidate.defaultCandidates( requireContext(), requireComponents.useCases.tabsUseCases, view, FenixSnackbarDelegate( view, if (getSessionById()?.isCustomTabSession() == true) null else nestedScrollQuickAction ) ), view.engineView ), owner = this, view = view ) downloadsFeature.set( feature = DownloadsFeature( requireContext().applicationContext, sessionManager = sessionManager, fragmentManager = childFragmentManager, sessionId = customTabSessionId, downloadManager = FetchDownloadManager(requireContext().applicationContext, DownloadService::class), onNeedToRequestPermissions = { permissions -> requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) }), owner = this, view = view ) appLinksFeature.set( feature = AppLinksFeature( requireContext(), sessionManager = sessionManager, sessionId = customTabSessionId, interceptLinkClicks = true, fragmentManager = requireFragmentManager() ), owner = this, view = view ) promptsFeature.set( feature = PromptFeature( fragment = this, sessionManager = sessionManager, sessionId = customTabSessionId, fragmentManager = requireFragmentManager(), onNeedToRequestPermissions = { permissions -> requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) }), owner = this, view = view ) sessionFeature.set( feature = SessionFeature( sessionManager, SessionUseCases(sessionManager), view.engineView, customTabSessionId ), owner = this, view = view ) val accentHighContrastColor = ThemeManager.resolveAttribute(R.attr.accentHighContrast, requireContext()) sitePermissionsFeature.set( feature = SitePermissionsFeature( context = requireContext(), sessionManager = sessionManager, fragmentManager = requireFragmentManager(), promptsStyling = SitePermissionsFeature.PromptsStyling( gravity = getAppropriateLayoutGravity(), shouldWidthMatchParent = true, positiveButtonBackgroundColor = accentHighContrastColor, positiveButtonTextColor = R.color.photonWhite ), sessionId = customTabSessionId ) { permissions -> requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS) }, owner = this, view = view ) fullScreenFeature.set( feature = FullScreenFeature( sessionManager, SessionUseCases(sessionManager), customTabSessionId ) { if (it) { FenixSnackbar.make(view.rootView, Snackbar.LENGTH_SHORT) .setText(getString(R.string.full_screen_notification)) .show() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE activity?.enterToImmersiveMode() toolbar.visibility = View.GONE nestedScrollQuickAction.visibility = View.GONE } else { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER activity?.exitImmersiveModeIfNeeded() (activity as HomeActivity).let { activity: HomeActivity -> ThemeManager.applyStatusBarTheme( activity.window, activity.themeManager, activity ) } toolbar.visibility = View.VISIBLE nestedScrollQuickAction.visibility = View.VISIBLE } changeEngineMargins(swipeRefresh = view.swipeRefresh, inFullScreen = it) }, owner = this, view = view ) thumbnailsFeature.set( feature = ThumbnailsFeature( requireContext(), view.engineView, requireComponents.core.sessionManager ), owner = this, view = view ) if (FeatureFlags.pullToRefreshEnabled) { val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, requireContext()) view.swipeRefresh.setColorSchemeColors(primaryTextColor) swipeRefreshFeature.set( feature = SwipeRefreshFeature( requireComponents.core.sessionManager, requireComponents.useCases.sessionUseCases.reload, view.swipeRefresh, customTabSessionId ), owner = this, view = view ) } else { // Disable pull to refresh view.swipeRefresh.setOnChildScrollUpCallback { _, _ -> true } } if ((activity as HomeActivity).browsingModeManager.isPrivate) { // We need to update styles for private mode programmatically for now: // https://github.com/mozilla-mobile/android-components/issues/3400 themeReaderViewControlsForPrivateMode(view.readerViewControlsBar) } readerViewFeature.set( feature = ReaderViewFeature( requireContext(), requireComponents.core.engine, requireComponents.core.sessionManager, view.readerViewControlsBar ) { available -> if (available) { requireComponents.analytics.metrics.track(Event.ReaderModeAvailable) } browserStore.apply { dispatch(QuickActionSheetAction.ReadableStateChange(available)) dispatch(QuickActionSheetAction.ReaderActiveStateChange( sessionManager.selectedSession?.readerMode ?: false )) } }, owner = this, view = view ) } private fun themeReaderViewControlsForPrivateMode(view: View) = with(view) { listOf( R.id.mozac_feature_readerview_font_size_decrease, R.id.mozac_feature_readerview_font_size_increase ).map { findViewById