HomeFragment.kt 45.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
ekager's avatar
ekager committed
90
import org.mozilla.fenix.components.tips.Tip
91
import org.mozilla.fenix.components.toolbar.TabCounterMenu
92
import org.mozilla.fenix.components.toolbar.ToolbarPosition
93
import org.mozilla.fenix.ext.components
Tiger Oakes's avatar
Tiger Oakes committed
94
import org.mozilla.fenix.ext.hideToolbar
95
import org.mozilla.fenix.ext.metrics
96
import org.mozilla.fenix.ext.nav
97
import org.mozilla.fenix.ext.requireComponents
98
import org.mozilla.fenix.ext.resetPoliciesAfter
Sawyer Blatz's avatar
Sawyer Blatz committed
99
import org.mozilla.fenix.ext.sessionsOfType
100
import org.mozilla.fenix.ext.settings
101
102
103
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
104
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
105
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView
106
import org.mozilla.fenix.onboarding.FenixOnboarding
107
import org.mozilla.fenix.settings.SupportUtils
108
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
109
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
110
import org.mozilla.fenix.theme.ThemeManager
111
import org.mozilla.fenix.utils.FragmentPreDrawManager
112
import org.mozilla.fenix.utils.ToolbarPopupWindow
113
import org.mozilla.fenix.utils.allowUndo
114
import org.mozilla.fenix.whatsnew.WhatsNew
115
import java.lang.ref.WeakReference
116
import kotlin.math.min
117

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

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

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

134
    private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
135

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

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

Tiger Oakes's avatar
Tiger Oakes committed
145
        override fun onCollectionRenamed(tabCollection: TabCollection, title: String) {
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
156
157
158
159
    private val onboarding by lazy {
        StrictMode.allowThreadDiskReads().resetPoliciesAfter {
            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
196
197
198
199
200
201
        // 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()

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

212
213
214
        homeFragmentStore = StoreProvider.get(this) {
            HomeFragmentStore(
                HomeFragmentState(
215
                    collections = components.core.tabCollectionStorage.cachedTabCollections,
216
217
                    expandedCollections = emptySet(),
                    mode = currentMode.getCurrentMode(),
218
                    topSites = components.core.topSitesStorage.cachedTopSites,
219
                    showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
220
                )
221
222
223
            )
        }

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

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

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

266
267
268
        updateSessionControlView(view)

        activity.themeManager.applyStatusBarTheme(activity)
269
270

        adjustHomeFragmentView(currentMode.getCurrentMode(), view)
271
        showSessionControlView(view)
272

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
    // This function should be paired with showSessionControlView()
    @SuppressWarnings("ComplexMethod", "NestedBlockDepth", "LongMethod")
346
    private fun adjustHomeFragmentView(mode: Mode, view: View?) {
347
348
349
350
        view?.sessionControlRecyclerView?.apply {
                visibility = View.INVISIBLE
        }

351
352
353
        if (mode == Mode.Bootstrap) {
            view?.sessionControlRecyclerView?.apply {
                setPadding(0, 0, 0, 0)
354
                (layoutParams as ViewGroup.MarginLayoutParams).setMargins(0, 0, 0, 0)
355
356
357
            }
            view?.homeAppBar?.apply {
                visibility = View.GONE
358
359
360
361
362
363
364
365
366

                // 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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
            }
            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)
381
                (layoutParams as ViewGroup.MarginLayoutParams).setMargins(
382
383
384
385
386
387
                    0,
                    0,
                    0,
                    DEFAULT_ONBOARDING_FINISH_MARGIN
                )
            }
388
            view?.toolbarLayout?.apply {
389
                visibility = View.VISIBLE
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
                // 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
                        )
                    }
                }
415
            }
416
417
418
419
420
421
422
423
424
            // Hide the onion pattern during Onboarding, too.
            view?.onion_pattern_image?.apply {
                visibility = if (onboarding.userHasBeenOnboarded()) {
                    View.VISIBLE
                } else {
                    View.GONE
                }
            }
            view?.homeAppBar?.apply {
425
                visibility = View.VISIBLE
426
427
428
429
430
431
432
433
434

                children.forEach {
                    (it.layoutParams as AppBarLayout.LayoutParams).scrollFlags =
                        if (onboarding.userHasBeenOnboarded()) {
                            AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
                        } else {
                            AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
                        }
                }
435
436
437
438
            }
        }
    }

