You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
924 lines
37 KiB
Kotlin
924 lines
37 KiB
Kotlin
/* 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.home
|
|
|
|
import android.animation.Animator
|
|
import android.content.Context
|
|
import android.content.DialogInterface
|
|
import android.graphics.drawable.BitmapDrawable
|
|
import android.graphics.drawable.ColorDrawable
|
|
import android.os.Bundle
|
|
import android.os.StrictMode
|
|
import android.view.Gravity
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.accessibility.AccessibilityEvent
|
|
import android.widget.Button
|
|
import android.widget.LinearLayout
|
|
import android.widget.PopupWindow
|
|
import androidx.annotation.StringRes
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.constraintlayout.widget.ConstraintLayout
|
|
import androidx.constraintlayout.widget.ConstraintSet
|
|
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
|
|
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
|
|
import androidx.constraintlayout.widget.ConstraintSet.TOP
|
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.view.doOnLayout
|
|
import androidx.core.view.updateLayoutParams
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.fragment.app.activityViewModels
|
|
import androidx.fragment.app.viewModels
|
|
import androidx.lifecycle.Observer
|
|
import androidx.lifecycle.ViewModelProvider
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.navigation.fragment.findNavController
|
|
import androidx.navigation.fragment.navArgs
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
|
import com.google.android.material.appbar.AppBarLayout
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import kotlinx.android.synthetic.main.fragment_home.*
|
|
import kotlinx.android.synthetic.main.fragment_home.view.*
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
import kotlinx.coroutines.Dispatchers.Main
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import mozilla.appservices.places.BookmarkRoot
|
|
import mozilla.components.browser.menu.BrowserMenu
|
|
import mozilla.components.browser.menu.BrowserMenuBuilder
|
|
import mozilla.components.browser.menu.item.BrowserMenuImageText
|
|
import mozilla.components.browser.menu.view.MenuButton
|
|
import mozilla.components.browser.session.Session
|
|
import mozilla.components.browser.session.SessionManager
|
|
import mozilla.components.browser.state.selector.normalTabs
|
|
import mozilla.components.browser.state.selector.privateTabs
|
|
import mozilla.components.concept.sync.AccountObserver
|
|
import mozilla.components.concept.sync.AuthType
|
|
import mozilla.components.concept.sync.OAuthAccount
|
|
import mozilla.components.feature.tab.collections.TabCollection
|
|
import mozilla.components.feature.top.sites.TopSite
|
|
import mozilla.components.lib.state.ext.consumeFrom
|
|
import mozilla.components.support.ktx.android.util.dpToPx
|
|
import org.mozilla.fenix.BrowserDirection
|
|
import org.mozilla.fenix.HomeActivity
|
|
import org.mozilla.fenix.R
|
|
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
|
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
|
import org.mozilla.fenix.cfr.SearchWidgetCFR
|
|
import org.mozilla.fenix.components.FenixSnackbar
|
|
import org.mozilla.fenix.components.PrivateShortcutCreateManager
|
|
import org.mozilla.fenix.components.StoreProvider
|
|
import org.mozilla.fenix.components.TabCollectionStorage
|
|
import org.mozilla.fenix.components.metrics.Event
|
|
import org.mozilla.fenix.components.tips.FenixTipManager
|
|
import org.mozilla.fenix.components.tips.providers.MigrationTipProvider
|
|
import org.mozilla.fenix.ext.components
|
|
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.sessionsOfType
|
|
import org.mozilla.fenix.ext.settings
|
|
import org.mozilla.fenix.ext.toTab
|
|
import org.mozilla.fenix.ext.resetPoliciesAfter
|
|
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
|
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
|
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
|
import org.mozilla.fenix.settings.SupportUtils
|
|
import org.mozilla.fenix.settings.SupportUtils.MozillaPage.PRIVATE_NOTICE
|
|
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
|
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
import org.mozilla.fenix.theme.ThemeManager
|
|
import org.mozilla.fenix.utils.FragmentPreDrawManager
|
|
import org.mozilla.fenix.utils.allowUndo
|
|
import org.mozilla.fenix.whatsnew.WhatsNew
|
|
import java.lang.ref.WeakReference
|
|
import kotlin.math.abs
|
|
import kotlin.math.min
|
|
|
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
|
class HomeFragment : Fragment() {
|
|
private val args by navArgs<HomeFragmentArgs>()
|
|
|
|
private val homeViewModel: HomeScreenViewModel by viewModels {
|
|
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
|
}
|
|
|
|
private val snackbarAnchorView: View?
|
|
get() {
|
|
return if (requireContext().settings().shouldUseBottomToolbar) {
|
|
toolbarLayout
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
|
|
private var homeAppBarOffset = 0
|
|
|
|
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
|
override fun onCollectionCreated(title: String, sessions: List<Session>) {
|
|
scrollAndAnimateCollection(sessions.size)
|
|
}
|
|
|
|
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
|
|
scrollAndAnimateCollection(sessions.size, tabCollection)
|
|
}
|
|
|
|
override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
|
|
showRenamedSnackbar()
|
|
}
|
|
}
|
|
|
|
private val sessionManager: SessionManager
|
|
get() = requireComponents.core.sessionManager
|
|
|
|
private lateinit var homeAppBarOffSetListener: AppBarLayout.OnOffsetChangedListener
|
|
private val onboarding by lazy { FenixOnboarding(requireContext()) }
|
|
private lateinit var homeFragmentStore: HomeFragmentStore
|
|
private var _sessionControlInteractor: SessionControlInteractor? = null
|
|
protected val sessionControlInteractor: SessionControlInteractor
|
|
get() = _sessionControlInteractor!!
|
|
|
|
private var sessionControlView: SessionControlView? = null
|
|
private lateinit var currentMode: CurrentMode
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
postponeEnterTransition()
|
|
lifecycleScope.launch(IO) {
|
|
if (!onboarding.userHasBeenOnboarded()) {
|
|
requireComponents.analytics.metrics.track(Event.OpenedAppFirstRun)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View? {
|
|
val view = inflater.inflate(R.layout.fragment_home, container, false)
|
|
val activity = activity as HomeActivity
|
|
|
|
currentMode = CurrentMode(
|
|
view.context,
|
|
onboarding,
|
|
browsingModeManager,
|
|
::dispatchModeChanges
|
|
)
|
|
|
|
homeFragmentStore = StoreProvider.get(this) {
|
|
HomeFragmentStore(
|
|
HomeFragmentState(
|
|
collections = requireComponents.core.tabCollectionStorage.cachedTabCollections,
|
|
expandedCollections = emptySet(),
|
|
mode = currentMode.getCurrentMode(),
|
|
topSites = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
|
|
requireComponents.core.topSiteStorage.cachedTopSites
|
|
},
|
|
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
|
|
)
|
|
)
|
|
}
|
|
|
|
_sessionControlInteractor = SessionControlInteractor(
|
|
DefaultSessionControlController(
|
|
activity = activity,
|
|
fragmentStore = homeFragmentStore,
|
|
navController = findNavController(),
|
|
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
|
|
getListOfTabs = ::getListOfTabs,
|
|
hideOnboarding = ::hideOnboardingAndOpenSearch,
|
|
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
|
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
|
|
openSettingsScreen = ::openSettingsScreen,
|
|
openWhatsNewLink = { openInNormalTab(SupportUtils.getWhatsNewUrl(activity)) },
|
|
openPrivacyNotice = { openInNormalTab(SupportUtils.getMozillaPageUrl(PRIVATE_NOTICE)) },
|
|
showTabTray = ::openTabTray
|
|
)
|
|
)
|
|
updateLayout(view)
|
|
setOffset(view)
|
|
sessionControlView = SessionControlView(
|
|
view.sessionControlRecyclerView,
|
|
sessionControlInteractor,
|
|
homeViewModel
|
|
)
|
|
|
|
updateSessionControlView(view)
|
|
|
|
activity.themeManager.applyStatusBarTheme(activity)
|
|
|
|
view.consumeFrom(requireComponents.core.store, viewLifecycleOwner) {
|
|
val tabCount = if (currentMode.getCurrentMode() == Mode.Normal) {
|
|
it.normalTabs.size
|
|
} else {
|
|
it.privateTabs.size
|
|
}
|
|
|
|
view.tab_button.setCountWithAnimation(tabCount)
|
|
}
|
|
|
|
return view
|
|
}
|
|
|
|
/**
|
|
* The [SessionControlView] is forced to update with our current state when we call
|
|
* [HomeFragment.onCreateView] in order to be able to draw everything at once with the current
|
|
* data in our store. The [View.consumeFrom] coroutine dispatch
|
|
* doesn't get run right away which means that we won't draw on the first layout pass.
|
|
*/
|
|
fun updateSessionControlView(view: View) {
|
|
sessionControlView?.update(homeFragmentStore.state)
|
|
|
|
view.consumeFrom(homeFragmentStore, viewLifecycleOwner) {
|
|
sessionControlView?.update(it)
|
|
}
|
|
}
|
|
|
|
private fun updateLayout(view: View) {
|
|
val shouldUseBottomToolbar = view.context.settings().shouldUseBottomToolbar
|
|
|
|
if (!shouldUseBottomToolbar) {
|
|
view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
|
|
ConstraintLayout.LayoutParams.MATCH_PARENT,
|
|
ConstraintLayout.LayoutParams.WRAP_CONTENT
|
|
)
|
|
.apply {
|
|
gravity = Gravity.TOP
|
|
}
|
|
|
|
ConstraintSet().apply {
|
|
clone(view.toolbarLayout)
|
|
clear(view.bottom_bar.id, BOTTOM)
|
|
clear(view.bottomBarShadow.id, BOTTOM)
|
|
connect(view.bottom_bar.id, TOP, PARENT_ID, TOP)
|
|
connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM)
|
|
connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
|
|
applyTo(view.toolbarLayout)
|
|
}
|
|
|
|
view.bottom_bar.background = resources.getDrawable(
|
|
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()),
|
|
null
|
|
)
|
|
|
|
view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics)
|
|
}
|
|
|
|
createNewAppBarListener(HEADER_MARGIN.dpToPx(resources.displayMetrics).toFloat())
|
|
view.homeAppBar.addOnOffsetChangedListener(
|
|
homeAppBarOffSetListener
|
|
)
|
|
} else {
|
|
createNewAppBarListener(0F)
|
|
view.homeAppBar.addOnOffsetChangedListener(
|
|
homeAppBarOffSetListener
|
|
)
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("LongMethod")
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
FragmentPreDrawManager(this).execute {
|
|
val homeViewModel: HomeScreenViewModel by activityViewModels {
|
|
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
|
}
|
|
homeViewModel.layoutManagerState?.also { parcelable ->
|
|
sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
|
|
}
|
|
homeViewModel.layoutManagerState = null
|
|
|
|
// We have to delay so that the keyboard collapses and the view is resized before the
|
|
// animation from SearchFragment happens
|
|
delay(ANIMATION_DELAY)
|
|
}
|
|
|
|
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
|
// This is necessary due to a bug in viewLifecycleOwner. See:
|
|
// https://github.com/mozilla-mobile/android-components/blob/master/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt#L32-L56
|
|
// TODO remove when viewLifecycleOwner is fixed
|
|
val context = context ?: return@launch
|
|
|
|
val iconSize =
|
|
context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
|
|
|
|
val searchEngine = context.components.search.provider.getDefaultEngine(context)
|
|
val searchIcon = BitmapDrawable(context.resources, searchEngine.icon)
|
|
searchIcon.setBounds(0, 0, iconSize, iconSize)
|
|
|
|
withContext(Main) {
|
|
search_engine_icon?.setImageDrawable(searchIcon)
|
|
}
|
|
}
|
|
|
|
createHomeMenu(requireContext(), WeakReference(view.menuButton))
|
|
view.tab_button.setOnLongClickListener {
|
|
createTabCounterMenu(requireContext()).show(view.tab_button)
|
|
true
|
|
}
|
|
|
|
view.menuButton.setColorFilter(
|
|
ContextCompat.getColor(
|
|
requireContext(),
|
|
ThemeManager.resolveAttribute(R.attr.primaryText, requireContext())
|
|
)
|
|
)
|
|
|
|
view.toolbar.compoundDrawablePadding =
|
|
view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
|
|
view.toolbar_wrapper.setOnClickListener {
|
|
hideOnboardingIfNeeded()
|
|
navigateToSearch()
|
|
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
|
|
}
|
|
|
|
view.tab_button.setOnClickListener {
|
|
openTabTray()
|
|
}
|
|
|
|
PrivateBrowsingButtonView(
|
|
privateBrowsingButton,
|
|
browsingModeManager
|
|
) { newMode ->
|
|
if (newMode == BrowsingMode.Private) {
|
|
requireContext().settings().incrementNumTimesPrivateModeOpened()
|
|
}
|
|
|
|
if (onboarding.userHasBeenOnboarded()) {
|
|
homeFragmentStore.dispatch(
|
|
HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode))
|
|
)
|
|
}
|
|
}
|
|
|
|
// We call this onLayout so that the bottom bar width is correctly set for us to center
|
|
// the CFR in.
|
|
view.toolbar_wrapper.doOnLayout {
|
|
if (!browsingModeManager.mode.isPrivate) {
|
|
SearchWidgetCFR(view.context) { view.toolbar_wrapper }.displayIfNecessary()
|
|
}
|
|
}
|
|
|
|
if (view.context.settings().accessibilityServicesEnabled && args.focusOnAddressBar) {
|
|
// We cannot put this in the fragment_home.xml file as it breaks tests
|
|
view.toolbar_wrapper.isFocusableInTouchMode = true
|
|
viewLifecycleOwner.lifecycleScope.launch {
|
|
view.toolbar_wrapper?.requestFocus()
|
|
view.toolbar_wrapper?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onDestroyView() {
|
|
super.onDestroyView()
|
|
_sessionControlInteractor = null
|
|
sessionControlView = null
|
|
requireView().homeAppBar.removeOnOffsetChangedListener(homeAppBarOffSetListener)
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
subscribeToTabCollections()
|
|
subscribeToTopSites()
|
|
|
|
val context = requireContext()
|
|
val components = context.components
|
|
|
|
homeFragmentStore.dispatch(
|
|
HomeFragmentAction.Change(
|
|
collections = components.core.tabCollectionStorage.cachedTabCollections,
|
|
mode = currentMode.getCurrentMode(),
|
|
topSites = components.core.topSiteStorage.cachedTopSites,
|
|
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip()
|
|
)
|
|
)
|
|
|
|
requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
|
|
// By the time this code runs, we may not be attached to a context or have a view lifecycle owner.
|
|
if ((this@HomeFragment).view?.context == null) {
|
|
return@runIfReadyOrQueue
|
|
}
|
|
|
|
requireComponents.backgroundServices.accountManager.register(
|
|
currentMode,
|
|
owner = this@HomeFragment.viewLifecycleOwner
|
|
)
|
|
requireComponents.backgroundServices.accountManager.register(object : AccountObserver {
|
|
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
|
if (authType != AuthType.Existing) {
|
|
view?.let {
|
|
FenixSnackbar.make(
|
|
view = it,
|
|
duration = Snackbar.LENGTH_SHORT,
|
|
isDisplayedWithBrowserToolbar = false
|
|
)
|
|
.setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
|
|
.setAnchorView(toolbarLayout)
|
|
.show()
|
|
}
|
|
}
|
|
}
|
|
}, owner = this@HomeFragment.viewLifecycleOwner)
|
|
}
|
|
|
|
if (context.settings().showPrivateModeContextualFeatureRecommender &&
|
|
browsingModeManager.mode.isPrivate
|
|
) {
|
|
recommendPrivateBrowsingShortcut()
|
|
}
|
|
|
|
// We only want this observer live just before we navigate away to the collection creation screen
|
|
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
|
|
}
|
|
|
|
private fun dispatchModeChanges(mode: Mode) {
|
|
if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
|
|
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))
|
|
}
|
|
}
|
|
|
|
private fun showDeleteCollectionPrompt(tabCollection: TabCollection) {
|
|
val context = context ?: return
|
|
AlertDialog.Builder(context).apply {
|
|
val message =
|
|
context.getString(R.string.tab_collection_dialog_message, tabCollection.title)
|
|
setMessage(message)
|
|
setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
|
|
dialog.cancel()
|
|
}
|
|
setPositiveButton(R.string.tab_collection_dialog_positive) { dialog: DialogInterface, _ ->
|
|
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
|
context.components.core.tabCollectionStorage.removeCollection(tabCollection)
|
|
context.components.analytics.metrics.track(Event.CollectionRemoved)
|
|
}.invokeOnCompletion {
|
|
dialog.dismiss()
|
|
}
|
|
}
|
|
create()
|
|
}.show()
|
|
}
|
|
|
|
override fun onStop() {
|
|
super.onStop()
|
|
val homeViewModel: HomeScreenViewModel by activityViewModels {
|
|
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
|
}
|
|
homeViewModel.layoutManagerState =
|
|
sessionControlView!!.view.layoutManager?.onSaveInstanceState()
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
if (browsingModeManager.mode == BrowsingMode.Private) {
|
|
activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
|
|
}
|
|
|
|
hideToolbar()
|
|
|
|
args.sessionToDelete?.also {
|
|
sessionManager.findSessionById(it)?.let { session ->
|
|
val snapshot = sessionManager.createSessionSnapshot(session)
|
|
val state = snapshot.engineSession?.saveState()
|
|
val isSelected =
|
|
session.id == requireComponents.core.store.state.selectedTabId ?: false
|
|
|
|
val snackbarMessage = if (snapshot.session.private) {
|
|
requireContext().getString(R.string.snackbar_private_tab_closed)
|
|
} else {
|
|
requireContext().getString(R.string.snackbar_tab_closed)
|
|
}
|
|
|
|
viewLifecycleOwner.lifecycleScope.allowUndo(
|
|
requireView(),
|
|
snackbarMessage,
|
|
requireContext().getString(R.string.snackbar_deleted_undo),
|
|
{
|
|
sessionManager.add(
|
|
snapshot.session,
|
|
isSelected,
|
|
engineSessionState = state
|
|
)
|
|
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null))
|
|
},
|
|
operation = { },
|
|
anchorView = snackbarAnchorView
|
|
)
|
|
requireComponents.useCases.tabsUseCases.removeTab.invoke(session)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
if (browsingModeManager.mode == BrowsingMode.Private) {
|
|
activity?.window?.setBackgroundDrawable(
|
|
ColorDrawable(
|
|
ContextCompat.getColor(
|
|
requireContext(),
|
|
R.color.foundation_private_theme
|
|
)
|
|
)
|
|
)
|
|
}
|
|
calculateNewOffset()
|
|
}
|
|
|
|
private fun recommendPrivateBrowsingShortcut() {
|
|
context?.let {
|
|
val layout = LayoutInflater.from(it)
|
|
.inflate(R.layout.pbm_shortcut_popup, null)
|
|
val privateBrowsingRecommend =
|
|
PopupWindow(
|
|
layout,
|
|
min(
|
|
(resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
|
|
(resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
|
|
),
|
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
true
|
|
)
|
|
layout.findViewById<Button>(R.id.cfr_pos_button).apply {
|
|
setOnClickListener {
|
|
context.metrics.track(Event.PrivateBrowsingAddShortcutCFR)
|
|
PrivateShortcutCreateManager.createPrivateShortcut(context)
|
|
privateBrowsingRecommend.dismiss()
|
|
}
|
|
}
|
|
layout.findViewById<Button>(R.id.cfr_neg_button).apply {
|
|
setOnClickListener {
|
|
context.metrics.track(Event.PrivateBrowsingCancelCFR)
|
|
privateBrowsingRecommend.dismiss()
|
|
}
|
|
}
|
|
// We want to show the popup only after privateBrowsingButton is available.
|
|
// Otherwise, we will encounter an activity token error.
|
|
privateBrowsingButton.post {
|
|
privateBrowsingRecommend.showAsDropDown(
|
|
privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun hideOnboardingIfNeeded() {
|
|
if (!onboarding.userHasBeenOnboarded()) {
|
|
onboarding.finish()
|
|
homeFragmentStore.dispatch(
|
|
HomeFragmentAction.ModeChange(
|
|
mode = currentMode.getCurrentMode()
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun hideOnboardingAndOpenSearch() {
|
|
hideOnboardingIfNeeded()
|
|
navigateToSearch()
|
|
}
|
|
|
|
private fun navigateToSearch() {
|
|
val directions = HomeFragmentDirections.actionGlobalSearch(
|
|
sessionId = null
|
|
)
|
|
|
|
nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
|
|
}
|
|
|
|
private fun openSettingsScreen() {
|
|
val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment()
|
|
nav(R.id.homeFragment, directions)
|
|
}
|
|
|
|
private fun openInNormalTab(url: String) {
|
|
(activity as HomeActivity).openToBrowserAndLoad(
|
|
searchTermOrURL = url,
|
|
newTab = true,
|
|
from = BrowserDirection.FromHome
|
|
)
|
|
}
|
|
|
|
private fun createTabCounterMenu(context: Context): BrowserMenu {
|
|
val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context)
|
|
val isPrivate = (activity as HomeActivity).browsingModeManager.mode == BrowsingMode.Private
|
|
val menuItems = listOf(
|
|
BrowserMenuImageText(
|
|
label = context.getString(if (isPrivate) {
|
|
R.string.browser_menu_new_tab
|
|
} else {
|
|
R.string.home_screen_shortcut_open_new_private_tab_2
|
|
}),
|
|
imageResource = if (isPrivate) {
|
|
R.drawable.ic_new
|
|
} else {
|
|
R.drawable.ic_private_browsing
|
|
},
|
|
iconTintColorResource = primaryTextColor,
|
|
textColorResource = primaryTextColor
|
|
) {
|
|
(activity as HomeActivity).browsingModeManager.mode =
|
|
BrowsingMode.fromBoolean(!isPrivate)
|
|
}
|
|
)
|
|
return BrowserMenuBuilder(menuItems).build(context)
|
|
}
|
|
|
|
@SuppressWarnings("ComplexMethod", "LongMethod")
|
|
private fun createHomeMenu(context: Context, menuButtonView: WeakReference<MenuButton>) =
|
|
HomeMenu(
|
|
this.viewLifecycleOwner,
|
|
context,
|
|
onItemTapped = {
|
|
when (it) {
|
|
HomeMenu.Item.Settings -> {
|
|
hideOnboardingIfNeeded()
|
|
nav(
|
|
R.id.homeFragment,
|
|
HomeFragmentDirections.actionGlobalSettingsFragment()
|
|
)
|
|
}
|
|
HomeMenu.Item.SyncedTabs -> {
|
|
hideOnboardingIfNeeded()
|
|
nav(
|
|
R.id.homeFragment,
|
|
HomeFragmentDirections.actionGlobalSyncedTabsFragment()
|
|
)
|
|
}
|
|
HomeMenu.Item.Bookmarks -> {
|
|
hideOnboardingIfNeeded()
|
|
nav(
|
|
R.id.homeFragment,
|
|
HomeFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
|
|
)
|
|
}
|
|
HomeMenu.Item.History -> {
|
|
hideOnboardingIfNeeded()
|
|
nav(
|
|
R.id.homeFragment,
|
|
HomeFragmentDirections.actionGlobalHistoryFragment()
|
|
)
|
|
}
|
|
HomeMenu.Item.Help -> {
|
|
hideOnboardingIfNeeded()
|
|
(activity as HomeActivity).openToBrowserAndLoad(
|
|
searchTermOrURL = SupportUtils.getSumoURLForTopic(context, HELP),
|
|
newTab = true,
|
|
from = BrowserDirection.FromHome
|
|
)
|
|
}
|
|
HomeMenu.Item.WhatsNew -> {
|
|
hideOnboardingIfNeeded()
|
|
WhatsNew.userViewedWhatsNew(context)
|
|
context.metrics.track(Event.WhatsNewTapped)
|
|
(activity as HomeActivity).openToBrowserAndLoad(
|
|
searchTermOrURL = SupportUtils.getWhatsNewUrl(context),
|
|
newTab = true,
|
|
from = BrowserDirection.FromHome
|
|
)
|
|
}
|
|
// We need to show the snackbar while the browsing data is deleting(if "Delete
|
|
// browsing data on quit" is activated). After the deletion is over, the snackbar
|
|
// is dismissed.
|
|
HomeMenu.Item.Quit -> activity?.let { activity ->
|
|
deleteAndQuit(
|
|
activity,
|
|
viewLifecycleOwner.lifecycleScope,
|
|
view?.let { view ->
|
|
FenixSnackbar.make(
|
|
view = view,
|
|
isDisplayedWithBrowserToolbar = false
|
|
)
|
|
}
|
|
)
|
|
}
|
|
HomeMenu.Item.Sync -> {
|
|
hideOnboardingIfNeeded()
|
|
nav(
|
|
R.id.homeFragment,
|
|
HomeFragmentDirections.actionGlobalAccountProblemFragment()
|
|
)
|
|
}
|
|
HomeMenu.Item.AddonsManager -> {
|
|
nav(
|
|
R.id.homeFragment,
|
|
HomeFragmentDirections.actionGlobalAddonsManagementFragment()
|
|
)
|
|
}
|
|
}
|
|
},
|
|
onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },
|
|
onMenuBuilderChanged = { menuButtonView.get()?.menuBuilder = it }
|
|
)
|
|
|
|
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
|
|
return Observer<List<TabCollection>> {
|
|
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
|
|
homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(it))
|
|
}.also { observer ->
|
|
requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
|
|
}
|
|
}
|
|
|
|
private fun subscribeToTopSites(): Observer<List<TopSite>> {
|
|
return Observer<List<TopSite>> { topSites ->
|
|
requireComponents.core.topSiteStorage.cachedTopSites = topSites
|
|
context?.settings()?.preferences?.edit()
|
|
?.putInt(getString(R.string.pref_key_top_sites_size), topSites.size)?.apply()
|
|
homeFragmentStore.dispatch(HomeFragmentAction.TopSitesChange(topSites))
|
|
}.also { observer ->
|
|
requireComponents.core.topSiteStorage.getTopSites().observe(this, observer)
|
|
}
|
|
}
|
|
|
|
private fun getListOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): List<Session> {
|
|
return sessionManager.sessionsOfType(private = private)
|
|
.toList()
|
|
}
|
|
|
|
private fun getListOfTabs(): List<Tab> {
|
|
return getListOfSessions().toTabs()
|
|
}
|
|
|
|
private fun registerCollectionStorageObserver() {
|
|
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
|
|
}
|
|
|
|
private fun scrollAndAnimateCollection(
|
|
tabsAddedToCollectionSize: Int,
|
|
changedCollection: TabCollection? = null
|
|
) {
|
|
if (view != null) {
|
|
viewLifecycleOwner.lifecycleScope.launch {
|
|
val recyclerView = sessionControlView!!.view
|
|
delay(ANIM_SCROLL_DELAY)
|
|
val tabsSize = getListOfSessions().size
|
|
|
|
var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM
|
|
changedCollection?.let { changedCollection ->
|
|
requireComponents.core.tabCollectionStorage.cachedTabCollections
|
|
.filterIndexed { index, tabCollection ->
|
|
if (tabCollection.id == changedCollection.id) {
|
|
indexOfCollection = tabsSize + NON_TAB_ITEM_NUM + index
|
|
return@filterIndexed true
|
|
}
|
|
false
|
|
}
|
|
}
|
|
val lastVisiblePosition =
|
|
(recyclerView.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition()
|
|
?: 0
|
|
if (lastVisiblePosition < indexOfCollection) {
|
|
val onScrollListener = object : RecyclerView.OnScrollListener() {
|
|
override fun onScrollStateChanged(
|
|
recyclerView: RecyclerView,
|
|
newState: Int
|
|
) {
|
|
super.onScrollStateChanged(recyclerView, newState)
|
|
if (newState == SCROLL_STATE_IDLE) {
|
|
animateCollection(tabsAddedToCollectionSize, indexOfCollection)
|
|
recyclerView.removeOnScrollListener(this)
|
|
}
|
|
}
|
|
}
|
|
recyclerView.addOnScrollListener(onScrollListener)
|
|
recyclerView.smoothScrollToPosition(indexOfCollection)
|
|
} else {
|
|
animateCollection(tabsAddedToCollectionSize, indexOfCollection)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun animateCollection(addedTabsSize: Int, indexOfCollection: Int) {
|
|
viewLifecycleOwner.lifecycleScope.launch {
|
|
val viewHolder =
|
|
sessionControlView!!.view.findViewHolderForAdapterPosition(indexOfCollection)
|
|
val border =
|
|
(viewHolder as? CollectionViewHolder)?.itemView?.findViewById<View>(R.id.selected_border)
|
|
val listener = object : Animator.AnimatorListener {
|
|
override fun onAnimationCancel(animation: Animator?) {
|
|
border?.visibility = View.GONE
|
|
}
|
|
|
|
override fun onAnimationStart(animation: Animator?) { /* noop */
|
|
}
|
|
|
|
override fun onAnimationRepeat(animation: Animator?) { /* noop */
|
|
}
|
|
|
|
override fun onAnimationEnd(animation: Animator?) {
|
|
border?.animate()?.alpha(0.0F)?.setStartDelay(ANIM_ON_SCREEN_DELAY)
|
|
?.setDuration(FADE_ANIM_DURATION)
|
|
?.start()
|
|
}
|
|
}
|
|
border?.animate()?.alpha(1.0F)?.setStartDelay(ANIM_ON_SCREEN_DELAY)
|
|
?.setDuration(FADE_ANIM_DURATION)
|
|
?.setListener(listener)?.start()
|
|
}.invokeOnCompletion {
|
|
showSavedSnackbar(addedTabsSize)
|
|
}
|
|
}
|
|
|
|
private fun showSavedSnackbar(tabSize: Int) {
|
|
viewLifecycleOwner.lifecycleScope.launch {
|
|
delay(ANIM_SNACKBAR_DELAY)
|
|
view?.let { view ->
|
|
@StringRes
|
|
val stringRes = if (tabSize > 1) {
|
|
R.string.create_collection_tabs_saved
|
|
} else {
|
|
R.string.create_collection_tab_saved
|
|
}
|
|
FenixSnackbar.make(
|
|
view = view,
|
|
duration = Snackbar.LENGTH_LONG,
|
|
isDisplayedWithBrowserToolbar = false
|
|
)
|
|
.setText(view.context.getString(stringRes))
|
|
.setAnchorView(snackbarAnchorView)
|
|
.show()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun showRenamedSnackbar() {
|
|
view?.let { view ->
|
|
val string = view.context.getString(R.string.snackbar_collection_renamed)
|
|
FenixSnackbar.make(
|
|
view = view,
|
|
duration = Snackbar.LENGTH_LONG,
|
|
isDisplayedWithBrowserToolbar = false
|
|
)
|
|
.setText(string)
|
|
.setAnchorView(snackbarAnchorView)
|
|
.show()
|
|
}
|
|
}
|
|
|
|
private fun List<Session>.toTabs(): List<Tab> {
|
|
val selected = sessionManager.selectedSession
|
|
|
|
return map {
|
|
it.toTab(requireContext(), it == selected)
|
|
}
|
|
}
|
|
|
|
private fun calculateNewOffset() {
|
|
homeAppBarOffset = ((homeAppBar.layoutParams as CoordinatorLayout.LayoutParams)
|
|
.behavior as AppBarLayout.Behavior).topAndBottomOffset
|
|
}
|
|
|
|
private fun setOffset(currentView: View) {
|
|
if (homeAppBarOffset <= 0) {
|
|
(currentView.homeAppBar.layoutParams as CoordinatorLayout.LayoutParams)
|
|
.behavior = AppBarLayout.Behavior().apply {
|
|
topAndBottomOffset = this@HomeFragment.homeAppBarOffset
|
|
}
|
|
} else {
|
|
currentView.homeAppBar.setExpanded(false)
|
|
}
|
|
}
|
|
|
|
private fun createNewAppBarListener(margin: Float) {
|
|
homeAppBarOffSetListener =
|
|
AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
|
|
val reduceScrollRanged = appBarLayout.totalScrollRange.toFloat() - margin
|
|
appBarLayout.alpha = 1.0f - abs(verticalOffset / reduceScrollRanged)
|
|
}
|
|
}
|
|
|
|
private fun openTabTray() {
|
|
TabTrayDialogFragment.show(parentFragmentManager)
|
|
}
|
|
|
|
companion object {
|
|
private const val ANIMATION_DELAY = 100L
|
|
|
|
private const val NON_TAB_ITEM_NUM = 3
|
|
private const val ANIM_SCROLL_DELAY = 100L
|
|
private const val ANIM_ON_SCREEN_DELAY = 200L
|
|
private const val FADE_ANIM_DURATION = 150L
|
|
private const val ANIM_SNACKBAR_DELAY = 100L
|
|
private const val CFR_WIDTH_DIVIDER = 1.7
|
|
private const val CFR_Y_OFFSET = -20
|
|
|
|
// Layout
|
|
private const val HEADER_MARGIN = 60
|
|
}
|
|
}
|