HomeFragment.kt 46.4 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.children
32
import androidx.core.view.doOnLayout
33
import androidx.core.view.isGone
34
import androidx.core.view.isVisible
35
import androidx.core.view.updateLayoutParams
36
import androidx.fragment.app.Fragment
37
import androidx.fragment.app.activityViewModels
38
import androidx.fragment.app.viewModels
39
import androidx.lifecycle.Observer
40
import androidx.lifecycle.ViewModelProvider
41
import androidx.lifecycle.lifecycleScope
42
import androidx.navigation.fragment.findNavController
43
import androidx.navigation.fragment.navArgs
44
45
46
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
47
import com.google.android.material.appbar.AppBarLayout
48
import com.google.android.material.snackbar.Snackbar
49
50
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
51
import kotlinx.android.synthetic.main.no_collections_message.view.*
52
53
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
54
import kotlinx.coroutines.ExperimentalCoroutinesApi
55
import kotlinx.coroutines.delay
56
import kotlinx.coroutines.launch
Tiger Oakes's avatar
Tiger Oakes committed
57
import kotlinx.coroutines.withContext
58
import mozilla.appservices.places.BookmarkRoot
59
import mozilla.components.browser.menu.view.MenuButton
60
61
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
62
import mozilla.components.browser.state.selector.findTab
63
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
64
65
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
66
import mozilla.components.browser.state.state.BrowserState
67
import mozilla.components.browser.state.store.BrowserStore
68
import mozilla.components.concept.sync.AccountObserver
69
import mozilla.components.concept.sync.AuthType
70
import mozilla.components.concept.sync.OAuthAccount
Tiger Oakes's avatar
Tiger Oakes committed
71
import mozilla.components.feature.tab.collections.TabCollection
72
73
import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
74
import mozilla.components.lib.state.ext.consumeFrom
75
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
76
import mozilla.components.support.ktx.android.content.res.resolveAttribute
77
import org.mozilla.fenix.BrowserDirection
78
import org.mozilla.fenix.BuildConfig
79
import org.mozilla.fenix.FeatureFlags
80
import org.mozilla.fenix.HomeActivity
Jeff Boek's avatar
Jeff Boek committed
81
import org.mozilla.fenix.R
82
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
83
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
84
import org.mozilla.fenix.cfr.SearchWidgetCFR
85
import org.mozilla.fenix.components.FenixSnackbar
86
import org.mozilla.fenix.components.PrivateShortcutCreateManager
87
import org.mozilla.fenix.components.StoreProvider
88
import org.mozilla.fenix.components.TabCollectionStorage
89
import org.mozilla.fenix.components.metrics.Event
90
import org.mozilla.fenix.components.tips.FenixTipManager
ekager's avatar
ekager committed
91
92
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
93
import org.mozilla.fenix.components.toolbar.TabCounterMenu
94
import org.mozilla.fenix.components.toolbar.ToolbarPosition
95
import org.mozilla.fenix.ext.components
Tiger Oakes's avatar
Tiger Oakes committed
96
import org.mozilla.fenix.ext.hideToolbar
97
import org.mozilla.fenix.ext.metrics
98
import org.mozilla.fenix.ext.nav
99
import org.mozilla.fenix.ext.requireComponents
100
import org.mozilla.fenix.ext.resetPoliciesAfter
Sawyer Blatz's avatar
Sawyer Blatz committed
101
import org.mozilla.fenix.ext.sessionsOfType
102
import org.mozilla.fenix.ext.settings
103
104
105
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
106
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
107
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView
108
import org.mozilla.fenix.onboarding.FenixOnboarding
109
import org.mozilla.fenix.settings.SupportUtils
110
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
111
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
112
import org.mozilla.fenix.theme.ThemeManager
113
import org.mozilla.fenix.utils.FragmentPreDrawManager
114
import org.mozilla.fenix.utils.ToolbarPopupWindow
115
import org.mozilla.fenix.utils.allowUndo
116
import org.mozilla.fenix.whatsnew.WhatsNew
117
import java.lang.ref.WeakReference
118
import kotlin.math.min
119

