From e85eeddda0c0ebb0bd8d5ae1cd289dc60b44bcef Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Tue, 17 May 2022 22:08:16 -0400 Subject: [PATCH] For #21894: Move Tabs Tray to compose: Individual tab viewholders: ListViewHolder. --- .../java/org/mozilla/fenix/FeatureFlags.kt | 4 + .../mozilla/fenix/compose/ThumbnailCard.kt | 78 ++++---- .../fenix/compose/tabstray/MediaImage.kt | 83 ++++++++ .../fenix/compose/tabstray/TabListItem.kt | 183 ++++++++++++++++++ .../fenix/tabstray/TabsTrayController.kt | 27 ++- .../fenix/tabstray/TabsTrayFragment.kt | 1 + .../fenix/tabstray/TrayPagerAdapter.kt | 11 +- .../browser/AbstractBrowserTabViewHolder.kt | 30 +-- .../tabstray/browser/BrowserTabsAdapter.kt | 61 ++++-- .../tabstray/browser/BrowserTrayInteractor.kt | 67 +++++++ .../fenix/tabstray/browser/TabGroupAdapter.kt | 22 ++- .../tabstray/browser/TabGroupListAdapter.kt | 49 ++++- .../tabstray/browser/TabGroupViewHolder.kt | 8 +- .../compose/ComposeAbstractTabViewHolder.kt | 52 +++++ .../browser/compose/ComposeListViewHolder.kt | 116 +++++++++++ .../res/drawable/media_state_pause_vector.xml | 13 ++ .../res/drawable/media_state_play_vector.xml | 13 ++ .../fenix/tabstray/TabsTrayFragmentTest.kt | 3 +- .../AbstractBrowserTabViewHolderTest.kt | 6 +- .../browser/BrowserTabsAdapterTest.kt | 4 +- .../AbstractBrowserPageViewHolderTest.kt | 4 +- 21 files changed, 739 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt create mode 100644 app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt create mode 100644 app/src/main/res/drawable/media_state_pause_vector.xml create mode 100644 app/src/main/res/drawable/media_state_play_vector.xml diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index eb385ed744d1..4a095e85b0bc 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -91,4 +91,8 @@ object FeatureFlags { * Enables receiving from the messaging framework. */ const val messagingFeature = true + /** + * Enables compose on the tabs tray items. + */ + val composeTabsTray = Config.channel.isDebug } diff --git a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt index 1b55a903a440..14a6231bc4e2 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt @@ -32,6 +32,7 @@ import mozilla.components.concept.base.images.ImageLoadRequest import org.mozilla.fenix.R import org.mozilla.fenix.components.components import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme /** * Card which will display a thumbnail. If a thumbnail is not available for [url], the favicon @@ -40,6 +41,8 @@ import org.mozilla.fenix.theme.FirefoxTheme * @param url Url to display thumbnail for. * @param key Key used to remember the thumbnail for future compositions. * @param modifier [Modifier] used to draw the image content. + * @param contentDescription Text used by accessibility services + * to describe what this image represents. * @param contentScale [ContentScale] used to draw image content. * @param alignment [Alignment] used to draw the image content. */ @@ -48,6 +51,7 @@ fun ThumbnailCard( url: String, key: String, modifier: Modifier = Modifier, + contentDescription: String? = null, contentScale: ContentScale = ContentScale.FillWidth, alignment: Alignment = Alignment.TopCenter ) { @@ -55,36 +59,42 @@ fun ThumbnailCard( modifier = modifier, backgroundColor = colorResource(id = R.color.photonGrey20) ) { - components.core.icons.Loader(url) { - Placeholder { - Box( - modifier = Modifier.background(color = FirefoxTheme.colors.layer3) - ) - } - - WithIcon { icon -> - Box( - modifier = Modifier.size(36.dp), - contentAlignment = Alignment.Center - ) { - Image( - painter = icon.painter, - contentDescription = null, - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(8.dp)), - contentScale = ContentScale.Fit + if (inComposePreview) { + Box( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3) + ) + } else { + components.core.icons.Loader(url) { + Placeholder { + Box( + modifier = Modifier.background(color = FirefoxTheme.colors.layer3) ) } + + WithIcon { icon -> + Box( + modifier = Modifier.size(36.dp), + contentAlignment = Alignment.Center + ) { + Image( + painter = icon.painter, + contentDescription = contentDescription, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Fit + ) + } + } } - } - ThumbnailImage( - key = key, - modifier = modifier, - contentScale = contentScale, - alignment = alignment - ) + ThumbnailImage( + key = key, + modifier = modifier, + contentScale = contentScale, + alignment = alignment + ) + } } } @@ -120,11 +130,13 @@ private fun ThumbnailImage( @Preview @Composable private fun ThumbnailCardPreview() { - ThumbnailCard( - url = "https://mozilla.com", - key = "123", - modifier = Modifier - .size(108.dp, 80.dp) - .clip(RoundedCornerShape(8.dp)) - ) + FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) { + ThumbnailCard( + url = "https://mozilla.com", + key = "123", + modifier = Modifier + .size(108.dp, 80.dp) + .clip(RoundedCornerShape(8.dp)) + ) + } } diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt new file mode 100644 index 000000000000..b5f6d30201bb --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt @@ -0,0 +1,83 @@ +/* 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.compose.tabstray + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.createTab +import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState +import org.mozilla.fenix.R +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * Draws controller buttons for the media (play/pause) state of the given [tab]. + * + * @param tab the tab which the image should be shown. + * @param onMediaIconClicked handles the click event when tab has media session like play/pause. + * @param modifier [Modifier] to be applied to the layout. + */ +@Composable +fun MediaImage( + tab: TabSessionState, + onMediaIconClicked: ((TabSessionState) -> Unit), + modifier: Modifier, +) { + val (icon, contentDescription) = when (tab.mediaSessionState?.playbackState) { + PlaybackState.PAUSED -> { + R.drawable.media_state_play_vector to R.string.mozac_feature_media_notification_action_play + } + PlaybackState.PLAYING -> { + R.drawable.media_state_pause_vector to R.string.mozac_feature_media_notification_action_pause + } + else -> return + } + + Card( + modifier = modifier + .size(size = 24.dp) + .clip(shape = CircleShape) + .border( + width = 2.dp, + color = FirefoxTheme.colors.layer2, + shape = CircleShape + ), + backgroundColor = FirefoxTheme.colors.layerAccent + ) { + Icon( + painter = painterResource(id = icon), + modifier = Modifier + .size(size = 24.dp) + .clickable { onMediaIconClicked(tab) }, + contentDescription = stringResource(contentDescription), + tint = FirefoxTheme.colors.borderPrimary + ) + } +} + +@Composable +@Preview +private fun ImagePreview() { + MediaImage( + tab = createTab(url = "https://mozilla.com"), + onMediaIconClicked = {}, + modifier = Modifier + .height(100.dp) + .width(200.dp) + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt new file mode 100644 index 000000000000..d3c6b8db0203 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt @@ -0,0 +1,183 @@ +/* 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.compose.tabstray + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.createTab +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.PrimaryText +import org.mozilla.fenix.compose.SecondaryText +import org.mozilla.fenix.compose.ThumbnailCard +import org.mozilla.fenix.ext.toShortUrl +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +/** + * A composable that renders tab list layout. + * + * @param tab The given tab to be render as view a list item. + * @param isSelected Indicates if the item should be render as selected. + * @param multiSelectionEnabled Indicates if the item should be render with multi selection options, + * enabled. + * @param multiSelectionSelected Indicates if the item should be render as multi selection selected + * option. + * @param onCloseClick Callback to handle the click event of the close button. + * @param onMediaClick Callback to handle when the media item is clicked. + * @param onClick Callback to handle when item is clicked. + * @param onLongClick Callback to handle when item is long clicked. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +@Suppress("MagicNumber") +fun TabListItem( + tab: TabSessionState, + isSelected: Boolean = false, + multiSelectionEnabled: Boolean = false, + multiSelectionSelected: Boolean = false, + onCloseClick: (tab: TabSessionState) -> Unit, + onMediaClick: (tab: TabSessionState) -> Unit, + onClick: (tab: TabSessionState) -> Unit, + onLongClick: (tab: TabSessionState) -> Unit, +) { + + val contentBackgroundColor = if (isSelected) { + FirefoxTheme.colors.layerAccentNonOpaque + } else { + FirefoxTheme.colors.layer1 + } + Row( + modifier = Modifier + .fillMaxWidth() + .background(contentBackgroundColor) + .combinedClickable( + onLongClick = { onLongClick(tab) }, + onClick = { + onClick(tab) + } + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Thumbnail( + tab = tab, + multiSelectionEnabled = multiSelectionEnabled, + isSelected = multiSelectionSelected, + onMediaIconClicked = { onMediaClick(it) } + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(weight = 1f) + ) { + + PrimaryText( + text = tab.content.title, + fontSize = 16.sp, + maxLines = 2 + ) + + SecondaryText( + text = tab.content.url.toShortUrl(), + fontSize = 12.sp, + ) + } + + if (!multiSelectionEnabled) { + IconButton( + onClick = { onCloseClick(tab) }, + modifier = Modifier.size(size = 24.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_close), + contentDescription = stringResource( + id = R.string.close_tab_title, + tab.content.title + ), + tint = FirefoxTheme.colors.iconPrimary + ) + } + } + } +} + +@Composable +private fun Thumbnail( + tab: TabSessionState, + multiSelectionEnabled: Boolean, + isSelected: Boolean, + onMediaIconClicked: ((TabSessionState) -> Unit) +) { + Box { + ThumbnailCard( + url = tab.content.url, + key = tab.id, + modifier = Modifier.size(width = 92.dp, height = 72.dp), + contentDescription = stringResource(id = R.string.mozac_browser_tabstray_open_tab), + ) + + if (isSelected) { + Card( + modifier = Modifier + .size(size = 40.dp) + .align(alignment = Alignment.Center), + shape = CircleShape, + backgroundColor = FirefoxTheme.colors.layerAccent, + ) { + Icon( + painter = painterResource(id = R.drawable.mozac_ic_check), + modifier = Modifier + .matchParentSize() + .padding(all = 8.dp), + contentDescription = null, + tint = colorResource(id = R.color.mozac_ui_icons_fill) + ) + } + } + if (!multiSelectionEnabled) { + MediaImage( + tab = tab, + onMediaIconClicked = onMediaIconClicked, + modifier = Modifier.align(Alignment.TopEnd) + ) + } + } +} + +@Composable +@Preview +private fun TabListItemPreview() { + FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) { + TabListItem( + tab = createTab(url = "www.mozilla.com", title = "Mozilla"), + onCloseClick = {}, + onMediaClick = {}, + onClick = {}, + onLongClick = {}, + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt index 3a21bcfa1a3d..655030629614 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -10,20 +10,23 @@ import mozilla.components.browser.state.action.DebugAction import mozilla.components.browser.state.action.LastAccessAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.getNormalOrPrivateTabs +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.base.profiler.Profiler +import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.lib.state.DelicateAction import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.ext.DEFAULT_ACTIVE_DAYS import org.mozilla.fenix.ext.potentialInactiveTabs +import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.tabstray.ext.isActiveDownload import java.util.concurrent.TimeUnit @@ -103,6 +106,11 @@ interface TabsTrayController { * Deletes all inactive tabs. */ fun handleDeleteAllInactiveTabs() + + /** + * Handles when a tab item is click either to play/pause. + */ + fun handleMediaClicked(tab: SessionState) } @Suppress("TooManyFunctions") @@ -282,4 +290,21 @@ class DefaultTabsTrayController( } showUndoSnackbarForTab(false) } + + override fun handleMediaClicked(tab: SessionState) { + when (tab.mediaSessionState?.playbackState) { + PlaybackState.PLAYING -> { + Tab.mediaPause.record(NoExtras()) + tab.mediaSessionState?.controller?.pause() + } + + PlaybackState.PAUSED -> { + Tab.mediaPlay.record(NoExtras()) + tab.mediaSessionState?.controller?.play() + } + else -> throw AssertionError( + "Play/Pause button clicked without play/pause state." + ) + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 49b6010e2c37..4f6a39a249ac 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -483,6 +483,7 @@ class TabsTrayFragment : AppCompatDialogFragment() { trayInteractor, requireComponents.core.store, requireComponents.appStore, + viewLifecycleOwner ) isUserInputEnabled = false } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index 3317d408d209..b95a65a400e0 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -9,6 +9,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.state.store.BrowserStore @@ -32,7 +33,8 @@ class TrayPagerAdapter( @VisibleForTesting internal val navInteractor: NavigationInteractor, @VisibleForTesting internal val interactor: TabsTrayInteractor, @VisibleForTesting internal val browserStore: BrowserStore, - @VisibleForTesting internal val appStore: AppStore + @VisibleForTesting internal val appStore: AppStore, + @VisibleForTesting internal val viewLifecycleOwner: LifecycleOwner ) : RecyclerView.Adapter() { /** @@ -43,9 +45,9 @@ class TrayPagerAdapter( private val normalAdapter by lazy { ConcatAdapter( InactiveTabsAdapter(context, browserInteractor, interactor, INACTIVE_TABS_FEATURE_NAME, context.settings()), - TabGroupAdapter(context, browserInteractor, tabsTrayStore, TAB_GROUP_FEATURE_NAME), + TabGroupAdapter(context, browserInteractor, tabsTrayStore, TAB_GROUP_FEATURE_NAME, viewLifecycleOwner), TitleHeaderAdapter(), - BrowserTabsAdapter(context, browserInteractor, tabsTrayStore, TABS_TRAY_FEATURE_NAME) + BrowserTabsAdapter(context, browserInteractor, tabsTrayStore, TABS_TRAY_FEATURE_NAME, viewLifecycleOwner) ) } private val privateAdapter by lazy { @@ -53,7 +55,8 @@ class TrayPagerAdapter( context, browserInteractor, tabsTrayStore, - TABS_TRAY_FEATURE_NAME + TABS_TRAY_FEATURE_NAME, + viewLifecycleOwner ) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt index 676d01bda2e8..a4f7c6080612 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolder.kt @@ -21,7 +21,7 @@ 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.TabViewHolder +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 @@ -31,7 +31,6 @@ import mozilla.components.concept.base.images.ImageLoader import mozilla.components.concept.engine.mediasession.MediaSession import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.FeatureFlags -import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.R import org.mozilla.fenix.ext.components @@ -44,7 +43,6 @@ import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayStore -import org.mozilla.fenix.tabstray.ext.isSelect /** * A RecyclerView ViewHolder implementation for "tab" items. @@ -61,10 +59,9 @@ abstract class AbstractBrowserTabViewHolder( private val imageLoader: ImageLoader, private val trayStore: TabsTrayStore, private val selectionHolder: SelectionHolder?, - @VisibleForTesting internal val featureName: String, private val store: BrowserStore = itemView.context.components.core.store, -) : TabViewHolder(itemView) { +) : SelectableTabViewHolder(itemView) { private val faviconView: ImageView? = itemView.findViewById(R.id.mozac_browser_tabstray_favicon_icon) @@ -109,7 +106,9 @@ abstract class AbstractBrowserTabViewHolder( if (selectionHolder != null) { setSelectionInteractor(tab, selectionHolder, browserTrayInteractor) } else { - itemView.setOnClickListener { browserTrayInteractor.onTabSelected(tab, featureName) } + itemView.setOnClickListener { + browserTrayInteractor.onTabSelected(tab, featureName) + } } if (tab.content.thumbnail != null) { @@ -119,7 +118,7 @@ abstract class AbstractBrowserTabViewHolder( } } - fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) { + override fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) { selectedMaskView?.isVisible = isSelected closeView.isInvisible = trayStore.state.mode is TabsTrayState.Mode.Select } @@ -217,24 +216,11 @@ abstract class AbstractBrowserTabViewHolder( interactor: BrowserTrayInteractor ) { itemView.setOnClickListener { - val selected = holder.selectedItems - when { - selected.isEmpty() && trayStore.state.mode.isSelect().not() -> { - interactor.onTabSelected(item, featureName) - } - item.id in selected.map { item -> item.id } -> interactor.deselect(item) - else -> interactor.select(item) - } + interactor.onMultiSelectClicked(item, holder, featureName) } itemView.setOnLongClickListener { - if (holder.selectedItems.isEmpty()) { - Collections.longPress.record(NoExtras()) - interactor.select(item) - true - } else { - false - } + interactor.onLongClicked(item, holder) } setDragInteractor(item, holder, interactor) } 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 c961c6bd2f2b..ba135dc18d0b 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 @@ -8,17 +8,22 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.tabstray.SelectableTabViewHolder import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM import mozilla.components.browser.thumbnails.loader.ThumbnailLoader +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.components.Components import org.mozilla.fenix.databinding.TabTrayGridItemBinding import org.mozilla.fenix.databinding.TabTrayItemBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.browser.compose.ComposeListViewHolder /** * A [RecyclerView.Adapter] for browser tabs. @@ -27,19 +32,22 @@ import org.mozilla.fenix.tabstray.TabsTrayStore * @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray. * @param store [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 viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. */ class BrowserTabsAdapter( private val context: Context, val interactor: BrowserTrayInteractor, private val store: TabsTrayStore, - override val featureName: String -) : TabsAdapter(interactor), FeatureNameHolder { + override val featureName: String, + internal val viewLifecycleOwner: LifecycleOwner +) : TabsAdapter(interactor), FeatureNameHolder { /** * The layout types for the tabs. */ enum class ViewType(val layoutRes: Int) { LIST(BrowserTabViewHolder.ListViewHolder.LAYOUT_ID), + COMPOSE_LIST(ComposeListViewHolder.LAYOUT_ID), GRID(BrowserTabViewHolder.GridViewHolder.LAYOUT_ID) } @@ -57,23 +65,52 @@ class BrowserTabsAdapter( ViewType.GRID.layoutRes } else -> { - ViewType.LIST.layoutRes + if (FeatureFlags.composeTabsTray) { + ViewType.COMPOSE_LIST.layoutRes + } else { + ViewType.LIST.layoutRes + } } } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractBrowserTabViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectableTabViewHolder { return when (viewType) { - ViewType.GRID.layoutRes -> - BrowserTabViewHolder.GridViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) - else -> - BrowserTabViewHolder.ListViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) + ViewType.COMPOSE_LIST.layoutRes -> + ComposeListViewHolder( + interactor, + store, + selectionHolder, + ComposeView(parent.context), + featureName, + viewLifecycleOwner + ) + else -> { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + if (viewType == ViewType.GRID.layoutRes) { + BrowserTabViewHolder.GridViewHolder( + imageLoader, + interactor, + store, + selectionHolder, + view, + featureName + ) + } else { + BrowserTabViewHolder.ListViewHolder( + imageLoader, + interactor, + store, + selectionHolder, + view, + featureName + ) + } + } } } - override fun onBindViewHolder(holder: AbstractBrowserTabViewHolder, position: Int) { + override fun onBindViewHolder(holder: SelectableTabViewHolder, position: Int) { super.onBindViewHolder(holder, position) var selectedMaskView: View? = null holder.tab?.let { tab -> @@ -103,7 +140,7 @@ class BrowserTabsAdapter( * Over-ridden [onBindViewHolder] that uses the payloads to notify the selected tab how to * display itself. */ - override fun onBindViewHolder(holder: AbstractBrowserTabViewHolder, position: Int, payloads: List) { + override fun onBindViewHolder(holder: SelectableTabViewHolder, position: Int, payloads: List) { if (currentList.isEmpty()) return if (payloads.isEmpty()) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt index a9ce6a8454a9..642161832046 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt @@ -8,12 +8,16 @@ import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.tabstray.TabsTray import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.base.feature.UserInteractionHandler +import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.fenix.GleanMetrics.Collections +import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.selection.SelectionInteractor import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.tabstray.TabsTrayController import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayState.Mode import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.ext.isSelect /** * For interacting with UI that is specifically for [AbstractBrowserTrayList] and other browser @@ -46,6 +50,28 @@ interface BrowserTrayInteractor : SelectionInteractor, UserInte * Recently Closed item is clicked. */ fun onRecentlyClosedClicked() + + /** + * Indicates Play/Pause item is clicked. + */ + fun onMediaClicked(tab: TabSessionState) + + /** + * Handles clicks when multi-selection is enabled. + */ + fun onMultiSelectClicked( + tab: TabSessionState, + holder: SelectionHolder, + source: String? + ) + + /** + * Handles long click events when tab item is clicked. + */ + fun onLongClicked( + tab: TabSessionState, + holder: SelectionHolder + ): Boolean } /** @@ -135,6 +161,47 @@ class DefaultBrowserTrayInteractor( controller.handleNavigateToRecentlyClosed() } + /** + * See [BrowserTrayInteractor.onMultiSelectClicked] + */ + override fun onMediaClicked(tab: TabSessionState) { + controller.handleMediaClicked(tab) + } + + /** + * See [BrowserTrayInteractor.onMultiSelectClicked] + */ + override fun onMultiSelectClicked( + tab: TabSessionState, + holder: SelectionHolder, + source: String? + ) { + val selected = holder.selectedItems + when { + selected.isEmpty() && store.state.mode.isSelect().not() -> { + onTabSelected(tab, source) + } + tab.id in selected.map { it.id } -> deselect(tab) + else -> select(tab) + } + } + + /** + * See [BrowserTrayInteractor.onLongClicked] + */ + override fun onLongClicked( + tab: TabSessionState, + holder: SelectionHolder + ): Boolean { + return if (holder.selectedItems.isEmpty()) { + Collections.longPress.record(NoExtras()) + select(tab) + true + } else { + false + } + } + private fun selectTab(tab: TabSessionState, source: String? = null) { selectTabWrapper.invoke(tab.id, source) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt index 22f40b1e8d8a..820ad9390200 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.tabstray.browser import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -27,6 +28,7 @@ import org.mozilla.fenix.tabstray.TabsTrayStore * @param context [Context] used for various platform interactions or accessing [Components] * @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray. * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. */ @Suppress("TooManyFunctions") class TabGroupAdapter( @@ -34,6 +36,7 @@ class TabGroupAdapter( private val browserTrayInteractor: BrowserTrayInteractor, private val store: TabsTrayStore, override val featureName: String, + private val viewLifecycleOwner: LifecycleOwner ) : ListAdapter(DiffCallback), TabsTray, FeatureNameHolder { /** @@ -44,14 +47,19 @@ class TabGroupAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabGroupViewHolder { val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - return when { - context.components.settings.gridTabView -> { - TabGroupViewHolder(view, HORIZONTAL, browserTrayInteractor, store, selectionHolder) - } - else -> { - TabGroupViewHolder(view, VERTICAL, browserTrayInteractor, store, selectionHolder) - } + val orientation = if (context.components.settings.gridTabView) { + HORIZONTAL + } else { + VERTICAL } + return TabGroupViewHolder( + view, + orientation, + browserTrayInteractor, + store, + selectionHolder, + viewLifecycleOwner + ) } override fun onBindViewHolder(holder: TabGroupViewHolder, position: Int) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt index 256eb92c25be..060f50e90909 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt @@ -8,14 +8,18 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.tabstray.SelectableTabViewHolder import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM import mozilla.components.browser.tabstray.TabsTrayStyling import mozilla.components.browser.thumbnails.loader.ThumbnailLoader +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.databinding.TabTrayGridItemBinding import org.mozilla.fenix.databinding.TabTrayItemBinding @@ -23,6 +27,7 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.home.topsites.dpToPx import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.browser.compose.ComposeListViewHolder import org.mozilla.fenix.tabstray.ext.MIN_COLUMN_WIDTH_DP /** @@ -32,6 +37,7 @@ import org.mozilla.fenix.tabstray.ext.MIN_COLUMN_WIDTH_DP * @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray. * @param store [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 viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. */ class TabGroupListAdapter( private val context: Context, @@ -39,14 +45,15 @@ class TabGroupListAdapter( private val store: TabsTrayStore, private val selectionHolder: SelectionHolder?, private val featureName: String, -) : ListAdapter(DiffCallback) { + private val viewLifecycleOwner: LifecycleOwner +) : ListAdapter(DiffCallback) { private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this) private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage) override fun onCreateViewHolder( parent: ViewGroup, viewType: Int - ): AbstractBrowserTabViewHolder { + ): SelectableTabViewHolder { return when { context.components.settings.gridTabView -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_grid_item, parent, false) @@ -54,13 +61,32 @@ class TabGroupListAdapter( BrowserTabViewHolder.GridViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) } else -> { - val view = LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_item, parent, false) - BrowserTabViewHolder.ListViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) + if (FeatureFlags.composeTabsTray) { + ComposeListViewHolder( + interactor, + store, + selectionHolder, + ComposeView(parent.context), + featureName, + viewLifecycleOwner + ) + } else { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.tab_tray_item, parent, false) + BrowserTabViewHolder.ListViewHolder( + imageLoader, + interactor, + store, + selectionHolder, + view, + featureName + ) + } } } } - override fun onBindViewHolder(holder: AbstractBrowserTabViewHolder, position: Int) { + override fun onBindViewHolder(holder: SelectableTabViewHolder, position: Int) { val tab = getItem(position) val selectedTabId = context.components.core.store.state.selectedTabId holder.bind(tab, tab.id == selectedTabId, TabsTrayStyling(), interactor) @@ -88,7 +114,7 @@ class TabGroupListAdapter( * * N.B: this is a modified version of [BrowserTabsAdapter.onBindViewHolder]. */ - override fun onBindViewHolder(holder: AbstractBrowserTabViewHolder, position: Int, payloads: List) { + override fun onBindViewHolder(holder: SelectableTabViewHolder, position: Int, payloads: List) { val tabs = currentList val selectedTabId = context.components.core.store.state.selectedTabId val selectedIndex = tabs.indexOfFirst { it.id == selectedTabId } @@ -120,7 +146,10 @@ class TabGroupListAdapter( selectedMaskView = listBinding.checkboxInclude.selectedMask } } - holder.showTabIsMultiSelectEnabled(selectedMaskView, it.selectedItems.contains(holder.tab)) + holder.showTabIsMultiSelectEnabled( + selectedMaskView, + it.selectedItems.contains(holder.tab) + ) } } @@ -130,7 +159,11 @@ class TabGroupListAdapter( BrowserTabsAdapter.ViewType.GRID.layoutRes } else -> { - BrowserTabsAdapter.ViewType.LIST.layoutRes + if (FeatureFlags.composeTabsTray) { + BrowserTabsAdapter.ViewType.COMPOSE_LIST.layoutRes + } else { + BrowserTabsAdapter.ViewType.LIST.layoutRes + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt index a6232b5c69dc..f6c5dabf5094 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.tabstray.browser import android.view.View +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.state.selector.normalTabs @@ -25,13 +26,15 @@ import org.mozilla.fenix.tabstray.TrayPagerAdapter * @param interactor the [BrowserTrayInteractor] for tab interactions. * @param store the [TabsTrayStore] instance. * @param selectionHolder the store that holds the currently selected tabs. + * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. */ class TabGroupViewHolder( itemView: View, val orientation: Int, val interactor: BrowserTrayInteractor, val store: TabsTrayStore, - val selectionHolder: SelectionHolder? = null + val selectionHolder: SelectionHolder? = null, + private val viewLifecycleOwner: LifecycleOwner ) : RecyclerView.ViewHolder(itemView) { private val binding = TabGroupItemBinding.bind(itemView) @@ -51,7 +54,8 @@ class TabGroupViewHolder( interactor = interactor, store = store, selectionHolder = selectionHolder, - featureName = TrayPagerAdapter.TAB_GROUP_FEATURE_NAME + featureName = TrayPagerAdapter.TAB_GROUP_FEATURE_NAME, + viewLifecycleOwner ) adapter = groupListAdapter diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt new file mode 100644 index 000000000000..10395927b571 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeAbstractTabViewHolder.kt @@ -0,0 +1,52 @@ +/* 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.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.ViewTreeSavedStateRegistryOwner +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.tabstray.SelectableTabViewHolder +import org.mozilla.fenix.compose.ComposeViewHolder +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * [RecyclerView.ViewHolder] used for Jetpack Compose UI content . + * + * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. + * @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view. + */ +abstract class ComposeAbstractTabViewHolder( + private val composeView: ComposeView, + private val viewLifecycleOwner: LifecycleOwner +) : SelectableTabViewHolder(composeView) { + + /** + * Composable that contains the content for a specific [ComposeViewHolder] implementation. + */ + @Composable + abstract fun Content(tab: TabSessionState) + + /** + * Binds binds the a composable to the [composeView]. + */ + fun bind(tab: TabSessionState) { + composeView.setContent { + FirefoxTheme { + Content(tab) + } + } + + ViewTreeLifecycleOwner.set(composeView, viewLifecycleOwner) + ViewTreeSavedStateRegistryOwner.set( + composeView, + viewLifecycleOwner as SavedStateRegistryOwner + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt new file mode 100644 index 000000000000..131267878f8d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/compose/ComposeListViewHolder.kt @@ -0,0 +1,116 @@ +/* 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.compose + +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.flow.MutableStateFlow +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.tabstray.TabsTray +import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.lib.state.ext.observeAsComposableState +import org.mozilla.fenix.compose.tabstray.TabListItem +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor + +/** + * A Compose ViewHolder implementation for "tab" items with list layout. + * + * @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray. + * @param tabsTrayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that. + * @param selectionHolder [SelectionHolder]<[TabSessionState]> for helping with selecting + * any number of displayed [TabSessionState]s. + * @param composeItemView that displays a "tab". + * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. + */ +class ComposeListViewHolder( + private val interactor: BrowserTrayInteractor, + val tabsTrayStore: TabsTrayStore, + val selectionHolder: SelectionHolder? = null, + composeItemView: ComposeView, + private val featureName: String, + viewLifecycleOwner: LifecycleOwner, +) : ComposeAbstractTabViewHolder(composeItemView, viewLifecycleOwner) { + + private var delegate: TabsTray.Delegate? = null + + override var tab: TabSessionState? = null + private val isMultiSelectionSelected = MutableStateFlow(false) + private val isSelectedTab = MutableStateFlow(false) + + override fun bind( + tab: TabSessionState, + isSelected: Boolean, + styling: TabsTrayStyling, + delegate: TabsTray.Delegate + ) { + this.tab = tab + this.delegate = delegate + isSelectedTab.value = isSelected + bind(tab) + } + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + isSelectedTab.value = showAsSelected + } + + override fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) { + isMultiSelectionSelected.value = isSelected + } + + private fun onCloseClicked(tab: TabSessionState) { + delegate?.onTabClosed(tab, featureName) + } + + private fun onMediaClicked(tab: TabSessionState) { + interactor.onMediaClicked(tab) + } + + private fun onClick(tab: TabSessionState) { + val holder = selectionHolder + if (holder != null) { + interactor.onMultiSelectClicked(tab, holder, featureName) + } else { + interactor.onTabSelected(tab, featureName) + } + } + + private fun onLongClick(tab: TabSessionState) { + val holder = selectionHolder ?: return + interactor.onLongClicked(tab, holder) + } + + @Composable + override fun Content(tab: TabSessionState) { + val isSelectModeActive = tabsTrayStore.observeAsComposableState { + state -> + state.mode is TabsTrayState.Mode.Select + }.value ?: false + val isSelectedTabState by isSelectedTab.collectAsState() + val isMultiSelectionSelectedState by isMultiSelectionSelected.collectAsState() + + TabListItem( + tab = tab, + isSelected = isSelectedTabState, + multiSelectionEnabled = isSelectModeActive, + multiSelectionSelected = isMultiSelectionSelectedState, + onCloseClick = ::onCloseClicked, + onMediaClick = ::onMediaClicked, + onClick = ::onClick, + onLongClick = ::onLongClick, + ) + } + + companion object { + val LAYOUT_ID = View.generateViewId() + } +} diff --git a/app/src/main/res/drawable/media_state_pause_vector.xml b/app/src/main/res/drawable/media_state_pause_vector.xml new file mode 100644 index 000000000000..b0ea098bf96a --- /dev/null +++ b/app/src/main/res/drawable/media_state_pause_vector.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/media_state_play_vector.xml b/app/src/main/res/drawable/media_state_play_vector.xml new file mode 100644 index 000000000000..c7873b1797bb --- /dev/null +++ b/app/src/main/res/drawable/media_state_play_vector.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt index cda5e9360c2a..7a6a9e786f0e 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt @@ -85,7 +85,8 @@ class TabsTrayFragmentTest { fragment._tabsTrayDialogBinding = tabsTrayDialogBinding fragment._fabButtonBinding = fabButtonBinding every { fragment.context } returns context - every { fragment.view } returns view + every { fragment.context } returns context + every { fragment.viewLifecycleOwner } returns mockk() } @Test diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolderTest.kt index cb767e766dbe..1344dc6d14c5 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTabViewHolderTest.kt @@ -77,12 +77,12 @@ class AbstractBrowserTabViewHolderTest { interactor ) - holder.bind(createTab(url = "url"), false, mockk(), mockk()) + val tab = createTab(url = "url") + holder.bind(tab, false, mockk(), mockk()) holder.itemView.performClick() - verify { interactor.onTabSelected(any(), holder.featureName) } - assertTrue(selectionHolder.invoked) + verify { interactor.onMultiSelectClicked(tab, any(), holder.featureName) } } @Test diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt index 336d4f62352b..30d9d0157a66 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt @@ -33,7 +33,7 @@ class BrowserTabsAdapterTest { @Test fun `WHEN bind with payloads is called THEN update the holder`() { every { testContext.components.core.thumbnailStorage } returns mockk() - val adapter = BrowserTabsAdapter(context, interactor, store, "Test") + val adapter = BrowserTabsAdapter(context, interactor, store, "Test", mockk()) val holder = mockk(relaxed = true) adapter.updateTabs( @@ -59,7 +59,7 @@ class BrowserTabsAdapterTest { every { testContext.components.core.store } returns BrowserStore() every { testContext.components.analytics } returns mockk(relaxed = true) every { testContext.components.settings } returns mockk(relaxed = true) - val adapter = BrowserTabsAdapter(context, interactor, store, "Test") + val adapter = BrowserTabsAdapter(context, interactor, store, "Test", mockk()) val binding = TabTrayItemBinding.inflate(LayoutInflater.from(testContext)) val holder = spyk( BrowserTabViewHolder.ListViewHolder( diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt index 8fa87059db04..093dea302de7 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt @@ -35,7 +35,9 @@ class AbstractBrowserPageViewHolderTest { every { testContext.components.core.thumbnailStorage } returns mockk() every { testContext.components.settings } returns mockk(relaxed = true) } - val adapter = BrowserTabsAdapter(testContext, browserTrayInteractor, tabsTrayStore, "Test") + + val adapter = + BrowserTabsAdapter(testContext, browserTrayInteractor, tabsTrayStore, "Test", mockk()) @Test fun `WHEN tabs inserted THEN show tray`() {