summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/feature/logins
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/feature/logins')
-rw-r--r--mobile/android/android-components/components/feature/logins/README.md19
-rw-r--r--mobile/android/android-components/components/feature/logins/build.gradle73
-rw-r--r--mobile/android/android-components/components/feature/logins/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json40
-rw-r--r--mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt139
-rw-r--r--mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt78
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt20
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt85
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt30
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt43
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt36
-rw-r--r--mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt22
-rw-r--r--mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
14 files changed, 612 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/logins/README.md b/mobile/android/android-components/components/feature/logins/README.md
new file mode 100644
index 0000000000..662567e3cb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Feature > Logins
+
+Feature component with features related to logins.
+
+## 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-logins:{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/logins/build.gradle b/mobile/android/android-components/components/feature/logins/build.gradle
new file mode 100644
index 0000000000..fd4febbe32
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/build.gradle
@@ -0,0 +1,73 @@
+/* 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")
+ }
+ }
+
+ 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.logins'
+}
+
+dependencies {
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+ implementation project(':feature-prompts')
+
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_paging
+ implementation ComponentsDependencies.androidx_lifecycle_livedata
+
+ implementation ComponentsDependencies.androidx_room_runtime
+ ksp ComponentsDependencies.androidx_room_compiler
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.kotlin_coroutines
+
+ 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
+}
+
+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/logins/proguard-rules.pro b/mobile/android/android-components/components/feature/logins/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/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/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json b/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json
new file mode 100644
index 0000000000..e136e78b0b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/schemas/mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase/1.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "bbf39f381a14e0e0a5544f54f0e1cedc",
+ "entities": [
+ {
+ "tableName": "logins_exceptions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `origin` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "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, 'bbf39f381a14e0e0a5544f54f0e1cedc')"
+ ]
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt
new file mode 100644
index 0000000000..749ee0b82f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/LoginExceptionStorageTest.kt
@@ -0,0 +1,139 @@
+/* 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.logins
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import mozilla.components.feature.logins.exceptions.LoginException
+import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
+import mozilla.components.feature.logins.exceptions.adapter.LoginExceptionAdapter
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@Suppress("LargeClass")
+class LoginExceptionStorageTest {
+ private lateinit var context: Context
+ private lateinit var storage: LoginExceptionStorage
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @get:Rule
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ LoginExceptionDatabase::class.java,
+ )
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+
+ context = ApplicationProvider.getApplicationContext()
+ val database =
+ Room.inMemoryDatabaseBuilder(context, LoginExceptionDatabase::class.java).build()
+
+ storage =
+ LoginExceptionStorage(
+ context,
+ )
+ storage.database = lazy { database }
+ }
+
+ @After
+ fun tearDown() {
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingExceptions() {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exceptions = getAllExceptions()
+
+ assertEquals(2, exceptions.size)
+
+ assertEquals("mozilla.org", exceptions[0].origin)
+ assertEquals("firefox.com", exceptions[1].origin)
+ }
+
+ @Test
+ fun testRemovingExceptions() {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ getAllExceptions().let { exceptions ->
+ assertEquals(2, exceptions.size)
+ storage.removeLoginException(exceptions[0])
+ }
+
+ getAllExceptions().let { exceptions ->
+ assertEquals(1, exceptions.size)
+ assertEquals("firefox.com", exceptions[0].origin)
+ }
+ }
+
+ @Test
+ fun testGettingExceptions() = runBlocking {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exceptions = storage.getLoginExceptions().first()
+
+ assertNotNull(exceptions)
+ assertEquals(2, exceptions.size)
+
+ with(exceptions[0]) {
+ assertEquals("mozilla.org", origin)
+ }
+
+ with(exceptions[1]) {
+ assertEquals("firefox.com", origin)
+ }
+ }
+
+ @Test
+ fun testGettingExceptionsByOrigin() = runBlocking {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exception = storage.findExceptionByOrigin("mozilla.org")
+
+ assertNotNull(exception)
+ assertEquals("mozilla.org", exception!!.origin)
+ }
+
+ @Test
+ fun testGettingNoExceptionsByOrigin() = runBlocking {
+ storage.addLoginException("mozilla.org")
+ storage.addLoginException("firefox.com")
+
+ val exception = storage.findExceptionByOrigin("testsite.org")
+
+ assertNull(exception)
+ }
+
+ private fun getAllExceptions(): List<LoginException> {
+ return storage.database.value.loginExceptionDao().getLoginExceptionsList()
+ .map { loginExceptionEntity ->
+ LoginExceptionAdapter(loginExceptionEntity)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt
new file mode 100644
index 0000000000..1354a7f15d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/androidTest/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDaoTest.kt
@@ -0,0 +1,78 @@
+/* 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.logins.exceptions.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class LoginExceptionDaoTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ private lateinit var database: LoginExceptionDatabase
+ private lateinit var loginExceptionDao: LoginExceptionDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ database = Room.inMemoryDatabaseBuilder(context, LoginExceptionDatabase::class.java).build()
+ loginExceptionDao = database.loginExceptionDao()
+ executor = Executors.newSingleThreadExecutor()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun testAddingLoginException() {
+ val exception = LoginExceptionEntity(
+ origin = "mozilla.org",
+ ).also {
+ it.id = loginExceptionDao.insertLoginException(it)
+ }
+
+ val loginExceptionsList = loginExceptionDao.getLoginExceptionsList()
+
+ assertEquals(1, loginExceptionsList.size)
+ assertEquals(exception, loginExceptionsList[0])
+ }
+
+ @Test
+ fun testRemovingLoginException() {
+ val exception1 = LoginExceptionEntity(
+ origin = "mozilla.org",
+ ).also {
+ it.id = loginExceptionDao.insertLoginException(it)
+ }
+
+ val exception2 = LoginExceptionEntity(
+ origin = "firefox.com",
+ ).also {
+ it.id = loginExceptionDao.insertLoginException(it)
+ }
+
+ loginExceptionDao.deleteLoginException(exception1)
+
+ val loginExceptionsList = loginExceptionDao.getLoginExceptionsList()
+
+ assertEquals(1, loginExceptionsList.size)
+ assertEquals(exception2, loginExceptionsList[0])
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/logins/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/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/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt
new file mode 100644
index 0000000000..bd4c348987
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginException.kt
@@ -0,0 +1,20 @@
+/* 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.logins.exceptions
+
+/**
+ * A login exception.
+ */
+interface LoginException {
+ /**
+ * Unique ID of this login exception.
+ */
+ val id: Long
+
+ /**
+ * The origin of the login exception site.
+ */
+ val origin: String
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt
new file mode 100644
index 0000000000..4b86ebb48b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/LoginExceptionStorage.kt
@@ -0,0 +1,85 @@
+/* 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.logins.exceptions
+
+import android.content.Context
+import androidx.paging.DataSource
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import mozilla.components.feature.logins.exceptions.adapter.LoginExceptionAdapter
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionDatabase
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionEntity
+import mozilla.components.feature.prompts.login.LoginExceptions
+
+/**
+ * A storage implementation for organizing login exceptions.
+ */
+class LoginExceptionStorage(
+ context: Context,
+) : LoginExceptions {
+ internal var database: Lazy<LoginExceptionDatabase> =
+ lazy { LoginExceptionDatabase.get(context) }
+
+ /**
+ * Adds a new [LoginException].
+ *
+ * @param origin The origin.
+ */
+ override fun addLoginException(origin: String) {
+ LoginExceptionEntity(
+ origin = origin,
+ ).also { entity ->
+ entity.id = database.value.loginExceptionDao().insertLoginException(entity)
+ }
+ }
+
+ /**
+ * Returns a [Flow] list of all the [LoginException] instances.
+ */
+ fun getLoginExceptions(): Flow<List<LoginException>> {
+ return database.value.loginExceptionDao().getLoginExceptions().map { list ->
+ list.map { entity -> LoginExceptionAdapter(entity) }
+ }
+ }
+
+ /**
+ * Returns all [LoginException]s as a [DataSource.Factory].
+ */
+ fun getLoginExceptionsPaged(): DataSource.Factory<Int, LoginException> = database.value
+ .loginExceptionDao()
+ .getLoginExceptionsPaged()
+ .map { entity -> LoginExceptionAdapter(entity) }
+
+ /**
+ * Removes the given [LoginException].
+ */
+ fun removeLoginException(site: LoginException) {
+ val exceptionEntity = (site as LoginExceptionAdapter).entity
+ database.value.loginExceptionDao().deleteLoginException(exceptionEntity)
+ }
+
+ override fun isLoginExceptionByOrigin(origin: String): Boolean {
+ return findExceptionByOrigin(origin) != null
+ }
+
+ /**
+ * Finds a [LoginException] by origin.
+ */
+ fun findExceptionByOrigin(origin: String): LoginException? {
+ val exception = database.value.loginExceptionDao().findExceptionByOrigin(origin)
+ return exception?.let {
+ LoginExceptionAdapter(
+ it,
+ )
+ }
+ }
+
+ /**
+ * Removes all [LoginException]s.
+ */
+ fun deleteAllLoginExceptions() {
+ database.value.loginExceptionDao().deleteAllLoginExceptions()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt
new file mode 100644
index 0000000000..9c0a35a8aa
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/adapter/LoginExceptionAdapter.kt
@@ -0,0 +1,30 @@
+/* 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.logins.exceptions.adapter
+
+import mozilla.components.feature.logins.exceptions.LoginException
+import mozilla.components.feature.logins.exceptions.db.LoginExceptionEntity
+
+internal class LoginExceptionAdapter(
+ internal val entity: LoginExceptionEntity,
+) : LoginException {
+ override val id: Long
+ get() = entity.id!!
+
+ override val origin: String
+ get() = entity.origin
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is LoginExceptionAdapter) {
+ return false
+ }
+
+ return entity == other.entity
+ }
+
+ override fun hashCode(): Int {
+ return entity.hashCode()
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.kt
new file mode 100644
index 0000000000..e7bac42abc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDao.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 mozilla.components.feature.logins.exceptions.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 [LoginExceptionEntity] instances.
+ */
+@Dao
+internal interface LoginExceptionDao {
+ @Insert
+ fun insertLoginException(exception: LoginExceptionEntity): Long
+
+ @Delete
+ fun deleteLoginException(exception: LoginExceptionEntity)
+
+ @Transaction
+ @Query("SELECT * FROM logins_exceptions")
+ fun getLoginExceptions(): Flow<List<LoginExceptionEntity>>
+
+ @Transaction
+ @Query("SELECT * FROM logins_exceptions")
+ fun getLoginExceptionsList(): List<LoginExceptionEntity>
+
+ @Query("DELETE FROM logins_exceptions")
+ fun deleteAllLoginExceptions()
+
+ @Query("SELECT * FROM logins_exceptions WHERE origin = :origin")
+ fun findExceptionByOrigin(origin: String): LoginExceptionEntity?
+
+ @Transaction
+ @Query("SELECT * FROM logins_exceptions")
+ fun getLoginExceptionsPaged(): DataSource.Factory<Int, LoginExceptionEntity>
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.kt
new file mode 100644
index 0000000000..e1125aa44f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionDatabase.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.logins.exceptions.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+/**
+ * Internal database for storing login exceptions.
+ */
+@Database(entities = [LoginExceptionEntity::class], version = 1)
+internal abstract class LoginExceptionDatabase : RoomDatabase() {
+ abstract fun loginExceptionDao(): LoginExceptionDao
+
+ companion object {
+ @Volatile
+ private var instance: LoginExceptionDatabase? = null
+
+ @Synchronized
+ fun get(context: Context): LoginExceptionDatabase {
+ instance?.let { return it }
+
+ return Room.databaseBuilder(
+ context,
+ LoginExceptionDatabase::class.java,
+ "login_exceptions",
+ ).build().also {
+ instance = it
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.kt
new file mode 100644
index 0000000000..fee109f835
--- /dev/null
+++ b/mobile/android/android-components/components/feature/logins/src/main/java/mozilla/components/feature/logins/exceptions/db/LoginExceptionEntity.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 mozilla.components.feature.logins.exceptions.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+/**
+ * Internal entity representing a login exception.
+ */
+@Entity(tableName = "logins_exceptions")
+internal data class LoginExceptionEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ var id: Long? = null,
+
+ @ColumnInfo(name = "origin")
+ var origin: String,
+)
diff --git a/mobile/android/android-components/components/feature/logins/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/logins/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/logins/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)