120
121
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
122
class HomeFragment : Fragment() {
123
    private val args by navArgs<HomeFragmentArgs>()
124
    private lateinit var bundleArgs: Bundle
125

126
127
128
129
    private val homeViewModel: HomeScreenViewModel by viewModels {
        ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
    }

130
    private val snackbarAnchorView: View?
131
132
133
        get() = when (requireContext().settings().toolbarPosition) {
            ToolbarPosition.BOTTOM -> toolbarLayout
            ToolbarPosition.TOP -> null
134
135
        }

136
    private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
137

138
139
    private val collectionStorageObserver = object : TabCollectionStorage.Observer {
        override fun onCollectionCreated(title: String, sessions: List<Session>) {
140
            scrollAndAnimateCollection()
141
142
        }

Tiger Oakes's avatar
Tiger Oakes committed
143
        override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
144
            scrollAndAnimateCollection(tabCollection)
145
146
        }

Tiger Oakes's avatar
Tiger Oakes committed
147
        override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
148
149
150
151
            showRenamedSnackbar()
        }
    }

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

157
158
159
160
161
    private val onboarding by lazy {
        StrictMode.allowThreadDiskReads().resetPoliciesAfter {
            FenixOnboarding(requireContext())
        }
    }
162

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

168
    private var sessionControlView: SessionControlView? = null
169
    private lateinit var currentMode: CurrentMode
170

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

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

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

195
196
197
198
199
200
201
202
203
        // Splits by full stops or commas and puts the parts in different lines.
        // Ignoring separators at the end of the string, it is expected
        // that there are at most two parts (e.g. "Explore. Privately.").
        view.exploreprivately.text = view
            .exploreprivately
            .text
            ?.replace(" *([.,。।]) *".toRegex(), "$1\n")
            ?.trim()

204
205
206
        currentMode = CurrentMode(
            view.context,
            onboarding,
207
208
209
            torQuickStart,
            !BuildConfig.DISABLE_TOR,
            components.torController,
210
            browsingModeManager,
211
            ::dispatchModeChanges
212
        )
213

214
215
216
        homeFragmentStore = StoreProvider.get(this) {
            HomeFragmentStore(
                HomeFragmentState(
217
                    collections = components.core.tabCollectionStorage.cachedTabCollections,
218
219
                    expandedCollections = emptySet(),
                    mode = currentMode.getCurrentMode(),
220
                    topSites = components.core.topSitesStorage.cachedTopSites,
ekager's avatar
ekager committed
221
222
223
224
225
226
227
                    tip = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
                        FenixTipManager(
                            listOf(
                                MasterPasswordTipProvider(
                                    requireContext(),
                                    ::navToSavedLogins,
                                    ::dismissTip
228
                                )
ekager's avatar
ekager committed
229
230
231
                            )
                        ).getTip()
                    },
232
                    showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
233
                )
234
235
236
            )
        }

237
238
239
        topSitesFeature.set(
            feature = TopSitesFeature(
                view = DefaultTopSitesView(homeFragmentStore),
240
                storage = components.core.topSitesStorage,
241
242
243
244
245
246
                config = ::getTopSitesConfig
            ),
            owner = this,
            view = view
        )

247
        _sessionControlInteractor = SessionControlInteractor(
248
249
            DefaultSessionControlController(
                activity = activity,
250
                settings = components.settings,
251
252
253
254
255
                engine = components.core.engine,
                metrics = components.analytics.metrics,
                sessionManager = sessionManager,
                tabCollectionStorage = components.core.tabCollectionStorage,
                addTabUseCase = components.useCases.tabsUseCases.addTab,
256
                fragmentStore = homeFragmentStore,
257
                navController = findNavController(),
258
                viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
259
                hideOnboarding = ::hideOnboardingAndOpenSearch,
260
                registerCollectionStorageObserver = ::registerCollectionStorageObserver,
261
                showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
262
                showTabTray = ::openTabTray,
263
264
265
                handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel,
                handleTorBootstrapConnect = ::handleTorBootstrapConnect,
                cancelTorBootstrap = ::cancelTorBootstrap,
266
267
                initiateTorBootstrap = ::initiateTorBootstrap,
                openTorNetworkSettings = ::openTorNetworkSettings
268
            )
269
        )
ekager's avatar
ekager committed
270

271
        updateLayout(view)
Marc Leclair's avatar
Marc Leclair committed
272
        sessionControlView = SessionControlView(
273
            view.sessionControlRecyclerView,
274
            viewLifecycleOwner,
275
            sessionControlInteractor,
276
            homeViewModel
Marc Leclair's avatar
Marc Leclair committed
277
        )
278

