diff --git a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagItemViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagItemViewModel.kt index 66f8fe38dad..c95d9b8b9ba 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagItemViewModel.kt @@ -1,7 +1,8 @@ package org.oppia.android.app.devoptions.featureflags -import androidx.annotation.ColorInt import androidx.databinding.ObservableField +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import org.oppia.android.app.model.FeatureFlagId import org.oppia.android.app.model.SyncStatus import org.oppia.android.app.translation.AppLanguageResourceHandler @@ -14,6 +15,9 @@ class FeatureFlagItemViewModel( val featureFlagId: FeatureFlagId, val currentValue: Boolean, val syncStatus: SyncStatus, + val nonOverriddenValue: Boolean, + val nonOverriddenSyncStatus: SyncStatus, + val resetFlags: LiveData>, private val machineLocale: OppiaLocale.MachineLocale, private val resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { @@ -24,19 +28,41 @@ class FeatureFlagItemViewModel( val featureFlagDisplayName: ObservableField = ObservableField(getFeatureFlagDisplayName(featureFlagId)) - /** The text representing the sync status of the feature flag. */ - val syncStatusDisplayText: ObservableField = - ObservableField(getSyncStatusText()) - /** * Callback to be invoked when the feature flag toggle is changed by the user. * Passes the [FeatureFlagId] and the new boolean value. */ var onFeatureFlagToggleCallback: ((FeatureFlagId, Boolean) -> Unit)? = null - /** The background color associated with the current sync status of the feature flag. */ - @ColorInt - val backgroundColor: Int = retrieveBackgroundColor().toInt() + /** Indicates whether this feature flag has been overridden locally. */ + val isFlagOverridden: ObservableField = + ObservableField(syncStatus == SyncStatus.LOCAL_OVERRIDE) + + /** Tracks whether the reset button is currently enabled (clickable). */ + val isResetButtonEnabled: LiveData by lazy { + Transformations.map(resetFlags) { featureFlagId !in it } + } + + /** Represents the feature flag’s server-sync or override state. */ + val syncDetails: LiveData = Transformations.map(resetFlags, ::processSyncDetails) + + private fun processSyncDetails(resetFlags: MutableMap): String { + return when { + resetFlags.containsKey(featureFlagId) -> getSyncDetails(nonOverriddenSyncStatus) + else -> getSyncDetails(syncStatus) + } + } + + private fun getSyncDetails(syncStatus: SyncStatus): String { + return when (syncStatus) { + SyncStatus.LOCAL_OVERRIDE -> + resourceHandler.getStringInLocale(R.string.platform_parameter_currently_overridden_message) + SyncStatus.SYNCED_FROM_SERVER -> + resourceHandler.getStringInLocale(R.string.platform_parameter_synced_from_server_message) + else -> + resourceHandler.getStringInLocale(R.string.platform_parameter_never_synced_message) + } + } /** Called when the feature flag switch is toggled in the UI. */ fun onToggleFeatureFlagSwitch() { @@ -57,30 +83,4 @@ class FeatureFlagItemViewModel( } } } - - private fun getSyncStatusText(): String { - return when (syncStatus) { - SyncStatus.SYNC_STATUS_UNSPECIFIED -> - resourceHandler.getStringInLocale(R.string.feature_flag_unknown_sync_status) - SyncStatus.NOT_SYNCED_FROM_SERVER -> - resourceHandler.getStringInLocale(R.string.feature_flag_default_sync_status) - SyncStatus.SYNCED_FROM_SERVER -> - resourceHandler.getStringInLocale(R.string.feature_flag_server_sync_status) - SyncStatus.LOCAL_OVERRIDE -> - resourceHandler.getStringInLocale(R.string.feature_flag_overridden_sync_status) - else -> - resourceHandler.getStringInLocale(R.string.feature_flag_unknown_sync_status) - } - } - - @ColorInt - private fun retrieveBackgroundColor(): Long { - return when (syncStatus) { - SyncStatus.SYNC_STATUS_UNSPECIFIED -> 0xFF4F4F4F - SyncStatus.NOT_SYNCED_FROM_SERVER -> 0xFFBE563C - SyncStatus.SYNCED_FROM_SERVER -> 0xFF00645C - SyncStatus.LOCAL_OVERRIDE -> 0xFFC2B71B - else -> 0xFF00645C - } - } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragment.kt b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragment.kt index ec32634489d..4b41639df10 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragment.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragment.kt @@ -37,33 +37,49 @@ class FeatureFlagsFragment : InjectableFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - var featureFlagStates: MutableMap = mutableMapOf() + val featureFlagStates: MutableMap = mutableMapOf() + val resetFlags: MutableMap = mutableMapOf() if (savedInstanceState != null) { val args = savedInstanceState.getProto( FEATURE_FLAGS_FRAGMENT_SAVED_STATE_KEY, FeatureFlagsFragmentStateBundle.getDefaultInstance() ) - featureFlagStates = args?.featureFlagStatesList - ?.associate { it.id to it.overriddenValue } - ?.toMutableMap() ?: mutableMapOf() + args?.featureFlagStatesList?.forEach { + featureFlagStates[it.id] = it.overriddenValue + } + args?.resetFeatureFlagsList?.forEach { + resetFlags[it.id] = it.overriddenValue + } } - return featureFlagsFragmentPresenter.handleCreateView(inflater, container, featureFlagStates) + return featureFlagsFragmentPresenter.handleCreateView( + inflater, + container, + featureFlagStates, + resetFlags + ) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val featureFlagStates = - featureFlagsFragmentPresenter.featureFlagStates.map { + featureFlagsFragmentPresenter.getFeatureFlagStates().map { OverriddenFeatureFlag.newBuilder() .setId(it.key) .setOverriddenValue(it.value) .build() } + val resetFlags = featureFlagsFragmentPresenter.getResetFeatureFlags().map { + OverriddenFeatureFlag.newBuilder() + .setId(it.key) + .setOverriddenValue(it.value) + .build() + } val proto = FeatureFlagsFragmentStateBundle.newBuilder() .addAllFeatureFlagStates(featureFlagStates) + .addAllResetFeatureFlags(resetFlags) .build() outState.putProto(FEATURE_FLAGS_FRAGMENT_SAVED_STATE_KEY, proto) diff --git a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragmentPresenter.kt index 349d87b0261..d04e8830d0d 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsFragmentPresenter.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import org.oppia.android.app.databinding.databinding.FeatureFlagsFragmentBinding @@ -12,7 +13,9 @@ import org.oppia.android.app.databinding.databinding.FeatureFlagsItemBinding import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.FeatureFlagId import org.oppia.android.app.model.OverriddenFeatureFlag +import org.oppia.android.app.model.SyncStatus import org.oppia.android.app.recyclerview.BindableAdapter +import org.oppia.android.app.view.models.R import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.platformparameter.PlatformParameterControllerDebugImpl import org.oppia.android.util.data.AsyncResult @@ -24,8 +27,8 @@ import javax.inject.Inject class FeatureFlagsFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val featureFlagsViewModel: FeatureFlagsViewModel, private val oppiaLogger: OppiaLogger, + private val featureFlagsViewModel: FeatureFlagsViewModel, private val platformParameterControllerDebugImpl: PlatformParameterControllerDebugImpl, private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory ) { @@ -33,14 +36,12 @@ class FeatureFlagsFragmentPresenter @Inject constructor( private lateinit var linearLayoutManager: LinearLayoutManager private lateinit var bindingAdapter: BindableAdapter - /** List of feature flag switch states to be used in the fragment. */ - var featureFlagStates: MutableMap = mutableMapOf() - /** Called when [FeatureFlagsFragment] is created. Handles UI for the fragment. */ fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, - featureFlagStates: Map + featureFlagStates: Map, + resetFlags: Map ): View { binding = FeatureFlagsFragmentBinding.inflate( inflater, @@ -50,28 +51,32 @@ class FeatureFlagsFragmentPresenter @Inject constructor( binding.featureFlagsToolbar.setNavigationOnClickListener { onBackNavigation() } + binding.saveButton.setOnClickListener { + onBackNavigation() + } activity.onBackPressedDispatcher.addCallback( fragment, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { onBackNavigation() - // The dispatcher can hold a reference to the host - // so we need to null it out to prevent memory leaks. - this.remove() } } ) if (featureFlagStates.isNotEmpty()) { - this.featureFlagStates = featureFlagStates.toMutableMap() + featureFlagsViewModel.featureFlagStates.value = featureFlagStates.toMutableMap() + } + if (resetFlags.isNotEmpty()) { + featureFlagsViewModel.resetFlags.value = resetFlags.toMutableMap() } + binding.apply { this.lifecycleOwner = fragment this.viewModel = featureFlagsViewModel } - linearLayoutManager = LinearLayoutManager(activity.applicationContext) + linearLayoutManager = LinearLayoutManager(activity.applicationContext) bindingAdapter = createRecyclerViewAdapter() binding.featureFlagsRecyclerView.apply { layoutManager = linearLayoutManager @@ -91,22 +96,64 @@ class FeatureFlagsFragmentPresenter @Inject constructor( } private fun onBackNavigation() { - val overriddenFeatureFlags = featureFlagStates.map { (id, value) -> - OverriddenFeatureFlag.newBuilder() - .setId(id) - .setOverriddenValue(value) - .build() + val overriddenFlags = computeOverriddenFlags() + val resetFlags = getResetFeatureFlags().toList() + + when { + resetFlags.isNotEmpty() -> applyResetsThenOverrides(overriddenFlags) + overriddenFlags.isNotEmpty() -> overrideFeatureFlags(overriddenFlags) + else -> activity.finish() } + } + + private fun computeOverriddenFlags(): List { + return featureFlagsViewModel.featureFlagStates.value + ?.filter { (id, value) -> featureFlagsViewModel.resetFlags.value?.get(id) != value } + ?.map { (id, value) -> + OverriddenFeatureFlag.newBuilder() + .setId(id) + .setOverriddenValue(value) + .build() + }.orEmpty() + } + + private fun applyResetsThenOverrides(overriddenFlags: List) { + val resetFlags = featureFlagsViewModel.resetFlags.value?.keys?.toList().orEmpty() + + platformParameterControllerDebugImpl + .resetFeatureFlags(resetFlags) + .toLiveData() + .observe(fragment) { result -> + when (result) { + is AsyncResult.Success -> { + overrideFeatureFlags(overriddenFlags) + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "FeatureFlagsFragmentPresenter", + "Failed to reset feature flags: ", + result.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. + } + } + } + private fun overrideFeatureFlags(overriddenFlags: List) { platformParameterControllerDebugImpl - .updateOverriddenFeatureFlags(overriddenFeatureFlags).toLiveData().observe(fragment) { - when (it) { - is AsyncResult.Success -> (activity as FeatureFlagsActivity).finish() + .updateOverriddenFeatureFlags(overriddenFlags) + .toLiveData() + .observe(fragment) { result -> + when (result) { + is AsyncResult.Success -> { + activity.finish() + } is AsyncResult.Failure -> { oppiaLogger.e( "FeatureFlagsFragmentPresenter", "Failed to override feature flags: ", - it.error + result.error ) } is AsyncResult.Pending -> {} // Wait for a result. @@ -120,15 +167,84 @@ class FeatureFlagsFragmentPresenter @Inject constructor( ) { binding.viewModel = model - if (featureFlagStates.containsKey(model.featureFlagId)) { - model.isChecked.set(featureFlagStates[model.featureFlagId]) + binding.resetButton.setOnClickListener { + handleResetFeatureFlag(model) + } + + featureFlagsViewModel.featureFlagStates.observe(fragment) { + binding.featureFlagConstraintLayout.setBackgroundColor( + setFeatureFlagBackgroundColor(it.containsKey(model.featureFlagId), model) + ) + } + + if (getResetFeatureFlags().containsKey(model.featureFlagId)) { + model.isFlagOverridden.set(true) } - model.onFeatureFlagToggleCallback = { id, value -> - if (model.currentValue == value) { - featureFlagStates.remove(id) - } else { - featureFlagStates[id] = value + + featureFlagsViewModel.featureFlagStates.value?.let { states -> + states[model.featureFlagId]?.let { state -> + model.isChecked.set(state) } } + + model.onFeatureFlagToggleCallback = { id, newValue -> + featureFlagsViewModel.updateFeatureFlagState(id, newValue, model.currentValue) + } + } + + private fun handleResetFeatureFlag( + model: FeatureFlagItemViewModel + ) { + val restoredFlagValue = model.nonOverriddenValue + featureFlagsViewModel.updateResetFlag(model.featureFlagId, model.nonOverriddenValue) + featureFlagsViewModel.updateFeatureFlagState( + model.featureFlagId, + restoredFlagValue, + model.currentValue + ) + model.isChecked.set(restoredFlagValue) + } + + private fun setFeatureFlagBackgroundColor( + isFlagModified: Boolean, + model: FeatureFlagItemViewModel + ): Int { + return when { + isFlagModified -> + ContextCompat.getColor( + fragment.requireContext(), + R.color.component_color_feature_flag_modified_background_color + ) + model.syncStatus == SyncStatus.LOCAL_OVERRIDE -> + ContextCompat.getColor( + fragment.requireContext(), + R.color.component_color_platform_parameter_overridden_background_color + ) + else -> + ContextCompat.getColor( + fragment.requireContext(), + R.color.component_color_shared_item_background_solid_color + ) + } + } + + /** + * Returns the feature flags which have been reset. + * + * @return a [MutableMap] mapping each [FeatureFlagId] to its boolean reset state, + * or an empty map if no reset flags are recorded. + */ + fun getResetFeatureFlags(): MutableMap { + return featureFlagsViewModel.resetFlags.value?.toMutableMap() ?: mutableMapOf() + } + + /** + * Returns the current states of all feature flags. + * + * @return a [Map] mapping each [FeatureFlagId] to its current boolean state, + * or an empty map if no feature flag states are recorded. + */ + fun getFeatureFlagStates(): Map { + return featureFlagsViewModel.featureFlagStates.value ?: mapOf() } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsViewModel.kt index fddffe3e918..4473144a386 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/featureflags/FeatureFlagsViewModel.kt @@ -1,9 +1,12 @@ package org.oppia.android.app.devoptions.featureflags import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.EphemeralFeatureFlag +import org.oppia.android.app.model.FeatureFlagId +import org.oppia.android.app.model.SyncStatus import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.platformparameter.PlatformParameterControllerDebugImpl @@ -22,6 +25,14 @@ class FeatureFlagsViewModel @Inject constructor( private val machineLocale: OppiaLocale.MachineLocale, private val resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { + /** + * LiveData that contains a list of [FeatureFlagItemViewModel] which is used to populate the + * recyclerview in [FeatureFlagsFragment]. + */ + val featureFlagList: LiveData> by lazy { + Transformations.map(ephemeralFlagsLiveData, ::processFeatureFlagList) + } + private val ephemeralFlagsLiveData: LiveData> by lazy { Transformations.map( platformParameterControllerDebugImpl.loadEphemeralFeatureFlags().toLiveData(), @@ -29,33 +40,82 @@ class FeatureFlagsViewModel @Inject constructor( ) } + /** List of feature flag switch states to be used in the fragment. */ + val featureFlagStates = MutableLiveData>(mutableMapOf()) + + /** List of feature flags that have been reset. */ + val resetFlags = MutableLiveData>(mutableMapOf()) + + /** Tracks whether the Save button is currently enabled (clickable). */ + val isSaveButtonEnabled: LiveData by lazy { + Transformations.map(featureFlagStates) { it.isNotEmpty() } + } + /** - * LiveData that contains a list of [FeatureFlagItemViewModel] which is used to populate the - * recycler view in [FeatureFlagsFragment]. + * Updates the state of a given feature flag with a new value. + * This method handles the logic for determining whether to add, update, or remove + * the flag from the tracked states based on the original value and reset status. + * + * @param id the [FeatureFlagId] of the feature flag being updated. + * @param newValue the new value for the feature flag. + * @param originalValue the original (non-overridden) value of the feature flag. */ - val featureFlagList: LiveData> by lazy { - Transformations.map(ephemeralFlagsLiveData, ::processFeatureFlagList) + fun updateFeatureFlagState( + id: FeatureFlagId, + newValue: Boolean, + currentValue: Boolean + ) { + val currentStates = featureFlagStates.value?.toMutableMap() ?: mutableMapOf() + val resetFlags = resetFlags.value ?: mutableMapOf() + if (newValue == currentValue && id !in resetFlags) { + currentStates.remove(id) + } else { + currentStates[id] = newValue + } + featureFlagStates.value = currentStates + } + + /** + * Updates the list of feature flags that are being reset to their non-overridden values. + * + * @param id the [FeatureFlagId] of the feature flag being reset. + * @param restoredValue the value to restore after reset. + */ + fun updateResetFlag(id: FeatureFlagId, restoredValue: Boolean) { + val currentResetFlags = resetFlags.value?.toMutableMap() ?: mutableMapOf() + currentResetFlags[id] = restoredValue + resetFlags.value = currentResetFlags } private fun processEphemeralFlagResult( result: AsyncResult> ): List { return when (result) { - is AsyncResult.Success -> result.value + is AsyncResult.Success -> { + result.value.sortedWith( + compareByDescending { + it.syncStatus == SyncStatus.LOCAL_OVERRIDE + }.thenBy { it.id.name } + ) + } else -> emptyList() } } - private fun processFeatureFlagList(ephemeralFeatureFlags: List): - List { - return ephemeralFeatureFlags.map { ephemeralFeatureFlag -> - FeatureFlagItemViewModel( - featureFlagId = ephemeralFeatureFlag.id, - currentValue = ephemeralFeatureFlag.currentValue, - syncStatus = ephemeralFeatureFlag.syncStatus, - machineLocale = machineLocale, - resourceHandler = resourceHandler - ) - } + private fun processFeatureFlagList( + ephemeralFeatureFlags: List + ): List { + return ephemeralFeatureFlags.map { ephemeralFeatureFlag -> + FeatureFlagItemViewModel( + featureFlagId = ephemeralFeatureFlag.id, + currentValue = ephemeralFeatureFlag.currentValue, + syncStatus = ephemeralFeatureFlag.syncStatus, + nonOverriddenValue = ephemeralFeatureFlag.nonOverriddenValue, + nonOverriddenSyncStatus = ephemeralFeatureFlag.nonOverriddenSyncStatus, + resetFlags = resetFlags, + machineLocale = machineLocale, + resourceHandler = resourceHandler + ) } + } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParameterItemViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParameterItemViewModel.kt index a7ac87e6c24..252a7a4f642 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParameterItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParameterItemViewModel.kt @@ -1,7 +1,9 @@ package org.oppia.android.app.devoptions.platformparameters -import androidx.annotation.ColorInt import androidx.databinding.ObservableField +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations import org.oppia.android.app.model.PlatformParameterId import org.oppia.android.app.model.PlatformParameterValue import org.oppia.android.app.model.SyncStatus @@ -18,6 +20,9 @@ class PlatformParameterItemViewModel( val platformParameterId: PlatformParameterId, val currentValue: PlatformParameterValue, val syncStatus: SyncStatus, + val nonOverriddenValue: PlatformParameterValue, + val nonOverriddenSyncStatus: SyncStatus, + val resetParameters: MutableLiveData>, private val machineLocale: OppiaLocale.MachineLocale, private val resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { @@ -36,9 +41,6 @@ class PlatformParameterItemViewModel( /** Error message to be displayed in case of invalid input type for a platform parameter. */ val inputErrorMsg = ObservableField("") - /** The display text representing the current sync status of the parameter. */ - val syncStatusDisplayText = ObservableField(getSyncStatusText()) - /** The user-editable value of the platform parameter (if it is a string or integer). */ val inputValue = ObservableField( when { @@ -60,9 +62,37 @@ class PlatformParameterItemViewModel( */ var onPlatformParameterTextChangedCallback: ((PlatformParameterId, String) -> Unit)? = null - /** The background color of the sync status chip, determined by its sync state. */ - @ColorInt - val syncStatusBackgroundColor: Int = retrieveSyncStatusBackgroundColor().toInt() + /** Indicates whether this platform parameter has been overridden locally. */ + val isParamOverridden: ObservableField = + ObservableField(syncStatus == SyncStatus.LOCAL_OVERRIDE) + + /** Tracks whether the reset button is currently enabled (clickable). */ + val isResetButtonEnabled: LiveData by lazy { + Transformations.map(resetParameters) { platformParameterId !in it } + } + + /** Represents the platform parameter’s server-sync or override state. */ + val syncDetails: LiveData = Transformations.map(resetParameters, ::processSyncDetails) + + private fun processSyncDetails( + resetParameters: MutableMap + ): String { + return when { + resetParameters.containsKey(platformParameterId) -> getSyncDetails(nonOverriddenSyncStatus) + else -> getSyncDetails(syncStatus) + } + } + + private fun getSyncDetails(syncStatus: SyncStatus): String { + return when (syncStatus) { + SyncStatus.LOCAL_OVERRIDE -> + resourceHandler.getStringInLocale(R.string.platform_parameter_currently_overridden_message) + SyncStatus.SYNCED_FROM_SERVER -> + resourceHandler.getStringInLocale(R.string.platform_parameter_synced_from_server_message) + else -> + resourceHandler.getStringInLocale(R.string.platform_parameter_never_synced_message) + } + } /** Called when the boolean toggle switch is clicked by the user. */ fun onTogglePlatformParameterSwitch() { @@ -83,30 +113,4 @@ class PlatformParameterItemViewModel( } } } - - private fun getSyncStatusText(): String { - return when (syncStatus) { - SyncStatus.SYNC_STATUS_UNSPECIFIED -> - resourceHandler.getStringInLocale(R.string.platform_parameter_unknown_sync_status) - SyncStatus.NOT_SYNCED_FROM_SERVER -> - resourceHandler.getStringInLocale(R.string.platform_parameter_default_sync_status) - SyncStatus.SYNCED_FROM_SERVER -> - resourceHandler.getStringInLocale(R.string.platform_parameter_server_sync_status) - SyncStatus.LOCAL_OVERRIDE -> - resourceHandler.getStringInLocale(R.string.platform_parameter_overridden_sync_status) - else -> - resourceHandler.getStringInLocale(R.string.platform_parameter_unknown_sync_status) - } - } - - @ColorInt - private fun retrieveSyncStatusBackgroundColor(): Long { - return when (syncStatus) { - SyncStatus.SYNC_STATUS_UNSPECIFIED -> 0xFF4F4F4F - SyncStatus.NOT_SYNCED_FROM_SERVER -> 0xFFBE563C - SyncStatus.SYNCED_FROM_SERVER -> 0xFF00645C - SyncStatus.LOCAL_OVERRIDE -> 0xFFC2B71B - else -> 0xFF00645C - } - } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragment.kt b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragment.kt index 8be70483ed8..cd77dd20dfa 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragment.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragment.kt @@ -41,6 +41,8 @@ class PlatformParametersFragment : InjectableFragment() { ): View { val platformParameterStates: MutableMap = mutableMapOf() + val resetParamList: MutableMap = mutableMapOf() + if (savedInstanceState != null) { val args = savedInstanceState.getProto( PLATFORM_PARAMETERS_FRAGMENT_SAVED_STATE_KEY, @@ -49,32 +51,43 @@ class PlatformParametersFragment : InjectableFragment() { args?.platformParameterStatesList?.forEach { platformParameterStates[it.id] = it.overriddenValue } - args?.invalidInputPlatformParametersList?.forEach { - platformParameterStates[it] = null + args?.resetPlatformParametersList?.forEach { + resetParamList[it.id] = it.overriddenValue } } return platformParametersFragmentPresenter - .handleCreateView(inflater, container, platformParameterStates) + .handleCreateView( + inflater, + container, + platformParameterStates, + resetParamList + ) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) val validParameterOverrides = - platformParametersFragmentPresenter.platformParameterStates.mapNotNull { (key, value) -> - value?.let { - OverriddenPlatformParameter.newBuilder() - .setId(key) - .setOverriddenValue(it) - .build() + platformParametersFragmentPresenter.getPlatformParameterStates() + .mapNotNull { (key, value) -> + value?.let { + OverriddenPlatformParameter.newBuilder() + .setId(key) + .setOverriddenValue(it) + .build() + } } + + val resetParamList = + platformParametersFragmentPresenter.getResetParameters().mapNotNull { (id, value) -> + OverriddenPlatformParameter.newBuilder() + .setId(id) + .setOverriddenValue(value) + .build() } - val invalidInputParameterIds = platformParametersFragmentPresenter.platformParameterStates - .filterValues { it == null } - .keys val proto = PlatformParametersFragmentStateBundle.newBuilder() .addAllPlatformParameterStates(validParameterOverrides) - .addAllInvalidInputPlatformParameters(invalidInputParameterIds) + .addAllResetPlatformParameters(resetParamList) .build() outState.putProto( PLATFORM_PARAMETERS_FRAGMENT_SAVED_STATE_KEY, proto diff --git a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragmentPresenter.kt index 96c0a056485..c09d8c54a36 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersFragmentPresenter.kt @@ -9,15 +9,16 @@ import android.view.ViewGroup import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.textfield.TextInputEditText import org.oppia.android.app.databinding.databinding.PlatformParameterItemBinding import org.oppia.android.app.databinding.databinding.PlatformParametersFragmentBinding import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.OverriddenPlatformParameter import org.oppia.android.app.model.PlatformParameterId import org.oppia.android.app.model.PlatformParameterValue +import org.oppia.android.app.model.SyncStatus import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.view.models.R @@ -32,7 +33,7 @@ import javax.inject.Inject class PlatformParametersFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, - private val platformParametersViewModel: PlatformParametersViewModel, + private val platformParameterViewModel: PlatformParametersViewModel, resourceHandler: AppLanguageResourceHandler, private val oppiaLogger: OppiaLogger, private val platformParameterControllerDebugImpl: PlatformParameterControllerDebugImpl, @@ -45,15 +46,12 @@ class PlatformParametersFragmentPresenter @Inject constructor( resourceHandler.getStringInLocale(R.string.platform_parameter_invalid_input_error_msg) private val boundParamIds = mutableSetOf() - /** List of platform parameter states to be used in the fragment. */ - var platformParameterStates: - MutableMap = mutableMapOf() - /** Called when [PlatformParametersFragment] is created. Handles UI for the fragment. */ fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, - platformParameterStates: Map + platformParameterStates: Map, + resetParameters: Map ): View { binding = PlatformParametersFragmentBinding.inflate( inflater, @@ -65,6 +63,18 @@ class PlatformParametersFragmentPresenter @Inject constructor( onBackNavigation() } + binding.saveButton.setOnClickListener { + onBackNavigation() + } + activity.onBackPressedDispatcher.addCallback( + fragment, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onBackNavigation() + } + } + ) + activity.onBackPressedDispatcher.addCallback( fragment, object : OnBackPressedCallback(true) { @@ -75,7 +85,11 @@ class PlatformParametersFragmentPresenter @Inject constructor( ) if (platformParameterStates.isNotEmpty()) { - this.platformParameterStates = platformParameterStates.toMutableMap() + platformParameterViewModel.platformParameterStates.value = + platformParameterStates.toMutableMap() + } + if (resetParameters.isNotEmpty()) { + platformParameterViewModel.resetParameters.value = resetParameters.toMutableMap() } linearLayoutManager = LinearLayoutManager(activity.applicationContext) @@ -87,7 +101,7 @@ class PlatformParametersFragmentPresenter @Inject constructor( binding.apply { this.lifecycleOwner = fragment - this.viewModel = platformParametersViewModel + this.viewModel = platformParameterViewModel } return binding.root @@ -103,41 +117,90 @@ class PlatformParametersFragmentPresenter @Inject constructor( } private fun onBackNavigation() { - val hasInvalidInput = platformParameterStates.containsValue(null) + val hasInvalidInput = platformParameterViewModel + .platformParameterStates.value?.containsValue(null) ?: false + + if (hasInvalidInput) { + showInvalidInputDialog() + return + } - if (!hasInvalidInput) { - val overriddenPlatformParameters = platformParameterStates.map { (id, value) -> + val overriddenParameters = computeOverriddenParameters() + val resetParameters = getResetParameters().keys.toList() + + when { + resetParameters.isNotEmpty() -> applyResetsThenOverrides(overriddenParameters) + overriddenParameters.isNotEmpty() -> overridePlatformParameters(overriddenParameters) + else -> activity.finish() + } + } + + private fun computeOverriddenParameters(): List { + val resetParamsValue = getResetParameters() + + return platformParameterViewModel.platformParameterStates.value + ?.filter { (id, value) -> resetParamsValue[id] != value } + ?.map { (id, value) -> OverriddenPlatformParameter.newBuilder() .setId(id) .setOverriddenValue(value) .build() } + .orEmpty() + } - platformParameterControllerDebugImpl - .updateOverriddenPlatformParameters(overriddenPlatformParameters) - .toLiveData().observe(fragment) { - when (it) { - is AsyncResult.Success -> (activity as PlatformParametersActivity).finish() - is AsyncResult.Failure -> { - oppiaLogger.e( - "PlatformParametersFragmentPresenter", - "Failed to override platform parameters: ", - it.error - ) - } - is AsyncResult.Pending -> {} // Wait for a result. + private fun applyResetsThenOverrides(overriddenParameters: List) { + val resetParameters = getResetParameters().keys.toList() + platformParameterControllerDebugImpl + .resetPlatformParameters(resetParameters) + .toLiveData() + .observe(fragment) { result -> + when (result) { + is AsyncResult.Success -> { + overridePlatformParameters(overriddenParameters) } + is AsyncResult.Failure -> { + oppiaLogger.e( + "PlatformParametersFragmentPresenter", + "Failed to reset platform parameters: ", + result.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } - } else { - AlertDialog.Builder(activity, R.style.OppiaAlertDialogTheme) - .setTitle(R.string.platform_parameter_invalid_input_alert_dialog_title) - .setMessage(R.string.platform_parameter_invalid_input_alert_dialog_message) - .setPositiveButton( - R.string.platform_parameter_invalid_input_alert_dialog_okay_button - ) { dialog, _ -> dialog.dismiss() } - .setCancelable(false) - .show() - } + } + } + + private fun overridePlatformParameters(overriddenParameters: List) { + platformParameterControllerDebugImpl + .updateOverriddenPlatformParameters(overriddenParameters) + .toLiveData() + .observe(fragment) { result -> + when (result) { + is AsyncResult.Success -> { + activity.finish() + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "PlatformParametersFragmentPresenter", + "Failed to override platform parameters: ", + result.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. + } + } + } + + private fun showInvalidInputDialog() { + AlertDialog.Builder(activity, R.style.OppiaAlertDialogTheme) + .setTitle(R.string.platform_parameter_invalid_input_alert_dialog_title) + .setMessage(R.string.platform_parameter_invalid_input_alert_dialog_message) + .setPositiveButton( + R.string.platform_parameter_invalid_input_alert_dialog_okay_button + ) { dialog, _ -> dialog.dismiss() } + .setCancelable(false) + .show() } private fun bindPlatformParameterItem( @@ -149,10 +212,28 @@ class PlatformParametersFragmentPresenter @Inject constructor( val previousWatcher = editText.getTag(R.id.platform_parameter_text_watcher) as? TextWatcher previousWatcher?.let { editText.removeTextChangedListener(it) } + platformParameterViewModel.platformParameterStates.observe(fragment) { + binding.platformParameterConstraintLayout.setBackgroundColor( + setPlatformParameterBackgroundColor( + it.containsKey(model.platformParameterId), + model + ) + ) + } + + val resetParamsValue = getResetParameters() + if (resetParamsValue.containsKey(model.platformParameterId)) { + model.isParamOverridden.set(true) + } + + binding.resetButton.setOnClickListener { + handleResetPlatformParameter(model) + } + if (model.currentValue.hasBoolean()) { handleBooleanParameter(model) } else { - handleTextInputParameter(model, editText) + handleTextInputParameter(model, binding) } val newWatcher = object : TextWatcher { @@ -169,29 +250,52 @@ class PlatformParametersFragmentPresenter @Inject constructor( editText.setTag(R.id.platform_parameter_text_watcher, newWatcher) } - private fun handleTextInputParameter( + private fun handleResetPlatformParameter( model: PlatformParameterItemViewModel, - editText: TextInputEditText ) { - val paramState = platformParameterStates[model.platformParameterId] + val restoredParameterValue = model.nonOverriddenValue + platformParameterViewModel + .updateResetParameter(model.platformParameterId, restoredParameterValue) + if (model.currentValue.hasBoolean()) { + platformParameterViewModel + .updatePlatformParameterState( + model.platformParameterId, + restoredParameterValue, + model.currentValue + ) + model.isChecked.set(restoredParameterValue.boolean) + } else { + when { + model.currentValue.hasInteger() -> { + model.inputValue.set(restoredParameterValue.integer.toString()) + } + model.currentValue.hasString() -> { + model.inputValue.set(restoredParameterValue.string) + } + } + } + } + + private fun handleTextInputParameter( + model: PlatformParameterItemViewModel, + binding: PlatformParameterItemBinding + ) { + val paramState = getPlatformParameterStates()[model.platformParameterId] + val editText = binding.platformParameterInputEditText when { model.currentValue.hasInteger() -> { editText.inputType = InputType.TYPE_CLASS_NUMBER - if (platformParameterStates.containsKey(model.platformParameterId)) { - if (paramState == null) { - model.inputErrorMsg.set(invalidInputErrorText) - model.inputValue.set("") - } else { - model.inputErrorMsg.set("") - model.inputValue.set(paramState.integer.toString()) - } + if (getPlatformParameterStates().containsKey(model.platformParameterId) && + paramState != null + ) { + model.inputErrorMsg.set("") + model.inputValue.set(paramState.integer.toString()) } else { model.inputErrorMsg.set("") model.inputValue.set(model.currentValue.integer.toString()) } } - model.currentValue.hasString() -> { editText.inputType = InputType.TYPE_CLASS_TEXT model.inputValue.set(paramState?.string ?: model.currentValue.string) @@ -206,55 +310,121 @@ class PlatformParametersFragmentPresenter @Inject constructor( if (boundParamIds.contains(id).not()) { return@onPlatformParameterTextChangedCallback } + + val lastValidValue = getLastValidValue(model) + editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + val currentText = editText.text?.toString().orEmpty() + if (currentText.isBlank()) { + editText.setText(lastValidValue) + model.inputErrorMsg.set("") + } + } + } + when { model.currentValue.hasInteger() -> { - if (text == model.currentValue.integer.toString()) { - platformParameterStates.remove(id) - model.inputErrorMsg.set("") + + val parsed = text.toIntOrNull() + if (parsed == null) { + model.inputErrorMsg.set(invalidInputErrorText) + platformParameterViewModel + .updatePlatformParameterState(id, null, model.currentValue) } else { - val parsed = text.toIntOrNull() - if (parsed == null) { - model.inputErrorMsg.set(invalidInputErrorText) - platformParameterStates[id] = null - } else { - model.inputErrorMsg.set("") - platformParameterStates[id] = - PlatformParameterValue.newBuilder().setInteger(parsed).build() - } + model.inputErrorMsg.set("") + val parameter = PlatformParameterValue.newBuilder().setInteger(parsed).build() + platformParameterViewModel + .updatePlatformParameterState(id, parameter, model.currentValue) } } model.currentValue.hasString() -> { - if (text == model.currentValue.string) { - platformParameterStates.remove(id) - model.inputErrorMsg.set("") + if (text.isBlank()) { + model.inputErrorMsg.set(invalidInputErrorText) + platformParameterViewModel + .updatePlatformParameterState(id, null, model.currentValue) } else { - if (text.isBlank()) { - model.inputErrorMsg.set(invalidInputErrorText) - } else { - model.inputErrorMsg.set("") - } - platformParameterStates[id] = PlatformParameterValue.newBuilder() + model.inputErrorMsg.set("") + val parameter = PlatformParameterValue.newBuilder() .setString(text) .build() + platformParameterViewModel + .updatePlatformParameterState(id, parameter, model.currentValue) } } } } } - private fun handleBooleanParameter(model: PlatformParameterItemViewModel) { - if (platformParameterStates.containsKey(model.platformParameterId)) { - model.isChecked.set(platformParameterStates[model.platformParameterId]?.boolean) + private fun handleBooleanParameter( + model: PlatformParameterItemViewModel + ) { + getPlatformParameterStates()[model.platformParameterId]?.let { state -> + model.isChecked.set(state.boolean) } model.onPlatformParameterToggledCallback = { id, value -> - if (value == model.currentValue.boolean) { - platformParameterStates.remove(id) - } else { - platformParameterStates[id] = PlatformParameterValue.newBuilder() - .setBoolean(value) - .build() - } + val parameter = PlatformParameterValue.newBuilder() + .setBoolean(value) + .build() + platformParameterViewModel.updatePlatformParameterState(id, parameter, model.currentValue) + } + } + + private fun getLastValidValue(model: PlatformParameterItemViewModel): String { + val resetParamsValue = getResetParameters() + val value = if (resetParamsValue.containsKey(model.platformParameterId)) { + model.nonOverriddenValue + } else { + model.currentValue + } + return when { + value.hasInteger() -> value.integer.toString() + value.hasString() -> value.string + else -> "" } } + + private fun setPlatformParameterBackgroundColor( + isParameterModified: Boolean, + model: PlatformParameterItemViewModel + ): Int { + return when { + isParameterModified -> + ContextCompat.getColor( + fragment.requireContext(), + R.color.component_color_platform_parameter_modified_background_color + ) + model.syncStatus == SyncStatus.LOCAL_OVERRIDE -> + ContextCompat.getColor( + fragment.requireContext(), + R.color.component_color_platform_parameter_overridden_background_color + ) + else -> + ContextCompat.getColor( + fragment.requireContext(), + R.color.component_color_shared_item_background_solid_color + ) + } + } + + /** + * Returns the current states of all platform parameters. + * + * @return a [MutableMap] mapping each [PlatformParameterId] to its current [PlatformParameterValue], + * or an empty map if no platform parameter states are recorded. + */ + fun getPlatformParameterStates(): + MutableMap { + return platformParameterViewModel.platformParameterStates.value ?: mutableMapOf() + } + + /** + * Returns the platform parameters which have been reset. + * + * @return a [MutableMap] mapping each [PlatformParameterId] to its reset [PlatformParameterValue], + * or an empty map if no reset parameters are recorded. + */ + fun getResetParameters(): MutableMap { + return platformParameterViewModel.resetParameters.value ?: mutableMapOf() + } } diff --git a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersViewModel.kt index 0b78fb26dfd..e44b9983296 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/platformparameters/PlatformParametersViewModel.kt @@ -1,9 +1,13 @@ package org.oppia.android.app.devoptions.platformparameters import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.EphemeralPlatformParameter +import org.oppia.android.app.model.PlatformParameterId +import org.oppia.android.app.model.PlatformParameterValue +import org.oppia.android.app.model.SyncStatus import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.platformparameter.PlatformParameterControllerDebugImpl @@ -22,6 +26,14 @@ class PlatformParametersViewModel @Inject constructor( private val machineLocale: OppiaLocale.MachineLocale, private val resourceHandler: AppLanguageResourceHandler ) : ObservableViewModel() { + /** + * LiveData that contains a list of [PlatformParameterItemViewModel] which is used to populate the + * recyclerview in [PlatformParametersFragment]. + */ + val platformParameterList: LiveData> by lazy { + Transformations.map(ephemeralParametersLiveData, ::processPlatformParameterList) + } + private val ephemeralParametersLiveData: LiveData> by lazy { Transformations.map( platformParameterControllerDebugImpl.loadEphemeralPlatformParameters().toLiveData(), @@ -29,19 +41,69 @@ class PlatformParametersViewModel @Inject constructor( ) } + /** List of platform parameter states to be used in the fragment. */ + val platformParameterStates: + MutableLiveData> = + MutableLiveData(mutableMapOf()) + + /** List of platform parameters that have been reset. */ + val resetParameters: MutableLiveData> = + MutableLiveData(mutableMapOf()) + + /** Tracks whether the Save button is currently enabled (clickable). */ + val isSaveButtonEnabled: LiveData by lazy { + Transformations.map(platformParameterStates) { it.isNotEmpty() } + } + /** - * LiveData that contains a list of [PlatformParameterItemViewModel] which is used to populate the - * recycler view in [PlatformParametersFragment]. + * Updates the state of a given platform parameter with a new value. + * This method handles the logic for determining whether to add, update, or remove + * the parameter from the tracked states based on the original value and reset status. + * + * @param id the [PlatformParameterId] of the parameter to be updated + * @param newValue the new [PlatformParameterValue] to assign to this parameter. + * @param originalValue the original (non-overridden) value of the platform parameter. */ - val platformParameterList: LiveData> by lazy { - Transformations.map(ephemeralParametersLiveData, ::processPlatformParameterList) + fun updatePlatformParameterState( + id: PlatformParameterId, + newValue: PlatformParameterValue?, + currentValue: PlatformParameterValue + ) { + val currentStates = platformParameterStates.value?.toMutableMap() ?: mutableMapOf() + val resetParams = resetParameters.value ?: emptyMap() + + if (newValue == currentValue && id !in resetParams) { + currentStates.remove(id) + } else { + currentStates[id] = newValue + } + + platformParameterStates.value = currentStates + } + + /** + * Updates the list of parameters that are being reset to their non-overridden values. + * + * @param id the [PlatformParameterId] of the parameter being reset. + * @param newValue the [PlatformParameterValue] to restore after reset. + */ + fun updateResetParameter(id: PlatformParameterId, newValue: PlatformParameterValue) { + val currentResets = resetParameters.value ?: mutableMapOf() + currentResets[id] = newValue + resetParameters.value = currentResets } private fun processEphemeralParameterResult( result: AsyncResult> ): List { return when (result) { - is AsyncResult.Success -> result.value + is AsyncResult.Success -> { + result.value.sortedWith( + compareByDescending { + it.syncStatus == SyncStatus.LOCAL_OVERRIDE + }.thenBy { it.id.name } + ) + } else -> emptyList() } } @@ -54,6 +116,9 @@ class PlatformParametersViewModel @Inject constructor( platformParameterId = ephemeralPlatformParameter.id, currentValue = ephemeralPlatformParameter.currentValue, syncStatus = ephemeralPlatformParameter.syncStatus, + nonOverriddenValue = ephemeralPlatformParameter.nonOverriddenValue, + nonOverriddenSyncStatus = ephemeralPlatformParameter.nonOverriddenSyncStatus, + resetParameters = resetParameters, machineLocale = machineLocale, resourceHandler = resourceHandler ) diff --git a/app/src/main/res/drawable/ic_alert_icon_yellow_24dp.xml b/app/src/main/res/drawable/ic_alert_icon_yellow_24dp.xml new file mode 100644 index 00000000000..dd21043b16d --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_icon_yellow_24dp.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/save_button_active_background.xml b/app/src/main/res/drawable/save_button_active_background.xml new file mode 100644 index 00000000000..53761b22fef --- /dev/null +++ b/app/src/main/res/drawable/save_button_active_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/save_button_inactive_background.xml b/app/src/main/res/drawable/save_button_inactive_background.xml new file mode 100644 index 00000000000..8fbd7259f6c --- /dev/null +++ b/app/src/main/res/drawable/save_button_inactive_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/layout/feature_flags_fragment.xml b/app/src/main/res/layout/feature_flags_fragment.xml index 44e5988150a..27f0016b2b3 100644 --- a/app/src/main/res/layout/feature_flags_fragment.xml +++ b/app/src/main/res/layout/feature_flags_fragment.xml @@ -28,14 +28,30 @@ android:id="@+id/feature_flags_toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/component_color_shared_activity_toolbar_color" android:minHeight="?attr/actionBarSize" + android:background="@color/component_color_shared_activity_toolbar_color" android:textSize="20sp" app:navigationContentDescription="@string/navigate_up" app:navigationIcon="?attr/homeAsUpIndicator" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:title="@string/developer_options_feature_flags" - app:titleTextAppearance="@style/ToolbarTextAppearance" /> + app:titleTextAppearance="@style/ToolbarTextAppearance" > + +