439
440
441
442
443
444
445
    // This function should be paired with adjustHomeFragmentView()
    private fun showSessionControlView(view: View?) {
        view?.sessionControlRecyclerView?.apply {
                visibility = View.VISIBLE
        }
    }

446
    @Suppress("LongMethod", "ComplexMethod")
Jeff Boek's avatar
Jeff Boek committed
447
448
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
449

450
        FragmentPreDrawManager(this).execute {
451
452
453
            val homeViewModel: HomeScreenViewModel by activityViewModels {
                ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
            }
454
            homeViewModel.layoutManagerState?.also { parcelable ->
455
                sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
456
457
            }
            homeViewModel.layoutManagerState = null
458
459
460
461

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

464
        viewLifecycleOwner.lifecycleScope.launch(IO) {
465
466
467
            // 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
468
469
            val context = context ?: return@launch

470
471
            val iconSize =
                context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
472

473
474
            val searchEngine = context.components.search.provider.getDefaultEngine(context)
            val searchIcon = BitmapDrawable(context.resources, searchEngine.icon)
475
476
            searchIcon.setBounds(0, 0, iconSize, iconSize)

477
            withContext(Main) {
478
                search_engine_icon?.setImageDrawable(searchIcon)
479
            }
480
        }
481

482
        createHomeMenu(requireContext(), WeakReference(view.menuButton))
483
484
485
486
487
488
489
490
491
492
493
494
495
        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)
496
        view.tab_button.setOnLongClickListener {
497
            tabCounterMenu.menuController.show(anchor = it)
498
499
            true
        }
500

501
502
503
504
505
506
        view.menuButton.setColorFilter(
            ContextCompat.getColor(
                requireContext(),
                ThemeManager.resolveAttribute(R.attr.primaryText, requireContext())
            )
        )
507

508
        view.toolbar.compoundDrawablePadding =
509
            view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
510
        view.toolbar_wrapper.setOnClickListener {
511
            hideOnboardingIfNeeded()
512
            navigateToSearch()
513
514
            requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
        }
515

516
517
        view.toolbar_wrapper.setOnLongClickListener {
            ToolbarPopupWindow.show(
518
                WeakReference(it),
519
520
521
522
523
524
525
                handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
                handlePaste = sessionControlInteractor::onPaste,
                copyVisible = false
            )
            true
        }

526
        view.tab_button.setOnClickListener {
527
            openTabTray()
528
529
        }

530
531
532
533
        PrivateBrowsingButtonView(
            privateBrowsingButton,
            browsingModeManager
        ) { newMode ->
534
            if (newMode == BrowsingMode.Private) {
535
                requireContext().settings().incrementNumTimesPrivateModeOpened()
536
537
            }

538
            if (onboarding.userHasBeenOnboarded()) {
539
                homeFragmentStore.dispatch(
540
541
                    HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode))
                )
542
            }
543
        }
544

545
546
        privateBrowsingButton.isGone = view.context.settings().shouldDisableNormalMode

547
548
549
        // 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
550
551
            val willNavigateToSearch =
                !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
552
            if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
Tiger Oakes's avatar
Tiger Oakes committed
553
554
555
556
557
558
559
                SearchWidgetCFR(
                    context = view.context,
                    settings = view.context.settings(),
                    metrics = view.context.components.analytics.metrics
                ) {
                    view.toolbar_wrapper
                }.displayIfNecessary()
560
561
            }
        }
562

563
564
565
566
567
        if (browsingModeManager.mode.isPrivate) {
            requireActivity().window.addFlags(FLAG_SECURE)
        } else {
            requireActivity().window.clearFlags(FLAG_SECURE)
        }
568
569

        consumeFrom(requireComponents.core.store) {
570
            updateTabCounter(it)
571
        }