279
280
281
        updateSessionControlView(view)

        activity.themeManager.applyStatusBarTheme(activity)
282
283

        adjustHomeFragmentView(currentMode.getCurrentMode(), view)
284
        showSessionControlView(view)
285

286
287
288
        return view
    }

ekager's avatar
ekager committed
289
290
291
292
    private fun dismissTip(tip: Tip) {
        sessionControlInteractor.onCloseTip(tip)
    }

293
294
295
296
297
298
299
300
301
    /**
     * 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)
    }

302
303
304
305
306
307
    /**
     * 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.
     */
308
309
310
311
312
313
314
    private fun updateSessionControlView(view: View) {
        if (browsingModeManager.mode == BrowsingMode.Private) {
            view.consumeFrom(homeFragmentStore, viewLifecycleOwner) {
                sessionControlView?.update(it)
            }
        } else {
            sessionControlView?.update(homeFragmentStore.state)
315

316
317
318
            view.consumeFrom(homeFragmentStore, viewLifecycleOwner) {
                sessionControlView?.update(it)
            }
319
320
321
        }
    }

322
    private fun updateLayout(view: View) {
323
324
325
326
327
328
        when (view.context.settings().toolbarPosition) {
            ToolbarPosition.TOP -> {
                view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
                    ConstraintLayout.LayoutParams.MATCH_PARENT,
                    ConstraintLayout.LayoutParams.WRAP_CONTENT
                ).apply {
329
330
                    gravity = Gravity.TOP
                }
331

332
333
334
335
336
337
338
339
340
                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)
                }
341

342
343
344
                view.bottom_bar.background = AppCompatResources.getDrawable(
                    view.context,
                    view.context.theme.resolveAttribute(R.attr.bottomBarBackgroundTop)
345
                )
346

347
                view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
348
349
                    topMargin =
                        resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
350
351
352
353
                }
            }
            ToolbarPosition.BOTTOM -> {
            }
354
        }
Jeff Boek's avatar
Jeff Boek committed
355
    }
Marc Leclair's avatar
Marc Leclair committed
356

357
358
    // This function should be paired with showSessionControlView()
    @SuppressWarnings("ComplexMethod", "NestedBlockDepth", "LongMethod")
359
    private fun adjustHomeFragmentView(mode: Mode, view: View?) {
360
361
362
363
        view?.sessionControlRecyclerView?.apply {
                visibility = View.INVISIBLE
        }

364
365
366
        if (mode == Mode.Bootstrap) {
            view?.sessionControlRecyclerView?.apply {
                setPadding(0, 0, 0, 0)
367
                (layoutParams as ViewGroup.MarginLayoutParams).setMargins(0, 0, 0, 0)
368
369
370
            }
            view?.homeAppBar?.apply {
                visibility = View.GONE
371
372
373
374
375
376
377
378
379

                // Reset this as SCROLL in case it was previously set as NO_SCROLL after bootstrap
                children.forEach {
                    (it.layoutParams as AppBarLayout.LayoutParams).scrollFlags =
                        AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
                }
            }
            view?.onion_pattern_image?.apply {
                visibility = View.GONE
380
381
382
383
384
385
386
387
388
389
390
391
392
393
            }
            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)
394
                (layoutParams as ViewGroup.MarginLayoutParams).setMargins(
395
396
397
398
399
400
                    0,
                    0,
                    0,
                    DEFAULT_ONBOARDING_FINISH_MARGIN
                )
            }
401
            view?.toolbarLayout?.apply {
402
                visibility = View.VISIBLE
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
                // If the Layout rendering pass was completed, then we have a |height| value,
                // if it wasn't completed then we have 0.
                if (height == 0) {
                    // Set the bottom margin after the toolbar height is defined during Layout
                    doOnLayout {
                        val toolbarLayoutHeight = view.toolbarLayout.height
                        view.sessionControlRecyclerView?.apply {
                            (layoutParams as ViewGroup.MarginLayoutParams).setMargins(
                                0,
                                0,
                                0,
                                toolbarLayoutHeight - SESSION_CONTROL_VIEW_PADDING
                            )
                        }
                    }
                } else {
                    view.sessionControlRecyclerView?.apply {
                        (layoutParams as ViewGroup.MarginLayoutParams).setMargins(
                            0,
                            0,
                            0,
                            height - SESSION_CONTROL_VIEW_PADDING
                        )
                    }
                }
428
            }
