diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/feature/containers | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/feature/containers')
15 files changed, 819 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/containers/README.md b/mobile/android/android-components/components/feature/containers/README.md new file mode 100644 index 0000000000..aea3596f2d --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/README.md @@ -0,0 +1,19 @@ +# [Android Components](../../../README.md) > Feature > Containers + +Feature component for working with contextual identities also known as containers. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:feature-containers:{latest-version}" +``` + +## License + + 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/ diff --git a/mobile/android/android-components/components/feature/containers/build.gradle b/mobile/android/android-components/components/feature/containers/build.gradle new file mode 100644 index 0000000000..9ebb3289db --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/build.gradle @@ -0,0 +1,80 @@ +/* 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/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'com.google.devtools.ksp' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + arg("room.generateKotlin", "true") + } + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.incremental": "true"] + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + packagingOptions { + resources { + excludes += ['META-INF/proguard/androidx-annotations.pro'] + } + } + + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + + namespace 'mozilla.components.feature.containers' +} + +dependencies { + implementation project(':browser-state') + implementation project(':support-ktx') + implementation project(':support-base') + + implementation ComponentsDependencies.kotlin_coroutines + + implementation ComponentsDependencies.androidx_paging + implementation ComponentsDependencies.androidx_lifecycle_livedata + + implementation ComponentsDependencies.androidx_room_runtime + ksp ComponentsDependencies.androidx_room_compiler + + androidTestImplementation project(':support-android-test') + + androidTestImplementation ComponentsDependencies.androidx_room_testing + androidTestImplementation ComponentsDependencies.androidx_arch_core_testing + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestImplementation ComponentsDependencies.androidx_test_rules + androidTestImplementation ComponentsDependencies.testing_coroutines + + testImplementation project(':support-test') + testImplementation project(':support-test-libstate') + + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_coroutines + testImplementation ComponentsDependencies.testing_robolectric +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/feature/containers/proguard-rules.pro b/mobile/android/android-components/components/feature/containers/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/feature/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json b/mobile/android/android-components/components/feature/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json new file mode 100644 index 0000000000..4a8eb5e60b --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/schemas/mozilla.components.feature.containers.db.ContainerDatabase/1.json @@ -0,0 +1,52 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "77d1905ab2c154b7ed655e58bd578a84", + "entities": [ + { + "tableName": "containers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`context_id` TEXT NOT NULL, `name` TEXT NOT NULL, `color` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`context_id`))", + "fields": [ + { + "fieldPath": "contextId", + "columnName": "context_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "context_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '77d1905ab2c154b7ed655e58bd578a84')" + ] + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt new file mode 100644 index 0000000000..199034309c --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt @@ -0,0 +1,107 @@ +/* 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 + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +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.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +@Suppress("LargeClass") +class ContainerStorageTest { + private lateinit var context: Context + private lateinit var storage: ContainerStorage + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + val database = Room.inMemoryDatabaseBuilder(context, ContainerDatabase::class.java).build() + + storage = ContainerStorage(context) + storage.database = lazy { database } + } + + @Test + fun testAddingContainer() = runTest { + 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(Color.RED, containers[0].color) + assertEquals(Icon.FINGERPRINT, containers[0].icon) + assertEquals("2", containers[1].contextId) + assertEquals("Shopping", containers[1].name) + assertEquals(Color.BLUE, containers[1].color) + assertEquals(Icon.CART, containers[1].icon) + } + + @Test + fun testRemovingContainers() = runTest { + storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT) + storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART) + + getAllContainers().let { containers -> + assertEquals(2, containers.size) + + storage.removeContainer(containers[0]) + } + + getAllContainers().let { containers -> + assertEquals(1, containers.size) + + assertEquals("2", containers[0].contextId) + assertEquals("Shopping", containers[0].name) + assertEquals(Color.BLUE, containers[0].color) + assertEquals(Icon.CART, containers[0].icon) + } + } + + @Test + fun testGettingContainers() = runTest { + storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT) + storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART) + + val containers = storage.getContainers().first() + + assertNotNull(containers) + assertEquals(2, containers.size) + + with(containers[0]) { + assertEquals("1", contextId) + assertEquals("Personal", name) + assertEquals(Color.RED, color) + assertEquals(Icon.FINGERPRINT, icon) + } + + with(containers[1]) { + assertEquals("2", contextId) + assertEquals("Shopping", name) + assertEquals(Color.BLUE, color) + assertEquals(Icon.CART, icon) + } + } + + private suspend fun getAllContainers(): List<Container> { + return storage.containerDao.getContainersList().map { containerEntity -> + containerEntity.toContainer() + } + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt new file mode 100644 index 0000000000..344bb5d956 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt @@ -0,0 +1,92 @@ +/* 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.db + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +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 +import org.junit.Rule +import org.junit.Test +import java.util.UUID +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@ExperimentalCoroutinesApi +class ContainerDaoTest { + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private lateinit var database: ContainerDatabase + private lateinit var containerDao: ContainerDao + private lateinit var executor: ExecutorService + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @Before + fun setUp() { + database = Room.inMemoryDatabaseBuilder(context, ContainerDatabase::class.java).build() + containerDao = database.containerDao() + executor = Executors.newSingleThreadExecutor() + } + + @After + fun tearDown() { + database.close() + executor.shutdown() + } + + @Test + fun testAddingContainer() = runTest { + val container = + ContainerEntity( + contextId = UUID.randomUUID().toString(), + name = "Personal", + color = Color.RED, + icon = Icon.FINGERPRINT, + ) + containerDao.insertContainer(container) + + val pagedList = containerDao.getContainersList() + + assertEquals(1, pagedList.size) + assertEquals(container, pagedList[0]) + } + + @Test + fun testRemovingContainer() = runTest { + val container1 = + ContainerEntity( + contextId = UUID.randomUUID().toString(), + name = "Personal", + color = Color.RED, + icon = Icon.FINGERPRINT, + ) + val container2 = + ContainerEntity( + contextId = UUID.randomUUID().toString(), + name = "Shopping", + color = Color.BLUE, + icon = Icon.CART, + ) + + containerDao.insertContainer(container1) + containerDao.insertContainer(container2) + containerDao.deleteContainer(container1) + + val pagedList = containerDao.getContainersList() + + assertEquals(1, pagedList.size) + assertEquals(container2, pagedList[0]) + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/containers/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/main/AndroidManifest.xml @@ -0,0 +1,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/. --> +<manifest /> diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt new file mode 100644 index 0000000000..772e16ee52 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerMiddleware.kt @@ -0,0 +1,106 @@ +/* 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 + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContainerAction +import mozilla.components.browser.state.action.InitAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.Container +import mozilla.components.browser.state.state.ContainerState +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.lib.state.Store +import java.util.UUID +import kotlin.coroutines.CoroutineContext + +/** + * [Middleware] implementation for handling [ContainerAction] and syncing the containers in + * [BrowserState.containers] with the [ContainerStorage]. + */ +class ContainerMiddleware( + applicationContext: Context, + coroutineContext: CoroutineContext = Dispatchers.IO, + private val containerStorage: Storage = ContainerStorage(applicationContext), +) : Middleware<BrowserState, BrowserAction> { + + private var scope = CoroutineScope(coroutineContext) + + override fun invoke( + context: MiddlewareContext<BrowserState, BrowserAction>, + next: (BrowserAction) -> Unit, + action: BrowserAction, + ) { + when (action) { + is InitAction -> initializeContainers(context.store) + is ContainerAction.AddContainerAction -> addContainer(action) + is ContainerAction.RemoveContainerAction -> removeContainer(context.store, action) + else -> { + // no-op + } + } + + next(action) + } + + private fun initializeContainers( + store: Store<BrowserState, BrowserAction>, + ) = scope.launch { + containerStorage.getContainers().collect { containers -> + store.dispatch(ContainerAction.AddContainersAction(containers)) + } + } + + private fun addContainer( + action: ContainerAction.AddContainerAction, + ) = scope.launch { + containerStorage.addContainer( + contextId = action.container.contextId, + name = action.container.name, + color = action.container.color, + icon = action.container.icon, + ) + } + + private fun removeContainer( + store: Store<BrowserState, BrowserAction>, + action: ContainerAction.RemoveContainerAction, + ) = scope.launch { + store.state.containers[action.contextId]?.let { + containerStorage.removeContainer(it) + } + } + + /** + * Interface for a storage to be passed to the middleware. + */ + interface Storage { + /** + * Returns a [Flow] list of all the [Container] instances. + */ + fun getContainers(): Flow<List<Container>> + + /** + * Adds a new [Container]. + */ + suspend fun addContainer( + contextId: String = UUID.randomUUID().toString(), + name: String, + color: ContainerState.Color, + icon: ContainerState.Icon, + ) + + /** + * Removes the given [Container]. + */ + suspend fun removeContainer(container: Container) + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt new file mode 100644 index 0000000000..1219923b58 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/ContainerStorage.kt @@ -0,0 +1,72 @@ +/* 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 + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.paging.DataSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +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 + +/** + * A storage implementation for organizing containers (contextual identities). + */ +internal class ContainerStorage(context: Context) : ContainerMiddleware.Storage { + + @VisibleForTesting + internal var database: Lazy<ContainerDatabase> = + lazy { ContainerDatabase.get(context) } + val containerDao by lazy { database.value.containerDao() } + + /** + * Adds a new [Container]. + */ + override suspend fun addContainer( + contextId: String, + name: String, + color: Color, + icon: Icon, + ) { + containerDao.insertContainer( + ContainerEntity( + contextId = contextId, + name = name, + color = color, + icon = icon, + ), + ) + } + + /** + * Returns a [Flow] list of all the [Container] instances. + */ + override fun getContainers(): Flow<List<Container>> { + return containerDao.getContainers().map { list -> + list.map { entity -> entity.toContainer() } + } + } + + /** + * Returns all saved [Container] instances as a [DataSource.Factory]. + */ + fun getContainersPaged(): DataSource.Factory<Int, Container> = containerDao + .getContainersPaged() + .map { entity -> + entity.toContainer() + } + + /** + * Removes the given [Container]. + */ + override suspend fun removeContainer(container: Container) { + containerDao.deleteContainer(container.toContainerEntity()) + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt new file mode 100644 index 0000000000..40e9f5cd41 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDao.kt @@ -0,0 +1,36 @@ +/* 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.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow + +/** + * Internal DAO for accessing [ContainerEntity] instances. + */ +@Dao +internal interface ContainerDao { + @Insert + suspend fun insertContainer(container: ContainerEntity): Long + + @Delete + suspend fun deleteContainer(identity: ContainerEntity) + + @Transaction + @Query("SELECT * FROM containers") + fun getContainers(): Flow<List<ContainerEntity>> + + @Query("SELECT * FROM containers") + suspend fun getContainersList(): List<ContainerEntity> + + @Transaction + @Query("SELECT * FROM containers") + fun getContainersPaged(): DataSource.Factory<Int, ContainerEntity> +} diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt new file mode 100644 index 0000000000..e6a7f55d87 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerDatabase.kt @@ -0,0 +1,63 @@ +/* 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.db + +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 + + companion object { + @Volatile + private var instance: ContainerDatabase? = null + + @Synchronized + fun get(context: Context): ContainerDatabase { + instance?.let { return it } + + return Room.databaseBuilder( + context, + ContainerDatabase::class.java, + "containers", + ).build().also { + instance = it + } + } + } +} + +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 } + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt new file mode 100644 index 0000000000..2532789d43 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/main/java/mozilla/components/feature/containers/db/ContainerEntity.kt @@ -0,0 +1,49 @@ +/* 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.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). + */ +@Entity(tableName = "containers") +internal data class ContainerEntity( + @PrimaryKey + @ColumnInfo(name = "context_id") + var contextId: String, + + @ColumnInfo(name = "name") + var name: String, + + @ColumnInfo(name = "color") + var color: Color, + + @ColumnInfo(name = "icon") + var icon: Icon, +) { + internal fun toContainer(): Container { + return Container( + contextId, + name, + color, + icon, + ) + } +} + +internal fun Container.toContainerEntity(): ContainerEntity { + return ContainerEntity( + contextId, + name, + color, + icon, + ) +} diff --git a/mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt b/mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt new file mode 100644 index 0000000000..2cef4eacbc --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt @@ -0,0 +1,115 @@ +/* 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 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flow +import mozilla.components.browser.state.action.ContainerAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ContainerState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.rule.runTestOnMain +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class ContainerMiddlewareTest { + @get:Rule + val mainCoroutineRule = MainCoroutineRule() + + // Test container + private val container = ContainerState( + contextId = "contextId", + name = "Personal", + color = ContainerState.Color.GREEN, + icon = ContainerState.Icon.CART, + ) + + @Test + fun `container storage stores the provided container on add container action`() = + runTestOnMain { + val storage = mockStorage() + val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(middleware), + ) + + store.waitUntilIdle() // wait to consume InitAction + store.waitUntilIdle() // wait to consume AddContainersAction + + store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking() + + verify(storage).addContainer( + container.contextId, + container.name, + container.color, + container.icon, + ) + } + + @Test + fun `fetch the containers from the container storage and load into browser state on initialize container state action`() = + runTestOnMain { + val storage = mockStorage(listOf(container)) + val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage) + val store = BrowserStore( + initialState = BrowserState(), + middleware = listOf(middleware), + ) + + store.waitUntilIdle() // wait to consume InitAction + store.waitUntilIdle() // wait to consume AddContainersAction + + verify(storage).getContainers() + assertEquals(container, store.state.containers["contextId"]) + } + + @Test + fun `container storage removes the provided container on remove container action`() = + runTestOnMain { + val storage = mockStorage() + val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage) + val store = BrowserStore( + initialState = BrowserState( + containers = mapOf( + container.contextId to container, + ), + ), + middleware = listOf(middleware), + ) + + store.waitUntilIdle() // wait to consume InitAction + store.waitUntilIdle() // wait to consume AddContainersAction + + store.dispatch(ContainerAction.RemoveContainerAction(container.contextId)) + .joinBlocking() + + verify(storage).removeContainer(container) + } + + private fun mockStorage( + containers: List<ContainerState> = emptyList(), + ): ContainerStorage { + val storage: ContainerStorage = mock() + whenever(storage.getContainers()).thenReturn( + flow { + emit(containers) + }, + ) + return storage + } +} diff --git a/mobile/android/android-components/components/feature/containers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/containers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..cf1c399ea8 --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) diff --git a/mobile/android/android-components/components/feature/containers/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/containers/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/feature/containers/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |