HomeFragment.kt 41.8 KB
Newer Older
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
3
 * 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/. */
Jeff Boek's avatar
Jeff Boek committed
4
5
6

package org.mozilla.fenix.home

7
import android.animation.Animator
8
import android.content.Context
9
import android.content.DialogInterface
10
import android.graphics.drawable.BitmapDrawable
11
import android.graphics.drawable.ColorDrawable
Jeff Boek's avatar
Jeff Boek committed
12
import android.os.Bundle
13
import android.os.StrictMode
14
import android.view.Display.FLAG_SECURE
15
import android.view.Gravity
Jeff Boek's avatar
Jeff Boek committed
16
17
18
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
19
20
21
import android.widget.Button
import android.widget.LinearLayout
import android.widget.PopupWindow
22
import androidx.appcompat.app.AlertDialog
23
import androidx.appcompat.content.res.AppCompatResources
24
import androidx.constraintlayout.widget.ConstraintLayout
25
26
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
27
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
28
import androidx.constraintlayout.widget.ConstraintSet.TOP
29
import androidx.coordinatorlayout.widget.CoordinatorLayout
30
import androidx.core.content.ContextCompat
31
import androidx.core.view.doOnLayout
32
import androidx.core.view.isGone
33
import androidx.core.view.isVisible
34
import androidx.core.view.updateLayoutParams
35
import androidx.fragment.app.Fragment
36
import androidx.fragment.app.activityViewModels
37
import androidx.lifecycle.Observer
38
import androidx.lifecycle.ViewModelProvider
39
import androidx.lifecycle.lifecycleScope
40
import androidx.navigation.fragment.findNavController
41
import androidx.navigation.fragment.navArgs
42
43
44
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
45
import com.google.android.material.snackbar.Snackbar
46
47
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
48
import kotlinx.android.synthetic.main.no_collections_message.view.*
49
50
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
51
import kotlinx.coroutines.ExperimentalCoroutinesApi
52
import kotlinx.coroutines.delay
53
import kotlinx.coroutines.launch
Tiger Oakes's avatar
Tiger Oakes committed
54
import kotlinx.coroutines.withContext
55
import mozilla.appservices.places.BookmarkRoot
56
import mozilla.components.browser.menu.view.MenuButton
57
58
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
59
import mozilla.components.browser.state.selector.findTab
60
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
61
62
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
63
import mozilla.components.browser.state.state.BrowserState
64
import mozilla.components.browser.state.store.BrowserStore
65
import mozilla.components.concept.sync.AccountObserver
66
import mozilla.components.concept.sync.AuthType
67
import mozilla.components.concept.sync.OAuthAccount
Tiger Oakes's avatar
Tiger Oakes committed
68
import mozilla.components.feature.tab.collections.TabCollection
69
70
import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
71
import mozilla.components.lib.state.ext.consumeFrom
72
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
73
import mozilla.components.support.ktx.android.content.res.resolveAttribute
74
import org.mozilla.fenix.BrowserDirection
75
import org.mozilla.fenix.BuildConfig
76
import org.mozilla.fenix.HomeActivity
Jeff Boek's avatar
Jeff Boek committed
77
import org.mozilla.fenix.R
78
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
79
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
80
import org.mozilla.fenix.cfr.SearchWidgetCFR
81
import org.mozilla.fenix.components.FenixSnackbar
82
import org.mozilla.fenix.components.PrivateShortcutCreateManager
83
import org.mozilla.fenix.components.StoreProvider
84
import org.mozilla.fenix.components.TabCollectionStorage
85
import org.mozilla.fenix.components.metrics.Event
86
import org.mozilla.fenix.components.tips.FenixTipManager
ekager's avatar
ekager committed
87
88
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
89
import org.mozilla.fenix.components.toolbar.TabCounterMenu
90
import org.mozilla.fenix.components.toolbar.ToolbarPosition
91
import org.mozilla.fenix.ext.components
Tiger Oakes's avatar
Tiger Oakes committed
92
import org.mozilla.fenix.ext.hideToolbar
93
import org.mozilla.fenix.ext.metrics
94
import org.mozilla.fenix.ext.nav
95
import org.mozilla.fenix.ext.requireComponents
96
import org.mozilla.fenix.ext.settings
97
98
99
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
100
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
101
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView
102
import org.mozilla.fenix.onboarding.FenixOnboarding
103
import org.mozilla.fenix.settings.SupportUtils
104
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
105
106
import org.mozilla.fenix.tor.TorEvents
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
107
import org.mozilla.fenix.theme.ThemeManager
108
import org.mozilla.fenix.utils.FragmentPreDrawManager
109
import org.mozilla.fenix.utils.ToolbarPopupWindow
110
import org.mozilla.fenix.utils.allowUndo
111
import org.mozilla.fenix.whatsnew.WhatsNew
112
import java.lang.ref.WeakReference
113
import kotlin.math.min
114

