TabTrayView.kt 28.1 KB
Newer Older
1
2
3
4
5
6
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.tabtray

7
import android.content.Context
8
9
import android.view.LayoutInflater
import android.view.View
10
11
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
12
import android.view.ViewGroup
13
import android.view.accessibility.AccessibilityEvent
14
import androidx.annotation.IdRes
15
import androidx.cardview.widget.CardView
16
17
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
18
import androidx.core.content.ContextCompat
19
import androidx.core.view.isVisible
20
import androidx.core.view.updateLayoutParams
21
import androidx.core.view.updatePadding
22
23
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
24
import androidx.recyclerview.widget.ConcatAdapter
25
import androidx.recyclerview.widget.GridLayoutManager
26
import androidx.recyclerview.widget.LinearLayoutManager
27
28
29
30
31
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
32
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
33
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
34
35
36
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
37
import mozilla.components.browser.menu.BrowserMenu
38
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
39
40
41
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
42
import mozilla.components.browser.state.state.TabSessionState
43
import mozilla.components.browser.tabstray.TabViewHolder
44
import mozilla.components.support.ktx.android.util.dpToPx
45
import mozilla.components.ui.tabcounter.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM
46
import org.mozilla.fenix.R
47
import org.mozilla.fenix.components.metrics.Event
48
49
import mozilla.components.ui.tabcounter.TabCounter.Companion.MAX_VISIBLE_TABS
import mozilla.components.ui.tabcounter.TabCounter.Companion.SO_MANY_TABS_OPEN
50
import org.mozilla.fenix.browser.infobanner.InfoBanner
51
import org.mozilla.fenix.ext.components
52
import org.mozilla.fenix.ext.settings
53
import org.mozilla.fenix.ext.updateAccessibilityCollectionInfo
54
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
55
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
56
import org.mozilla.fenix.utils.Settings
57
import java.text.NumberFormat
58
import kotlin.math.max
59
import kotlin.math.roundToInt
60
61
62
63

/**
 * View that contains and configures the BrowserAwesomeBar
 */
64
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "ForbiddenComment")
65
66
class TabTrayView(
    private val container: ViewGroup,
67
    private val tabsAdapter: FenixTabsAdapter,
68
    private val interactor: TabTrayInteractor,
69
    isPrivate: Boolean,
70
    private val isInLandscape: () -> Boolean,
71
    lifecycleOwner: LifecycleOwner,
72
    private val filterTabs: (Boolean) -> Unit
73
) : LayoutContainer, TabLayout.OnTabSelectedListener {
74
    val lifecycleScope = lifecycleOwner.lifecycleScope
75
76
77
    val fabView = LayoutInflater.from(container.context)
        .inflate(R.layout.component_tabstray_fab, container, true)

78
79
    private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled

80
81
82
    val view = LayoutInflater.from(container.context)
        .inflate(R.layout.component_tabstray, container, true)

83
    private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
84

85
    private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
86

87
    private val concatAdapter = ConcatAdapter(tabsAdapter)
88
89
    private val tabTrayItemMenu: TabTrayItemMenu
    private var menu: BrowserMenu? = null
90

91
92
93
    private val multiselectSelectionMenu: MultiselectSelectionMenu
    private var multiselectMenu: BrowserMenu? = null

94
    private var tabsTouchHelper: TabsTouchHelper
95
    private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
96

97
98
    private var hasLoaded = false

99
100
101
    override val containerView: View?
        get() = container

102
103
    private val components = container.context.components

104
105
106
107
108
109
110
111
    private val checkOpenTabs = {
        if (isPrivateModeSelected) {
            view.context.components.core.store.state.privateTabs.isNotEmpty()
        } else {
            view.context.components.core.store.state.normalTabs.isNotEmpty()
        }
    }

112
    init {
113
        components.analytics.metrics.track(Event.TabsTrayOpened)
114

115
        toggleFabText(isPrivate)
116

117
118
119
        view.topBar.setOnClickListener {
            // no-op, consume the touch event to prevent it advancing the tray to the next state.
        }
120
        behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
121
            override fun onSlide(bottomSheet: View, slideOffset: Float) {
122
123
124
125
126
127
                if (
                    interactor.onModeRequested() is Mode.Normal &&
                    !hasAccessibilityEnabled &&
                    slideOffset >= SLIDE_OFFSET
                ) {
                    fabView.new_tab_button.show()
128
129
130
131
132
                }
            }

            override fun onStateChanged(bottomSheet: View, newState: Int) {
                if (newState == BottomSheetBehavior.STATE_HIDDEN) {
133
                    components.analytics.metrics.track(Event.TabsTrayClosed)
134
135
                    interactor.onTabTrayDismissed()
                }
136
137
138
139
                // We only support expanded and collapsed states. Don't allow STATE_HALF_EXPANDED.
                else if (newState == BottomSheetBehavior.STATE_HALF_EXPANDED) {
                    behavior.state = BottomSheetBehavior.STATE_HIDDEN
                }
140
            }
141
        })
