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
51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,47 @@ The app stores your settings behind a content provider, ensuring configurations

## Advanced Examples

### Scoped Toggles (Per-User Feature Flags)

Want to enable different features for different users or environments? Use scoped instances:

```kotlin
import se.eelde.toggles.flow.TogglesImpl

class MultiUserActivity : AppCompatActivity() {
// Create separate toggle instances for each user
private val adminToggles = TogglesImpl(this, scope = "admin")
private val guestToggles = TogglesImpl(this, scope = "guest")

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
// Admin sees advanced features
adminToggles.toggle("advanced_mode", false).collect { enabled ->
if (enabled) showAdminFeatures()
}
}

lifecycleScope.launch {
// Guest has limited features
guestToggles.toggle("advanced_mode", false).collect { enabled ->
// Will use guest scope value
if (enabled) showAdminFeatures()
}
}
}
}
```

**Use cases for scoped toggles:**
- Per-user feature flags (admin vs regular users)
- A/B testing with different user groups
- Environment-specific settings (dev, staging, prod)
- Multi-tenant applications

The scope parameter is optional. If not provided, the system uses the currently selected scope in the Toggles app.

### Working with Enums
[![Flow](https://maven-badges.herokuapp.com/maven-central/se.eelde.toggles/toggles-flow/badge.png)](https://maven-badges.herokuapp.com/maven-central/se.eelde.toggles/toggles-flow)

Expand Down Expand Up @@ -108,12 +149,18 @@ implementation("se.eelde.toggles:toggles-prefs:0.0.2")

```kotlin
import se.eelde.toggles.prefs.TogglesPreferences
import se.eelde.toggles.prefs.TogglesPreferencesImpl

val togglesPrefs = TogglesPreferences(context)
// Default scope
val togglesPrefs = TogglesPreferencesImpl(context)
val isEnabled = togglesPrefs.getBoolean("feature_toggle_key", false)

// With custom scope for per-user toggles
val userToggles = TogglesPreferencesImpl(context, scope = "user_123")
val userFeature = userToggles.getBoolean("feature_toggle_key", false)
```

**Note:** Consider using toggles-flow for reactive updates and better integration with modern Android development patterns.
**Note:** Consider using toggles-flow for reactive updates and better integration with modern Android development patterns. Scoped toggles are also supported in the prefs library.

### Toggles-core library (Advanced)
[![Core](https://maven-badges.herokuapp.com/maven-central/se.eelde.toggles/toggles-core/badge.png)](https://maven-badges.herokuapp.com/maven-central/se.eelde.toggles/toggles-core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ interface ProviderScopeDao {
)
fun getDefaultScope(applicationId: Long): TogglesScope?

@Query(
"SELECT * FROM " + ScopeTable.TABLE_NAME + " WHERE " + ScopeTable.COL_APP_ID + " = (:applicationId) AND " + ScopeTable.COL_NAME + " = (:scopeName)"
)
fun getScopeByName(applicationId: Long, scopeName: String): TogglesScope?

@Update
suspend fun update(scope: TogglesScope)
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class TogglesProvider : ContentProvider() {

when (togglesUriMatcher.match(uri)) {
UriMatch.CURRENT_CONFIGURATION_ID -> {
val scope = getSelectedScope(scopeDao, callingApplication.id)
val scope = getScopeFromUri(uri, scopeDao, callingApplication.id)
cursor = configurationDao.getToggle(
java.lang.Long.valueOf(uri.lastPathSegment!!),
scope.id
Expand All @@ -142,7 +142,7 @@ class TogglesProvider : ContentProvider() {
}

UriMatch.CURRENT_CONFIGURATION_KEY -> {
val scope = getSelectedScope(scopeDao, callingApplication.id)
val scope = getScopeFromUri(uri, scopeDao, callingApplication.id)
cursor = configurationDao.getToggle(uri.lastPathSegment!!, scope.id)

if (cursor.count == 0) {
Expand Down Expand Up @@ -485,6 +485,21 @@ class TogglesProvider : ContentProvider() {
): TogglesScope = scopeDao.getSelectedScope(applicationId)
?: error("No selected scope for application $applicationId")

@Synchronized
private fun getScopeFromUri(
uri: Uri,
scopeDao: ProviderScopeDao,
applicationId: Long
): TogglesScope {
val scopeName = uri.getQueryParameter("SCOPE")
return if (scopeName != null) {
scopeDao.getScopeByName(applicationId, scopeName)
?: getDefaultScope(scopeDao, applicationId)
} else {
getSelectedScope(scopeDao, applicationId)
}
}

private fun createDefaultScope(
scopeDao: ProviderScopeDao,
applicationId: Long
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package se.eelde.toggles.provider

import android.app.Application
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import se.eelde.toggles.core.Toggle
import se.eelde.toggles.core.TogglesProviderContract
import se.eelde.toggles.database.TogglesDatabase
import se.eelde.toggles.database.TogglesScope
import java.util.Date
import javax.inject.Inject

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@Config(application = HiltTestApplication::class, sdk = [Build.VERSION_CODES.P])
class ScopedTogglesProviderTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)

private lateinit var togglesProvider: TogglesProvider

@Inject
lateinit var togglesDatabase: TogglesDatabase

@Before
fun setUp() {
hiltRule.inject()

val contentProviderController =
Robolectric.buildContentProvider(TogglesProvider::class.java)
.create("se.eelde.toggles.configprovider")
togglesProvider = contentProviderController.get()

val context = ApplicationProvider.getApplicationContext<Application>()
shadowOf(context.packageManager).setApplicationIcon(
context.applicationInfo.packageName,
ColorDrawable(Color.RED)
)
}

@Test
fun testQueryWithScopeParameter() {
val toggleKey = "scopedToggleKey"

// Insert a toggle (will go to default scope)
val uri = TogglesProviderContract.toggleUri()
val insertToggle = getToggle(toggleKey, "default_value")
val insertToggleUri = togglesProvider.insert(uri, insertToggle.toContentValues())
assertNotNull(insertToggleUri)

// Create a custom scope
val customScope = TogglesScope(
id = 0,
applicationId = 1,
name = "custom_scope",
timeStamp = Date()
)
customScope.id = togglesDatabase.togglesScopeDao().insert(customScope)

// Insert a different value for the custom scope
val config = togglesDatabase.togglesConfigurationDao()
.getTogglesConfiguration(1, toggleKey)
assertNotNull(config)

togglesDatabase.togglesConfigurationValueDao().insert(
se.eelde.toggles.database.TogglesConfigurationValue(
id = 0,
configurationId = config!!.id,
value = "custom_value",
scope = customScope.id
)
)

// Query without scope parameter - should get default scope value
var cursor = togglesProvider.query(
TogglesProviderContract.toggleUri(toggleKey),
null,
null,
null,
null
)
assertNotNull(cursor)
assertTrue(cursor.moveToFirst())
var toggle = Toggle.fromCursor(cursor)
assertEquals("default_value", toggle.value)
cursor.close()

// Query with custom scope parameter - should get custom scope value
cursor = togglesProvider.query(
TogglesProviderContract.toggleUri(toggleKey, "custom_scope"),
null,
null,
null,
null
)
assertNotNull(cursor)
assertTrue(cursor.moveToFirst())
toggle = Toggle.fromCursor(cursor)
assertEquals("custom_value", toggle.value)
cursor.close()
}

@Test
fun testQueryWithNonExistentScope() {
val toggleKey = "nonExistentScopeKey"

// Insert a toggle in default scope
val uri = TogglesProviderContract.toggleUri()
val insertToggle = getToggle(toggleKey, "default_value")
val insertToggleUri = togglesProvider.insert(uri, insertToggle.toContentValues())
assertNotNull(insertToggleUri)

// Query with non-existent scope - should fall back to default scope
val cursor = togglesProvider.query(
TogglesProviderContract.toggleUri(toggleKey, "nonexistent_scope"),
null,
null,
null,
null
)
assertNotNull(cursor)
assertTrue(cursor.moveToFirst())
val toggle = Toggle.fromCursor(cursor)
assertEquals("default_value", toggle.value)
cursor.close()
}

@Test
fun testMultipleScopesWithDifferentValues() {
val toggleKey = "multiScopeKey"

// Insert base toggle
val uri = TogglesProviderContract.toggleUri()
val insertToggle = getToggle(toggleKey, "value_default")
togglesProvider.insert(uri, insertToggle.toContentValues())

// Create scope1
val scope1 = TogglesScope(
id = 0,
applicationId = 1,
name = "scope1",
timeStamp = Date()
)
scope1.id = togglesDatabase.togglesScopeDao().insert(scope1)

// Create scope2
val scope2 = TogglesScope(
id = 0,
applicationId = 1,
name = "scope2",
timeStamp = Date()
)
scope2.id = togglesDatabase.togglesScopeDao().insert(scope2)

// Get configuration
val config = togglesDatabase.togglesConfigurationDao()
.getTogglesConfiguration(1, toggleKey)
assertNotNull(config)

// Insert different values for each scope
togglesDatabase.togglesConfigurationValueDao().insert(
se.eelde.toggles.database.TogglesConfigurationValue(
id = 0,
configurationId = config!!.id,
value = "value_scope1",
scope = scope1.id
)
)

togglesDatabase.togglesConfigurationValueDao().insert(
se.eelde.toggles.database.TogglesConfigurationValue(
id = 0,
configurationId = config.id,
value = "value_scope2",
scope = scope2.id
)
)

// Verify each scope returns its own value
var cursor = togglesProvider.query(
TogglesProviderContract.toggleUri(toggleKey, "scope1"),
null,
null,
null,
null
)
assertTrue(cursor.moveToFirst())
assertEquals("value_scope1", Toggle.fromCursor(cursor).value)
cursor.close()

cursor = togglesProvider.query(
TogglesProviderContract.toggleUri(toggleKey, "scope2"),
null,
null,
null,
null
)
assertTrue(cursor.moveToFirst())
assertEquals("value_scope2", Toggle.fromCursor(cursor).value)
cursor.close()
}

private fun getToggle(key: String, value: String): Toggle {
return Toggle {
id = 0L
type = Toggle.TYPE.STRING
this.key = key
this.value = value
}
}
}
Loading
Loading