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..57c39d03d8
--- /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 bindNavigator(navigator: 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..03d065c982
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.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.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.databinding.FragmentGenericRecyclerBinding
+import im.vector.app.features.DefaultVectorFeatures
+import im.vector.app.features.themes.ActivityOtherThemes
+import im.vector.app.features.themes.ThemeUtils
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class DebugFeaturesSettingsActivity : AppCompatActivity() {
+
+ @Inject lateinit var debugFeatures: DebugVectorFeatures
+ @Inject lateinit var defaultFeatures: DefaultVectorFeatures
+
+ private lateinit var views: FragmentGenericRecyclerBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ ThemeUtils.setActivityTheme(this, ActivityOtherThemes.Default)
+ views = FragmentGenericRecyclerBinding.inflate(layoutInflater)
+ setContentView(views.root)
+ val controller = FeaturesController(object : EnumFeatureItem.Listener {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun > onOptionSelected(option: Any?, feature: Feature.EnumFeature) {
+ debugFeatures.overrideEnum(option as? T, feature.type)
+ }
+ })
+ views.genericRecyclerView.configureWith(controller)
+ controller.setData(createState())
+ }
+
+ private fun createState(): 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
+ )
+ }
+
+ override fun onDestroy() {
+ views.genericRecyclerView.cleanup()
+ super.onDestroy()
+ }
+}
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..822adcb61f
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 android.content.SharedPreferences
+import im.vector.app.features.DefaultVectorFeatures
+import im.vector.app.features.VectorFeatures
+import kotlin.reflect.KClass
+
+class DebugVectorFeatures(
+ context: Context,
+ private val vectorFeatures: DefaultVectorFeatures
+) : VectorFeatures {
+
+ private val featurePrefs = context.getSharedPreferences("debug-features", Context.MODE_PRIVATE)
+
+ override fun loginVersion(): VectorFeatures.LoginVersion {
+ return featurePrefs.readEnum() ?: vectorFeatures.loginVersion()
+ }
+
+ fun > hasEnumOverride(type: KClass): Boolean {
+ return featurePrefs.containsEnum(type)
+ }
+
+ fun > overrideEnum(value: T?, type: KClass) {
+ if (value == null) {
+ featurePrefs.removeEnum(type)
+ } else {
+ featurePrefs.putEnum(value, type)
+ }
+ }
+}
+
+private fun > SharedPreferences.removeEnum(type: KClass) {
+ edit().remove("enum-${type.simpleName}").apply()
+}
+
+private fun > SharedPreferences.containsEnum(type: KClass): Boolean {
+ return contains("enum-${type.simpleName}")
+}
+
+private inline fun > SharedPreferences.readEnum(): T? {
+ val value = T::class.simpleName
+ return getString("enum-$value", null)?.let { enumValueOf(it) }
+}
+
+private fun > SharedPreferences.putEnum(value: T, type: KClass) {
+ edit()
+ .putString("enum-${type.simpleName}", value.name)
+ .apply()
+}
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..1e2ec56ea8
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/EnumFeatureItem.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 -> {
+ val option: Any = feature.options[position - 1]
+ listener?.onOptionSelected(option, feature)
+ }
+ }
+ }
+
+ override fun onNothingSelected(parent: AdapterView<*>?) {
+ // do nothing
+ }
+ }
+ }
+ }
+
+ 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: Any?, 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..50897e6bba
--- /dev/null
+++ b/vector/src/debug/java/im/vector/app/features/debug/features/FeaturesController.kt
@@ -0,0 +1,52 @@
+/*
+ * 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 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(private val listener: EnumFeatureItem.Listener) : TypedEpoxyController() {
+
+ 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">
+
+