572
573

        bundleArgs.getString(SESSION_TO_DELETE)?.also {
574
575
576
577
578
579
            if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
                removeAllTabsAndShowSnackbar(it)
            } else {
                removeTabAndShowSnackbar(it)
            }
        }
580

581
        updateTabCounter(requireComponents.core.store.state)
582

583
        if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) {
584
585
            navigateToSearch()
        }
586
587
588
589
590
    }

    private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
        val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList()
        val selectedIndex = sessionManager
591
592
            .selectedSession?.let { sessionManager.sessions.indexOf(it) }
            ?: SessionManager.NO_SELECTION
593
594
595
596
597
598

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

        tabs.forEach {
ekager's avatar
ekager committed
599
            requireComponents.useCases.tabsUseCases.removeTab(it)
600
601
        }

602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
        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)
623
            val state = store.state.findTab(sessionId)?.engineState?.engineSessionState
624
625
626
            val isSelected =
                session.id == requireComponents.core.store.state.selectedTabId ?: false

ekager's avatar
ekager committed
627
            requireComponents.useCases.tabsUseCases.removeTab(sessionId)
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644

            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
                    )
645
                    findNavController().navigate(
646
                        HomeFragmentDirections.actionGlobalBrowser(null)
647
                    )
648
649
650
651
652
                },
                operation = { },
                anchorView = snackbarAnchorView
            )
        }
653
654
    }

655
    override fun onDestroyView() {
656
        super.onDestroyView()
657
        _sessionControlInteractor = null
658
        sessionControlView = null
659
        bundleArgs.clear()
660
        requireActivity().window.clearFlags(FLAG_SECURE)
661
662
    }

663
664
665
    override fun onStart() {
        super.onStart()
        subscribeToTabCollections()
666

667
668
669
        val context = requireContext()
        val components = context.components

670
671
672
673
        homeFragmentStore.dispatch(
            HomeFragmentAction.Change(
                collections = components.core.tabCollectionStorage.cachedTabCollections,
                mode = currentMode.getCurrentMode(),
674
                topSites = components.core.topSitesStorage.cachedTopSites,
675
                showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
676
677
            )
        )
Colin Lee's avatar
Colin Lee committed
678

679
        requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
680
681
            // 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) {
682
683
                return@runIfReadyOrQueue
            }
684
685
686
687
688

            requireComponents.backgroundServices.accountManager.register(
                currentMode,
                owner = this@HomeFragment.viewLifecycleOwner
            )
689
690
691
692
            requireComponents.backgroundServices.accountManager.register(object : AccountObserver {
                override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
                    if (authType != AuthType.Existing) {
                        view?.let {
693
694
                            FenixSnackbar.make(
                                view = it,
695
                                duration = Snackbar.LENGTH_SHORT,
696
                                isDisplayedWithBrowserToolbar = false
697
                            )
698
699
700
701
                                .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
                                .setAnchorView(toolbarLayout)
                                .show()
                        }
702
703
                    }
                }
704
            }, owner = this@HomeFragment.viewLifecycleOwner)
705
        }
706

707
708
        if (browsingModeManager.mode.isPrivate &&
            context.settings().showPrivateModeCfr
709
        ) {
710
711
            recommendPrivateBrowsingShortcut()
        }
712
713
714

        // We only want this observer live just before we navigate away to the collection creation screen
        requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
715
716
717
718

        lifecycleScope.launch(IO) {
            requireComponents.reviewPromptController.promptReview(requireActivity())
        }
719
720
    }

ekager's avatar
ekager committed
721
722
723
724
    private fun navToSavedLogins() {
        findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
    }

725
    private fun dispatchModeChanges(mode: Mode) {
726
727
728
729
        homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))

        val localView = view
        if (localView != null) {
730
            adjustHomeFragmentView(mode, localView)
731
            updateSessionControlView(localView)
732
            showSessionControlView(localView)
733
        }
734
735
    }

