1
0
Fork 0
fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt

1031 lines
40 KiB
Kotlin
Raw Normal View History

/* 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/. */
2019-01-09 23:22:58 +01:00
package org.mozilla.fenix.home
import android.animation.Animator
import android.content.DialogInterface
2019-01-29 00:26:37 +01:00
import android.graphics.drawable.BitmapDrawable
2019-01-09 23:22:58 +01:00
import android.os.Bundle
import android.view.Gravity
2019-01-09 23:22:58 +01:00
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
2019-07-23 23:15:46 +02:00
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.Observer
2019-07-23 23:15:46 +02:00
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
2019-08-08 00:41:52 +02:00
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
2019-08-08 00:41:52 +02:00
import androidx.transition.TransitionInflater
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
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
2019-07-23 23:15:46 +02:00
import kotlinx.coroutines.withContext
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.sync.AccountObserver
2019-09-04 22:56:22 +02:00
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
2019-09-07 01:11:40 +02:00
import mozilla.components.feature.media.ext.getSession
import mozilla.components.feature.media.ext.pauseIfPlaying
import mozilla.components.feature.media.ext.playIfPaused
import mozilla.components.feature.media.state.MediaState
import mozilla.components.feature.media.state.MediaStateMachine
2019-07-23 23:15:46 +02:00
import mozilla.components.feature.tab.collections.TabCollection
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.START
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP
2019-04-06 06:24:28 +02:00
import org.jetbrains.anko.constraint.layout.applyConstraintSet
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity
2019-01-09 23:22:58 +01:00
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.CreateCollectionViewModel
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
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.home.sessioncontrol.CollectionAction
import org.mozilla.fenix.home.sessioncontrol.OnboardingAction
2019-04-06 06:24:28 +02:00
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent
import org.mozilla.fenix.home.sessioncontrol.SessionControlState
import org.mozilla.fenix.home.sessioncontrol.SessionControlViewModel
import org.mozilla.fenix.home.sessioncontrol.Tab
2019-04-06 06:24:28 +02:00
import org.mozilla.fenix.home.sessioncontrol.TabAction
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
2019-04-06 06:24:28 +02:00
import org.mozilla.fenix.lib.Do
2019-01-28 17:46:39 +01:00
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.share.ShareTab
import org.mozilla.fenix.utils.FragmentPreDrawManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.whatsnew.WhatsNew
2019-01-29 00:26:37 +01:00
2019-03-21 01:26:13 +01:00
@SuppressWarnings("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment() {
2019-08-07 22:02:08 +02:00
private val bus = ActionBusFactory.get(this)
2019-08-07 22:02:08 +02:00
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
private val singleSessionObserver = object : Session.Observer {
override fun onTitleChanged(session: Session, title: String) {
2019-07-23 23:15:46 +02:00
if (deleteAllSessionsJob == null) emitSessionChanges()
}
}
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) {
scrollAndAnimateCollection(sessions.size)
}
2019-07-23 23:15:46 +02:00
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
scrollAndAnimateCollection(sessions.size, tabCollection)
}
2019-07-23 23:15:46 +02:00
override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
showRenamedSnackbar()
}
}
private var homeMenu: HomeMenu? = null
private val sessionManager: SessionManager
get() = requireComponents.core.sessionManager
var deleteAllSessionsJob: (suspend () -> Unit)? = null
private var pendingSessionDeletion: PendingSessionDeletion? = null
data class PendingSessionDeletion(val deletionJob: (suspend () -> Unit), val sessionId: String)
private val onboarding by lazy { FenixOnboarding(requireContext()) }
2019-04-06 06:24:28 +02:00
private lateinit var sessionControlComponent: SessionControlComponent
private lateinit var currentMode: CurrentMode
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2019-08-08 00:41:52 +02:00
postponeEnterTransition()
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
.setDuration(SHARED_TRANSITION_MS)
2019-07-23 23:15:46 +02:00
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
emitSessionChanges()
}
2019-09-07 01:11:40 +02:00
2019-07-23 23:15:46 +02:00
lifecycle.addObserver(sessionObserver)
if (!onboarding.userHasBeenOnboarded()) {
requireComponents.analytics.metrics.track(Event.OpenedAppFirstRun)
}
}
2019-01-09 23:22:58 +01:00
override fun onCreateView(
2019-01-30 17:36:14 +01:00
inflater: LayoutInflater,
container: ViewGroup?,
2019-01-09 23:22:58 +01:00
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
currentMode = CurrentMode(
view.context,
onboarding,
browsingModeManager,
getManagedEmitter()
)
2019-04-06 06:24:28 +02:00
sessionControlComponent = SessionControlComponent(
view.homeLayout,
bus,
FenixViewModelProvider.create(
this,
SessionControlViewModel::class.java
) {
SessionControlViewModel(
SessionControlState(
emptyList(),
emptySet(),
requireComponents.core.tabCollectionStorage.cachedTabCollections,
currentMode.getCurrentMode()
)
)
}
)
2019-04-06 06:24:28 +02:00
view.homeLayout.applyConstraintSet {
sessionControlComponent.view {
connect(
TOP to BOTTOM of view.wordmark_spacer,
2019-04-06 06:24:28 +02:00
START to START of PARENT_ID,
END to END of PARENT_ID,
BOTTOM to TOP of view.bottom_bar
2019-04-06 06:24:28 +02:00
)
}
}
ActionBusFactory.get(this).logMergedObservables()
val activity = activity as HomeActivity
2019-08-07 22:02:08 +02:00
activity.themeManager.applyStatusBarTheme(activity)
return view
2019-01-09 23:22:58 +01:00
}
@SuppressWarnings("LongMethod")
2019-01-10 01:07:33 +01:00
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 ->
sessionControlComponent.view.layoutManager?.onRestoreInstanceState(parcelable)
}
homeLayout?.progress = homeViewModel.motionLayoutProgress
homeViewModel.layoutManagerState = null
}
setupHomeMenu()
2019-07-23 23:15:46 +02:00
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
val searchEngine = requireComponents.search.searchEngineManager.getDefaultSearchEngineAsync(
requireContext(),
requireContext().settings().defaultSearchEngineName
)
2019-07-23 23:15:46 +02:00
val searchIcon = BitmapDrawable(resources, searchEngine.icon)
searchIcon.setBounds(0, 0, iconSize, iconSize)
2019-07-23 23:15:46 +02:00
withContext(Dispatchers.Main) {
search_engine_icon?.setImageDrawable(searchIcon)
}
2019-01-29 00:26:37 +01:00
}
2019-01-25 17:11:43 +01:00
view.menuButton.setOnClickListener {
homeMenu?.menuBuilder?.build(requireContext())?.show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN
)
}
view.toolbar.compoundDrawablePadding =
view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
view.toolbar_wrapper.setOnClickListener {
invokePendingDeleteJobs()
onboarding.finish()
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(
sessionId = null,
showShortcutEnginePicker = true
)
val extras =
FragmentNavigator.Extras.Builder()
.addSharedElement(toolbar_wrapper, "toolbar_wrapper_transition")
.build()
nav(R.id.homeFragment, directions, extras)
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
}
view.add_tab_button.setOnClickListener {
invokePendingDeleteJobs()
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(
sessionId = null,
showShortcutEnginePicker = true
)
nav(R.id.homeFragment, directions)
}
PrivateBrowsingButtonView(
privateBrowsingButton,
browsingModeManager
) { newMode ->
invokePendingDeleteJobs()
if (newMode == BrowsingMode.Private) {
requireContext().settings().incrementNumTimesPrivateModeOpened()
}
2019-05-21 20:03:32 +02:00
if (onboarding.userHasBeenOnboarded()) {
2019-08-08 00:41:52 +02:00
getManagedEmitter<SessionControlChange>().onNext(
SessionControlChange.ModeChange(Mode.fromBrowsingMode(newMode))
2019-08-08 00:41:52 +02:00
)
2019-05-21 20:03:32 +02:00
}
}
// We need the shadow to be above the components.
bottomBarShadow.bringToFront()
}
override fun onDestroyView() {
homeMenu = null
super.onDestroyView()
}
override fun onResume() {
super.onResume()
getAutoDisposeObservable<SessionControlAction>()
.subscribe {
when (it) {
is SessionControlAction.Tab -> handleTabAction(it.action)
is SessionControlAction.Collection -> handleCollectionAction(it.action)
is SessionControlAction.Onboarding -> handleOnboardingAction(it.action)
2019-02-26 00:43:30 +01:00
}
}
val context = requireContext()
val components = context.components
getManagedEmitter<SessionControlChange>().onNext(
SessionControlChange.Change(
tabs = getListOfSessions().toTabs(),
mode = currentMode.getCurrentMode(),
collections = components.core.tabCollectionStorage.cachedTabCollections
)
)
(activity as AppCompatActivity).supportActionBar?.hide()
requireComponents.backgroundServices.accountManager.register(currentMode, owner = this)
requireComponents.backgroundServices.accountManager.register(object : AccountObserver {
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
if (authType != AuthType.Existing) {
view?.let {
FenixSnackbar.make(it, Snackbar.LENGTH_SHORT).setText(
it.context.getString(R.string.onboarding_firefox_account_sync_is_on)
).show()
}
}
}
}, owner = this)
if (context.settings().showPrivateModeContextualFeatureRecommender &&
browsingModeManager.mode.isPrivate) {
recommendPrivateBrowsingShortcut()
}
}
override fun onStart() {
super.onStart()
2019-07-23 23:15:46 +02:00
subscribeToTabCollections()
// We only want this observer live just before we navigate away to the collection creation screen
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
}
private fun handleOnboardingAction(action: OnboardingAction) {
Do exhaustive when (action) {
is OnboardingAction.Finish -> {
homeLayout?.progress = 0F
hideOnboarding()
}
}
}
@SuppressWarnings("ComplexMethod", "LongMethod")
2019-04-06 06:24:28 +02:00
private fun handleTabAction(action: TabAction) {
Do exhaustive when (action) {
is TabAction.SaveTabGroup -> {
if (browsingModeManager.mode.isPrivate) return
invokePendingDeleteJobs()
saveTabToCollection(action.selectedTabSessionId)
}
2019-04-06 06:24:28 +02:00
is TabAction.Select -> {
invokePendingDeleteJobs()
val session = sessionManager.findSessionById(action.sessionId)
sessionManager.select(session!!)
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
2019-08-08 00:41:52 +02:00
val extras =
FragmentNavigator.Extras.Builder()
.addSharedElement(
action.tabView,
"$TAB_ITEM_TRANSITION_NAME${action.sessionId}"
)
.build()
nav(R.id.homeFragment, directions, extras)
2019-04-06 06:24:28 +02:00
}
is TabAction.Close -> {
if (pendingSessionDeletion?.deletionJob == null) {
removeTabWithUndo(action.sessionId)
} else {
pendingSessionDeletion?.deletionJob?.let {
viewLifecycleOwner.lifecycleScope.launch {
it.invoke()
}.invokeOnCompletion {
pendingSessionDeletion = null
removeTabWithUndo(action.sessionId)
}
}
}
2019-04-06 06:24:28 +02:00
}
is TabAction.Share -> {
invokePendingDeleteJobs()
sessionManager.findSessionById(action.sessionId)?.let { session ->
share(session.url)
}
}
2019-09-07 01:11:40 +02:00
is TabAction.PauseMedia -> {
MediaStateMachine.state.pauseIfPlaying()
}
is TabAction.PlayMedia -> {
MediaStateMachine.state.playIfPaused()
}
2019-04-06 06:24:28 +02:00
is TabAction.CloseAll -> {
if (pendingSessionDeletion?.deletionJob == null) {
removeAllTabsWithUndo(
sessionManager.sessionsOfType(private = action.private),
action.private
)
} else {
pendingSessionDeletion?.deletionJob?.let {
viewLifecycleOwner.lifecycleScope.launch {
it.invoke()
}.invokeOnCompletion {
pendingSessionDeletion = null
removeAllTabsWithUndo(
sessionManager.sessionsOfType(private = action.private),
action.private
)
}
}
}
2019-04-06 06:24:28 +02:00
}
is TabAction.PrivateBrowsingLearnMore -> {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
(SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
newTab = true,
from = BrowserDirection.FromHome
)
2019-04-06 06:24:28 +02:00
}
is TabAction.ShareTabs -> {
invokePendingDeleteJobs()
val shareTabs = sessionManager
.sessionsOfType(private = browsingModeManager.mode.isPrivate)
.map { ShareTab(it.url, it.title) }
.toList()
share(tabs = shareTabs)
}
2019-04-06 06:24:28 +02:00
}
}
private fun invokePendingDeleteJobs() {
pendingSessionDeletion?.deletionJob?.let {
viewLifecycleOwner.lifecycleScope.launch {
it.invoke()
}.invokeOnCompletion {
pendingSessionDeletion = null
}
}
deleteAllSessionsJob?.let {
viewLifecycleOwner.lifecycleScope.launch {
it.invoke()
}.invokeOnCompletion {
deleteAllSessionsJob = null
}
}
}
private fun createDeleteCollectionPrompt(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()
}
@SuppressWarnings("LongMethod")
private fun handleCollectionAction(action: CollectionAction) {
when (action) {
is CollectionAction.Expand -> {
getManagedEmitter<SessionControlChange>()
.onNext(SessionControlChange.ExpansionChange(action.collection, true))
}
is CollectionAction.Collapse -> {
getManagedEmitter<SessionControlChange>()
.onNext(SessionControlChange.ExpansionChange(action.collection, false))
}
is CollectionAction.Delete -> {
createDeleteCollectionPrompt(action.collection)
}
is CollectionAction.AddTab -> {
requireComponents.analytics.metrics.track(Event.CollectionAddTabPressed)
updateCollection(action.collection, SaveCollectionStep.SelectTabs)
}
is CollectionAction.Rename -> {
updateCollection(action.collection, SaveCollectionStep.RenameCollection)
requireComponents.analytics.metrics.track(Event.CollectionRenamePressed)
}
is CollectionAction.OpenTab -> {
invokePendingDeleteJobs()
val context = requireContext()
val components = context.components
val session = action.tab.restore(
context = context,
engine = components.core.engine,
tab = action.tab,
restoreSessionId = false
)
if (session == null) {
// We were unable to create a snapshot, so just load the tab instead
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = action.tab.url,
newTab = true,
from = BrowserDirection.FromHome
)
} else {
components.core.sessionManager.add(
session,
true
)
(activity as HomeActivity).openToBrowser(BrowserDirection.FromHome)
}
components.analytics.metrics.track(Event.CollectionTabRestored)
}
is CollectionAction.OpenTabs -> {
invokePendingDeleteJobs()
val context = requireContext()
val components = context.components
action.collection.tabs.reversed().forEach {
val session = it.restore(
context = context,
engine = components.core.engine,
tab = it,
restoreSessionId = false
)
if (session == null) {
// We were unable to create a snapshot, so just load the tab instead
components.useCases.tabsUseCases.addTab.invoke(it.url)
} else {
components.core.sessionManager.add(
session,
context.components.core.sessionManager.selectedSession == null
)
}
}
viewLifecycleOwner.lifecycleScope.launch(Main) {
delay(ANIM_SCROLL_DELAY)
sessionControlComponent.view.smoothScrollToPosition(0)
}
components.analytics.metrics.track(Event.CollectionAllTabsRestored)
}
is CollectionAction.ShareTabs -> {
val shareTabs = action.collection.tabs.map { ShareTab(it.url, it.title) }
share(tabs = shareTabs)
requireComponents.analytics.metrics.track(Event.CollectionShared)
}
is CollectionAction.RemoveTab -> {
viewLifecycleOwner.lifecycleScope.launch(IO) {
2019-08-08 00:41:52 +02:00
requireComponents.core.tabCollectionStorage.removeTabFromCollection(
action.collection,
action.tab
)
}
requireComponents.analytics.metrics.track(Event.CollectionTabRemoved)
}
}
}
override fun onPause() {
invokePendingDeleteJobs()
super.onPause()
val homeViewModel: HomeScreenViewModel by activityViewModels {
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
}
homeViewModel.layoutManagerState =
sessionControlComponent.view.layoutManager?.onSaveInstanceState()
homeViewModel.motionLayoutProgress = homeLayout?.progress ?: 0F
}
private fun recommendPrivateBrowsingShortcut() {
context?.let {
val layout = LayoutInflater.from(it)
.inflate(R.layout.pbm_shortcut_popup, null)
val trackingOnboarding =
PopupWindow(
layout,
(resources.displayMetrics.widthPixels / 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)
trackingOnboarding.dismiss()
}
}
layout.findViewById<Button>(R.id.cfr_neg_button).apply {
setOnClickListener {
context.metrics.track(Event.PrivateBrowsingCancelCFR)
trackingOnboarding.dismiss()
}
}
// We want to show the popup only after privateBrowsingButton is available.
// Otherwise, we will encounter an activity token error.
privateBrowsingButton.post {
trackingOnboarding.showAsDropDown(privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END)
}
}
}
private fun hideOnboardingIfNeeded() {
if (!onboarding.userHasBeenOnboarded()) hideOnboarding()
}
private fun hideOnboarding() {
onboarding.finish()
currentMode.emitModeChanges()
}
private fun setupHomeMenu() {
val context = requireContext()
homeMenu = HomeMenu(context) {
when (it) {
HomeMenu.Item.Settings -> {
invokePendingDeleteJobs()
hideOnboardingIfNeeded()
nav(
R.id.homeFragment,
HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
)
}
HomeMenu.Item.Library -> {
invokePendingDeleteJobs()
hideOnboardingIfNeeded()
nav(
R.id.homeFragment,
HomeFragmentDirections.actionHomeFragmentToLibraryFragment()
)
}
HomeMenu.Item.Help -> {
invokePendingDeleteJobs()
hideOnboardingIfNeeded()
(activity as HomeActivity).openToBrowserAndLoad(
2019-06-15 00:00:09 +02:00
searchTermOrURL = SupportUtils.getSumoURLForTopic(
context,
2019-06-15 00:00:09 +02:00
SupportUtils.SumoTopic.HELP
),
newTab = true,
from = BrowserDirection.FromHome
)
}
HomeMenu.Item.WhatsNew -> {
invokePendingDeleteJobs()
hideOnboardingIfNeeded()
WhatsNew.userViewedWhatsNew(context)
context.metrics.track(Event.WhatsNewTapped(Event.WhatsNewTapped.Source.HOME))
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
context,
SupportUtils.SumoTopic.WHATS_NEW
),
newTab = true,
from = BrowserDirection.FromHome
)
}
}
}
}
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
2019-07-23 23:15:46 +02:00
return Observer<List<TabCollection>> {
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
2019-08-08 00:41:52 +02:00
getManagedEmitter<SessionControlChange>().onNext(
SessionControlChange.CollectionsChange(
it
)
)
2019-07-23 23:15:46 +02:00
}.also { observer ->
requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
}
}
private fun removeAllTabsWithUndo(listOfSessionsToDelete: Sequence<Session>, private: Boolean) {
val sessionManager = requireComponents.core.sessionManager
getManagedEmitter<SessionControlChange>().onNext(SessionControlChange.TabsChange(listOf()))
val deleteOperation: (suspend () -> Unit) = {
listOfSessionsToDelete.forEach {
sessionManager.remove(it)
}
}
deleteAllSessionsJob = deleteOperation
val snackbarMessage = if (private) {
getString(R.string.snackbar_private_tabs_deleted)
} else {
getString(R.string.snackbar_tab_deleted)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
view!!,
snackbarMessage,
getString(R.string.snackbar_deleted_undo), {
if (private) {
requireComponents.analytics.metrics.track(Event.PrivateBrowsingSnackbarUndoTapped)
}
deleteAllSessionsJob = null
emitSessionChanges()
},
operation = deleteOperation
)
}
private fun removeTabWithUndo(sessionId: String) {
val sessionManager = requireComponents.core.sessionManager
val deleteOperation: (suspend () -> Unit) = {
sessionManager.findSessionById(sessionId)
?.let { session ->
pendingSessionDeletion = null
sessionManager.remove(session)
}
}
pendingSessionDeletion = PendingSessionDeletion(deleteOperation, sessionId)
viewLifecycleOwner.lifecycleScope.allowUndo(
view!!,
getString(R.string.snackbar_tab_deleted),
getString(R.string.snackbar_deleted_undo), {
pendingSessionDeletion = null
emitSessionChanges()
},
operation = deleteOperation
)
// Update the UI with the tab removed, but don't remove it from storage yet
emitSessionChanges()
}
private fun emitSessionChanges() {
getManagedEmitter<SessionControlChange>().onNext(
SessionControlChange.TabsChange(
getListOfSessions().toTabs()
)
)
}
private fun getListOfSessions(): List<Session> {
return sessionManager.sessionsOfType(private = browsingModeManager.mode.isPrivate)
.filter { session: Session -> session.id != pendingSessionDeletion?.sessionId }
.toList()
}
private fun showCollectionCreationFragment(
setupViewModel: (CreateCollectionViewModel, tabs: List<Tab>, cachedTabCollections: List<TabCollection>) -> Unit
) {
if (findNavController().currentDestination?.id == R.id.createCollectionFragment) return
val viewModel: CreateCollectionViewModel by activityViewModels {
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
}
val tabs = getListOfSessions().toTabs()
val storage = requireComponents.core.tabCollectionStorage
setupViewModel(viewModel, tabs, storage.cachedTabCollections)
viewModel.previousFragmentId = R.id.homeFragment
// Only register the observer right before moving to collection creation
storage.register(collectionStorageObserver, this)
view?.let {
val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment()
nav(R.id.homeFragment, directions)
}
}
private fun saveTabToCollection(selectedTabId: String?) {
showCollectionCreationFragment { viewModel, tabs, cachedTabCollections ->
viewModel.saveTabToCollection(
tabs = tabs,
selectedTab = tabs.find { it.sessionId == selectedTabId } ?: if (tabs.size == 1) tabs[0] else null,
cachedTabCollections = cachedTabCollections
)
}
}
private fun updateCollection(selectedTabCollection: TabCollection, step: SaveCollectionStep) {
showCollectionCreationFragment { viewModel, tabs, cachedTabCollections ->
viewModel.updateCollection(
tabs = tabs,
saveCollectionStep = step,
selectedTabCollection = selectedTabCollection,
cachedTabCollections = cachedTabCollections
)
}
}
private fun share(url: String? = null, tabs: List<ShareTab>? = null) {
val directions =
HomeFragmentDirections.actionHomeFragmentToShareFragment(
url = url,
tabs = tabs?.toTypedArray()
)
nav(R.id.homeFragment, directions)
}
2019-08-08 00:41:52 +02:00
private fun scrollAndAnimateCollection(
tabsAddedToCollectionSize: Int,
changedCollection: TabCollection? = null
) {
if (view != null) {
viewLifecycleOwner.lifecycleScope.launch {
val recyclerView = sessionControlComponent.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 =
2019-08-08 00:41:52 +02:00
(recyclerView.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition()
?: 0
if (lastVisiblePosition < indexOfCollection) {
val onScrollListener = object : RecyclerView.OnScrollListener() {
2019-08-08 00:41:52 +02:00
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 {
2019-08-08 00:41:52 +02:00
val viewHolder =
sessionControlComponent.view.findViewHolderForAdapterPosition(indexOfCollection)
val border =
(viewHolder as? CollectionViewHolder)?.view?.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()
}
}
2019-08-08 00:41:52 +02:00
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
}
2019-08-08 00:41:52 +02:00
FenixSnackbar.make(view, Snackbar.LENGTH_LONG)
.setText(view.context.getString(stringRes)).show()
}
}
}
private fun showRenamedSnackbar() {
view?.let { view ->
val string = view.context.getString(R.string.snackbar_collection_renamed)
FenixSnackbar.make(view, Snackbar.LENGTH_LONG).setText(string).show()
}
}
private fun List<Session>.toTabs(): List<Tab> {
val selected = sessionManager.selectedSession
2019-09-07 01:11:40 +02:00
val mediaStateSession = MediaStateMachine.state.getSession()
return this.map {
val mediaState = if (mediaStateSession?.id == it.id) {
MediaStateMachine.state
} else {
null
}
it.toTab(requireContext(), it == selected, mediaState)
}
}
2019-01-30 17:36:14 +01:00
companion object {
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 SHARED_TRANSITION_MS = 200L
private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
private const val CFR_WIDTH_DIVIDER = 1.7
private const val CFR_Y_OFFSET = -20
2019-01-30 17:36:14 +01:00
}
2019-01-09 23:22:58 +01:00
}
/**
* Wrapper around sessions manager to observe changes in sessions.
* Similar to [mozilla.components.browser.session.utils.AllSessionsObserver] but ignores CustomTab sessions.
*
* Call [onStart] to start receiving updates into [onChanged] callback.
* Call [onStop] to stop receiving updates.
*
* @param manager [SessionManager] instance to subscribe to.
* @param observer [Session.Observer] instance that will recieve updates.
* @param onChanged callback that will be called when any of [SessionManager.Observer]'s events are fired.
*/
private class BrowserSessionsObserver(
private val manager: SessionManager,
private val observer: Session.Observer,
private val onChanged: () -> Unit
2019-07-23 23:15:46 +02:00
) : LifecycleObserver {
/**
* Start observing
*/
2019-07-23 23:15:46 +02:00
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
2019-09-07 01:11:40 +02:00
MediaStateMachine.register(managerObserver)
manager.register(managerObserver)
subscribeToAll()
}
/**
* Stop observing (will not receive updates till next [onStop] call)
*/
2019-07-23 23:15:46 +02:00
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
2019-09-07 01:11:40 +02:00
MediaStateMachine.unregister(managerObserver)
manager.unregister(managerObserver)
unsubscribeFromAll()
}
private fun subscribeToAll() {
manager.sessions.forEach(::subscribeTo)
}
private fun unsubscribeFromAll() {
manager.sessions.forEach(::unsubscribeFrom)
}
private fun subscribeTo(session: Session) {
2019-07-23 23:15:46 +02:00
session.register(observer)
}
private fun unsubscribeFrom(session: Session) {
2019-07-23 23:15:46 +02:00
session.unregister(observer)
}
2019-09-07 01:11:40 +02:00
private val managerObserver = object : SessionManager.Observer, MediaStateMachine.Observer {
override fun onStateChanged(state: MediaState) {
onChanged()
}
override fun onSessionAdded(session: Session) {
subscribeTo(session)
onChanged()
}
override fun onSessionsRestored() {
subscribeToAll()
onChanged()
}
override fun onAllSessionsRemoved() {
unsubscribeFromAll()
onChanged()
}
override fun onSessionRemoved(session: Session) {
unsubscribeFrom(session)
onChanged()
}
override fun onSessionSelected(session: Session) {
onChanged()
}
}
}