115
116
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
117
class HomeFragment : Fragment() {
118
    private val args by navArgs<HomeFragmentArgs>()
119
    private lateinit var bundleArgs: Bundle
120

121
122
    private val homeViewModel: HomeScreenViewModel by activityViewModels {
        ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
123
124
    }

125
    private val snackbarAnchorView: View?
126
127
128
        get() = when (requireContext().settings().toolbarPosition) {
            ToolbarPosition.BOTTOM -> toolbarLayout
            ToolbarPosition.TOP -> null
129
130
        }

131
    private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
132

133
134
    private val collectionStorageObserver = object : TabCollectionStorage.Observer {
        override fun onCollectionCreated(title: String, sessions: List<Session>) {
135
            scrollAndAnimateCollection()
136
137
        }

Tiger Oakes's avatar
Tiger Oakes committed
138
        override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
139
            scrollAndAnimateCollection(tabCollection)
140
141
        }

Tiger Oakes's avatar
Tiger Oakes committed
142
        override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
143
144
145
            lifecycleScope.launch(Main) {
                view?.sessionControlRecyclerView?.adapter?.notifyDataSetChanged()
            }
146
147
148
149
            showRenamedSnackbar()
        }
    }

150
151
    private val sessionManager: SessionManager
        get() = requireComponents.core.sessionManager
152
153
    private val store: BrowserStore
        get() = requireComponents.core.store
154

155
    private val onboarding by lazy {
156
        requireComponents.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
157
158
159
            FenixOnboarding(requireContext())
        }
    }
160

161
    private lateinit var homeFragmentStore: HomeFragmentStore
162
163
164
165
    private var _sessionControlInteractor: SessionControlInteractor? = null
    protected val sessionControlInteractor: SessionControlInteractor
        get() = _sessionControlInteractor!!

166
    private var sessionControlView: SessionControlView? = null
167
    private lateinit var currentMode: CurrentMode
168

169
    private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
170
    private val torQuickStart by lazy { TorQuickStart(requireContext()) }
171

172
173
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
174
        postponeEnterTransition()
175
        bundleArgs = args.toBundle()
176
        lifecycleScope.launch(IO) {
177
178
179
            if (!onboarding.userHasBeenOnboarded()) {
                requireComponents.analytics.metrics.track(Event.OpenedAppFirstRun)
            }
180
        }
181
182
    }

ekager's avatar
ekager committed
183
    @Suppress("LongMethod")
Jeff Boek's avatar
Jeff Boek committed
184
    override fun onCreateView(
185
186
        inflater: LayoutInflater,
        container: ViewGroup?,
Jeff Boek's avatar
Jeff Boek committed
187
188
        savedInstanceState: Bundle?
    ): View? {
189
        val view = inflater.inflate(R.layout.fragment_home, container, false)
190
        val activity = activity as HomeActivity
191
        val components = requireComponents
192

193
194
195
        currentMode = CurrentMode(
            view.context,
            onboarding,
196
197
198
            torQuickStart,
            !BuildConfig.DISABLE_TOR,
            components.torController,
199
            browsingModeManager,
200
            ::dispatchModeChanges
201
        )
202

203
204
205
        homeFragmentStore = StoreProvider.get(this) {
            HomeFragmentStore(
                HomeFragmentState(
206
                    collections = components.core.tabCollectionStorage.cachedTabCollections,
207
208
                    expandedCollections = emptySet(),
                    mode = currentMode.getCurrentMode(),
209
                    topSites = components.core.topSitesStorage.cachedTopSites,
210
                    tip = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
ekager's avatar
ekager committed
211
212
213
214
215
216
                        FenixTipManager(
                            listOf(
                                MasterPasswordTipProvider(
                                    requireContext(),
                                    ::navToSavedLogins,
                                    ::dismissTip
217
                                )
ekager's avatar
ekager committed
218
219
220
                            )
                        ).getTip()
                    },
221
                    showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
222
                )
223
224
225
            )
        }

