diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayGridViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayGridViewHolder.kt new file mode 100644 index 000000000..06abaadd3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayGridViewHolder.kt @@ -0,0 +1,58 @@ +/* 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.tabstray + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatImageButton +import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.concept.tabstray.Tab +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.increaseTapArea +import kotlin.math.max +import kotlinx.android.synthetic.main.tab_tray_grid_item.view.tab_tray_grid_item + +/** + * A RecyclerView ViewHolder implementation for "tab" items with grid layout. + */ +class TabsTrayGridViewHolder( + parent: ViewGroup, + imageLoader: ImageLoader, + private val itemView: View = + LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_grid_item, parent, false), + thumbnailSize: Int = + max( + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height), + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width) + ) +) : TabsTrayViewHolder(itemView, imageLoader, thumbnailSize) { + + private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close) + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + itemView.tab_tray_grid_item.background = if (showAsSelected) { + AppCompatResources.getDrawable(itemView.context, R.drawable.tab_tray_grid_item_selected_border) + } else { + null + } + return + } + + override fun bind( + tab: Tab, + isSelected: Boolean, + styling: TabsTrayStyling, + observable: Observable + ) { + super.bind(tab, isSelected, styling, observable) + + closeButton.increaseTapArea(GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayListViewHolder.kt new file mode 100644 index 000000000..26d2e1b75 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayListViewHolder.kt @@ -0,0 +1,43 @@ +/* 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.tabstray + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import mozilla.components.concept.base.images.ImageLoader +import org.mozilla.fenix.R +import kotlin.math.max + +/** + * A RecyclerView ViewHolder implementation for "tab" items with list layout. + */ +class TabsTrayListViewHolder( + parent: ViewGroup, + imageLoader: ImageLoader, + private val itemView: View = + LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_item, parent, false), + thumbnailSize: Int = + max( + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height), + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width) + ) +) : TabsTrayViewHolder(itemView, imageLoader, thumbnailSize) { + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + val color = if (showAsSelected) { + R.color.tab_tray_item_selected_background_normal_theme + } else { + R.color.tab_tray_item_background_normal_theme + } + itemView.setBackgroundColor( + ContextCompat.getColor( + itemView.context, + color + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt new file mode 100644 index 000000000..5afbba864 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt @@ -0,0 +1,181 @@ +/* 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.tabstray + +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatImageButton +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.tabstray.TabViewHolder +import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView +import mozilla.components.browser.toolbar.MAX_URI_LENGTH +import mozilla.components.concept.base.images.ImageLoadRequest +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.concept.engine.mediasession.MediaSession +import mozilla.components.concept.tabstray.Tab +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.increaseTapArea +import org.mozilla.fenix.ext.removeAndDisable +import org.mozilla.fenix.ext.removeTouchDelegate +import org.mozilla.fenix.ext.showAndEnable +import org.mozilla.fenix.ext.toShortUrl + +/** + * A RecyclerView ViewHolder implementation for "tab" items. + */ +abstract class TabsTrayViewHolder( + private val itemView: View, + private val imageLoader: ImageLoader, + private val thumbnailSize: Int, + private val store: BrowserStore = itemView.context.components.core.store, + private val metrics: MetricController = itemView.context.components.analytics.metrics +) : TabViewHolder(itemView) { + + private val faviconView: ImageView? = + itemView.findViewById(R.id.mozac_browser_tabstray_favicon_icon) + private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title) + private val closeView: AppCompatImageButton = + itemView.findViewById(R.id.mozac_browser_tabstray_close) + private val thumbnailView: TabThumbnailView = + itemView.findViewById(R.id.mozac_browser_tabstray_thumbnail) + + @VisibleForTesting + internal val urlView: TextView? = itemView.findViewById(R.id.mozac_browser_tabstray_url) + 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. + */ + @Suppress("ComplexMethod", "LongMethod") + override fun bind( + tab: Tab, + isSelected: Boolean, + styling: TabsTrayStyling, + observable: Observable + ) { + this.tab = tab + + updateTitle(tab) + updateUrl(tab) + updateFavicon(tab) + updateCloseButtonDescription(tab.title) + updateSelectedTabIndicator(isSelected) + + if (tab.thumbnail != null) { + thumbnailView.setImageBitmap(tab.thumbnail) + } else { + loadIntoThumbnailView(thumbnailView, tab.id) + } + + // Media state + playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS) + + with(playPauseButtonView) { + invalidate() + val sessionState = store.state.findTabOrCustomTab(tab.id) + when (sessionState?.mediaSessionState?.playbackState) { + MediaSession.PlaybackState.PAUSED -> { + showAndEnable() + contentDescription = + context.getString(R.string.mozac_feature_media_notification_action_play) + setImageDrawable( + AppCompatResources.getDrawable(context, R.drawable.media_state_play) + ) + } + + MediaSession.PlaybackState.PLAYING -> { + showAndEnable() + contentDescription = + context.getString(R.string.mozac_feature_media_notification_action_pause) + setImageDrawable( + AppCompatResources.getDrawable(context, R.drawable.media_state_pause) + ) + } + + else -> { + removeTouchDelegate() + removeAndDisable() + } + } + + setOnClickListener { + when (sessionState?.mediaSessionState?.playbackState) { + MediaSession.PlaybackState.PLAYING -> { + metrics.track(Event.TabMediaPause) + sessionState.mediaSessionState?.controller?.pause() + } + + MediaSession.PlaybackState.PAUSED -> { + metrics.track(Event.TabMediaPlay) + sessionState.mediaSessionState?.controller?.play() + } + else -> throw AssertionError( + "Play/Pause button clicked without play/pause state." + ) + } + } + } + + closeView.setOnClickListener { + observable.notifyObservers { onTabClosed(tab) } + } + } + + private fun updateFavicon(tab: Tab) { + if (tab.icon != null) { + faviconView?.visibility = View.VISIBLE + faviconView?.setImageBitmap(tab.icon) + } else { + faviconView?.visibility = View.GONE + } + } + + private fun updateTitle(tab: Tab) { + val title = if (tab.title.isNotEmpty()) { + tab.title + } else { + tab.url + } + titleView.text = title + } + + private fun updateUrl(tab: Tab) { + // 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 + urlView?.text = tab.url + .toShortUrl(itemView.context.components.publicSuffixList) + .take(MAX_URI_LENGTH) + } + + private fun updateCloseButtonDescription(title: String) { + closeView.contentDescription = + closeView.context.getString(R.string.close_tab_title, title) + } + + private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) { + imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize)) + } + + companion object { + internal const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24 + internal const val GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS = 24 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt index ca9759fb8..9d64dbb3e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.tabstray.browser import android.content.Context -import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.GridLayoutManager @@ -15,10 +14,10 @@ import mozilla.components.browser.thumbnails.loader.ThumbnailLoader import mozilla.components.concept.tabstray.TabsTray import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.ObserverRegistry -import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.tabtray.TabTrayViewHolder +import org.mozilla.fenix.tabstray.TabsTrayGridViewHolder +import org.mozilla.fenix.tabstray.TabsTrayListViewHolder /** * A [RecyclerView.Adapter] for browser tabs. @@ -42,21 +41,17 @@ class BrowserTabsAdapter( override fun getItemViewType(position: Int): Int { return if (context.settings().gridTabView) { - // ViewType.GRID.ordinal - R.layout.tab_tray_grid_item + ViewType.GRID.ordinal } else { - // ViewType.LIST.ordinal - R.layout.tab_tray_item + ViewType.LIST.ordinal } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder { - // TODO make this into separate view holders for each layout - // For this, we need to separate the TabTrayViewHolder as well. - // See https://github.com/mozilla-mobile/fenix/issues/18535 - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - - return TabTrayViewHolder(view, imageLoader) + return when (viewType) { + ViewType.GRID.ordinal -> TabsTrayGridViewHolder(parent, imageLoader) + else -> TabsTrayListViewHolder(parent, imageLoader) + } } override fun onBindViewHolder(holder: TabViewHolder, position: Int) {