142

143
        val selectedTabIndex = if (!isPrivate) {
144
            DEFAULT_TAB_ID
145
        } else {
146
            PRIVATE_TAB_ID
147
148
149
150
151
152
        }

        view.tab_layout.getTabAt(selectedTabIndex)?.also {
            view.tab_layout.selectTab(it, true)
        }

153
154
        view.tab_layout.addOnTabSelectedListener(this)

155
        val tabs = getTabs(isPrivate)
156

157
        updateBottomSheetBehavior()
158

159
        setTopOffset(isInLandscape())
160

161
162
        updateTabsTrayLayout()

163
        view.tabsTray.apply {
164
            adapter = concatAdapter
165
166
167
168
169

            tabsTouchHelper = TabsTouchHelper(
                observable = tabsAdapter,
                onViewHolderTouched = { it is TabViewHolder }
            )
170

171
            tabsTouchHelper.attachToRecyclerView(this)
172

173
            tabsAdapter.tabTrayInteractor = interactor
174
            tabsAdapter.onTabsUpdated = {
175
                concatAdapter.addAdapter(collectionsButtonAdapter)
176

177
                if (hasAccessibilityEnabled) {
178
                    tabsAdapter.notifyItemRangeChanged(0, tabs.size)
179
180
181
                }
                if (!hasLoaded) {
                    hasLoaded = true
182
                    scrollToSelectedBrowserTab()
183
184
185
186
                    if (view.context.settings().accessibilityServicesEnabled) {
                        lifecycleScope.launch {
                            delay(SELECTION_DELAY.toLong())
                            lifecycleScope.launch(Main) {
187
                                layoutManager?.findViewByPosition(getSelectedBrowserTabViewIndex())
188
                                    ?.requestFocus()
189
                                layoutManager?.findViewByPosition(getSelectedBrowserTabViewIndex())
190
191
192
                                    ?.sendAccessibilityEvent(
                                        AccessibilityEvent.TYPE_VIEW_FOCUSED
                                    )
193
194
                            }
                        }
195
196
197
                    }
                }
            }
198
199
        }

200
        tabTrayItemMenu =
201
            TabTrayItemMenu(
202
                context = view.context,
203
                shouldShowShareAllTabs = { checkOpenTabs.invoke() && view.tab_layout.selectedTabPosition == 0 },
204
                shouldShowSelectTabs = { checkOpenTabs.invoke() && view.tab_layout.selectedTabPosition == 0 },
205
206
                hasOpenTabs = checkOpenTabs
            ) {
207
                when (it) {
208
                    is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsOfTypeClicked(
209
210
                        isPrivateModeSelected
                    )
211
                    is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked()
212
                    is TabTrayItemMenu.Item.SelectTabs -> interactor.onEnterMultiselect()
213
214
215
                    is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
                        isPrivateModeSelected
                    )
ekager's avatar
ekager committed
216
                    is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked()
217
                }
218
219
            }

220
221
222
223
224
225
226
227
228
229
230
231
232
        multiselectSelectionMenu = MultiselectSelectionMenu(
            context = view.context
        ) {
            when (it) {
                is MultiselectSelectionMenu.Item.BookmarkTabs -> interactor.onBookmarkSelectedTabs(
                    mode.selectedItems
                )
                is MultiselectSelectionMenu.Item.DeleteTabs -> interactor.onDeleteSelectedTabs(
                    mode.selectedItems
                )
            }
        }

233
        view.tab_tray_overflow.setOnClickListener {
234
            components.analytics.metrics.track(Event.TabsTrayMenuOpened)
235
            menu = tabTrayItemMenu.menuBuilder.build(container.context)
236
237
238
239
240
            menu?.show(it)?.also { popupMenu ->
                (popupMenu.contentView as? CardView)?.setCardBackgroundColor(
                    ContextCompat.getColor(
                        view.context,
                        R.color.foundation_normal_theme
241
                    )
242
243
                )
            }
244
245
        }