226
227
228
        topSitesFeature.set(
            feature = TopSitesFeature(
                view = DefaultTopSitesView(homeFragmentStore),
229
                storage = components.core.topSitesStorage,
230
231
232
233
234
235
                config = ::getTopSitesConfig
            ),
            owner = this,
            view = view
        )

236
        _sessionControlInteractor = SessionControlInteractor(
237
238
            DefaultSessionControlController(
                activity = activity,
239
                settings = components.settings,
240
241
242
243
244
                engine = components.core.engine,
                metrics = components.analytics.metrics,
                sessionManager = sessionManager,
                tabCollectionStorage = components.core.tabCollectionStorage,
                addTabUseCase = components.useCases.tabsUseCases.addTab,
245
                fragmentStore = homeFragmentStore,
246
                navController = findNavController(),
247
                viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
248
                hideOnboarding = ::hideOnboardingAndOpenSearch,
249
                registerCollectionStorageObserver = ::registerCollectionStorageObserver,
250
                showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
251
                showTabTray = ::openTabTray,
252
253
254
255
                handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel,
                handleTorBootstrapConnect = ::handleTorBootstrapConnect,
                cancelTorBootstrap = ::cancelTorBootstrap,
                initiateTorBootstrap = ::initiateTorBootstrap
256
            )
257
        )
ekager's avatar
ekager committed
258

259
        updateLayout(view)
Marc Leclair's avatar
Marc Leclair committed
260
        sessionControlView = SessionControlView(
261
            view.sessionControlRecyclerView,
262
            viewLifecycleOwner,
263
            sessionControlInteractor,
264
            homeViewModel
Marc Leclair's avatar
Marc Leclair committed
265
        )
266

267
268
269
        updateSessionControlView(view)

        activity.themeManager.applyStatusBarTheme(activity)
270
271
272

        adjustHomeFragmentView(currentMode.getCurrentMode(), view)

273
274
275
        return view
    }

ekager's avatar
ekager committed
276
277
278
279
    private fun dismissTip(tip: Tip) {
        sessionControlInteractor.onCloseTip(tip)
    }

280
281
282
283
284
285
286
287
288
    /**
     * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
     * not frequently visited sites should be displayed.
     */
    private fun getTopSitesConfig(): TopSitesConfig {
        val settings = requireContext().settings()
        return TopSitesConfig(settings.topSitesMaxLimit, settings.showTopFrecentSites)
    }

289
290
291
292
293
294
    /**
     * 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.
     */
295
296
297
298
299
300
301
    private fun updateSessionControlView(view: View) {
        if (browsingModeManager.mode == BrowsingMode.Private) {
            view.consumeFrom(homeFragmentStore, viewLifecycleOwner) {
                sessionControlView?.update(it)
            }
        } else {
            sessionControlView?.update(homeFragmentStore.state)
302

303
304
305
            view.consumeFrom(homeFragmentStore, viewLifecycleOwner) {
                sessionControlView?.update(it)
            }
306
307
308
        }
    }

309
    private fun updateLayout(view: View) {
310
311
312
313
314
315
        when (view.context.settings().toolbarPosition) {
            ToolbarPosition.TOP -> {
                view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
                    ConstraintLayout.LayoutParams.MATCH_PARENT,
                    ConstraintLayout.LayoutParams.WRAP_CONTENT
                ).apply {
316
317
                    gravity = Gravity.TOP
                }
318

319
320
321
322
323
324
325
326
327
                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)
                }
328

329
330
331
                view.bottom_bar.background = AppCompatResources.getDrawable(
                    view.context,
                    view.context.theme.resolveAttribute(R.attr.bottomBarBackgroundTop)
332
                )
333

