TabTrayViewHolder.kt 8 KB
Newer Older
1 2 3 4 5 6 7
/* 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

import android.view.View
8
import android.view.accessibility.AccessibilityNodeInfo
9
import android.widget.ImageButton
10
import android.widget.ImageView
11
import android.widget.TextView
12
import androidx.annotation.VisibleForTesting
13
import androidx.appcompat.content.res.AppCompatResources
14 15 16
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import mozilla.components.browser.state.state.MediaState
17
import mozilla.components.browser.state.store.BrowserStore
18
import mozilla.components.browser.tabstray.TabViewHolder
19
import mozilla.components.browser.tabstray.TabsTrayStyling
20
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
21
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
22 23 24 25 26
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.media.ext.pauseIfPlaying
import mozilla.components.feature.media.ext.playIfPaused
import mozilla.components.support.base.observer.Observable
27
import mozilla.components.support.images.ImageLoadRequest
28
import mozilla.components.support.images.loader.ImageLoader
29 30
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
31
import org.mozilla.fenix.components.metrics.MetricController
32
import org.mozilla.fenix.ext.components
33
import org.mozilla.fenix.ext.getMediaStateForSession
34 35 36 37
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.showAndEnable
38
import org.mozilla.fenix.ext.toShortUrl
39
import org.mozilla.fenix.utils.Do
40
import kotlin.math.max
41 42 43 44

/**
 * A RecyclerView ViewHolder implementation for "tab" items.
 */
Jeff Boek's avatar
Jeff Boek committed
45 46
class TabTrayViewHolder(
    itemView: View,
47
    private val imageLoader: ImageLoader,
48
    private val store: BrowserStore = itemView.context.components.core.store,
49
    private val metrics: MetricController = itemView.context.components.analytics.metrics
Jeff Boek's avatar
Jeff Boek committed
50
) : TabViewHolder(itemView) {
51

52
    private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
53 54 55 56 57
    private val closeView: AppCompatImageButton =
        itemView.findViewById(R.id.mozac_browser_tabstray_close)
    private val thumbnailView: TabThumbnailView =
        itemView.findViewById(R.id.mozac_browser_tabstray_thumbnail)

58 59
    @VisibleForTesting
    internal val urlView: TextView? = itemView.findViewById(R.id.mozac_browser_tabstray_url)
60 61 62 63 64 65 66
    private val playPauseButtonView: ImageButton = itemView.findViewById(R.id.play_pause_button)

    override var tab: Tab? = null

    /**
     * Displays the data of the given session and notifies the given observable about events.
     */
67 68 69 70 71 72
    override fun bind(
        tab: Tab,
        isSelected: Boolean,
        styling: TabsTrayStyling,
        observable: Observable<TabsTray.Observer>
    ) {
73 74 75 76 77 78 79 80
        this.tab = tab

        // Basic text
        updateTitle(tab)
        updateUrl(tab)
        updateCloseButtonDescription(tab.title)

        // Drawables and theme
81
        updateBackgroundColor(isSelected)
82 83 84 85

        if (tab.thumbnail != null) {
            thumbnailView.setImageBitmap(tab.thumbnail)
        } else {
86
            loadIntoThumbnailView(thumbnailView, tab.id)
87
        }
88 89 90 91 92

        // Media state
        playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
        with(playPauseButtonView) {
            invalidate()
93
            Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
94 95 96 97 98
                MediaState.State.PAUSED -> {
                    showAndEnable()
                    contentDescription =
                        context.getString(R.string.mozac_feature_media_notification_action_play)
                    setImageDrawable(
99
                        AppCompatResources.getDrawable(context, R.drawable.media_state_play)
100 101 102 103 104 105 106 107
                    )
                }

                MediaState.State.PLAYING -> {
                    showAndEnable()
                    contentDescription =
                        context.getString(R.string.mozac_feature_media_notification_action_pause)
                    setImageDrawable(
108
                        AppCompatResources.getDrawable(context, R.drawable.media_state_pause)
109 110 111 112 113 114 115 116 117 118 119
                    )
                }

                MediaState.State.NONE -> {
                    removeTouchDelegate()
                    removeAndDisable()
                }
            }
        }

        playPauseButtonView.setOnClickListener {
120
            Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
121
                MediaState.State.PLAYING -> {
122 123
                    metrics.track(Event.TabMediaPause)
                    store.state.media.pauseIfPlaying()
124 125 126
                }

                MediaState.State.PAUSED -> {
127 128
                    metrics.track(Event.TabMediaPlay)
                    store.state.media.playIfPaused()
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
                }

                MediaState.State.NONE -> throw AssertionError(
                    "Play/Pause button clicked without play/pause state."
                )
            }
        }

        closeView.setOnClickListener {
            observable.notifyObservers { onTabClosed(tab) }
        }
    }

    private fun updateTitle(tab: Tab) {
        val title = if (tab.title.isNotEmpty()) {
            tab.title
        } else {
            tab.url
        }
        titleView.text = title
    }
150

151
    private fun updateUrl(tab: Tab) {
152 153 154 155 156
        // Truncate to MAX_URI_LENGTH to prevent the UI from locking up for
        // extremely large URLs such as data URIs or bookmarklets. The same
        // is done in the toolbar and awesomebar:
        // https://github.com/mozilla-mobile/fenix/issues/1824
        // https://github.com/mozilla-mobile/android-components/issues/6985
157 158 159
        urlView?.text = tab.url
            .toShortUrl(itemView.context.components.publicSuffixList)
            .take(MAX_URI_LENGTH)
160 161
    }

162 163
    @VisibleForTesting
    internal fun updateBackgroundColor(isSelected: Boolean) {
Jeff Boek's avatar
Jeff Boek committed
164 165
        val color = if (isSelected) {
            R.color.tab_tray_item_selected_background_normal_theme
166
        } else {
Jeff Boek's avatar
Jeff Boek committed
167
            R.color.tab_tray_item_background_normal_theme
168 169 170 171
        }
        itemView.setBackgroundColor(
            ContextCompat.getColor(
                itemView.context,
Jeff Boek's avatar
Jeff Boek committed
172
                color
173 174 175 176 177 178 179 180 181
            )
        )
    }

    private fun updateCloseButtonDescription(title: String) {
        closeView.contentDescription =
            closeView.context.getString(R.string.close_tab_title, title)
    }

182 183 184 185 186 187 188 189
    private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) {
        val thumbnailSize = max(
            itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_thumbnail_height),
            itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_thumbnail_width)
        )
        imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize))
    }

190
    internal fun updateAccessibilityRowIndex(item: View, newIndex: Int) {
191
        item.accessibilityDelegate = object : View.AccessibilityDelegate() {
192 193 194 195 196 197
            override fun onInitializeAccessibilityNodeInfo(
                host: View?,
                info: AccessibilityNodeInfo?
            ) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info?.let {
198 199 200 201 202 203 204 205 206 207
                    info.collectionItemInfo = info.collectionItemInfo?.let { initialInfo ->
                        AccessibilityNodeInfo.CollectionItemInfo.obtain(
                            newIndex,
                            initialInfo.rowSpan,
                            initialInfo.columnIndex,
                            initialInfo.columnSpan,
                            false,
                            initialInfo.isSelected
                        )
                    }
208 209
                }
            }
210
        }
211 212
    }

213 214 215 216
    companion object {
        private const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24
    }
}