Merge pull request #7040 from thundernest/add_feature_flags

Add feature flags
This commit is contained in:
Wolf-Martell Montwé 2023-07-03 16:04:27 +02:00 committed by GitHub
commit 878cc45c04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 231 additions and 3 deletions

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.backend.pop3)
debugImplementation(projects.backend.demo)
implementation(projects.core.featureflags)
implementation(projects.feature.launcher)
// TODO remove account setup dependency

View file

@ -1,6 +1,9 @@
package com.fsck.k9
import app.k9mail.core.common.oauth.OAuthConfigurationFactory
import app.k9mail.core.featureflag.FeatureFlagFactory
import app.k9mail.core.featureflag.FeatureFlagProvider
import app.k9mail.core.featureflag.InMemoryFeatureFlagProvider
import app.k9mail.ui.widget.list.messageListWidgetModule
import com.fsck.k9.account.newAccountModule
import com.fsck.k9.auth.AppOAuthConfigurationFactory
@ -9,6 +12,7 @@ import com.fsck.k9.controller.ControllerExtension
import com.fsck.k9.crypto.EncryptionExtractor
import com.fsck.k9.crypto.openpgp.OpenPgpEncryptionExtractor
import com.fsck.k9.feature.featureModule
import com.fsck.k9.featureflag.InMemoryFeatureFlagFactory
import com.fsck.k9.notification.notificationModule
import com.fsck.k9.preferences.K9StoragePersister
import com.fsck.k9.preferences.StoragePersister
@ -33,6 +37,12 @@ private val mainAppModule = module {
single<EncryptionExtractor> { OpenPgpEncryptionExtractor.newInstance() }
single<StoragePersister> { K9StoragePersister(get()) }
single<OAuthConfigurationFactory> { AppOAuthConfigurationFactory() }
single<FeatureFlagFactory> { InMemoryFeatureFlagFactory() }
single<FeatureFlagProvider> {
InMemoryFeatureFlagProvider(
featureFlagFactory = get(),
)
}
}
val appModules = listOf(

View file

@ -0,0 +1,13 @@
package com.fsck.k9.featureflag
import app.k9mail.core.featureflag.FeatureFlag
import app.k9mail.core.featureflag.FeatureFlagFactory
import app.k9mail.core.featureflag.FeatureFlagKey
class InMemoryFeatureFlagFactory : FeatureFlagFactory {
override fun createFeatureCatalog(): List<FeatureFlag> {
return listOf(
FeatureFlag(FeatureFlagKey("new_onboarding"), false),
)
}
}

View file

@ -11,6 +11,7 @@ dependencies {
implementation(projects.mail.common)
implementation(projects.uiUtils.toolbarBottomSheet)
implementation(projects.core.featureflags)
implementation(projects.feature.launcher)
// TODO: Remove AccountOauth dependency
implementation(projects.feature.account.oauth)

View file

@ -28,6 +28,8 @@ import androidx.fragment.app.commit
import androidx.fragment.app.commitNow
import app.k9mail.core.android.common.contact.CachingRepository
import app.k9mail.core.android.common.contact.ContactRepository
import app.k9mail.core.featureflag.FeatureFlagKey
import app.k9mail.core.featureflag.FeatureFlagProvider
import app.k9mail.feature.launcher.FeatureLauncherActivity
import com.fsck.k9.Account
import com.fsck.k9.K9
@ -92,6 +94,7 @@ open class MessageList :
private val generalSettingsManager: GeneralSettingsManager by inject()
private val messagingController: MessagingController by inject()
private val contactRepository: ContactRepository by inject()
private val featureFlagProvider: FeatureFlagProvider by inject()
private val permissionUiHelper: PermissionUiHelper = K9PermissionUiHelper(this)
@ -154,10 +157,12 @@ open class MessageList :
deleteIncompleteAccounts(accounts)
val hasAccountSetup = accounts.any { it.isFinishedSetup }
if (!hasAccountSetup) {
val useNewOnboarding = false
if (useNewOnboarding) {
featureFlagProvider.provide(FeatureFlagKey("new_onboarding")).onEnabled {
FeatureLauncherActivity.launchOnboarding(this)
} else {
}.onDisabled {
OnboardingActivity.launch(this)
}.onUnavailable {
Timber.d("Feature flag 'new_onboarding' is unavailable, falling back to old onboarding")
OnboardingActivity.launch(this)
}
finish()

View file

@ -0,0 +1,8 @@
plugins {
id(ThunderbirdPlugins.Library.jvm)
alias(libs.plugins.android.lint)
}
dependencies {
testImplementation(projects.core.testing)
}

View file

@ -0,0 +1,6 @@
package app.k9mail.core.featureflag
data class FeatureFlag(
val key: FeatureFlagKey,
val enabled: Boolean = false,
)

View file

@ -0,0 +1,5 @@
package app.k9mail.core.featureflag
fun interface FeatureFlagFactory {
fun createFeatureCatalog(): List<FeatureFlag>
}

View file

@ -0,0 +1,4 @@
package app.k9mail.core.featureflag
@JvmInline
value class FeatureFlagKey(val key: String)

View file

@ -0,0 +1,5 @@
package app.k9mail.core.featureflag
fun interface FeatureFlagProvider {
fun provide(key: FeatureFlagKey): FeatureFlagResult
}

View file

@ -0,0 +1,31 @@
package app.k9mail.core.featureflag
sealed interface FeatureFlagResult {
object Enabled : FeatureFlagResult
object Disabled : FeatureFlagResult
object Unavailable : FeatureFlagResult
fun onEnabled(action: () -> Unit): FeatureFlagResult {
if (this is Enabled) {
action()
}
return this
}
fun onDisabled(action: () -> Unit): FeatureFlagResult {
if (this is Disabled) {
action()
}
return this
}
fun onUnavailable(action: () -> Unit): FeatureFlagResult {
if (this is Unavailable) {
action()
}
return this
}
}

View file

@ -0,0 +1,17 @@
package app.k9mail.core.featureflag
class InMemoryFeatureFlagProvider(
featureFlagFactory: FeatureFlagFactory,
) : FeatureFlagProvider {
private val features: Map<FeatureFlagKey, FeatureFlag> =
featureFlagFactory.createFeatureCatalog().associateBy { it.key }
override fun provide(key: FeatureFlagKey): FeatureFlagResult {
return when (features[key]?.enabled) {
null -> FeatureFlagResult.Unavailable
true -> FeatureFlagResult.Enabled
false -> FeatureFlagResult.Disabled
}
}
}

View file

@ -0,0 +1,60 @@
package app.k9mail.core.featureflags
import app.k9mail.core.featureflag.FeatureFlagResult
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
class FeatureFlagResultTest {
@Test
fun `should only call onEnabled when enabled`() {
val testSubject = FeatureFlagResult.Enabled
var result = ""
testSubject.onEnabled {
result = "enabled"
}.onDisabled {
result = "disabled"
}.onUnavailable {
result = "unavailable"
}
assertThat(result).isEqualTo("enabled")
}
@Test
fun `should only call onDisabled when disabled`() {
val testSubject = FeatureFlagResult.Disabled
var result = ""
testSubject.onEnabled {
result = "enabled"
}.onDisabled {
result = "disabled"
}.onUnavailable {
result = "unavailable"
}
assertThat(result).isEqualTo("disabled")
}
@Test
fun `should only call onUnavailable when unavailable`() {
val testSubject = FeatureFlagResult.Unavailable
var result = ""
testSubject.onEnabled {
result = "enabled"
}.onDisabled {
result = "disabled"
}.onUnavailable {
result = "unavailable"
}
assertThat(result).isEqualTo("unavailable")
}
}

View file

@ -0,0 +1,61 @@
package app.k9mail.core.featureflags
import app.k9mail.core.featureflag.FeatureFlag
import app.k9mail.core.featureflag.FeatureFlagKey
import app.k9mail.core.featureflag.FeatureFlagResult
import app.k9mail.core.featureflag.InMemoryFeatureFlagProvider
import assertk.assertThat
import assertk.assertions.isInstanceOf
import org.junit.Test
class InMemoryFeatureFlagProviderTest {
@Test
fun `should return FeatureFlagResult#Enabled when feature is enabled`() {
val feature1Key = FeatureFlagKey("feature1")
val featureFlagProvider = InMemoryFeatureFlagProvider(
featureFlagFactory = {
listOf(
FeatureFlag(key = feature1Key, enabled = true),
)
},
)
val result = featureFlagProvider.provide(feature1Key)
assertThat(result).isInstanceOf<FeatureFlagResult.Enabled>()
}
@Test
fun `should return FeatureFlagResult#Disabled when feature is disabled`() {
val feature1Key = FeatureFlagKey("feature1")
val featureFlagProvider = InMemoryFeatureFlagProvider(
featureFlagFactory = {
listOf(
FeatureFlag(key = feature1Key, enabled = false),
)
},
)
val result = featureFlagProvider.provide(feature1Key)
assertThat(result).isInstanceOf<FeatureFlagResult.Disabled>()
}
@Test
fun `should return FeatureFlagResult#Unavailable when feature is not found`() {
val feature1Key = FeatureFlagKey("feature1")
val feature2Key = FeatureFlagKey("feature2")
val featureFlagProvider = InMemoryFeatureFlagProvider(
featureFlagFactory = {
listOf(
FeatureFlag(key = feature1Key, enabled = false),
)
},
)
val result = featureFlagProvider.provide(feature2Key)
assertThat(result).isInstanceOf<FeatureFlagResult.Unavailable>()
}
}

View file

@ -52,6 +52,7 @@ include(
include(
":core:common",
":core:featureflags",
":core:testing",
":core:android:common",
":core:android:testing",