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"> +