429
430
431
432
433
434
435
436
437
            // Hide the onion pattern during Onboarding, too.
            view?.onion_pattern_image?.apply {
                visibility = if (onboarding.userHasBeenOnboarded()) {
                    View.VISIBLE
                } else {
                    View.GONE
                }
            }
            view?.homeAppBar?.apply {
438
                visibility = View.VISIBLE
439
440
441
442
443
444
445
446
447

                children.forEach {
                    (it.layoutParams as AppBarLayout.LayoutParams).scrollFlags =
                        if (onboarding.userHasBeenOnboarded()) {
                            AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
                        } else {
                            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
                        }
                }
448
449
450
451
            }
        }
    }

452
453
454
455
456
457
458
    // This function should be paired with adjustHomeFragmentView()
    private fun showSessionControlView(view: View?) {
        view?.sessionControlRecyclerView?.apply {
                visibility = View.VISIBLE
        }
    }

459
    @Suppress("LongMethod", "ComplexMethod")
Jeff Boek's avatar
Jeff Boek committed
460
461
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
462

463
        FragmentPreDrawManager(this).execute {
464
465
466
            val homeViewModel: HomeScreenViewModel by activityViewModels {
                ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
            }
467
            homeViewModel.layoutManagerState?.also { parcelable ->
468
                sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
469
470
            }
            homeViewModel.layoutManagerState = null
471
472
473
474

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

477
        viewLifecycleOwner.lifecycleScope.launch(IO) {
478
479
480
            // 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
481
482
            val context = context ?: return@launch

483
484
            val iconSize =
                context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
485

486
487
            val searchEngine = context.components.search.provider.getDefaultEngine(context)
            val searchIcon = BitmapDrawable(context.resources, searchEngine.icon)
488
489
            searchIcon.setBounds(0, 0, iconSize, iconSize)

490
            withContext(Main) {
491
                search_engine_icon?.setImageDrawable(searchIcon)
492
            }
493
        }
494

495
        createHomeMenu(requireContext(), WeakReference(view.menuButton))
496
497
498
499
500
501
502
503
504
505
506
507
508
        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)
509
        view.tab_button.setOnLongClickListener {
510
            tabCounterMenu.menuController.show(anchor = it)
511
512
            true
        }
513

514
515
516
517
518
519
        view.menuButton.setColorFilter(
            ContextCompat.getColor(
                requireContext(),
                ThemeManager.resolveAttribute(R.attr.primaryText, requireContext())
            )
        )
520

521
        view.toolbar.compoundDrawablePadding =
522
            view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
523
        view.toolbar_wrapper.setOnClickListener {
524
            hideOnboardingIfNeeded()
525
            navigateToSearch()
526
527
            requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
        }
528

529
530
        view.toolbar_wrapper.setOnLongClickListener {
            ToolbarPopupWindow.show(
531
                WeakReference(it),
532
533
534
535
536
537
538
                handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
                handlePaste = sessionControlInteractor::onPaste,
                copyVisible = false
            )
            true
        }

539
        view.tab_button.setOnClickListener {
540
            openTabTray()
541
542
        }

543
544
545
546
        PrivateBrowsingButtonView(
            privateBrowsingButton,
            browsingModeManager
        ) { newMode ->
547
            if (newMode == BrowsingMode.Private) {
548
                requireContext().settings().incrementNumTimesPrivateModeOpened()
549
550
            }

551
            if (onboarding.userHasBeenOnboarded()) {
552
                homeFragmentStore.dispatch(
553
554
                    HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode))
                )
555
            }
556
        }
557

558
559
        privateBrowsingButton.isGone = view.context.settings().shouldDisableNormalMode

560
561
562
        // We call this onLayout so that the bottom bar width is correctly set for us to center
        // the CFR in.
        view.toolbar_wrapper.doOnLayout {
ekager's avatar
ekager committed
563
564
            val willNavigateToSearch =
                !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
565
            if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
Tiger Oakes's avatar
Tiger Oakes committed
566
567
568
569
570
571
572
                SearchWidgetCFR(
                    context = view.context,
                    settings = view.context.settings(),
                    metrics = view.context.components.analytics.metrics
                ) {
                    view.toolbar_wrapper
                }.displayIfNecessary()
573
574
            }
        }
575

576
577
578
579
580
        if (browsingModeManager.mode.isPrivate) {
            requireActivity().window.addFlags(FLAG_SECURE)
        } else {
            requireActivity().window.clearFlags(FLAG_SECURE)
        }
