Add ContrastDialog class and tests, activity version

Creates the SystemUIDialog class for contrast, as well as the intent
ACTION_SHOW_CONTRAST_DIALOG and a helper class that listen to the intent to
open the dialog.

Bug: 266071578
Test: atest ContrastDialogTest
Change-Id: I3c5f25ccd99d5b8875a8fa1fdb9f24ef4886c72b
This commit is contained in:
Aurélien Pomini
2023-02-10 14:37:44 +00:00
parent 901bee878f
commit 568d3a22ab
14 changed files with 530 additions and 0 deletions

View File

@@ -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

View File

@@ -872,6 +872,20 @@
</intent-filter>
</activity>
<activity
android:name=".contrast.ContrastDialogActivity"
android:label="@string/quick_settings_contrast_label"
android:theme="@style/Theme.SystemUI.ContrastDialog"
android:finishOnCloseSystemDialogs="true"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:exported="true">
<intent-filter>
<action android:name="com.android.intent.action.SHOW_CONTRAST_DIALOG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ForegroundServicesDialog"
android:process=":fgservices"
android:excludeFromRecents="true"

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright 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.
*/
-->
<selector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="?androidprv:attr/colorSurfaceHighlight" />
<stroke
android:color="?androidprv:attr/colorAccentPrimary"
android:width="@dimen/contrast_dialog_button_stroke_width" />
<corners android:radius="@dimen/contrast_dialog_button_radius"/>
</shape>
</item>
<item>
<layer-list>
<item android:top="@dimen/contrast_dialog_button_stroke_width"
android:bottom="@dimen/contrast_dialog_button_stroke_width"
android:left="@dimen/contrast_dialog_button_stroke_width"
android:right="@dimen/contrast_dialog_button_stroke_width">
<shape android:shape="rectangle">
<solid android:color="?androidprv:attr/colorSurfaceHighlight" />
<corners android:radius="@dimen/contrast_dialog_button_radius"/>
</shape>
</item>
</layer-list>
</item>
</selector>

View File

@@ -0,0 +1,25 @@
<!--
~ 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.
-->
<vector android:autoMirrored="true" android:height="20dp"
android:viewportHeight="20" android:viewportWidth="66"
android:width="66dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F2F1E8"
android:pathData="M0.5,8C0.5,3.858 3.858,0.5 8,0.5H58C62.142,0.5 65.5,3.858 65.5,8V12C65.5,16.142 62.142,19.5 58,19.5H8C3.858,19.5 0.5,16.142 0.5,12V8Z"
android:strokeColor="#1B1C17" android:strokeWidth="1"/>
<path android:fillColor="#1B1C17" android:pathData="M11,10m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
<path android:fillColor="#1B1C17" android:pathData="M23,5L43,5A2,2 0,0 1,45 7L45,7A2,2 0,0 1,43 9L23,9A2,2 0,0 1,21 7L21,7A2,2 0,0 1,23 5z"/>
<path android:fillColor="#1B1C17" android:pathData="M23,11L55,11A2,2 0,0 1,57 13L57,13A2,2 0,0 1,55 15L23,15A2,2 0,0 1,21 13L21,13A2,2 0,0 1,23 11z"/>
</vector>

View File

@@ -0,0 +1,23 @@
<!--
~ 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.
-->
<vector android:autoMirrored="true" android:height="20dp"
android:viewportHeight="20" android:viewportWidth="66"
android:width="66dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F2F1E8" android:pathData="M0,8C0,3.582 3.582,0 8,0H58C62.418,0 66,3.582 66,8V12C66,16.418 62.418,20 58,20H8C3.582,20 0,16.418 0,12V8Z"/>
<path android:fillColor="#919283" android:pathData="M11,10m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
<path android:fillColor="#919283" android:pathData="M23,5L43,5A2,2 0,0 1,45 7L45,7A2,2 0,0 1,43 9L23,9A2,2 0,0 1,21 7L21,7A2,2 0,0 1,23 5z"/>
<path android:fillColor="#919283" android:pathData="M23,11L55,11A2,2 0,0 1,57 13L57,13A2,2 0,0 1,55 15L23,15A2,2 0,0 1,21 13L21,13A2,2 0,0 1,23 11z"/>
</vector>

View File

