fenix/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolde...

270 lines
11 KiB
Kotlin

/* 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.browser
import android.annotation.SuppressLint
import android.graphics.PointF
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
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 androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.SelectableTabViewHolder
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.browser.tabstray.TabsTrayStyling
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
import mozilla.components.concept.base.images.ImageLoadRequest
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.engine.mediasession.MediaSession
import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.R
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
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A RecyclerView ViewHolder implementation for "tab" items.
*
* @param itemView [View] that displays a "tab".
* @param imageLoader [ImageLoader] used to load tab thumbnails.
* @param trayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
* @param store [BrowserStore] containing the complete state of the browser and methods to update that.
*/
@Suppress("LongParameterList")
abstract class AbstractBrowserTabViewHolder(
itemView: View,
private val imageLoader: ImageLoader,
private val trayStore: TabsTrayStore,
private val selectionHolder: SelectionHolder<TabSessionState>?,
internal val featureName: String,
private val store: BrowserStore = itemView.context.components.core.store,
) : SelectableTabViewHolder(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)
abstract val browserTrayInteractor: BrowserTrayInteractor
abstract val thumbnailSize: Int
override var tab: TabSessionState? = null
internal var beingDragged: Boolean = false
private var touchStartPoint: PointF? = null
/**
* Displays the data of the given session and notifies the given observable about events.
*/
@Suppress("ComplexMethod", "LongMethod")
override fun bind(
tab: TabSessionState,
isSelected: Boolean,
styling: TabsTrayStyling,
delegate: TabsTray.Delegate,
) {
this.tab = tab
beingDragged = false
updateTitle(tab)
updateUrl(tab)
updateFavicon(tab)
updateCloseButtonDescription(tab.content.title)
updateSelectedTabIndicator(isSelected)
updateMediaState(tab)
if (selectionHolder != null) {
setSelectionInteractor(tab, selectionHolder, browserTrayInteractor)
} else {
itemView.setOnClickListener {
browserTrayInteractor.onTabSelected(tab, featureName)
}
}
loadIntoThumbnailView(thumbnailView, tab.id)
}
override fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) {
selectedMaskView?.isVisible = isSelected
closeView.isInvisible = trayStore.state.mode is TabsTrayState.Mode.Select
}
private fun updateFavicon(tab: TabSessionState) {
if (tab.content.icon != null) {
faviconView?.visibility = View.VISIBLE
faviconView?.setImageBitmap(tab.content.icon)
} else {
faviconView?.visibility = View.GONE
}
}
private fun updateTitle(tab: TabSessionState) {
val title = tab.content.title.ifEmpty {
tab.content.url
}
titleView.text = title
}
private fun updateUrl(tab: TabSessionState) {
// 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.content.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 updateMediaState(tab: TabSessionState) {
// 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 -> {
Tab.mediaPause.record(NoExtras())
sessionState.mediaSessionState?.controller?.pause()
}
MediaSession.PlaybackState.PAUSED -> {
Tab.mediaPlay.record(NoExtras())
sessionState.mediaSessionState?.controller?.play()
}
else -> throw AssertionError(
"Play/Pause button clicked without play/pause state.",
)
}
}
}
}
private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) {
imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize))
}
private fun setSelectionInteractor(
item: TabSessionState,
holder: SelectionHolder<TabSessionState>,
interactor: BrowserTrayInteractor,
) {
itemView.setOnClickListener {
interactor.onMultiSelectClicked(item, holder, featureName)
}
itemView.setOnLongClickListener {
interactor.onLongClicked(item, holder)
}
setDragInteractor(item, holder, interactor)
}
@SuppressLint("ClickableViewAccessibility")
private fun setDragInteractor(
item: TabSessionState,
holder: SelectionHolder<TabSessionState>,
interactor: BrowserTrayInteractor,
) {
// Since I immediately pass the event to onTouchEvent if it's not a move
// The ClickableViewAccessibility warning isn't useful
itemView.setOnTouchListener { view, motionEvent ->
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
touchStartPoint = PointF(motionEvent.x, motionEvent.y)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
touchStartPoint = null
}
MotionEvent.ACTION_MOVE -> {
val touchStart = touchStartPoint
val selected = holder.selectedItems
val selectsOnlyThis = (selected.size == 1 && selected.contains(item))
if (selectsOnlyThis && touchStart != null) {
// If the parent is null then return early and mark the event as unhandled
val parent = itemView.parent as? AbstractBrowserTrayList ?: return@setOnTouchListener false
// Prevent scrolling if the user tries to start drag vertically
parent.requestDisallowInterceptTouchEvent(true)
// Only start deselect+drag if the user drags far enough
val dist = PointF.length(touchStart.x - motionEvent.x, touchStart.y - motionEvent.y)
if (dist > ViewConfiguration.get(parent.context).scaledTouchSlop) {
interactor.deselect(item) // Exit selection mode
touchStartPoint = null
val dragOffset = PointF(motionEvent.x, motionEvent.y)
val shadow = BlankDragShadowBuilder()
ViewCompat.startDragAndDrop(itemView, null, shadow, TabDragData(item, dragOffset), 0)
}
return@setOnTouchListener true
}
}
}
view.onTouchEvent(motionEvent)
}
}
companion object {
internal const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24
internal const val GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS = 24
}
}