334
                view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
335
336
                    topMargin =
                        resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
337
338
339
340
                }
            }
            ToolbarPosition.BOTTOM -> {
            }
341
        }
Jeff Boek's avatar
Jeff Boek committed
342
    }
Marc Leclair's avatar
Marc Leclair committed
343

344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
    private fun adjustHomeFragmentView(mode: Mode, view: View?) {
        if (mode == Mode.Bootstrap) {
            view?.sessionControlRecyclerView?.apply {
                setPadding(0, 0, 0, 0)
                (getLayoutParams() as ViewGroup.MarginLayoutParams).setMargins(0, 0, 0, 0)
            }
            view?.homeAppBar?.apply {
                visibility = View.GONE
            }
            view?.toolbarLayout?.apply {
                visibility = View.GONE
            }
        } else {
            // Keep synchronized with xml layout (somehow).
            view?.sessionControlRecyclerView?.apply {
                setPadding(
                    SESSION_CONTROL_VIEW_PADDING,
                    SESSION_CONTROL_VIEW_PADDING,
                    SESSION_CONTROL_VIEW_PADDING,
                    SESSION_CONTROL_VIEW_PADDING
                )
                // Default margin until it is re-set below (either set immediately or after Layout)
                (getLayoutParams() as ViewGroup.MarginLayoutParams).setMargins(
                    0,
                    0,
                    0,
                    DEFAULT_ONBOARDING_FINISH_MARGIN
                )
            }
            view?.homeAppBar?.apply {
                visibility = View.VISIBLE
            }
            view?.toolbarLayout?.apply {
                visibility = View.VISIBLE
            }
        }
    }

382
    @Suppress("LongMethod", "ComplexMethod")
Jeff Boek's avatar
Jeff Boek committed
383
384
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
385

386
        FragmentPreDrawManager(this).execute {
387
388
389
            val homeViewModel: HomeScreenViewModel by activityViewModels {
                ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
            }
390
            homeViewModel.layoutManagerState?.also { parcelable ->
391
                sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
392
393
            }
            homeViewModel.layoutManagerState = null
394
395
396
397

            // We have to delay so that the keyboard collapses and the view is resized before the
            // animation from SearchFragment happens
            delay(ANIMATION_DELAY)
398
399
        }

400
        viewLifecycleOwner.lifecycleScope.launch(IO) {
401
402
403
            // 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
404
405
            val context = context ?: return@launch

406
407
            val iconSize =
                context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
408

409
410
            val searchEngine = context.components.search.provider.getDefaultEngine(context)
            val searchIcon = BitmapDrawable(context.resources, searchEngine.icon)
411
412
            searchIcon.setBounds(0, 0, iconSize, iconSize)

413
            withContext(Main) {
414
                search_engine_icon?.setImageDrawable(searchIcon)
415
            }
416
        }
417

418
        createHomeMenu(requireContext(), WeakReference(view.menuButton))
419
420
421
422
423
424
425
426
427
428
429
430
431
        val tabCounterMenu = TabCounterMenu(
            view.context,
            metrics = view.context.components.analytics.metrics
        ) {
            if (it is TabCounterMenu.Item.NewTab) {
                (activity as HomeActivity).browsingModeManager.mode = it.mode
            }
        }
        val inverseBrowsingMode = when ((activity as HomeActivity).browsingModeManager.mode) {
            BrowsingMode.Normal -> BrowsingMode.Private
            BrowsingMode.Private -> BrowsingMode.Normal
        }
        tabCounterMenu.updateMenu(showOnly = inverseBrowsingMode)
432
        view.tab_button.setOnLongClickListener {
433
            tabCounterMenu.menuController.show(anchor = it)
434
435
            true
        }
436

437
438
439
440
441
442
        view.menuButton.setColorFilter(
            ContextCompat.getColor(
                requireContext(),
                ThemeManager.resolveAttribute(R.attr.primaryText, requireContext())
            )
        )
443

444
        view.toolbar.compoundDrawablePadding =
445
            view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
446
        view.toolbar_wrapper.setOnClickListener {
447
            hideOnboardingIfNeeded()
448
            navigateToSearch()
449
450
            requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
        }
451