246
        adjustNewTabButtonsForNormalMode()
247

248
249
250
251
        displayInfoBannerIfNeccessary(tabs, view.context.settings())
    }

    private fun displayInfoBannerIfNeccessary(tabs: List<TabSessionState>, settings: Settings) {
252
        @Suppress("ComplexCondition")
253
254
255
256
        val infoBanner = if (
            settings.shouldShowGridViewBanner &&
            settings.canShowCfr &&
            settings.listTabView &&
257
258
259
260
261
262
263
264
265
            tabs.size >= TAB_COUNT_SHOW_CFR
        ) {
            InfoBanner(
                context = view.context,
                message = view.context.getString(R.string.tab_tray_grid_view_banner_message),
                dismissText = view.context.getString(R.string.tab_tray_grid_view_banner_negative_button_text),
                actionText = view.context.getString(R.string.tab_tray_grid_view_banner_positive_button_text),
                container = view.infoBanner,
                dismissByHiding = true,
266
267
268
269
                dismissAction = {
                    components.analytics.metrics.track(Event.TabsTrayCfrDismissed)
                    settings.shouldShowGridViewBanner = false
                }
270
271
            ) {
                interactor.onGoToTabsSettings()
272
                settings.shouldShowGridViewBanner = false
273
274
            }
        } else if (
275
276
            settings.shouldShowAutoCloseTabsBanner &&
            settings.canShowCfr &&
277
278
279
280
281
282
283
284
285
            tabs.size >= TAB_COUNT_SHOW_CFR
        ) {
            InfoBanner(
                context = view.context,
                message = view.context.getString(R.string.tab_tray_close_tabs_banner_message),
                dismissText = view.context.getString(R.string.tab_tray_close_tabs_banner_negative_button_text),
                actionText = view.context.getString(R.string.tab_tray_close_tabs_banner_positive_button_text),
                container = view.infoBanner,
                dismissByHiding = true,
286
                dismissAction = { settings.shouldShowAutoCloseTabsBanner = false }
287
            ) {
288
                interactor.onGoToTabsSettings()
289
                settings.shouldShowAutoCloseTabsBanner = false
290
            }
291
292
293
294
295
        } else {
            null
        }

        infoBanner?.apply {
296
            view.infoBanner.visibility = VISIBLE
297
            showBanner()
298
        }
299
300
    }

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
    private fun getTabs(isPrivate: Boolean): List<TabSessionState> = if (isPrivate) {
        view.context.components.core.store.state.privateTabs
    } else {
        view.context.components.core.store.state.normalTabs
    }

    private fun getTabsNumberInAnyMode(): Int {
        return max(
            view.context.components.core.store.state.normalTabs.size,
            view.context.components.core.store.state.privateTabs.size
        )
    }

    private fun getTabsNumberForExpandingTray(): Int {
        return if (container.context.settings().gridTabView) {
            EXPAND_AT_GRID_SIZE
        } else {
            EXPAND_AT_LIST_SIZE
        }
    }

322
    private fun adjustNewTabButtonsForNormalMode() {
323
324
325
        view.tab_tray_new_tab.apply {
            isVisible = hasAccessibilityEnabled
            setOnClickListener {
326
                sendNewTabEvent(isPrivateModeSelected)
327
328
329
330
331
332
333
                interactor.onNewTabTapped(isPrivateModeSelected)
            }
        }

        fabView.new_tab_button.apply {
            isVisible = !hasAccessibilityEnabled
            setOnClickListener {
334
                sendNewTabEvent(isPrivateModeSelected)
335
336
                interactor.onNewTabTapped(isPrivateModeSelected)
            }
337
338
339
        }
    }

340
341
342
343
344
345
346
    private fun sendNewTabEvent(isPrivateModeSelected: Boolean) {
        val eventToSend = if (isPrivateModeSelected) {
            Event.NewPrivateTabTapped
        } else {
            Event.NewTabTapped
        }

347
        components.analytics.metrics.track(eventToSend)
348
349
    }

350
351
352
353
354
355
356
357
358
359
360
    /**
     * Updates the bottom sheet height based on the number tabs or screen orientation.
     * Show the bottom sheet fully expanded if it is in landscape mode or the number of
     * tabs are greater or equal to the expand size limit.
     */
    fun updateBottomSheetBehavior() {
        if (isInLandscape() || getTabsNumberInAnyMode() >= getTabsNumberForExpandingTray()) {
            behavior.state = BottomSheetBehavior.STATE_EXPANDED
        } else {
            behavior.state = BottomSheetBehavior.STATE_COLLAPSED
        }
361
362
    }