581
582

        consumeFrom(requireComponents.core.store) {
583
            updateTabCounter(it)
584
        }
585
586

        bundleArgs.getString(SESSION_TO_DELETE)?.also {
587
588
589
590
591
592
            if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
                removeAllTabsAndShowSnackbar(it)
            } else {
                removeTabAndShowSnackbar(it)
            }
        }
593

594
        updateTabCounter(requireComponents.core.store.state)
595

596
        if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) {
597
598
            navigateToSearch()
        }
599
600
601
602
603
    }

    private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
        val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList()
        val selectedIndex = sessionManager
604
605
            .selectedSession?.let { sessionManager.sessions.indexOf(it) }
            ?: SessionManager.NO_SELECTION
606
607
608
609
610
611

        val snapshot = tabs
            .map(sessionManager::createSessionSnapshot)
            .let { SessionManager.Snapshot(it, selectedIndex) }

        tabs.forEach {
ekager's avatar
ekager committed
612
            requireComponents.useCases.tabsUseCases.removeTab(it)
613
614
        }

615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
        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),
            {
                sessionManager.restore(snapshot)
            },
            operation = { },
            anchorView = snackbarAnchorView
        )
    }

    private fun removeTabAndShowSnackbar(sessionId: String) {
        sessionManager.findSessionById(sessionId)?.let { session ->
            val snapshot = sessionManager.createSessionSnapshot(session)
636
            val state = store.state.findTab(sessionId)?.engineState?.engineSessionState
637
638
639
            val isSelected =
                session.id == requireComponents.core.store.state.selectedTabId ?: false

ekager's avatar
ekager committed
640
            requireComponents.useCases.tabsUseCases.removeTab(sessionId)
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657

            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
                    )
658
                    findNavController().navigate(
659
                        HomeFragmentDirections.actionGlobalBrowser(null)
660
                    )
661
662
663
664
665
                },
                operation = { },
                anchorView = snackbarAnchorView
            )
        }
666
667
    }

668
    override fun onDestroyView() {
669
        super.onDestroyView()
670
        _sessionControlInteractor = null
671
        sessionControlView = null
672
        bundleArgs.clear()
673
        requireActivity().window.clearFlags(FLAG_SECURE)
674
675
    }

676
677
678
    override fun onStart() {
        super.onStart()
        subscribeToTabCollections()
679

680
681
682
        val context = requireContext()
        val components = context.components

683
684
685
686
        homeFragmentStore.dispatch(
            HomeFragmentAction.Change(
                collections = components.core.tabCollectionStorage.cachedTabCollections,
                mode = currentMode.getCurrentMode(),
687
                topSites = components.core.topSitesStorage.cachedTopSites,
ekager's avatar
ekager committed
688
689
690
691
692
693
694
                tip = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
                    FenixTipManager(
                        listOf(
                            MasterPasswordTipProvider(
                                requireContext(),
                                ::navToSavedLogins,
                                ::dismissTip
695
                            )
ekager's avatar
ekager committed
696
697
698
                        )
                    ).getTip()
                },
699
                showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
700
701
            )
        )
Colin Lee's avatar
Colin Lee committed
702

703
        requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
704
705
            // 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) {
706
707
                return@runIfReadyOrQueue
            }
708
709
710
711
712

            requireComponents.backgroundServices.accountManager.register(
                currentMode,
                owner = this@HomeFragment.viewLifecycleOwner
            )
713
714
715
716
            requireComponents.backgroundServices.accountManager.register(object : AccountObserver {
                override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
                    if (authType != AuthType.Existing) {
                        view?.let {
717
718
                            FenixSnackbar.make(
                                view = it,
719
                                duration = Snackbar.LENGTH_SHORT,
720
                                isDisplayedWithBrowserToolbar = false
721
                            )
722
723
724
725
                                .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
                                .setAnchorView(toolbarLayout)
                                .show()
                        }
726
727
                    }
                }
728
            }, owner = this@HomeFragment.viewLifecycleOwner)
729
        }
730

731
732
        if (browsingModeManager.mode.isPrivate &&
            context.settings().showPrivateModeCfr
733
        ) {
734
735
            recommendPrivateBrowsingShortcut()
        }
736
737
738

        // We only want this observer live just before we navigate away to the collection creation screen
        requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