452
453
        view.toolbar_wrapper.setOnLongClickListener {
            ToolbarPopupWindow.show(
454
                WeakReference(it),
455
456
457
458
459
460
461
                handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
                handlePaste = sessionControlInteractor::onPaste,
                copyVisible = false
            )
            true
        }

462
        view.tab_button.setOnClickListener {
463
            openTabTray()
464
465
        }

466
467
468
469
        PrivateBrowsingButtonView(
            privateBrowsingButton,
            browsingModeManager
        ) { newMode ->
470
            if (newMode == BrowsingMode.Private) {
471
                requireContext().settings().incrementNumTimesPrivateModeOpened()
472
473
            }

474
            if (onboarding.userHasBeenOnboarded()) {
475
                homeFragmentStore.dispatch(
476
477
                    HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode))
                )
478
            }
479
        }
480

481
482
        privateBrowsingButton.isGone = view.context.settings().shouldDisableNormalMode

483
484
485
        // We call this onLayout so that the bottom bar width is correctly set for us to center
        // the CFR in.
        view.toolbar_wrapper.doOnLayout {
486
            val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)
487
            if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
Tiger Oakes's avatar
Tiger Oakes committed
488
489
490
491
492
493
494
                SearchWidgetCFR(
                    context = view.context,
                    settings = view.context.settings(),
                    metrics = view.context.components.analytics.metrics
                ) {
                    view.toolbar_wrapper
                }.displayIfNecessary()
495
496
            }
        }
497

498
499
500
501
502
        if (browsingModeManager.mode.isPrivate) {
            requireActivity().window.addFlags(FLAG_SECURE)
        } else {
            requireActivity().window.clearFlags(FLAG_SECURE)
        }
503
504

        consumeFrom(requireComponents.core.store) {
505
            updateTabCounter(it)
506
        }
507

508
        homeViewModel.sessionToDelete?.also {
509
510
511
512
513
514
            if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
                removeAllTabsAndShowSnackbar(it)
            } else {
                removeTabAndShowSnackbar(it)
            }
        }
515

516
517
        homeViewModel.sessionToDelete = null

518
        updateTabCounter(requireComponents.core.store.state)
519

520
        if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
521
522
            navigateToSearch()
        }
523
524
525
    }

    private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
526
527
528
529
        if (sessionCode == ALL_PRIVATE_TABS) {
            sessionManager.removePrivateSessions()
        } else {
            sessionManager.removeNormalSessions()
530
531
        }

532
533
534
535
536
537
538
539
540
541
542
        val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
            getString(R.string.snackbar_private_tabs_closed)
        } else {
            getString(R.string.snackbar_tabs_closed)
        }

        viewLifecycleOwner.lifecycleScope.allowUndo(
            requireView(),
            snackbarMessage,
            requireContext().getString(R.string.snackbar_deleted_undo),
            {
543
                requireComponents.useCases.tabsUseCases.undo.invoke()
544
545
546
547
548
549
550
            },
            operation = { },
            anchorView = snackbarAnchorView
        )
    }

    private fun removeTabAndShowSnackbar(sessionId: String) {
551
        val tab = store.state.findTab(sessionId) ?: return
552

553
        requireComponents.useCases.tabsUseCases.removeTab(sessionId)
554

555
556
557
558
        val snackbarMessage = if (tab.content.private) {
            requireContext().getString(R.string.snackbar_private_tab_closed)
        } else {
            requireContext().getString(R.string.snackbar_tab_closed)
559
        }
560
561
562
563
564
565
566
567
568
569
570
571
572
573

        viewLifecycleOwner.lifecycleScope.allowUndo(
            requireView(),
            snackbarMessage,
            requireContext().getString(R.string.snackbar_deleted_undo),
            {
                requireComponents.useCases.tabsUseCases.undo.invoke()
                findNavController().navigate(
                    HomeFragmentDirections.actionGlobalBrowser(null)
                )
            },
            operation = { },
            anchorView = snackbarAnchorView
        )
574
575
    }

576
    override fun onDestroyView() {
577
        super.onDestroyView()
578
        _sessionControlInteractor = null
579
        sessionControlView = null
580
        bundleArgs.clear()
581
        requireActivity().window.clearFlags(FLAG_SECURE)
582
583
    }