363
364
365
366
367
368
369
370
371
372
373
    enum class TabChange {
        PRIVATE, NORMAL
    }

    private fun toggleSaveToCollectionButton(isPrivate: Boolean) {
        collectionsButtonAdapter.notifyItemChanged(
            0,
            if (isPrivate) TabChange.PRIVATE else TabChange.NORMAL
        )
    }

374
    override fun onTabSelected(tab: TabLayout.Tab?) {
375
        toggleFabText(isPrivateModeSelected)
376
        filterTabs.invoke(isPrivateModeSelected)
377
        toggleSaveToCollectionButton(isPrivateModeSelected)
378

379
        updateUINormalMode(view.context.components.core.store.state)
380
        scrollToSelectedBrowserTab()
381
382

        if (isPrivateModeSelected) {
383
            components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
384
        } else {
385
            components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
386
        }
387
    }
388

389
390
    override fun onTabReselected(tab: TabLayout.Tab?) = Unit
    override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
391

392
    var mode: Mode = Mode.Normal
393
394
        private set

395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
    fun updateTabsTrayLayout() {
        if (container.context.settings().gridTabView) {
            setupGridTabView()
        } else {
            setupListTabView()
        }
    }

    private fun setupGridTabView() {
        view.tabsTray.apply {
            val gridLayoutManager =
                GridLayoutManager(container.context, getNumberOfGridColumns(container.context))

            gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    val numTabs = tabsAdapter.itemCount
                    return if (position < numTabs) {
                        1
                    } else {
                        getNumberOfGridColumns(container.context)
                    }
                }
            }

            layoutManager = gridLayoutManager
420
421
422
423
424
425
426
427
428

            // Ensure items have the same all around padding - 16 dp. Avoid the double spacing issue.
            // A 8dp padding is already set in xml, pad the parent with the remaining needed 8dp.
            updateLayoutParams<ConstraintLayout.LayoutParams> {
                val padding = GRID_ITEM_PARENT_PADDING.dpToPx(resources.displayMetrics)
                // Account for the already set bottom padding needed to accommodate the fab.
                val bottomPadding = paddingBottom + padding
                setPadding(padding, padding, padding, bottomPadding)
            }
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
        }
    }

    /**
     * Returns the number of columns that will fit in the grid layout for the current screen.
     */
    private fun getNumberOfGridColumns(context: Context): Int {
        val displayMetrics = context.resources.displayMetrics
        val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density
        val columnCount = (screenWidthDp / COLUMN_WIDTH_DP).toInt()
        return if (columnCount >= 2) columnCount else 2
    }

    private fun setupListTabView() {
        view.tabsTray.apply {
444
            layoutManager = LinearLayoutManager(container.context)
445
446
447
        }
    }

448
449
450
    fun updateState(state: TabTrayDialogFragmentState) {
        val oldMode = mode

451
        if (oldMode::class != state.mode::class) {
452
            updateTabsForMultiselectModeChanged(state.mode is Mode.MultiSelect)
453
454
            if (view.context.settings().accessibilityServicesEnabled) {
                view.announceForAccessibility(
455
                    if (state.mode == Mode.Normal) view.context.getString(
456
457
458
459
                        R.string.tab_tray_exit_multiselect_content_description
                    ) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description)
                )
            }
460
461
462
463
        }

        mode = state.mode
        when (state.mode) {
464
            Mode.Normal -> {
465
466
467
468
469
470
471
472
                view.tabsTray.apply {
                    tabsTouchHelper.attachToRecyclerView(this)
                }

                toggleUIMultiselect(multiselect = false)

                updateUINormalMode(state.browserState)
            }
473
            is Mode.MultiSelect -> {
474
475
476
477
478
479
480
                // Disable swipe to delete while in multiselect
                tabsTouchHelper.attachToRecyclerView(null)

                toggleUIMultiselect(multiselect = true)

                fabView.new_tab_button.isVisible = false
                view.tab_tray_new_tab.isVisible = false
481
                view.collect_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
482
483
                view.share_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
                view.menu_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
484
485
486
487
488
489
490
491

                view.multiselect_title.text = view.context.getString(
                    R.string.tab_tray_multi_select_title,
                    state.mode.selectedItems.size
                )
                view.collect_multi_select.setOnClickListener {
                    interactor.onSaveToCollectionClicked(state.mode.selectedItems)
                }
492
493
494
495
496
497
498
499
500
501
502
503
504
505
                view.share_multi_select.setOnClickListener {
                    interactor.onShareSelectedTabsClicked(state.mode.selectedItems)
                }
                view.menu_multi_select.setOnClickListener {
                    multiselectMenu = multiselectSelectionMenu.menuBuilder.build(container.context)
                    multiselectMenu?.show(it)?.also { popupMenu ->
                        (popupMenu.contentView as? CardView)?.setCardBackgroundColor(
                            ContextCompat.getColor(
                                view.context,
                                R.color.foundation_normal_theme
                            )
                        )
                    }
                }
506
507
508
                view.exit_multi_select.setOnClickListener {
                    interactor.onBackPressed()
                }
509
            }
510
        }
