diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 58b0571653f16..154068eb9e092 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -4232,6 +4232,13 @@ public class Intent implements Parcelable, Cloneable {
public static final String ACTION_SHOW_BRIGHTNESS_DIALOG =
"com.android.intent.action.SHOW_BRIGHTNESS_DIALOG";
+ /**
+ * Activity Action: Shows the contrast setting dialog.
+ * @hide
+ */
+ public static final String ACTION_SHOW_CONTRAST_DIALOG =
+ "com.android.intent.action.SHOW_CONTRAST_DIALOG";
+
/**
* Broadcast Action: A global button was pressed. Includes a single
* extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index ff570524ca0ee..36a0b5dd72b83 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -872,6 +872,20 @@
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/drawable/ic_contrast_high.xml b/packages/SystemUI/res/drawable/ic_contrast_high.xml
new file mode 100644
index 0000000000000..aa5b5abc33aa8
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_contrast_high.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/drawable/ic_contrast_medium.xml b/packages/SystemUI/res/drawable/ic_contrast_medium.xml
new file mode 100644
index 0000000000000..89519b86b9749
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_contrast_medium.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/drawable/ic_contrast_standard.xml b/packages/SystemUI/res/drawable/ic_contrast_standard.xml
new file mode 100644
index 0000000000000..f914975823dac
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_contrast_standard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/layout/contrast_dialog.xml b/packages/SystemUI/res/layout/contrast_dialog.xml
new file mode 100644
index 0000000000000..8e885cf39e2be
--- /dev/null
+++ b/packages/SystemUI/res/layout/contrast_dialog.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 2663ffb1fed94..714d495af7620 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1735,6 +1735,15 @@
44dp
16dp
+
+ 90dp
+ 82dp
+ 20dp
+ 4dp
+ 14sp
+ 4dp
+ 16dp
+
0dp
0dp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 1dd12eef4639b..f1777f84cd6d8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -757,6 +757,15 @@
One-handed mode
+
+ Contrast
+
+ Standard
+
+ Medium
+
+ High
+
Unblock device microphone?
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 8a86fd560655a..064cea112b5a1 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -377,6 +377,10 @@
- @android:color/transparent
+
+
diff --git a/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt
new file mode 100644
index 0000000000000..9e15c7e311272
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialog.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.systemui.contrast
+
+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.CONTRAST_LEVEL_STANDARD
+import android.app.UiModeManager.ContrastUtils.fromContrastLevel
+import android.app.UiModeManager.ContrastUtils.toContrastLevel
+import android.content.Context
+import android.os.Bundle
+import android.provider.Settings
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.settings.SecureSettings
+import java.util.concurrent.Executor
+
+/** Dialog to select contrast options */
+class ContrastDialog(
+ context: Context?,
+ @Main private val mainExecutor: Executor,
+ private val uiModeManager: UiModeManager,
+ private val userTracker: UserTracker,
+ private val secureSettings: SecureSettings,
+) : SystemUIDialog(context), UiModeManager.ContrastChangeListener {
+
+ @VisibleForTesting lateinit var contrastButtons: Map
+ lateinit var dialogView: View
+ @VisibleForTesting var initialContrast: Float = fromContrastLevel(CONTRAST_LEVEL_STANDARD)
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ dialogView = LayoutInflater.from(context).inflate(R.layout.contrast_dialog, null)
+ setView(dialogView)
+
+ setTitle(R.string.quick_settings_contrast_label)
+ setNeutralButton(R.string.cancel) { _, _ ->
+ secureSettings.putFloatForUser(
+ Settings.Secure.CONTRAST_LEVEL,
+ initialContrast,
+ userTracker.userId
+ )
+ dismiss()
+ }
+ setPositiveButton(R.string.done) { _, _ -> dismiss() }
+ super.onCreate(savedInstanceState)
+
+ contrastButtons =
+ mapOf(
+ CONTRAST_LEVEL_STANDARD to findViewById(R.id.contrast_button_standard),
+ CONTRAST_LEVEL_MEDIUM to findViewById(R.id.contrast_button_medium),
+ CONTRAST_LEVEL_HIGH to findViewById(R.id.contrast_button_high)
+ )
+
+ contrastButtons.forEach { (contrastLevel, contrastButton) ->
+ contrastButton.setOnClickListener {
+ val contrastValue = fromContrastLevel(contrastLevel)
+ secureSettings.putFloatForUser(
+ Settings.Secure.CONTRAST_LEVEL,
+ contrastValue,
+ userTracker.userId
+ )
+ }
+ }
+
+ initialContrast = uiModeManager.contrast
+ highlightContrast(toContrastLevel(initialContrast))
+ }
+
+ override fun onStart() {
+ super.onStart()
+ uiModeManager.addContrastChangeListener(mainExecutor, this)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ uiModeManager.removeContrastChangeListener(this)
+ }
+
+ override fun onContrastChanged(contrast: Float) {
+ highlightContrast(toContrastLevel(contrast))
+ }
+
+ private fun highlightContrast(contrast: Int) {
+ contrastButtons.forEach { (level, button) -> button.isSelected = level == contrast }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialogActivity.kt b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialogActivity.kt
new file mode 100644
index 0000000000000..70d713845f00a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/contrast/ContrastDialogActivity.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.systemui.contrast
+
+import android.app.Activity
+import android.app.UiModeManager
+import android.content.Context
+import android.os.Bundle
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.settings.SecureSettings
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/** Trampoline activity responsible for creating a [ContrastDialog] */
+class ContrastDialogActivity
+@Inject
+constructor(
+ private val context: Context,
+ @Main private val mainExecutor: Executor,
+ private val uiModeManager: UiModeManager,
+ private val userTracker: UserTracker,
+ private val secureSettings: SecureSettings
+) : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val contrastDialog =
+ ContrastDialog(context, mainExecutor, uiModeManager, userTracker, secureSettings)
+ contrastDialog.show()
+ finish()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 3cf26b381d7d3..dba353b0d70e1 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -19,6 +19,7 @@ package com.android.systemui.dagger;
import android.app.Activity;
import com.android.systemui.ForegroundServicesDialog;
+import com.android.systemui.contrast.ContrastDialogActivity;
import com.android.systemui.hdmi.HdmiCecSetMenuLanguageActivity;
import com.android.systemui.keyguard.WorkLockActivity;
import com.android.systemui.people.PeopleSpaceActivity;
@@ -73,6 +74,12 @@ public abstract class DefaultActivityBinder {
@ClassKey(BrightnessDialog.class)
public abstract Activity bindBrightnessDialog(BrightnessDialog activity);
+ /** Inject into ContrastDialogActivity. */
+ @Binds
+ @IntoMap
+ @ClassKey(ContrastDialogActivity.class)
+ public abstract Activity bindContrastDialogActivity(ContrastDialogActivity activity);
+
/** Inject into UsbDebuggingActivity. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogTest.kt
new file mode 100644
index 0000000000000..7753197afc9b2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/contrast/ContrastDialogTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.systemui.contrast
+
+import android.app.UiModeManager
+import android.app.UiModeManager.ContrastUtils.fromContrastLevel
+import android.os.Looper
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.SecureSettings
+import com.google.common.util.concurrent.MoreExecutors
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/** Test the behaviour of buttons of the [ContrastDialog]. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ContrastDialogTest : SysuiTestCase() {
+
+ private lateinit var mainExecutor: Executor
+ private lateinit var contrastDialog: ContrastDialog
+ @Mock private lateinit var mockUiModeManager: UiModeManager
+ @Mock private lateinit var mockUserTracker: UserTracker
+ @Mock private lateinit var mockSecureSettings: SecureSettings
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mainExecutor = MoreExecutors.directExecutor()
+ whenever(mockUserTracker.userId).thenReturn(context.userId)
+ }
+
+ @Test
+ fun testClickButtons_putsContrastInSettings() {
+ if (Looper.myLooper() == null) Looper.prepare()
+ contrastDialog =
+ ContrastDialog(
+ context,
+ mainExecutor,
+ mockUiModeManager,
+ mockUserTracker,
+ mockSecureSettings
+ )
+ contrastDialog.show()
+ try {
+ contrastDialog.contrastButtons.forEach {
+ (contrastLevel: Int, clickedButton: FrameLayout) ->
+ clickedButton.performClick()
+ verify(mockSecureSettings)
+ .putFloatForUser(
+ eq(Settings.Secure.CONTRAST_LEVEL),
+ eq(fromContrastLevel(contrastLevel)),
+ eq(context.userId)
+ )
+ }
+ } finally {
+ contrastDialog.dismiss()
+ }
+ }
+}