584
585
586
    override fun onStart() {
        super.onStart()
        subscribeToTabCollections()
587

588
589
590
        val context = requireContext()
        val components = context.components

591
592
593
594
        homeFragmentStore.dispatch(
            HomeFragmentAction.Change(
                collections = components.core.tabCollectionStorage.cachedTabCollections,
                mode = currentMode.getCurrentMode(),
595
                topSites = components.core.topSitesStorage.cachedTopSites,
596
                tip = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
ekager's avatar
ekager committed
597
598
599
600
601
602
                    FenixTipManager(
                        listOf(
                            MasterPasswordTipProvider(
                                requireContext(),
                                ::navToSavedLogins,
                                ::dismissTip
603
                            )
ekager's avatar
ekager committed
604
605
606
                        )
                    ).getTip()
                },
607
                showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
608
609
            )
        )
Colin Lee's avatar
Colin Lee committed
610

611
        requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
612
613
            // 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) {
614
615
                return@runIfReadyOrQueue
            }
616
617
618
619
620

            requireComponents.backgroundServices.accountManager.register(
                currentMode,
                owner = this@HomeFragment.viewLifecycleOwner
            )
621
622
623
624
            requireComponents.backgroundServices.accountManager.register(object : AccountObserver {
                override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
                    if (authType != AuthType.Existing) {
                        view?.let {
625
626
                            FenixSnackbar.make(
                                view = it,
627
                                duration = Snackbar.LENGTH_SHORT,
628
                                isDisplayedWithBrowserToolbar = false
629
                            )
630
631
632
633
                                .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
                                .setAnchorView(toolbarLayout)
                                .show()
                        }
634
635
                    }
                }
636
            }, owner = this@HomeFragment.viewLifecycleOwner)
637
        }
638

639
640
        if (browsingModeManager.mode.isPrivate &&
            context.settings().showPrivateModeCfr
641
        ) {
642
643
            recommendPrivateBrowsingShortcut()
        }
644
645
646

        // We only want this observer live just before we navigate away to the collection creation screen
        requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
647
648
649
650

        lifecycleScope.launch(IO) {
            requireComponents.reviewPromptController.promptReview(requireActivity())
        }
651
652
    }

ekager's avatar
ekager committed
653
654
655
656
    private fun navToSavedLogins() {
        findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
    }

657
    private fun dispatchModeChanges(mode: Mode) {
658
659
660
661
662
663
        homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))

        val localView = view
        adjustHomeFragmentView(mode, view)
        if (localView != null) {
            updateSessionControlView(localView)
664
        }
665
666
    }

667
668
669
670
671
672
673
    private fun showDeleteCollectionPrompt(
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) {
674
675
        val context = context ?: return
        AlertDialog.Builder(context).apply {
676
            setTitle(title)
677
678
            setMessage(message)
            setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
679
680
681
                if (wasSwiped) {
                    handleSwipedItemDeletionCancel()
                }
682
683
684
                dialog.cancel()
            }
            setPositiveButton(R.string.tab_collection_dialog_positive) { dialog: DialogInterface, _ ->
685
686
                // Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
                lifecycleScope.launch(IO) {
687
688
689
690
                    context.components.core.tabCollectionStorage.removeCollection(tabCollection)
                    context.components.analytics.metrics.track(Event.CollectionRemoved)
                }.invokeOnCompletion {
                    dialog.dismiss()
691
                }
692
693
694
            }
            create()
        }.show()
695
696
    }

697
698
    override fun onStop() {
        super.onStop()
699
700
701
        val homeViewModel: HomeScreenViewModel by activityViewModels {
            ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
        }
702
        homeViewModel.layoutManagerState =
703
            sessionControlView!!.view.layoutManager?.onSaveInstanceState()
704
705

        currentMode.unregisterTorListener()
706
707
    }

708
709
    override fun onResume() {
        super.onResume()
710
711
712
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
        }
713

714
715
716
        hideToolbar()
    }

717
718
    override fun onPause() {
        super.onPause()
719
720
721
722
723
724
725
726
727
728
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawable(
                ColorDrawable(
                    ContextCompat.getColor(
                        requireContext(),
                        R.color.foundation_private_theme
                    )
                )
            )
        }
