Commit 53f44cec authored by MozLando's avatar MozLando
Browse files

Merge #7795



7795: Issue #6429: Add container page action icon in the browser toolbar r=Amejia481 a=gabrielluong
Co-authored-by: default avatarGabriel Luong <gabriel.luong@gmail.com>
parents c93fc20a 118033b8
/* 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 mozilla.components.feature.toolbar
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat.getColor
import mozilla.components.browser.state.state.ContainerState
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
import mozilla.components.concept.toolbar.Toolbar.Action
import mozilla.components.support.base.android.Padding
import mozilla.components.support.ktx.android.view.setPadding
import mozilla.components.support.utils.DrawableUtils.loadAndTintDrawable
/**
* An action button that represents a container to be added to the toolbar.
*
* @param container Associated [ContainerState]'s icon and color to render in the toolbar.
* @param padding A optional custom padding.
* @param listener A optional callback that will be invoked whenever the button is pressed.
*/
class ContainerToolbarAction(
internal val container: ContainerState,
internal val padding: Padding? = null,
private var listener: (() -> Unit)? = null
) : Action {
override fun createView(parent: ViewGroup): View {
val rootView = LayoutInflater.from(parent.context)
.inflate(R.layout.mozac_feature_toolbar_container_action_layout, parent, false)
listener?.let { clickListener ->
rootView.setOnClickListener { clickListener.invoke() }
}
padding?.let { rootView.setPadding(it) }
return rootView
}
override fun bind(view: View) {
val imageView = view.findViewById<ImageView>(R.id.container_action_image)
imageView.contentDescription = container.name
imageView.setImageDrawable(getIcon(view.context, container))
}
@Suppress("ComplexMethod")
internal fun getIcon(context: Context, container: ContainerState): Drawable {
@ColorInt val tint = getTint(context, container.color)
return when (container.icon) {
Icon.FINGERPRINT -> loadAndTintDrawable(context, R.drawable.mozac_ic_fingerprint, tint)
Icon.BRIEFCASE -> loadAndTintDrawable(context, R.drawable.mozac_ic_briefcase, tint)
Icon.DOLLAR -> loadAndTintDrawable(context, R.drawable.mozac_ic_dollar, tint)
Icon.CART -> loadAndTintDrawable(context, R.drawable.mozac_ic_cart, tint)
Icon.CIRCLE -> loadAndTintDrawable(context, R.drawable.mozac_ic_circle, tint)
Icon.GIFT -> loadAndTintDrawable(context, R.drawable.mozac_ic_gift, tint)
Icon.VACATION -> loadAndTintDrawable(context, R.drawable.mozac_ic_vacation, tint)
Icon.FOOD -> loadAndTintDrawable(context, R.drawable.mozac_ic_food, tint)
Icon.FRUIT -> loadAndTintDrawable(context, R.drawable.mozac_ic_fruit, tint)
Icon.PET -> loadAndTintDrawable(context, R.drawable.mozac_ic_pet, tint)
Icon.TREE -> loadAndTintDrawable(context, R.drawable.mozac_ic_tree, tint)
Icon.CHILL -> loadAndTintDrawable(context, R.drawable.mozac_ic_chill, tint)
Icon.FENCE -> loadAndTintDrawable(context, R.drawable.mozac_ic_fence, tint)
}
}
private fun getTint(context: Context, color: Color): Int {
return when (color) {
Color.BLUE -> getColor(context, R.color.mozac_feature_toolbar_container_blue)
Color.TURQUOISE -> getColor(context, R.color.mozac_feature_toolbar_container_turquoise)
Color.GREEN -> getColor(context, R.color.mozac_feature_toolbar_container_green)
Color.YELLOW -> getColor(context, R.color.mozac_feature_toolbar_container_yellow)
Color.ORANGE -> getColor(context, R.color.mozac_feature_toolbar_container_orange)
Color.RED -> getColor(context, R.color.mozac_feature_toolbar_container_red)
Color.PINK -> getColor(context, R.color.mozac_feature_toolbar_container_pink)
Color.PURPLE -> getColor(context, R.color.mozac_feature_toolbar_container_purple)
Color.TOOLBAR -> getColor(context, R.color.mozac_feature_toolbar_container_toolbar)
}
}
}
/* 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 mozilla.components.feature.toolbar
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
/**
* Container toolbar implementation that updates the toolbar with the container page action
* whenever the selected tab changes.
*/
class ContainerToolbarFeature(
private val toolbar: Toolbar,
private var store: BrowserStore
) : LifecycleAwareFeature {
private var containerPageAction: ContainerToolbarAction? = null
private var scope: CoroutineScope? = null
init {
renderContainerAction(store.state)
}
override fun start() {
scope = store.flowScoped { flow ->
flow.ifChanged { it.selectedTab }
.collect { state ->
renderContainerAction(state, state.selectedTab)
}
}
}
override fun stop() {
scope?.cancel()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun renderContainerAction(state: BrowserState, tab: SessionState? = null) {
val containerState = state.containers[tab?.contextId]
if (containerState == null) {
// Entered a normal tab from a container tab. Remove the old container
// page action.
containerPageAction?.let {
toolbar.removePageAction(it)
toolbar.invalidateActions()
containerPageAction = null
}
return
} else if (containerState == containerPageAction?.container) {
// Do nothing since we're still in a tab with same container.
return
}
// Remove the old container page action and create a new action with the new
// container state.
containerPageAction?.let {
toolbar.removePageAction(it)
containerPageAction = null
}
containerPageAction = ContainerToolbarAction(containerState).also { action ->
toolbar.addPageAction(action)
toolbar.invalidateActions()
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?android:selectableItemBackgroundBorderless"
tools:ignore="Overdraw">
<ImageView
android:id="@+id/container_action_image"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:importantForAccessibility="no" />
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<resources>
<color name="mozac_feature_toolbar_container_blue">#37adff</color>
<color name="mozac_feature_toolbar_container_turquoise">#00c79a</color>
<color name="mozac_feature_toolbar_container_green">#51cd00</color>
<color name="mozac_feature_toolbar_container_yellow">#ffcb00</color>
<color name="mozac_feature_toolbar_container_orange">#ff9f00</color>
<color name="mozac_feature_toolbar_container_red">#ff613d</color>
<color name="mozac_feature_toolbar_container_pink">#ff4bda</color>
<color name="mozac_feature_toolbar_container_purple">#af51f5</color>
<color name="mozac_feature_toolbar_container_toolbar">#7c7c7d</color>
</resources>
/* 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 mozilla.components.feature.toolbar
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.state.state.ContainerState
import mozilla.components.support.base.android.Padding
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class ContainerToolbarActionTest {
// Test container
private val container = ContainerState(
contextId = "contextId",
name = "Personal",
color = ContainerState.Color.GREEN,
icon = ContainerState.Icon.CART
)
@Test
fun bind() {
val imageView: ImageView = spy(ImageView(testContext))
val view: View = mock()
whenever(view.findViewById<ImageView>(R.id.container_action_image)).thenReturn(imageView)
whenever(view.context).thenReturn(testContext)
val action = spy(ContainerToolbarAction(container))
action.bind(view)
verify(imageView).contentDescription = container.name
verify(imageView).setImageDrawable(any())
}
@Test
fun createView() {
var listenerWasClicked = false
val action = ContainerToolbarAction(container, padding = Padding(1, 2, 3, 4)) {
listenerWasClicked = true
}
val rootView = action.createView(LinearLayout(testContext))
rootView.performClick()
assertTrue(listenerWasClicked)
assertEquals(rootView.paddingLeft, 1)
assertEquals(rootView.paddingTop, 2)
assertEquals(rootView.paddingRight, 3)
assertEquals(rootView.paddingBottom, 4)
}
}
/* 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 mozilla.components.feature.toolbar
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContainerState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class ContainerToolbarFeatureTest {
private val testDispatcher = TestCoroutineDispatcher()
// Test container
private val container = ContainerState(
contextId = "1",
name = "Personal",
color = ContainerState.Color.GREEN,
icon = ContainerState.Icon.FINGERPRINT
)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
@Test
fun `render a container action from browser state`() {
val toolbar: Toolbar = mock()
val store = spy(
BrowserStore(
BrowserState(
tabs = listOf(
createTab("https://www.example.org", id = "tab1", contextId = "1")
),
selectedTabId = "tab1",
containers = mapOf(
container.contextId to container
)
)
)
)
val containerToolbarFeature = getContainerToolbarFeature(toolbar, store)
verify(store).observeManually(any())
verify(containerToolbarFeature).renderContainerAction(any(), any())
val pageActionCaptor = argumentCaptor<ContainerToolbarAction>()
verify(toolbar).addPageAction(pageActionCaptor.capture())
assertEquals(container, pageActionCaptor.value.container)
}
@Test
fun `remove container page action when selecting a normal tab`() {
val toolbar: Toolbar = mock()
val store = spy(
BrowserStore(
BrowserState(
tabs = listOf(
createTab("https://www.example.org", id = "tab1", contextId = "1"),
createTab("https://www.mozilla.org", id = "tab2")
),
selectedTabId = "tab1",
containers = mapOf(
container.contextId to container
)
)
)
)
val containerToolbarFeature = getContainerToolbarFeature(toolbar, store)
store.dispatch(TabListAction.SelectTabAction("tab2")).joinBlocking()
testDispatcher.advanceUntilIdle()
verify(store).observeManually(any())
verify(containerToolbarFeature, times(2)).renderContainerAction(any(), any())
verify(toolbar).removePageAction(any())
}
private fun getContainerToolbarFeature(
toolbar: Toolbar = mock(),
store: BrowserStore = BrowserStore()
): ContainerToolbarFeature {
val containerToolbarFeature = spy(ContainerToolbarFeature(toolbar, store))
containerToolbarFeature.start()
return containerToolbarFeature
}
}
......@@ -27,6 +27,10 @@ permalink: /changelog/
* **lib-push-firebase**
* Removed non-essential dependency on `com.google.firebase:firebase-core`.
* **feature-toolbar**
* Added `ContainerToolbarFeature` to update the toolbar with the container page action whenever
the selected tab changes.
# 56.0.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v55.0.0...v56.0.0)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment