HomeFragment.kt 42.9 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.fragment.app.viewModels
38
import androidx.lifecycle.Observer
39
import androidx.lifecycle.ViewModelProvider
40
import androidx.lifecycle.lifecycleScope
41
import androidx.navigation.fragment.findNavController
42
import androidx.navigation.fragment.navArgs
43
44
45
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
46
import com.google.android.material.snackbar.Snackbar
47
48
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
49
import kotlinx.android.synthetic.main.no_collections_message.view.*
50
51
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
52
import kotlinx.coroutines.ExperimentalCoroutinesApi
53
import kotlinx.coroutines.delay
54
import kotlinx.coroutines.launch
Tiger Oakes's avatar
Tiger Oakes committed
55
import kotlinx.coroutines.withContext
56
import mozilla.appservices.places.BookmarkRoot
57
import mozilla.components.browser.menu.view.MenuButton
58
59
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
60
import mozilla.components.browser.state.selector.findTab
61
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
62
63
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
64
import mozilla.components.browser.state.state.BrowserState
65
import mozilla.components.browser.state.store.BrowserStore
66
import mozilla.components.concept.sync.AccountObserver
67
import mozilla.components.concept.sync.AuthType
68
import mozilla.components.concept.sync.OAuthAccount
Tiger Oakes's avatar
Tiger Oakes committed
69
import mozilla.components.feature.tab.collections.TabCollection
70
71
import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
72
import mozilla.components.lib.state.ext.consumeFrom
73
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
74
import mozilla.components.support.ktx.android.content.res.resolveAttribute
75
import org.mozilla.fenix.BrowserDirection
76
import org.mozilla.fenix.BuildConfig
77
import org.mozilla.fenix.FeatureFlags
78
import org.mozilla.fenix.HomeActivity
Jeff Boek's avatar
Jeff Boek committed
79
import org.mozilla.fenix.R
80
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
81
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
82
import org.mozilla.fenix.cfr.SearchWidgetCFR
83
import org.mozilla.fenix.components.FenixSnackbar
84
import org.mozilla.fenix.components.PrivateShortcutCreateManager
85
import org.mozilla.fenix.components.StoreProvider
86
import org.mozilla.fenix.components.TabCollectionStorage
87
import org.mozilla.fenix.components.metrics.Event
88
import org.mozilla.fenix.components.tips.FenixTipManager
ekager's avatar
ekager committed
89
90
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
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
110
import org.mozilla.fenix.tor.TorEvents
import org.mozilla.fenix.tor.bootstrap.TorQuickStart
111
import org.mozilla.fenix.theme.ThemeManager
112
import org.mozilla.fenix.utils.FragmentPreDrawManager
113
import org.mozilla.fenix.utils.ToolbarPopupWindow
114
import org.mozilla.fenix.utils.allowUndo
115
import org.mozilla.fenix.whatsnew.WhatsNew
116
import java.lang.ref.WeakReference
117
import kotlin.math.min
118

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

269
270
271
        updateSessionControlView(view)

        activity.themeManager.applyStatusBarTheme(activity)
272
273
274

        adjustHomeFragmentView(currentMode.getCurrentMode(), view)

275
276
277
        return view
    }

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