736
737
738
739
740
741
742
    private fun showDeleteCollectionPrompt(
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) {
743
744
        val context = context ?: return
        AlertDialog.Builder(context).apply {
745
            setTitle(title)
746
747
            setMessage(message)
            setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
748
749
750
                if (wasSwiped) {
                    handleSwipedItemDeletionCancel()
                }
751
752
753
754
755
756
757
758
                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()
759
                }
760
761
762
            }
            create()
        }.show()
763
764
    }

765
766
    override fun onStop() {
        super.onStop()
767
768
769
        val homeViewModel: HomeScreenViewModel by activityViewModels {
            ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
        }
770
        homeViewModel.layoutManagerState =
771
            sessionControlView!!.view.layoutManager?.onSaveInstanceState()
772
773

        currentMode.unregisterTorListener()
774
775
    }

776
777
    override fun onResume() {
        super.onResume()
778
779
780
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
        }
781

782
783
784
        hideToolbar()
    }

785
786
    override fun onPause() {
        super.onPause()
787
788
789
790
791
792
793
794
795
796
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawable(
                ColorDrawable(
                    ContextCompat.getColor(
                        requireContext(),
                        R.color.foundation_private_theme
                    )
                )
            )
        }
797
798
    }

799
    private fun recommendPrivateBrowsingShortcut() {
800
801
        context?.let { context ->
            val layout = LayoutInflater.from(context)
802
                .inflate(R.layout.pbm_shortcut_popup, null)
803
            val privateBrowsingRecommend =
804
805
                PopupWindow(
                    layout,
806
807
808
809
                    min(
                        (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
                        (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
                    ),
810
811
812
813
814
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    true
                )
            layout.findViewById<Button>(R.id.cfr_pos_button).apply {
                setOnClickListener {
815
                    context.metrics.track(Event.PrivateBrowsingAddShortcutCFR)
816
                    PrivateShortcutCreateManager.createPrivateShortcut(context)
817
                    privateBrowsingRecommend.dismiss()
818
819
820
                }
            }
            layout.findViewById<Button>(R.id.cfr_neg_button).apply {
821
822
                setOnClickListener {
                    context.metrics.track(Event.PrivateBrowsingCancelCFR)
823
                    privateBrowsingRecommend.dismiss()
824
                }
825
826
827
828
            }
            // We want to show the popup only after privateBrowsingButton is available.
            // Otherwise, we will encounter an activity token error.
            privateBrowsingButton.post {
829
                context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
830
                privateBrowsingRecommend.showAsDropDown(
831
832
                    privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
                )
833
834
835
836
            }
        }
    }

837
    private fun hideOnboardingIfNeeded() {
838
839
840
841
        if (!onboarding.userHasBeenOnboarded()) {
            onboarding.finish()
            homeFragmentStore.dispatch(
                HomeFragmentAction.ModeChange(
842
                    mode = currentMode.getCurrentMode()
843
844
                )
            )
845
846
847
848
849
            adjustHomeFragmentView(
                currentMode.getCurrentMode(),
                view
            )
            showSessionControlView(view)
850
851
852
853
854
855
        }
    }

    private fun hideOnboardingAndOpenSearch() {
        hideOnboardingIfNeeded()
        navigateToSearch()
856
857
    }

858
    private fun navigateToSearch() {
859
        val directions = if (FeatureFlags.newSearchExperience) {
860
861
862
            HomeFragmentDirections.actionGlobalSearchDialog(
                sessionId = null
            )
863
864
865
866
867
        } else {
            HomeFragmentDirections.actionGlobalSearch(
                sessionId = null
            )
        }
868

869
        nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
870
871
    }

872
    @SuppressWarnings("ComplexMethod", "LongMethod")
873
874
875
876
877
878
879
880
881
882
883
    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()
884
                        )
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
                    }
                    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
907
908
909
910
911
912
913
914
915

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

916
917
918
                    HomeMenu.Item.Help -> {
                        hideOnboardingIfNeeded()
                        (activity as HomeActivity).openToBrowserAndLoad(
919
                            searchTermOrURL = SupportUtils.getTorHelpPageUrl(),
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
                            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