diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecorator.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecorator.kt
index 56e3950d9..b2e6ad625 100644
--- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecorator.kt
+++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecorator.kt
@@ -74,7 +74,7 @@ sealed class RecentTabViewDecorator {
val context = itemView.context
itemView.background =
- AppCompatResources.getDrawable(context, R.drawable.home_list_row_background)
+ AppCompatResources.getDrawable(context, R.drawable.card_list_row_background)
return itemView
}
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt
index 83a44e619..1323be2bb 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt
@@ -12,6 +12,7 @@ import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.InactiveFooterItemBinding
import org.mozilla.fenix.databinding.InactiveRecentlyClosedItemBinding
+import org.mozilla.fenix.databinding.InactiveHeaderItemBinding
import org.mozilla.fenix.databinding.InactiveTabListItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
@@ -22,9 +23,32 @@ import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneMonth
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneWeek
sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
- class HeaderHolder(itemView: View) : InactiveTabViewHolder(itemView) {
+
+ class HeaderHolder(
+ itemView: View,
+ interactor: InactiveTabsInteractor
+ ) : InactiveTabViewHolder(itemView) {
+
+ private val binding = InactiveHeaderItemBinding.bind(itemView)
+
+ init {
+ itemView.apply {
+ isActivated = InactiveTabsState.isExpanded
+
+ setOnClickListener {
+ val newState = !it.isActivated
+
+ interactor.onHeaderClicked(newState)
+
+ it.isActivated = newState
+ binding.chevron.rotation = ROTATION_DEGREE
+ }
+ }
+ }
+
companion object {
const val LAYOUT_ID = R.layout.inactive_header_item
+ private const val ROTATION_DEGREE = 180F
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt
index 60e0d015e..165f6e333 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt
@@ -39,12 +39,14 @@ class InactiveTabsAdapter(
delegate: Observable = ObserverRegistry()
) : Adapter(DiffCallback), TabsTray, Observable by delegate {
+ internal lateinit var inactiveTabsInteractor: InactiveTabsInteractor
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InactiveTabViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(viewType, parent, false)
return when (viewType) {
- HeaderHolder.LAYOUT_ID -> HeaderHolder(view)
+ HeaderHolder.LAYOUT_ID -> HeaderHolder(view, inactiveTabsInteractor)
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor)
FooterHolder.LAYOUT_ID -> FooterHolder(view)
RecentlyClosedHolder.LAYOUT_ID -> RecentlyClosedHolder(view, browserTrayInteractor)
@@ -81,12 +83,18 @@ class InactiveTabsAdapter(
}
override fun updateTabs(tabs: Tabs) {
+ // Early return with an empty list to remove the header/footer items.
if (tabs.list.isEmpty()) {
- // Early return with an empty list to remove the header/footer items.
submitList(emptyList())
return
}
+ // If we have items, but we should be in a collapsed state.
+ if (!InactiveTabsState.isExpanded) {
+ submitList(listOf(Item.Header))
+ return
+ }
+
val items = tabs.list.map { Item.Tab(it) }
val footer = Item.Footer(context.autoCloseInterval)
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt
new file mode 100644
index 000000000..16f9e94b6
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt
@@ -0,0 +1,28 @@
+/* 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 mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.tabstray.TabsTray
+import mozilla.components.feature.tabs.ext.toTabs
+
+class InactiveTabsController(
+ private val browserStore: BrowserStore,
+ private val tabFilter: (TabSessionState) -> Boolean,
+ private val tray: TabsTray
+) {
+ /**
+ * Updates the inactive card to be expanded to display all the tabs, or collapsed with only
+ * the title showing.
+ */
+ fun updateCardExpansion(isExpanded: Boolean) {
+ InactiveTabsState.isExpanded = isExpanded
+
+ val tabs = browserStore.state.toTabs { tabFilter.invoke(it) }
+
+ tray.updateTabs(tabs)
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt
new file mode 100644
index 000000000..08d01debb
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt
@@ -0,0 +1,26 @@
+/* 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
+
+interface InactiveTabsInteractor {
+ fun onHeaderClicked(activated: Boolean)
+}
+
+class DefaultInactiveTabsInteractor(
+ private val controller: InactiveTabsController
+) : InactiveTabsInteractor {
+ override fun onHeaderClicked(activated: Boolean) {
+ controller.updateCardExpansion(activated)
+ }
+}
+
+/**
+ * An experimental state holder for [InactiveTabsAdapter] that lives at the application lifetime.
+ *
+ * TODO This should be replaced with the AppStore.
+ */
+object InactiveTabsState {
+ var isExpanded = true
+}
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt
index 21504c8a7..9e12a46a4 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt
@@ -7,6 +7,7 @@ package org.mozilla.fenix.tabstray.browser
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.ConcatAdapter
+import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.FeatureFlags
@@ -53,20 +54,30 @@ class NormalBrowserTrayList @JvmOverloads constructor(
)
}
+ /**
+ * NB: The setup for this feature is a bit complicated without a better dependency injection
+ * solution to scope it down to just this view.
+ */
private val inactiveFeature by lazy {
- val tabsAdapter = concatAdapter.inactiveTabsAdapter
+ val store = context.components.core.store
+ val tabFilter: (TabSessionState) -> Boolean = filter@{
+ if (!FeatureFlags.inactiveTabs) {
+ return@filter false
+ }
+ it.isNormalTabInactive(maxActiveTime)
+ }
+ val tabsAdapter = concatAdapter.inactiveTabsAdapter.apply {
+ inactiveTabsInteractor = DefaultInactiveTabsInteractor(
+ InactiveTabsController(store, tabFilter, this)
+ )
+ }
TabsFeature(
tabsAdapter,
- context.components.core.store,
+ store,
selectTabUseCase,
removeTabUseCase,
- { state ->
- if (!FeatureFlags.inactiveTabs) {
- return@TabsFeature false
- }
- state.isNormalTabInactive(maxActiveTime)
- },
+ tabFilter,
{}
)
}
diff --git a/app/src/main/res/drawable/home_list_row_background.xml b/app/src/main/res/drawable/card_list_row_background.xml
similarity index 100%
rename from app/src/main/res/drawable/home_list_row_background.xml
rename to app/src/main/res/drawable/card_list_row_background.xml
diff --git a/app/src/main/res/layout/collection_home_list_row.xml b/app/src/main/res/layout/collection_home_list_row.xml
index 63e5afbbd..22957cc81 100644
--- a/app/src/main/res/layout/collection_home_list_row.xml
+++ b/app/src/main/res/layout/collection_home_list_row.xml
@@ -8,7 +8,7 @@
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="12dp"
- android:background="@drawable/home_list_row_background"
+ android:background="@drawable/card_list_row_background"
android:clickable="true"
android:clipToPadding="false"
android:elevation="@dimen/home_item_elevation"
diff --git a/app/src/main/res/layout/inactive_header_item.xml b/app/src/main/res/layout/inactive_header_item.xml
index 1bfee4139..2af0c5d37 100644
--- a/app/src/main/res/layout/inactive_header_item.xml
+++ b/app/src/main/res/layout/inactive_header_item.xml
@@ -9,7 +9,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
- android:background="@drawable/rounded_top_corners"
+ android:background="@drawable/card_list_row_background"
android:clickable="false"
android:clipToPadding="false"
android:focusable="true"
@@ -31,4 +31,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Inactive tabs" />
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecoratorTest.kt b/app/src/test/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecoratorTest.kt
index d4f725152..ee359f6aa 100644
--- a/app/src/test/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecoratorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewDecoratorTest.kt
@@ -63,7 +63,7 @@ class RecentTabViewDecoratorTest {
RecentTabViewDecorator.SingleTabDecoration(view)
verify { view.background = drawable }
- assertEquals(R.drawable.home_list_row_background, drawableResCaptor.captured)
+ assertEquals(R.drawable.card_list_row_background, drawableResCaptor.captured)
} finally {
unmockkStatic(AppCompatResources::class)
}
diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt
new file mode 100644
index 000000000..36c27520b
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt
@@ -0,0 +1,22 @@
+/* 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 io.mockk.mockk
+import io.mockk.verify
+import org.junit.Test
+
+class DefaultInactiveTabsInteractorTest {
+
+ @Test
+ fun `WHEN onHeaderClicked THEN updateCardExpansion`() {
+ val controller: InactiveTabsController = mockk(relaxed = true)
+ val interactor = DefaultInactiveTabsInteractor(controller)
+
+ interactor.onHeaderClicked(true)
+
+ verify { controller.updateCardExpansion(true) }
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt
new file mode 100644
index 000000000..d33dc9626
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt
@@ -0,0 +1,33 @@
+/* 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.view.LayoutInflater
+import io.mockk.mockk
+import io.mockk.verify
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder
+
+@RunWith(FenixRobolectricTestRunner::class)
+class InactiveTabViewHolderTest {
+ @Test
+ fun `HeaderHolder - WHEN clicked THEN notify the interactor`() {
+ val view = LayoutInflater.from(testContext).inflate(HeaderHolder.LAYOUT_ID, null)
+ val interactor: InactiveTabsInteractor = mockk(relaxed = true)
+ val viewHolder = HeaderHolder(view, interactor)
+
+ val initialActivatedState = view.isActivated
+
+ viewHolder.itemView.performClick()
+
+ verify { interactor.onHeaderClicked(any()) }
+
+ assertEquals(!initialActivatedState, view.isActivated)
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt
new file mode 100644
index 000000000..207f619fa
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.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.browser
+
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.tabstray.Tabs
+import mozilla.components.concept.tabstray.TabsTray
+import org.junit.Assert.assertEquals
+import mozilla.components.browser.state.state.createTab as createTabState
+import org.junit.Test
+
+class InactiveTabsControllerTest {
+ @Test
+ fun `WHEN expanded THEN notify filtered card`() {
+ val filter: (TabSessionState) -> Boolean = { !it.content.private }
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTabState("https://mozilla.org", id = "1"),
+ createTabState("https://firefox.com", id = "2"),
+ createTabState("https://getpocket.com", id = "3", private = true)
+ )
+ )
+ )
+ val tray: TabsTray = mockk(relaxed = true)
+ val tabsSlot = slot()
+ val controller = InactiveTabsController(store, filter, tray)
+
+ controller.updateCardExpansion(true)
+
+ verify { tray.updateTabs(capture(tabsSlot)) }
+
+ assertEquals(2, tabsSlot.captured.list.size)
+ assertEquals("1", tabsSlot.captured.list.first().id)
+ }
+}