diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml
index dba8440602..ceeb0353db 100644
--- a/vector/src/debug/AndroidManifest.xml
+++ b/vector/src/debug/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt
index 64de648a23..4916ab1e5d 100644
--- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt
+++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt
@@ -34,6 +34,7 @@ import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityDebugMenuBinding
+import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity
import im.vector.app.features.debug.sas.DebugSasEmojiActivity
import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity
import im.vector.app.features.qrcode.QrCodeScannerActivity
@@ -76,6 +77,7 @@ class DebugMenuActivity : VectorBaseActivity() {
}
private fun setupViews() {
+ views.debugFeatures.setOnClickListener { startActivity(Intent(this, DebugFeaturesSettingsActivity::class.java)) }
views.debugPrivateSetting.setOnClickListener { openPrivateSettings() }
views.debugTestTextViewLink.setOnClickListener { testTextViewLink() }
views.debugOpenButtonStylesLight.setOnClickListener {
diff --git a/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt b/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt
new file mode 100644
index 0000000000..0c4a3ef637
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/di/FeaturesModule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * 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 im.vector.app.features.debug.di
+
+import android.content.Context
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import im.vector.app.features.DefaultVectorFeatures
+import im.vector.app.features.VectorFeatures
+import im.vector.app.features.debug.features.DebugVectorFeatures
+
+@InstallIn(SingletonComponent::class)
+@Module
+interface FeaturesModule {
+
+ @Binds
+ fun bindFeatures(debugFeatures: DebugVectorFeatures): VectorFeatures
+
+ companion object {
+
+ @Provides
+ fun providesDefaultVectorFeatures(): DefaultVectorFeatures {
+ return DefaultVectorFeatures()
+ }
+
+ @Provides
+ fun providesDebugVectorFeatures(context: Context, defaultVectorFeatures: DefaultVectorFeatures): DebugVectorFeatures {
+ return DebugVectorFeatures(context, defaultVectorFeatures)
+ }
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt
new file mode 100644
index 0000000000..e31f073614
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * 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 im.vector.app.features.debug.features
+
+import android.os.Bundle
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.platform.VectorBaseActivity
+import im.vector.app.databinding.FragmentGenericRecyclerBinding
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class DebugFeaturesSettingsActivity : VectorBaseActivity() {
+
+ @Inject lateinit var debugFeatures: DebugVectorFeatures
+ @Inject lateinit var debugFeaturesStateFactory: DebugFeaturesStateFactory
+ @Inject lateinit var controller: FeaturesController
+
+ override fun getBinding() = FragmentGenericRecyclerBinding.inflate(layoutInflater)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ controller.listener = object : EnumFeatureItem.Listener {
+ override fun > onOptionSelected(option: T?, feature: Feature.EnumFeature) {
+ debugFeatures.overrideEnum(option, feature.type)
+ }
+ }
+ views.genericRecyclerView.configureWith(controller)
+ controller.setData(debugFeaturesStateFactory.create())
+ }
+
+ override fun onDestroy() {
+ controller.listener = null
+ views.genericRecyclerView.cleanup()
+ super.onDestroy()
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
new file mode 100644
index 0000000000..8d22fc599f
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * 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 im.vector.app.features.debug.features
+
+import im.vector.app.features.DefaultVectorFeatures
+import javax.inject.Inject
+
+class DebugFeaturesStateFactory @Inject constructor(
+ private val debugFeatures: DebugVectorFeatures,
+ private val defaultFeatures: DefaultVectorFeatures
+) {
+
+ fun create(): FeaturesState {
+ return FeaturesState(listOf(
+ createEnumFeature(
+ label = "Login version",
+ selection = debugFeatures.loginVersion(),
+ default = defaultFeatures.loginVersion()
+ )
+ ))
+ }
+
+ private inline fun > createEnumFeature(label: String, selection: T, default: T): Feature {
+ return Feature.EnumFeature(
+ label = label,
+ selection = selection.takeIf { debugFeatures.hasEnumOverride(T::class) },
+ default = default,
+ options = enumValues().toList(),
+ type = T::class
+ )
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
new file mode 100644
index 0000000000..0831609e4f
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * 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 im.vector.app.features.debug.features
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.MutablePreferences
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import im.vector.app.features.DefaultVectorFeatures
+import im.vector.app.features.VectorFeatures
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import kotlin.reflect.KClass
+
+private val Context.dataStore: DataStore by preferencesDataStore(name = "debug_features")
+
+class DebugVectorFeatures(
+ context: Context,
+ private val vectorFeatures: DefaultVectorFeatures
+) : VectorFeatures {
+
+ private val dataStore = context.dataStore
+
+ override fun loginVersion(): VectorFeatures.LoginVersion {
+ return readPreferences().getEnum() ?: vectorFeatures.loginVersion()
+ }
+
+ fun > hasEnumOverride(type: KClass) = readPreferences().containsEnum(type)
+
+ fun > overrideEnum(value: T?, type: KClass) {
+ if (value == null) {
+ updatePreferences { it.removeEnum(type) }
+ } else {
+ updatePreferences { it.putEnum(value, type) }
+ }
+ }
+
+ private fun readPreferences() = runBlocking { dataStore.data.first() }
+
+ private fun updatePreferences(block: (MutablePreferences) -> Unit) = runBlocking {
+ dataStore.edit { block(it) }
+ }
+}
+
+private fun > MutablePreferences.removeEnum(type: KClass) {
+ remove(enumPreferencesKey(type))
+}
+
+private fun > Preferences.containsEnum(type: KClass) = contains(enumPreferencesKey(type))
+
+private fun > MutablePreferences.putEnum(value: T, type: KClass) {
+ this[enumPreferencesKey(type)] = value.name
+}
+
+private inline fun > Preferences.getEnum(): T? {
+ return get(enumPreferencesKey())?.let { enumValueOf(it) }
+}
+
+private inline fun > enumPreferencesKey() = enumPreferencesKey(T::class)
+
+private fun > enumPreferencesKey(type: KClass) = stringPreferencesKey("enum-${type.simpleName}")
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt b/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt
new file mode 100644
index 0000000000..5dd2f9efa9
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * 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 im.vector.app.features.debug.features
+
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+
+@EpoxyModelClass(layout = im.vector.app.R.layout.item_feature)
+abstract class EnumFeatureItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute
+ lateinit var feature: Feature.EnumFeature<*>
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var listener: Listener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.label.text = feature.label
+
+ holder.optionsSpinner.apply {
+ val arrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item)
+ arrayAdapter.add("DEFAULT - ${feature.default.name}")
+ arrayAdapter.addAll(feature.options.map { it.name })
+ adapter = arrayAdapter
+
+ feature.selection?.let {
+ setSelection(feature.options.indexOf(it) + 1, false)
+ }
+
+ onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ when (position) {
+ 0 -> listener?.onOptionSelected(option = null, feature)
+ else -> feature.onOptionSelected(position - 1)
+ }
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {
+ // do nothing
+ }
+ }
+ }
+ }
+
+ private fun > Feature.EnumFeature.onOptionSelected(selection: Int) {
+ listener?.onOptionSelected(options[selection], this)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val label by bind(im.vector.app.R.id.feature_label)
+ val optionsSpinner by bind(im.vector.app.R.id.feature_options)
+ }
+
+ interface Listener {
+ fun > onOptionSelected(option: T?, feature: Feature.EnumFeature)
+ }
+}
diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt b/vector/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt
new file mode 100644
index 0000000000..0a05c76d69
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * 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 im.vector.app.features.debug.features
+
+import com.airbnb.epoxy.TypedEpoxyController
+import javax.inject.Inject
+import kotlin.reflect.KClass
+
+data class FeaturesState(
+ val features: List
+)
+
+sealed interface Feature {
+
+ data class EnumFeature>(
+ val label: String,
+ val selection: T?,
+ val default: T,
+ val options: List,
+ val type: KClass
+ ) : Feature
+}
+
+class FeaturesController @Inject constructor() : TypedEpoxyController() {
+
+ var listener: EnumFeatureItem.Listener? = null
+
+ override fun buildModels(data: FeaturesState?) {
+ if (data == null) return
+
+ data.features.forEachIndexed { index, feature ->
+ when (feature) {
+ is Feature.EnumFeature<*> -> enumFeatureItem {
+ id(index)
+ feature(feature)
+ listener(this@FeaturesController.listener)
+ }
+ }
+ }
+ }
+}
diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml
index ac70e4ef0e..7aa69becde 100644
--- a/vector/src/debug/res/layout/activity_debug_menu.xml
+++ b/vector/src/debug/res/layout/activity_debug_menu.xml
@@ -20,6 +20,12 @@
android:padding="@dimen/layout_horizontal_margin"
android:showDividers="middle">
+
+