729
730
    }

731
    private fun recommendPrivateBrowsingShortcut() {
732
733
        context?.let { context ->
            val layout = LayoutInflater.from(context)
734
                .inflate(R.layout.pbm_shortcut_popup, null)
735
            val privateBrowsingRecommend =
736
737
                PopupWindow(
                    layout,
738
739
740
741
                    min(
                        (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
                        (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
                    ),
742
743
744
745
746
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    true
                )
            layout.findViewById<Button>(R.id.cfr_pos_button).apply {
                setOnClickListener {
747
                    context.metrics.track(Event.PrivateBrowsingAddShortcutCFR)
748
                    PrivateShortcutCreateManager.createPrivateShortcut(context)
749
                    privateBrowsingRecommend.dismiss()
750
751
752
                }
            }
            layout.findViewById<Button>(R.id.cfr_neg_button).apply {
753
754
                setOnClickListener {
                    context.metrics.track(Event.PrivateBrowsingCancelCFR)
755
                    privateBrowsingRecommend.dismiss()
756
                }
757
758
759
760
            }
            // We want to show the popup only after privateBrowsingButton is available.
            // Otherwise, we will encounter an activity token error.
            privateBrowsingButton.post {
761
                context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
762
                privateBrowsingRecommend.showAsDropDown(
763
764
                    privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
                )
765
766
767
768
            }
        }
    }

769
    private fun hideOnboardingIfNeeded() {
770
771
772
773
        if (!onboarding.userHasBeenOnboarded()) {
            onboarding.finish()
            homeFragmentStore.dispatch(
                HomeFragmentAction.ModeChange(
774
                    mode = currentMode.getCurrentMode()
775
776
                )
            )
777
778
779
780
781
782
        }
    }

    private fun hideOnboardingAndOpenSearch() {
        hideOnboardingIfNeeded()
        navigateToSearch()
783
784
    }

785
    private fun navigateToSearch() {
786
        val directions =
787
788
789
            HomeFragmentDirections.actionGlobalSearchDialog(
                sessionId = null
            )
790

791
        nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
792
793
    }

794
    @SuppressWarnings("ComplexMethod", "LongMethod")
795
796
797
798
799
800
801
802
803
804
805
    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()
806
                        )
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
                    }
                    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()
                        )
                    }
Kate Glazko's avatar
Kate Glazko committed
829
830
831
832
833
834
835
836
837

                    HomeMenu.Item.Downloads -> {
                        hideOnboardingIfNeeded()
                        nav(
                            R.id.homeFragment,
                            HomeFragmentDirections.actionGlobalDownloadsFragment()
                        )
                    }

838
839
840
                    HomeMenu.Item.Help -> {
                        hideOnboardingIfNeeded()
                        (activity as HomeActivity).openToBrowserAndLoad(
841
                            searchTermOrURL = SupportUtils.getTorHelpPageUrl(),
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
                            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()
                        )
                    }
884
                }
885
886
887
888
            },
            onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },
            onMenuBuilderChanged = { menuButtonView.get()?.menuBuilder = it }
        )
889

890
    private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
Tiger Oakes's avatar
Tiger Oakes committed
891
        return Observer<List<TabCollection>> {
892
            requireComponents.core.tabCollectionStorage.cachedTabCollections = it
893
            homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(it))
Tiger Oakes's avatar
Tiger Oakes committed
894
895
        }.also { observer ->
            requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
896
        }
897
898
    }

899
900
    private fun registerCollectionStorageObserver() {
        requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
901
902
    }

903
904
905
    private fun scrollAndAnimateCollection(
        changedCollection: TabCollection? = null
    ) {
906
907
        if (view != null) {
            viewLifecycleOwner.lifecycleScope.launch {
908
                val recyclerView = sessionControlView!!.view
909
                delay(ANIM_SCROLL_DELAY)
910
911
912
                val tabsSize = store.state
                    .getNormalOrPrivateTabs(browsingModeManager.mode.isPrivate)
                    .size
913
914
915
916
917
918
919
920
921
922
923

                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
                        }
924
                }
925 <