Add contrast setting in dev options

See the attached video from b/266071578#comment2 to see how the setting
looks like.

Bug: 266071578
Test: atest ContrastDialogTest
Test: atest ContrastDialogControllerTest
Change-Id: Id9fc31a0562059814d4e342ea8095adc5b53d5f3
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ccf002a..863ae70 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2785,6 +2785,7 @@
             </intent-filter>
             <intent-filter>
                 <action android:name="com.android.settings.action.SETTINGS" />
+                <action android:name="com.android.intent.action.SHOW_CONTRAST_DIALOG" />
             </intent-filter>
             <meta-data android:name="com.android.settings.order" android:value="-40"/>
             <meta-data android:name="com.android.settings.category"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f23fb5f..d08972d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4944,6 +4944,8 @@
     <string name="keywords_color_correction">adjust color </string>
     <!-- List of synonyms used in the settings search bar to find the “Color inversion”. [CHAR LIMIT=NONE] -->
     <string name="keywords_color_inversion">turn screen dark, turn screen light</string>
+    <!-- List of synonyms used in the settings search bar to find the “Contrast”. [CHAR LIMIT=NONE] -->
+    <string name="keywords_contrast">color contrast</string>
     <!-- List of synonyms used in the settings search bar to find the “Accessibility Menu”. [CHAR LIMIT=NONE] -->
     <string name="keywords_accessibility_menu"></string>
     <!-- List of synonyms used in the settings search bar to find the “Switch Access”. [CHAR LIMIT=NONE] -->
@@ -12017,6 +12019,15 @@
     <!-- Button to close the dialog without saving in screen flash color selection dialog. [CHAR LIMIT=20] -->
     <string name="color_selector_dialog_cancel">Cancel</string>
 
+    <!-- Title for the contrast preference fragment [CHAR LIMIT=35] -->
+    <string name="contrast_title">Contrast</string>
+    <!-- 'Standard' contrast option [CHAR LIMIT=20] -->
+    <string name="contrast_standard">Standard</string>
+    <!-- 'Medium' contrast option [CHAR LIMIT=20] -->
+    <string name="contrast_medium">Medium</string>
+    <!-- 'High' contrast option [CHAR LIMIT=20] -->
+    <string name="contrast_high">High</string>
+
     <!-- Warning message when we try to dock an app not supporting multiple instances split into multiple sides [CHAR LIMIT=NONE] -->
     <string name="dock_multi_instances_not_supported_text">"This app can only be opened in 1 window"</string>
 </resources>
diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml
index f3ac926..55a63d6 100644
--- a/res/xml/development_settings.xml
+++ b/res/xml/development_settings.xml
@@ -515,6 +515,12 @@
             android:title="@string/transparent_navigation_bar"
             android:summary="@string/transparent_navigation_bar_summary" />
 
+        <Preference
+            android:key="contrast_preference"
+            android:title="@string/contrast_title"
+            android:persistent="false"
+            settings:keywords="@string/keywords_contrast" />
+
     </PreferenceCategory>
 
     <PreferenceCategory
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 3180a79..68fe08e 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -19,6 +19,7 @@
 import static android.service.quicksettings.TileService.ACTION_QS_TILE_PREFERENCES;
 
 import android.app.Activity;
+import android.app.UiModeManager;
 import android.app.settings.SettingsEnums;
 import android.bluetooth.BluetoothA2dp;
 import android.bluetooth.BluetoothAdapter;
@@ -63,6 +64,7 @@
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.search.actionbar.SearchMenuController;
+import com.android.settings.theme.ContrastPreferenceController;
 import com.android.settings.widget.SettingsMainSwitchBar;
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -685,6 +687,8 @@
         controllers.add(new IngressRateLimitPreferenceController((context)));
         controllers.add(new BackAnimationPreferenceController(context, fragment));
         controllers.add(new PhantomProcessPreferenceController(context));
+        controllers.add(new ContrastPreferenceController(
+                context, context.getSystemService(UiModeManager.class)));
 
         return controllers;
     }
