Commit 3c4477d0 authored by Gabriel Luong's avatar Gabriel Luong
Browse files

Issue #7529: Add ContainerState to BrowserState

parent 27dd1be4
......@@ -6,6 +6,7 @@ package mozilla.components.browser.state.action
import android.graphics.Bitmap
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContainerState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.EngineState
......@@ -588,3 +589,18 @@ sealed class DownloadAction : BrowserAction() {
*/
data class UpdateQueuedDownloadAction(val download: DownloadState) : DownloadAction()
}
/**
* [BrowserAction] implementations related to updating [BrowserState.containers]
*/
sealed class ContainerAction : BrowserAction() {
/**
* Updates [BrowserState.containers] to register the given added [container].
*/
data class AddContainerAction(val container: ContainerState) : ContainerAction()
/**
* Removes all state of the removed container from [BrowserState.containers].
*/
data class RemoveContainerAction(val contextId: String) : ContainerAction()
}
......@@ -5,6 +5,7 @@
package mozilla.components.browser.state.reducer
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContainerAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.DownloadAction
......@@ -31,6 +32,7 @@ import mozilla.components.lib.state.Action
internal object BrowserStateReducer {
fun reduce(state: BrowserState, action: BrowserAction): BrowserState {
return when (action) {
is ContainerAction -> ContainerReducer.reduce(state, action)
is ContentAction -> ContentStateReducer.reduce(state, action)
is CustomTabListAction -> CustomTabListReducer.reduce(state, action)
is EngineAction -> EngineStateReducer.reduce(state, action)
......
/* 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.browser.state.reducer
import mozilla.components.browser.state.action.ContainerAction
import mozilla.components.browser.state.state.BrowserState
internal object ContainerReducer {
fun reduce(state: BrowserState, action: ContainerAction): BrowserState {
return when (action) {
is ContainerAction.AddContainerAction -> {
val existingContainer = state.containers[action.container.contextId]
if (existingContainer == null) {
state.copy(
containers = state.containers + (action.container.contextId to action.container)
)
} else {
state
}
}
is ContainerAction.RemoveContainerAction -> {
state.copy(
containers = state.containers - action.contextId
)
}
}
}
}
......@@ -13,7 +13,8 @@ import mozilla.components.lib.state.State
* @property tabs the list of open tabs, defaults to an empty list.
* @property selectedTabId the ID of the currently selected (active) tab.
* @property customTabs the list of custom tabs, defaults to an empty list.
* @property extensions A map of extension ids and web extensions of all installed web extensions.
* @property containers A map of [SessionState.contextId] and their respective container [ContainerState].
* @property extensions A map of extension IDs and web extensions of all installed web extensions.
* The extensions here represent the default values for all [BrowserState.extensions] and can
* be overridden per [SessionState].
* @property media The state of all media elements and playback states for all tabs.
......@@ -23,6 +24,7 @@ data class BrowserState(
val tabs: List<TabSessionState> = emptyList(),
val selectedTabId: String? = null,
val customTabs: List<CustomTabSessionState> = emptyList(),
val containers: Map<String, ContainerState> = emptyMap(),
val extensions: Map<String, WebExtensionState> = emptyMap(),
val media: MediaState = MediaState(),
val queuedDownloads: Map<Long, DownloadState> = emptyMap()
......
/* 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.browser.state.state
/**
* Value type that represents the state of a container also known as a contextual identity.
*
* @property contextId The session context ID also known as cookie store ID for the container.
* @property name Name of the container.
* @property color The color for the container. This can be shown in tabs belonging to this container.
* @property icon The icon for the container.
*/
data class ContainerState(
val contextId: String,
val name: String,
val color: Color,
val icon: Icon
) {
/**
* Enum of container color.
*/
enum class Color(val color: String) {
BLUE("blue"),
TURQUOISE("turquoise"),
GREEN("green"),
YELLOW("yellow"),
ORANGE("orange"),
RED("red"),
PINK("pink"),
PURPLE("purple"),
TOOLBAR("toolbar")
}
/**
* Enum of container icon.
*/
enum class Icon(val icon: String) {
FINGERPRINT("fingerprint"),
BRIEFCASE("briefcase"),
DOLLAR("dollar"),
CART("cart"),
CIRCLE("circle"),
GIFT("gift"),
VACATION("vacation"),
FOOD("food"),
FRUIT("fruit"),
PET("pet"),
TREE("tree"),
CHILL("chill"),
FENCE("fence")
}
}
typealias Container = ContainerState
/* 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.browser.state.action
import mozilla.components.browser.state.state.ContainerState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
class ContainerActionTest {
@Test
fun `AddContainerAction - Adds a container to the BrowserState containers`() {
val store = BrowserStore()
assertTrue(store.state.containers.isEmpty())
val container = ContainerState(
contextId = "contextId",
name = "Personal",
color = ContainerState.Color.GREEN,
icon = ContainerState.Icon.CART
)
store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking()
assertFalse(store.state.containers.isEmpty())
assertEquals(container, store.state.containers.values.first())
val state = store.state
store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking()
assertSame(state, store.state)
}
@Test
fun `RemoveContainerAction - Removes a container from the BrowserState containers`() {
val store = BrowserStore()
assertTrue(store.state.containers.isEmpty())
val container1 = ContainerState(
contextId = "1",
name = "Personal",
color = ContainerState.Color.BLUE,
icon = ContainerState.Icon.BRIEFCASE
)
val container2 = ContainerState(
contextId = "2",
name = "Shopping",
color = ContainerState.Color.GREEN,
icon = ContainerState.Icon.CIRCLE
)
store.dispatch(ContainerAction.AddContainerAction(container1)).joinBlocking()
store.dispatch(ContainerAction.AddContainerAction(container2)).joinBlocking()
assertFalse(store.state.containers.isEmpty())
assertEquals(container1, store.state.containers.values.first())
assertEquals(container2, store.state.containers.values.last())
store.dispatch(ContainerAction.RemoveContainerAction(container1.contextId)).joinBlocking()
assertEquals(1, store.state.containers.size)
assertEquals(container2, store.state.containers.values.first())
}
}
......@@ -38,6 +38,7 @@ android {
}
dependencies {
implementation project(':browser-state')
implementation project(':support-ktx')
implementation project(':support-base')
......
......@@ -12,6 +12,9 @@ import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.state.state.Container
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
import mozilla.components.feature.containers.db.ContainerDatabase
import org.junit.After
import org.junit.Assert.assertEquals
......@@ -50,25 +53,27 @@ class ContainerStorageTest {
@Test
fun testAddingContainer() = runBlockingTest {
storage.addContainer("Personal", "red", "fingerprint")
storage.addContainer("Shopping", "blue", "cart")
storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
val containers = getAllContainers()
assertEquals(2, containers.size)
assertEquals("1", containers[0].contextId)
assertEquals("Personal", containers[0].name)
assertEquals("red", containers[0].color)
assertEquals("fingerprint", containers[0].icon)
assertEquals(Color.RED, containers[0].color)
assertEquals(Icon.FINGERPRINT, containers[0].icon)
assertEquals("2", containers[1].contextId)
assertEquals("Shopping", containers[1].name)
assertEquals("blue", containers[1].color)
assertEquals("cart", containers[1].icon)
assertEquals(Color.BLUE, containers[1].color)
assertEquals(Icon.CART, containers[1].icon)
}
@Test
fun testRemovingContainers() = runBlockingTest {
storage.addContainer("Personal", "red", "fingerprint")
storage.addContainer("Shopping", "blue", "cart")
storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
getAllContainers().let { containers ->
assertEquals(2, containers.size)
......@@ -79,16 +84,17 @@ class ContainerStorageTest {
getAllContainers().let { containers ->
assertEquals(1, containers.size)
assertEquals("2", containers[0].contextId)
assertEquals("Shopping", containers[0].name)
assertEquals("blue", containers[0].color)
assertEquals("cart", containers[0].icon)
assertEquals(Color.BLUE, containers[0].color)
assertEquals(Icon.CART, containers[0].icon)
}
}
@Test
fun testGettingContainers() = runBlockingTest {
storage.addContainer("Personal", "red", "fingerprint")
storage.addContainer("Shopping", "blue", "cart")
storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
val containers = storage.getContainers().first()
......@@ -96,15 +102,17 @@ class ContainerStorageTest {
assertEquals(2, containers.size)
with(containers[0]) {
assertEquals("1", contextId)
assertEquals("Personal", name)
assertEquals("red", color)
assertEquals("fingerprint", icon)
assertEquals(Color.RED, color)
assertEquals(Icon.FINGERPRINT, icon)
}
with(containers[1]) {
assertEquals("2", contextId)
assertEquals("Shopping", name)
assertEquals("blue", color)
assertEquals("cart", icon)
assertEquals(Color.BLUE, color)
assertEquals(Icon.CART, icon)
}
}
......
......@@ -11,6 +11,8 @@ import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
......@@ -51,8 +53,8 @@ class ContainerDaoTest {
ContainerEntity(
contextId = UUID.randomUUID().toString(),
name = "Personal",
color = "red",
icon = "fingerprint"
color = Color.RED,
icon = Icon.FINGERPRINT
)
containerDao.insertContainer(container)
......@@ -73,15 +75,15 @@ class ContainerDaoTest {
ContainerEntity(
contextId = UUID.randomUUID().toString(),
name = "Personal",
color = "red",
icon = "fingerprint"
color = Color.RED,
icon = Icon.FINGERPRINT
)
val container2 =
ContainerEntity(
contextId = UUID.randomUUID().toString(),
name = "Shopping",
color = "blue",
icon = "cart"
color = Color.BLUE,
icon = Icon.CART
)
containerDao.insertContainer(container1)
......
/* 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.containers
/**
* A container also known as a contextual identity.
*/
interface Container {
/**
* The session context ID also known as cookie store ID for the container.
*/
val contextId: String
/**
* Name of the container.
*/
val name: String
/**
* The color for the container. This can be shown in tabs belonging to this container.
*/
val color: String
/**
* The name of an icon for the container.
*/
val icon: String
}
......@@ -9,15 +9,18 @@ import androidx.annotation.VisibleForTesting
import androidx.paging.DataSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import mozilla.components.feature.containers.adapter.ContainerAdapter
import mozilla.components.browser.state.state.Container
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
import mozilla.components.feature.containers.db.ContainerDatabase
import mozilla.components.feature.containers.db.ContainerEntity
import mozilla.components.feature.containers.db.toContainerEntity
import java.util.UUID
/**
* A storage implementation for organizing containers (contextual identities).
*/
class ContainerStorage(context: Context) {
internal class ContainerStorage(context: Context) {
@VisibleForTesting
internal var database: Lazy<ContainerDatabase> =
......@@ -27,10 +30,15 @@ class ContainerStorage(context: Context) {
/**
* Adds a new [Container].
*/
suspend fun addContainer(name: String, color: String, icon: String) {
database.value.containerDao().insertContainer(
suspend fun addContainer(
contextId: String = UUID.randomUUID().toString(),
name: String,
color: Color,
icon: Icon
) {
containerDao.insertContainer(
ContainerEntity(
contextId = UUID.randomUUID().toString(),
contextId = contextId,
name = name,
color = color,
icon = icon
......@@ -43,7 +51,7 @@ class ContainerStorage(context: Context) {
*/
fun getContainers(): Flow<List<Container>> {
return containerDao.getContainers().map { list ->
list.map { entity -> ContainerAdapter(entity) }
list.map { entity -> entity.toContainer() }
}
}
......@@ -53,16 +61,13 @@ class ContainerStorage(context: Context) {
fun getContainersPaged(): DataSource.Factory<Int, Container> = containerDao
.getContainersPaged()
.map { entity ->
ContainerAdapter(
entity
)
entity.toContainer()
}
/**
* Removes the given [Container].
*/
suspend fun removeContainer(container: Container) {
val containerEntity = (container as ContainerAdapter).entity
containerDao.deleteContainer(containerEntity)
containerDao.deleteContainer(container.toContainerEntity())
}
}
/* 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.containers.adapter
import mozilla.components.feature.containers.Container
import mozilla.components.feature.containers.db.ContainerEntity
internal class ContainerAdapter(
internal val entity: ContainerEntity
) : Container {
override val contextId: String
get() = entity.contextId
override val name: String
get() = entity.name
override val color: String
get() = entity.color
override val icon: String
get() = entity.icon
override fun equals(other: Any?): Boolean {
if (other !is ContainerAdapter) {
return false
}
return entity == other.entity
}
override fun hashCode(): Int {
return entity.hashCode()
}
}
......@@ -8,11 +8,16 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
/**
* Internal database for storing containers (contextual identities).
*/
@Database(entities = [ContainerEntity::class], version = 1)
@TypeConverters(Converter::class)
internal abstract class ContainerDatabase : RoomDatabase() {
abstract fun containerDao(): ContainerDao
......@@ -34,3 +39,25 @@ internal abstract class ContainerDatabase : RoomDatabase() {
}
}
}
internal class Converter {
@TypeConverter
fun toColorString(color: Color): String {
return color.color
}
@TypeConverter
fun toColor(color: String): Color? {
return Color.values().find { it.color == color }
}
@TypeConverter
fun toIconString(icon: Icon): String {
return icon.icon
}
@TypeConverter
fun toIcon(icon: String): Icon? {
return Icon.values().find { it.icon == icon }
}
}
......@@ -7,6 +7,9 @@ package mozilla.components.feature.containers.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import mozilla.components.browser.state.state.Container
import mozilla.components.browser.state.state.Container.Color
import mozilla.components.browser.state.state.Container.Icon
/**
* Internal entity representing a container (contextual identity).
......@@ -21,8 +24,26 @@ internal data class ContainerEntity(
var name: String,
@ColumnInfo(name = "color")
var color: String,
var color: Color,
@ColumnInfo(name = "icon")