Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"disabledMcpjsonServers": [
"doc-bot"
]
}
1 change: 1 addition & 0 deletions data-store/data-store-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {

implementation KotlinX.coroutines.core
implementation AndroidX.appCompat
implementation AndroidX.room.ktx

coreLibraryDesugaring Android.tools.desugarJdkLibs
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.data.store.api

import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteOpenHelper

/**
* Configuration options for building a Room database
* @property openHelperFactory Custom SQLite open helper factory (e.g., for encryption). Null by default.
* @property enableMultiInstanceInvalidation Whether to enable multi-instance invalidation. False by default.
* @property journalMode The journal mode to use for the database. Null by default.
* @property callbacks List of database callbacks to be added. Empty list by default.
* @property fallbackToDestructiveMigration If true, the database will be recreated if a migration is not found. False by default.
* @property fallbackToDestructiveMigrationFromVersion List of versions to fallback to destructive migration from. Empty list by default.
* @property migrations List of migrations to apply to the database. Empty list by default.
*/
data class RoomDatabaseConfig(
val openHelperFactory: SupportSQLiteOpenHelper.Factory? = null,
val enableMultiInstanceInvalidation: Boolean = false,
val journalMode: RoomDatabase.JournalMode? = null,
val callbacks: List<RoomDatabase.Callback> = emptyList(),
val fallbackToDestructiveMigration: Boolean = false,
val fallbackToDestructiveMigrationFromVersion: List<Int> = emptyList(),
val migrations: List<Migration> = emptyList(),
val executor: DatabaseExecutor = DatabaseExecutor.Default,
)

sealed class DatabaseExecutor {

/**
* Custom executor configuration for Room database operations.
* @property transactionPoolSize The size of the thread pool for transaction operations.
* @property queryPoolSize The size of the thread pool for query operations.
* @property transactionQueueSize Optional size of the queue for transaction operations. If not set, 2 * [transactionPoolSize] will be used.
* @property queryQueueSize Optional size of the queue for query operations. If not set, 2 * [queryPoolSize] will be used.
*/
class Custom(
val transactionPoolSize: Int,
val queryPoolSize: Int,
val transactionQueueSize: Int = 2 * transactionPoolSize,
val queryQueueSize: Int = 2 * queryPoolSize,
) : DatabaseExecutor()

data object Default : DatabaseExecutor()
}

interface DatabaseProvider {
/**
* @param klass - The abstract class which is annotated with Database and extends RoomDatabase.
* @param name - The name of the database file.
* @param T - The type of the database class.
* @param config - Optional configuration for the database build process. Empty by default.
* @return T: RoomDatabase - A database
*/
fun<T : RoomDatabase> buildRoomDatabase(
klass: Class<T>,
name: String,
config: RoomDatabaseConfig = RoomDatabaseConfig(),
): T
}
8 changes: 8 additions & 0 deletions data-store/data-store-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
anvil project(':anvil-compiler')
implementation project(':anvil-annotations')
implementation project(':di')
implementation project(':feature-toggles-api')
ksp AndroidX.room.compiler

implementation KotlinX.coroutines.android
Expand All @@ -45,8 +46,15 @@ dependencies {
// Security crypto
implementation AndroidX.security.crypto

// Room
implementation AndroidX.room.runtime
implementation AndroidX.room.rxJava2
implementation AndroidX.room.ktx
ksp AndroidX.room.compiler

testImplementation "org.mockito.kotlin:mockito-kotlin:_"
testImplementation project(':common-test')
testImplementation project(':feature-toggles-test')
testImplementation Testing.junit4
testImplementation AndroidX.test.ext.junit
testImplementation CashApp.turbine
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.duckduckgo.data.store.impl

import com.duckduckgo.data.store.api.DatabaseExecutor
import com.duckduckgo.data.store.api.DatabaseExecutor.Custom
import com.duckduckgo.data.store.api.DatabaseExecutor.Default
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import javax.inject.Inject

interface DatabaseExecutorProvider {
fun createQueryExecutor(executor: DatabaseExecutor): Executor?
fun createTransactionExecutor(executor: DatabaseExecutor): Executor?
}

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealDatabaseExecutorProvider @Inject constructor(
databaseProviderFeature: DatabaseProviderFeature,
) : DatabaseExecutorProvider {

private val defaultPoolSize = 6
private val defaultExecutor =
ThreadPoolExecutor(
defaultPoolSize,
defaultPoolSize,
0L,
TimeUnit.SECONDS,
ArrayBlockingQueue(defaultPoolSize * 2),
ThreadPoolExecutor.CallerRunsPolicy(),
)

private val isFeatureFlagEnabled = databaseProviderFeature.self().isEnabled()

override fun createQueryExecutor(executor: DatabaseExecutor): Executor? {
return when (executor) {
is Custom -> {
if (isFeatureFlagEnabled) {
createCustomExecutor(executor.queryPoolSize, executor.queryQueueSize)
} else {
createLegacyQueryExecutor()
}
}
Default -> {
if (isFeatureFlagEnabled) {
defaultExecutor
} else {
null
}
}
}
}

override fun createTransactionExecutor(executor: DatabaseExecutor): Executor? {
return when (executor) {
is Custom -> {
if (isFeatureFlagEnabled) {
createCustomExecutor(executor.transactionPoolSize, executor.transactionQueueSize)
} else {
createLegacyTransactionExecutor()
}
}
Default -> {
if (isFeatureFlagEnabled) {
defaultExecutor
} else {
null
}
}
}
}

private fun createCustomExecutor(
poolSize: Int,
queueSize: Int,
): Executor {
val queue = object : ArrayBlockingQueue<Runnable>(queueSize) {
override fun add(element: Runnable): Boolean {
return super.add(element)
}
}

return ThreadPoolExecutor(
poolSize,
poolSize,
60L,
TimeUnit.SECONDS,
queue,
ThreadPoolExecutor.CallerRunsPolicy(),
).apply {
allowCoreThreadTimeOut(true)
}
}

private fun createLegacyQueryExecutor(): Executor {
return Executors.newFixedThreadPool(4)
}

private fun createLegacyTransactionExecutor(): Executor {
return Executors.newSingleThreadExecutor()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.data.store.impl

import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue

@ContributesRemoteFeature(
scope = AppScope::class,
featureName = "databaseProvider",
)
interface DatabaseProviderFeature {

@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
fun self(): Toggle
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.data.store.impl

import android.annotation.SuppressLint
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import javax.inject.Inject

/**
* Factory for creating Room database builders to enable better testing
*/
interface RoomDatabaseBuilderFactory {
fun <T : RoomDatabase> createBuilder(
context: Context,
klass: Class<T>,
name: String,
): RoomDatabase.Builder<T>
}

/**
* Real implementation of RoomDatabaseBuilderFactory that uses Room.databaseBuilder
*/
@ContributesBinding(AppScope::class)
class RealRoomDatabaseBuilderFactory @Inject constructor() : RoomDatabaseBuilderFactory {
@SuppressLint("DenyListedApi")
override fun <T : RoomDatabase> createBuilder(
context: Context,
klass: Class<T>,
name: String,
): RoomDatabase.Builder<T> {
return Room.databaseBuilder(context, klass, name)
}
}
Loading
Loading