diff --git a/src/com/android/settings/theme/ContrastPreferenceController.kt b/src/com/android/settings/theme/ContrastPreferenceController.kt
new file mode 100644
index 0000000..7f3844a
--- /dev/null
+++ b/src/com/android/settings/theme/ContrastPreferenceController.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.settings.theme
+
+import android.app.UiModeManager
+import android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_HIGH
+import android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_MEDIUM
+import android.app.UiModeManager.ContrastUtils.toContrastLevel
+import android.content.Context
+import android.content.Intent
+import android.os.UserHandle
+import android.text.TextUtils
+import androidx.preference.Preference
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+
+/**
+ * Controller that opens the contrast dialog and updates the text describing the current contrast
+ */
+class ContrastPreferenceController(
+        private val context: Context,
+        private val uiModeManager: UiModeManager) : BasePreferenceController(context, KEY) {
+
+    companion object {
+        @VisibleForTesting
+        const val KEY = "contrast_preference"
+    }
+
+    override fun getAvailabilityStatus(): Int {
+        return AVAILABLE
+    }
+
+    override fun handlePreferenceTreeClick(preference: Preference): Boolean {
+        if (TextUtils.equals(preference.key, preferenceKey)) {
+            val intent = Intent(Intent.ACTION_SHOW_CONTRAST_DIALOG)
+            context.startActivityAsUser(intent, UserHandle(UserHandle.USER_CURRENT))
+            return true
+        }
+        return false
+    }
+
+    override fun getSummary(): CharSequence = getSummary(toContrastLevel(uiModeManager.contrast))
+
+    @VisibleForTesting
+    fun getSummary(contrast: Int): String {
+        return when (contrast) {
+            CONTRAST_LEVEL_HIGH -> context.getString(R.string.contrast_high)
+            CONTRAST_LEVEL_MEDIUM -> context.getString(R.string.contrast_medium)
+            else -> context.getString(R.string.contrast_standard)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/theme/ContrastPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/theme/ContrastPreferenceControllerTest.java
new file mode 100644
index 0000000..dbd3372
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/theme/ContrastPreferenceControllerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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 com.android.settings.theme;
+
+import static android.app.UiModeManager.ContrastUtils;
+import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_HIGH;
+import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_MEDIUM;
+import static android.app.UiModeManager.ContrastUtils.CONTRAST_LEVEL_STANDARD;
+import static android.provider.Settings.Secure.CONTRAST_LEVEL;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.UiModeManager;
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.stream.Stream;
+
+@RunWith(RobolectricTestRunner.class)
+public class ContrastPreferenceControllerTest {
+
+    @Rule
+    public MockitoRule mocks = MockitoJUnit.rule();
+
+    private ContrastPreferenceController mController;
+
+    @Mock
+    private UiModeManager mMockUiModeManager;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = ApplicationProvider.getApplicationContext();
+        mController = new ContrastPreferenceController(mContext, mMockUiModeManager);
+    }
+
+    @Test
+    public void controllerIsAvailable() {
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void testHandlePreferenceTreeClick() {
+        Preference preference = new Preference(mContext);
+        preference.setKey(ContrastPreferenceController.KEY);
+        assertThat(mController.handlePreferenceTreeClick(preference)).isTrue();
+
+        Preference otherPreference = new Preference(mContext);
+        otherPreference.setKey("wrong key");
+        assertThat(mController.handlePreferenceTreeClick(otherPreference)).isFalse();
+    }
+
+    @Test
+    public void controllerSummary() {
+        float initialContrast = mContext.getSystemService(UiModeManager.class).getContrast();
+        try {
+            allContrastValues().forEach(contrastLevel -> {
+                float contrast = ContrastUtils.fromContrastLevel(contrastLevel);
+                clearInvocations(mMockUiModeManager);
+                when(mMockUiModeManager.getContrast()).thenReturn(contrast);
+                String summary = mController.getSummary().toString();
+                verify(mMockUiModeManager).getContrast();
+                assertThat(summary).isEqualTo(mController.getSummary(contrastLevel));
+            });
+        } finally {
+            putContrastInSettings(initialContrast);
+        }
+    }
+
+    private static Stream<Integer> allContrastValues() {
+        return Stream.of(CONTRAST_LEVEL_STANDARD, CONTRAST_LEVEL_MEDIUM, CONTRAST_LEVEL_HIGH);
+    }
+
+    private void putContrastInSettings(float contrast) {
+        Settings.Secure.putFloat(mContext.getContentResolver(), CONTRAST_LEVEL, contrast);
+    }
+}