282
283
284
285
286
287
288
289
290
    /**
     * 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)
    }

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

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

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

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

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

336
                view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
337
338
                    topMargin =
                        resources.getDimensionPixelSize(R.dimen.home_fragment_top_toolbar_header_margin)
339
340
341
342
                }
            }
            ToolbarPosition.BOTTOM -> {
            }
343
        }
Jeff Boek's avatar
Jeff Boek committed
344
    }
Marc Leclair's avatar
Marc Leclair committed
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
382
383
    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
            }
        }
    }

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

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

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

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

408
409
            val iconSize =
                context.resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
410

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

415
            withContext(Main) {
416
                search_engine_icon?.setImageDrawable(searchIcon)
417
            }
418
        }
419

420
        createHomeMenu(requireContext(), WeakReference(view.menuButton))
421
422
423
424
425
426
427
428
429
430
431
432
433
        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)
434
        view.tab_button.setOnLongClickListener {
435
            tabCounterMenu.menuController.show(anchor = it)
436
437
            true
        }
438

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

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

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

464
        view.tab_button.setOnClickListener {
465
            openTabTray()
466
467
        }

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

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

483
484
        privateBrowsingButton.isGone = view.context.settings().shouldDisableNormalMode

485
486
487
        // 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
488
489
            val willNavigateToSearch =
                !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
490
            if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
Tiger Oakes's avatar
Tiger Oakes committed
491
492
493
494
495
496
497
                SearchWidgetCFR(
                    context = view.context,
                    settings = view.context.settings(),
                    metrics = view.context.components.analytics.metrics
                ) {
                    view.toolbar_wrapper
                }.displayIfNecessary()
498
499
            }
        }
500

501
502
503
504
505
        if (browsingModeManager.mode.isPrivate) {
            requireActivity().window.addFlags(FLAG_SECURE)
        } else {
            requireActivity().window.clearFlags(FLAG_SECURE)
        }
506
507

        consumeFrom(requireComponents.core.store) {
508
            updateTabCounter(it)
509
        }
510
511

        bundleArgs.getString(SESSION_TO_DELETE)?.also {
512
513
514
515
516
517
            if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
                removeAllTabsAndShowSnackbar(it)
            } else {
                removeTabAndShowSnackbar(it)
            }
        }
518

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

521
        if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) {
522
523
            navigateToSearch()
        }
524
525
526
527
528
    }

    private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
        val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList()
        val selectedIndex = sessionManager
529
530
            .selectedSession?.let { sessionManager.sessions.indexOf(it) }
            ?: SessionManager.NO_SELECTION
531
532
533
534
535
536

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

        tabs.forEach {
ekager's avatar
ekager committed
537
            requireComponents.useCases.tabsUseCases.removeTab(it)
538
539
        }

540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
        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)
561
            val state = store.state.findTab(sessionId)?.engineState?.engineSessionState
562
563
564
            val isSelected =
                session.id == requireComponents.core.store.state.selectedTabId ?: false

ekager's avatar
ekager committed
565
            requireComponents.useCases.tabsUseCases.removeTab(sessionId)
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582

            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
                    )
583
                    findNavController().navigate(
584
                        HomeFragmentDirections.actionGlobalBrowser(null)
585
                    )
586
587
588
589
590
                },
                operation = { },
                anchorView = snackbarAnchorView
            )
        }
591
592
    }

593
    override fun onDestroyView() {
594
        super.onDestroyView()
595
        _sessionControlInteractor = null
596
        sessionControlView = null
597
        bundleArgs.clear()
598
        requireActivity().window.clearFlags(FLAG_SECURE)
599
600
    }

601
602
603
    override fun onStart() {
        super.onStart()
        subscribeToTabCollections()
604

605
606
607
        val context = requireContext()
        val components = context.components

608
609
610
611
        homeFragmentStore.dispatch(
            HomeFragmentAction.Change(
                collections = components.core.tabCollectionStorage.cachedTabCollections,
                mode = currentMode.getCurrentMode(),
612
                topSites = components.core.topSitesStorage.cachedTopSites,
ekager's avatar
ekager committed
613
614
615
616
617
618
619
                tip = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
                    FenixTipManager(
                        listOf(
                            MasterPasswordTipProvider(
                                requireContext(),
                                ::navToSavedLogins,
                                ::dismissTip
620
                            )
ekager's avatar
ekager committed
621
622
623
                        )
                    ).getTip()
                },
624
                showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
625
626
            )
        )
Colin Lee's avatar
Colin Lee committed
627

628
        requireComponents.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
629
630
            // 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) {
631
632
                return@runIfReadyOrQueue
            }
633
634
635
636
637

            requireComponents.backgroundServices.accountManager.register(
                currentMode,
                owner = this@HomeFragment.viewLifecycleOwner
            )
638
639
640
641
            requireComponents.backgroundServices.accountManager.register(object : AccountObserver {
                override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
                    if (authType != AuthType.Existing) {
                        view?.let {
642
643
                            FenixSnackbar.make(
                                view = it,
644
                                duration = Snackbar.LENGTH_SHORT,
645
                                isDisplayedWithBrowserToolbar = false
646
                            )
647
648
649
650
                                .setText(it.context.getString(R.string.onboarding_firefox_account_sync_is_on))
                                .setAnchorView(toolbarLayout)
                                .show()
                        }
651
652
                    }
                }
653
            }, owner = this@HomeFragment.viewLifecycleOwner)
654
        }
655

656
657
        if (browsingModeManager.mode.isPrivate &&
            context.settings().showPrivateModeCfr
658
        ) {
659
660
            recommendPrivateBrowsingShortcut()
        }
661
662
663

        // We only want this observer live just before we navigate away to the collection creation screen
        requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
664
665
666
667

        lifecycleScope.launch(IO) {
            requireComponents.reviewPromptController.promptReview(requireActivity())
        }
668
669
    }

ekager's avatar
ekager committed
670
671
672
673
    private fun navToSavedLogins() {
        findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
    }

674
    private fun dispatchModeChanges(mode: Mode) {
675
676
677
678
679
680
        homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))

        val localView = view
        adjustHomeFragmentView(mode, view)
        if (localView != null) {
            updateSessionControlView(localView)
681
        }
682
683
    }

684
685
686
687
688
689
690
    private fun showDeleteCollectionPrompt(
        tabCollection: TabCollection,
        title: String?,
        message: String,
        wasSwiped: Boolean,
        handleSwipedItemDeletionCancel: () -> Unit
    ) {
691
692
        val context = context ?: return
        AlertDialog.Builder(context).apply {
693
            setTitle(title)
694
695
            setMessage(message)
            setNegativeButton(R.string.tab_collection_dialog_negative) { dialog: DialogInterface, _ ->
696
697
698
                if (wasSwiped) {
                    handleSwipedItemDeletionCancel()
                }
699
700
701
702
703
704
705
706
                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()
707
                }
708
709
710
            }
            create()
        }.show()
711
712
    }

713
714
    override fun onStop() {
        super.onStop()
715
716
717
        val homeViewModel: HomeScreenViewModel by activityViewModels {
            ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
        }
718
        homeViewModel.layoutManagerState =
719
            sessionControlView!!.view.layoutManager?.onSaveInstanceState()
720
721

        currentMode.unregisterTorListener()
722
723
    }

724
725
    override fun onResume() {
        super.onResume()
726
727
728
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawableResource(R.drawable.private_home_background_gradient)
        }
729

730
731
732
        hideToolbar()
    }

733
734
    override fun onPause() {
        super.onPause()
735
736
737
738
739
740
741
742
743
744
        if (browsingModeManager.mode == BrowsingMode.Private) {
            activity?.window?.setBackgroundDrawable(
                ColorDrawable(
                    ContextCompat.getColor(
                        requireContext(),
                        R.color.foundation_private_theme
                    )
                )
            )
        }
745
746
    }

747
    private fun recommendPrivateBrowsingShortcut() {
748
749
        context?.let { context ->
            val layout = LayoutInflater.from(context)
750
                .inflate(R.layout.pbm_shortcut_popup, null)
751
            val privateBrowsingRecommend =
752
753
                PopupWindow(
                    layout,
754
755
756
757
                    min(
                        (resources.displayMetrics.widthPixels / CFR_WIDTH_DIVIDER).toInt(),
                        (resources.displayMetrics.heightPixels / CFR_WIDTH_DIVIDER).toInt()
                    ),
758
759
760
761
762
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    true
                )
            layout.findViewById<Button>(R.id.cfr_pos_button).apply {
                setOnClickListener {
763
                    context.metrics.track(Event.PrivateBrowsingAddShortcutCFR)
764
                    PrivateShortcutCreateManager.createPrivateShortcut(context)
765
                    privateBrowsingRecommend.dismiss()
766
767
768
                }
            }
            layout.findViewById<Button>(R.id.cfr_neg_button).apply {
769
770
                setOnClickListener {
                    context.metrics.track(Event.PrivateBrowsingCancelCFR)
771
                    privateBrowsingRecommend.dismiss()
772
                }
773
774
775
776
            }
            // We want to show the popup only after privateBrowsingButton is available.
            // Otherwise, we will encounter an activity token error.
            privateBrowsingButton.post {
777
                context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
778
                privateBrowsingRecommend.showAsDropDown(
779
780
                    privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
                )
781
782
783
784
            }
        }
    }

785
    private fun hideOnboardingIfNeeded() {
786
787
788
789
        if (!onboarding.userHasBeenOnboarded()) {
            onboarding.finish()
            homeFragmentStore.dispatch(
                HomeFragmentAction.ModeChange(
790
                    mode = currentMode.getCurrentMode()
791
792
                )
            )
793
794
795
796
797
798
        }
    }

    private fun hideOnboardingAndOpenSearch() {
        hideOnboardingIfNeeded()
        navigateToSearch()
799
800
    }

801
    private fun navigateToSearch() {
802
        val directions = if (FeatureFlags.newSearchExperience) {
803
804
805
            HomeFragmentDirections.actionGlobalSearchDialog(
                sessionId = null
            )
806
807
808
809
810
        } else {
            HomeFragmentDirections.actionGlobalSearch(
                sessionId = null
            )
        }
811

812
        nav(R.id.homeFragment, directions, getToolbarNavOptions(requireContext()))
813
814
    }

815
    @SuppressWarnings("ComplexMethod", "LongMethod")
816
817
818
819
820
821
822
823
824
825
826
    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()
827
                        )
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
                    }
                    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
850
851
852
853
854
855
856
857
858

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

859
860
861
                    HomeMenu.Item.Help -> {
                        hideOnboardingIfNeeded()
                        (activity as HomeActivity).openToBrowserAndLoad(
862
                            searchTermOrURL = SupportUtils.getTorHelpPageUrl(),
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
                            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()
                        )
                    }
905
                }
906
907
908
909
            },
            onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },
            onMenuBuilderChanged = { menuButtonView.get()?.menuBuilder = it }
        )
910

911
    private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
Tiger Oakes's avatar
Tiger Oakes committed
912
        return Observer<List<TabCollection>> {
913
            requireComponents.core.tabCollectionStorage.cachedTabCollections = it
914
            homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(it))
Tiger Oakes's avatar
Tiger Oakes committed
915
916
        }.also { observer ->
            requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
917
        }
918
919
    }

920
921
    private fun registerCollectionStorageObserver() {
        requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
922
923
    }

924
925
926
    private fun scrollAndAnimateCollection(
        changedCollection: TabCollection? = null
    ) {
927
928
        if (view != null) {
            viewLifecycleOwner.lifecycleScope.launch {
929
                val recyclerView = sessionControlView!!.view
930
                delay(ANIM_SCROLL_DELAY)
931
932
933
                val tabsSize = store.state
                    .getNormalOrPrivateTabs(browsingModeManager.mode.isPrivate)
                    .size
934
935
936
937
938
939
940
941
942
943
944

                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
                        }
945
                }