511

512
513
514
515
516
517
518
519
520
521
522
523
524
525
        if (oldMode.selectedItems != state.mode.selectedItems) {
            val unselectedItems = oldMode.selectedItems - state.mode.selectedItems

            state.mode.selectedItems.union(unselectedItems).forEach { item ->
                if (view.context.settings().accessibilityServicesEnabled) {
                    view.announceForAccessibility(
                        if (unselectedItems.contains(item)) view.context.getString(
                            R.string.tab_tray_item_unselected_multiselect_content_description,
                            item.title
                        ) else view.context.getString(
                            R.string.tab_tray_item_selected_multiselect_content_description,
                            item.title
                        )
                    )
526
                }
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
                updateTabsForSelectionChanged(item.id)
            }
        }
    }

    private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) {
        this.findViewById<View>(childId)?.let {
            val constraintSet = ConstraintSet()
            constraintSet.clone(this)
            constraintSet.constrainPercentWidth(it.id, percentage)
            constraintSet.applyTo(this)
            it.requestLayout()
        }
    }

    private fun updateUINormalMode(browserState: BrowserState) {
        val hasNoTabs = if (isPrivateModeSelected) {
            browserState.privateTabs.isEmpty()
        } else {
            browserState.normalTabs.isEmpty()
        }

        view.tab_tray_empty_view.isVisible = hasNoTabs
        if (hasNoTabs) {
            view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
                view.context.getString(R.string.no_private_tabs_description)
            } else {
                view.context?.getString(R.string.no_open_tabs_description)
555
            }
556
557
558
        }

        view.tabsTray.visibility = if (hasNoTabs) {
559
            INVISIBLE
560
        } else {
561
            VISIBLE
562
563
        }

564
        counter_text.text = updateTabCounter(browserState.normalTabs.size)
565
        updateTabTrayViewAccessibility(browserState.normalTabs.size)
566
567
568
569
570
571
572

        adjustNewTabButtonsForNormalMode()
    }

    private fun toggleUIMultiselect(multiselect: Boolean) {
        view.multiselect_title.isVisible = multiselect
        view.collect_multi_select.isVisible = multiselect
573
574
        view.share_multi_select.isVisible = multiselect
        view.menu_multi_select.isVisible = multiselect
575
576
577
578
579
580
581
582
583
584
        view.exit_multi_select.isVisible = multiselect

        view.topBar.setBackgroundColor(
            ContextCompat.getColor(
                view.context,
                if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme
            )
        )

        view.handle.updateLayoutParams<ViewGroup.MarginLayoutParams> {
585
586
587
588
            height = view.resources.getDimensionPixelSize(
                if (multiselect) {
                    R.dimen.tab_tray_multiselect_handle_height
                } else {
589
                    R.dimen.bottom_sheet_handle_height
590
591
592
593
594
595
                }
            )
            topMargin = view.resources.getDimensionPixelSize(
                if (multiselect) {
                    R.dimen.tab_tray_multiselect_handle_top_margin
                } else {
596
                    R.dimen.bottom_sheet_handle_top_margin
597
                }
598
599
            )
        }
600

601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
        view.tab_wrapper.setChildWPercent(
            if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH,
            view.handle.id
        )

        view.handle.setBackgroundColor(
            ContextCompat.getColor(
                view.context,
                if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme
            )
        )

        view.tab_layout.isVisible = !multiselect
        view.tab_tray_empty_view.isVisible = !multiselect
        view.tab_tray_overflow.isVisible = !multiselect
        view.tab_layout.isVisible = !multiselect
    }