739
740
741
742

        lifecycleScope.launch(IO) {
            requireComponents.reviewPromptController.promptReview(requireActivity())
        }
743
744
    }

ekager's avatar
ekager committed
745
746
747
748
    private fun navToSavedLogins() {
        findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
    }

749
    private fun dispatchModeChanges(mode: Mode) {
750
751
752
753
        homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))

        val localView = view
        if (localView != null) {
754
            adjustHomeFragmentView(mode, localView)
755
            updateSessionControlView(localView)
756
            showSessionControlView(localView)
757
        }
758
759
    }

760
761
762
763
764
765
766
    private fun showDeleteCollectionPrompt(
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) {
767
768
        val context = context ?: return
        AlertDialog.Builder(context).apply {
769
            setTitle(title)
770
771
            setMessage(message)
            setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
772
773
774
                if (wasSwiped) {
                    handleSwipedItemDeletionCancel()
                }
775
776
777
778
779
780
781
782
                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()
783
                }
784
785
786
            }
            create()
        }.show()
787
788
    }

789
790
    override fun onStop() {
        super.onStop()
791
792
793
        val homeViewModel: HomeScreenViewModel by activityViewModels {
            ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
        }
794
        homeViewModel.layoutManagerState =
795
            sessionControlView!!.view.layoutManager?.onSaveInstanceState()
796
797

        currentMode.unregisterTorListener()
798
799
    }

800
801
    override fun onResume() {
        super.onResume()
802
803
804
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
        }
805

806
807
808
        hideToolbar()
    }

809
810
    override fun onPause() {
        super.onPause()
811
812
813
814
815
816
817
818
819
820
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawable(
                ColorDrawable(
                    ContextCompat.getColor(
                        requireContext(),
                        R.color.foundation_private_theme
                    )
                )
            )
        }
821
822
    }

823
    private fun recommendPrivateBrowsingShortcut() {
824
825
        context?.let { context ->
            val layout = LayoutInflater.from(context)
826
                .inflate(R.layout.pbm_shortcut_popup, null)
827
            val privateBrowsingRecommend =
828
829
                PopupWindow(
                    layout,
830
831
832
833
                    min(
                        (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
                        (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
                    ),
834
835
836
837
838
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    true
                )
            layout.findViewById<Button>(R.id.cfr_pos_button).apply {
                setOnClickListener {
839
                    context.metrics.track(Event.PrivateBrowsingAddShortcutCFR)
840
                    PrivateShortcutCreateManager.createPrivateShortcut(context)
841
                    privateBrowsingRecommend.dismiss()
842
843
844
                }
            }
            layout.findViewById<Button>(R.id.cfr_neg_button).apply {
845
846
                setOnClickListener {
                    context.metrics.track(Event.PrivateBrowsingCancelCFR)
847
                    privateBrowsingRecommend.dismiss()
848
                }
849
850
851
852
            }
            // We want to show the popup only after privateBrowsingButton is available.
            // Otherwise, we will encounter an activity token error.
            privateBrowsingButton.post {
853
                context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
854
                privateBrowsingRecommend.showAsDropDown(
855
856
                    privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
                )
857
858
859
860
            }
        }
    }

861
    private fun hideOnboardingIfNeeded() {
862
863
864
865
        if (!onboarding.userHasBeenOnboarded()) {
            onboarding.finish()
            homeFragmentStore.dispatch(
                HomeFragmentAction.ModeChange(
866
                    mode = currentMode.getCurrentMode()
867
868
                )
            )
869
870
871
872
873
            adjustHomeFragmentView(
                currentMode.getCurrentMode(),
                view
            )
            showSessionControlView(view)
874
875
876
877
878
879
        }
    }

    private fun hideOnboardingAndOpenSearch() {
        hideOnboardingIfNeeded()
        navigateToSearch()
880
881
    }

882
    private fun navigateToSearch() {
883
        val directions = if (FeatureFlags.newSearchExperience) {
884
885
886
            HomeFragmentDirections.actionGlobalSearchDialog(
                sessionId = null
            )
887
888
889
890
891
        } else {
            HomeFragmentDirections.actionGlobalSearch(
                sessionId = null
            )
        }
892

893
        nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
894
895
    }

896
    @SuppressWarnings("ComplexMethod", "LongMethod")
897
898
899
900
901
902
903
904
905
906
907
    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()
908
                        )
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
                    }
                    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
931
932
933
934
935
936