@@ -0,0 +1,23 @@
<!--
~ 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.
-->
<vector android:autoMirrored="true" android:height="20dp"
android:viewportHeight="20" android:viewportWidth="66"
android:width="66dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#C7C8B7" android:pathData="M0,8C0,3.582 3.582,0 8,0H58C62.418,0 66,3.582 66,8V12C66,16.418 62.418,20 58,20H8C3.582,20 0,16.418 0,12V8Z"/>
<path android:fillColor="#919283" android:pathData="M11,10m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"/>
<path android:fillColor="#919283" android:pathData="M23,5L43,5A2,2 0,0 1,45 7L45,7A2,2 0,0 1,43 9L23,9A2,2 0,0 1,21 7L21,7A2,2 0,0 1,23 5z"/>
<path android:fillColor="#919283" android:pathData="M23,11L55,11A2,2 0,0 1,57 13L57,13A2,2 0,0 1,55 15L23,15A2,2 0,0 1,21 13L21,13A2,2 0,0 1,23 11z"/>
</vector>

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/contrast_button_standard"
android:layout_width="@dimen/contrast_dialog_button_total_size"
android:layout_height="@dimen/contrast_dialog_button_total_size"
android:background="@drawable/contrast_dialog_button_background">
<ImageView
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_contrast_standard"/>
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/contrast_dialog_button_text_spacing"
android:gravity="center_horizontal|top"
android:textSize="@dimen/contrast_dialog_button_text_size"
android:text="@string/quick_settings_contrast_standard"
android:textColor="?androidprv:attr/textColorPrimary"/>
</LinearLayout>
<Space
android:layout_width="@dimen/contrast_dialog_button_horizontal_spacing"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/contrast_button_medium"
android:layout_width="@dimen/contrast_dialog_button_total_size"
android:layout_height="@dimen/contrast_dialog_button_total_size"
android:background="@drawable/contrast_dialog_button_background">
<ImageView
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_contrast_medium"/>
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/contrast_dialog_button_text_spacing"
android:gravity="center_horizontal|top"
android:textSize="@dimen/contrast_dialog_button_text_size"
android:text="@string/quick_settings_contrast_medium"
android:textColor="?androidprv:attr/textColorPrimary"/>
</LinearLayout>
<Space
android:layout_width="@dimen/contrast_dialog_button_horizontal_spacing"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/contrast_button_high"
android:layout_width="@dimen/contrast_dialog_button_total_size"
android:layout_height="@dimen/contrast_dialog_button_total_size"
android:background="@drawable/contrast_dialog_button_background">
<ImageView
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/ic_contrast_high"/>
</FrameLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/contrast_dialog_button_text_spacing"
android:gravity="center_horizontal|top"
android:textSize="@dimen/contrast_dialog_button_text_size"
android:text="@string/quick_settings_contrast_high"
android:textColor="?androidprv:attr/textColorPrimary"/>
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -1735,6 +1735,15 @@
<dimen name="broadcast_dialog_btn_minHeight">44dp</dimen>
<dimen name="broadcast_dialog_margin">16dp</dimen>
<!-- Contrast dialog -->
<dimen name="contrast_dialog_button_total_size">90dp</dimen>
<dimen name="contrast_dialog_button_inner_size">82dp</dimen>
<dimen name="contrast_dialog_button_radius">20dp</dimen>
<dimen name="contrast_dialog_button_stroke_width">4dp</dimen>
<dimen name="contrast_dialog_button_text_size">14sp</dimen>
<dimen name="contrast_dialog_button_text_spacing">4dp</dimen>
<dimen name="contrast_dialog_button_horizontal_spacing">16dp</dimen>
<!-- Shadow for dream overlay clock complication -->
<dimen name="dream_overlay_clock_key_text_shadow_dx">0dp</dimen>
<dimen name="dream_overlay_clock_key_text_shadow_dy">0dp</dimen>

View File

@@ -757,6 +757,15 @@
<!-- QuickSettings: Label for the toggle that controls whether One-handed mode is enabled. [CHAR LIMIT=NONE] -->
<string name="quick_settings_onehanded_label">One-handed mode</string>
<!-- QuickSettings: Contrast tile [CHAR LIMIT=NONE] -->
<string name="quick_settings_contrast_label">Contrast</string>
<!-- QuickSettings: Contrast tile description: standard [CHAR LIMIT=NONE] -->
<string name="quick_settings_contrast_standard">Standard</string>
<!-- QuickSettings: Contrast tile description: medium [CHAR LIMIT=NONE] -->
<string name="quick_settings_contrast_medium">Medium</string>
<!-- QuickSettings: Contrast tile description: high [CHAR LIMIT=NONE] -->
<string name="quick_settings_contrast_high">High</string>
<!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
<string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
<!--- Title of dialog triggered if the camera is disabled but an app tried to access it. [CHAR LIMIT=150] -->

View File

@@ -377,6 +377,10 @@
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="Theme.SystemUI.ContrastDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="Theme.SystemUI.QuickSettings.Dialog" parent="@android:style/Theme.DeviceDefault.Dialog">
<item name="android:dialogCornerRadius">@dimen/notification_corner_radius</item>
</style>

View File

@@ -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<Int, FrameLayout>
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 }
}
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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()
}
}
}