619
    private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) {
620
621
622
623
624
        view.tabsTray.apply {
            val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs(
                isPrivateModeSelected
            )

625
626
627
628
629
            collectionsButtonAdapter.notifyItemChanged(
                0,
                if (inMultiselectMode) MultiselectModeChange.MULTISELECT else MultiselectModeChange.NORMAL
            )

630
            tabsAdapter.notifyItemRangeChanged(0, tabs.size, true)
631
632
633
        }
    }

634
635
    private fun updateTabsForSelectionChanged(itemId: String) {
        view.tabsTray.apply {
636
637
638
            val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs(
                isPrivateModeSelected
            )
639

640
641
            val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }

642
            tabsAdapter.notifyItemChanged(
643
644
                selectedBrowserTabIndex, true
            )
645
646
647
        }
    }

648
    private fun updateTabTrayViewAccessibility(count: Int) {
649
650
651
        view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) {
            view.context?.getString(R.string.open_tab_tray_single)
        } else {
ekager's avatar
ekager committed
652
            String.format(view.context.getString(R.string.open_tab_tray_plural), count.toString())
653
        }
654

655
656
657
        val isListTabView = view.context.settings().listTabView
        val columnCount = if (isListTabView) 1 else getNumberOfGridColumns(view.context)
        val rowCount = count.toDouble().div(columnCount).roundToInt()
658

659
        view.tabsTray.updateAccessibilityCollectionInfo(rowCount, columnCount)
660
    }
661

662
663
    private fun updateTabCounter(count: Int): String {
        if (count > MAX_VISIBLE_TABS) {
664
            counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM)
665
666
667
668
669
            return SO_MANY_TABS_OPEN
        }
        return NumberFormat.getInstance().format(count.toLong())
    }

670
671
672
673
    fun setTopOffset(landscape: Boolean) {
        val topOffset = if (landscape) {
            0
        } else {
674
            view.resources.getDimensionPixelSize(R.dimen.tab_tray_top_offset)
675
676
        }

677
        behavior.expandedOffset = topOffset
678
679
    }

680
681
682
683
    fun dismissMenu() {
        menu?.dismiss()
    }

684
685
686
    private fun toggleFabText(private: Boolean) {
        if (private) {
            fabView.new_tab_button.extend()
687
            fabView.new_tab_button.contentDescription =
688
                view.context.getString(R.string.add_private_tab)
689
690
        } else {
            fabView.new_tab_button.shrink()
691
            fabView.new_tab_button.contentDescription =
692
                view.context.getString(R.string.add_tab)
693
694
695
        }
    }

696
697
698
699
    fun onBackPressed(): Boolean {
        return interactor.onBackPressed()
    }

700
    fun scrollToSelectedBrowserTab(selectedTabId: String? = null) {
701
        view.tabsTray.apply {
702
            val recyclerViewIndex = getSelectedBrowserTabViewIndex(selectedTabId)
703
704

            layoutManager?.scrollToPosition(recyclerViewIndex)
705
706
            smoothScrollBy(
                0,
707
                -resources.getDimensionPixelSize(R.dimen.tab_tray_tab_item_height) / 2
708
            )
709
710
711
        }
    }

712
713
714
715
716
717
718
    private fun getSelectedBrowserTabViewIndex(sessionId: String? = null): Int {
        val tabs = if (isPrivateModeSelected) {
            view.context.components.core.store.state.privateTabs
        } else {
            view.context.components.core.store.state.normalTabs
        }

719
        return if (sessionId != null) {
720
721
722
723
724
725
            tabs.indexOfFirst { it.id == sessionId }
        } else {
            tabs.indexOfFirst { it.id == view.context.components.core.store.state.selectedTabId }
        }
    }

726
    companion object {
727
        private const val TAB_COUNT_SHOW_CFR = 6
728
729
        private const val DEFAULT_TAB_ID = 0
        private const val PRIVATE_TAB_ID = 1
730

731
732
        // Minimum number of list items for which to show the tabs tray as expanded.
        private const val EXPAND_AT_LIST_SIZE = 4
733

734
735
        // Minimum number of grid items for which to show the tabs tray as expanded.
        private const val EXPAND_AT_GRID_SIZE = 3
736
        private const val SLIDE_OFFSET = 0
737
        private const val SELECTION_DELAY = 500
738
        private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
739
        private const val COLUMN_WIDTH_DP = 180
740

741
742
        // The remaining padding offset needed to provide a 16dp column spacing between the grid items.
        const val GRID_ITEM_PARENT_PADDING = 8
743
    }
744
}