From 05fa18f8474cd5fed798e7ee7ef099682c4a4475 Mon Sep 17 00:00:00 2001 From: Romain Guy Date: Tue, 24 Oct 2017 21:59:40 +0100 Subject: [PATCH] Add new color mode setting Replace the "vivid" mode setting with a new 3 choice screen: - Natural (sRGB) - Boosted (sRGB + 10% saturation) - Saturated (unmanaged, colors are assigned the native color space) The "Colors" setting that replaces "Vivid" only appears on devices that support wide gamut rendering and color management. Changing the color mode has an immediate effect and triggers a configuration change in running application so that applications that request wide color gamut rendering can react properly (since they lose that ability). Bug: 68159303 Test: make RunSettingsRoboTests Change-Id: I7009723e11ee164c93719f0e42fed9d1b3cf6e01 --- res/xml/display_settings.xml | 6 +- .../ColorModePreferenceController.java | 87 ++------ .../display/ColorModePreferenceFragment.java | 207 ++++++++++++++++++ .../ColorModePreferenceControllerTest.java | 113 ---------- .../ColorModePreferenceFragmentTest.java | 170 ++++++++++++++ 5 files changed, 404 insertions(+), 179 deletions(-) create mode 100644 src/com/android/settings/display/ColorModePreferenceFragment.java delete mode 100644 tests/robotests/src/com/android/settings/display/ColorModePreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/display/ColorModePreferenceFragmentTest.java diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index 27ef86948d1..87d3e5e1d12 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -64,9 +64,11 @@ android:key="auto_rotate" android:title="@string/accelerometer_title" /> - + android:title="@string/color_mode_title" + android:fragment="com.android.settings.display.ColorModePreferenceFragment" + settings:keywords="@string/keywords_color_mode" /> 1.0f); - } - @Override public boolean isAvailable() { return mConfigWrapper.isScreenWideColorGamut(); } - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - float saturation = (boolean) newValue - ? COLOR_SATURATION_VIVID : COLOR_SATURATION_DEFAULT; - - SystemProperties.set(PERSISTENT_PROPERTY_SATURATION, Float.toString(saturation)); - applySaturation(saturation); - - return true; - } - - /** - * Propagates the provided saturation to the SurfaceFlinger. - */ - private void applySaturation(float saturation) { - if (mSurfaceFlinger != null) { - final Parcel data = Parcel.obtain(); - data.writeInterfaceToken("android.ui.ISurfaceComposer"); - data.writeFloat(saturation); - try { - mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_SATURATION, data, null, 0); - } catch (RemoteException ex) { - Log.e(TAG, "Failed to set saturation", ex); - } finally { - data.recycle(); - } - } - } - - private static float getSaturationValue() { - try { - return Float.parseFloat(SystemProperties.get( - PERSISTENT_PROPERTY_SATURATION, Float.toString(COLOR_SATURATION_DEFAULT))); - } catch (NumberFormatException e) { - return COLOR_SATURATION_DEFAULT; - } - } - @VisibleForTesting static class ConfigurationWrapper { - private final Context mContext; + private final IBinder mSurfaceFlinger; - ConfigurationWrapper(Context context) { - mContext = context; + ConfigurationWrapper() { + mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger"); } boolean isScreenWideColorGamut() { - return mContext.getResources().getConfiguration().isScreenWideColorGamut(); + if (mSurfaceFlinger != null) { + final Parcel data = Parcel.obtain(); + final Parcel reply = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + try { + mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_QUERY_WIDE_COLOR, + data, reply, 0); + return reply.readBoolean(); + } catch (RemoteException ex) { + Log.e(TAG, "Failed to query wide color support", ex); + } finally { + data.recycle(); + reply.recycle(); + } + } + return false; } } } diff --git a/src/com/android/settings/display/ColorModePreferenceFragment.java b/src/com/android/settings/display/ColorModePreferenceFragment.java new file mode 100644 index 00000000000..7c8b841b38b --- /dev/null +++ b/src/com/android/settings/display/ColorModePreferenceFragment.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2017 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.display; + +import android.app.ActivityManager; +import android.app.IActivityManager; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.internal.logging.nano.MetricsProto; + +import com.android.settings.R; +import com.android.settings.widget.RadioButtonPickerFragment; + +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("WeakerAccess") +public class ColorModePreferenceFragment extends RadioButtonPickerFragment { + private static final String TAG = "ColorModePreferenceFragment"; + + @VisibleForTesting + static final float COLOR_SATURATION_NATURAL = 1.0f; + @VisibleForTesting + static final float COLOR_SATURATION_BOOSTED = 1.1f; + + private static final int SURFACE_FLINGER_TRANSACTION_SATURATION = 1022; + private static final int SURFACE_FLINGER_TRANSACTION_NATIVE_MODE = 1023; + + @VisibleForTesting + static final String PERSISTENT_PROPERTY_SATURATION = "persist.sys.sf.color_saturation"; + @VisibleForTesting + static final String PERSISTENT_PROPERTY_NATIVE_MODE = "persist.sys.sf.native_mode"; + + @VisibleForTesting + static final String KEY_COLOR_MODE_NATURAL = "color_mode_natural"; + @VisibleForTesting + static final String KEY_COLOR_MODE_BOOSTED = "color_mode_boosted"; + @VisibleForTesting + static final String KEY_COLOR_MODE_SATURATED = "color_mode_saturated"; + + private IBinder mSurfaceFlinger; + private IActivityManager mActivityManager; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger"); + mActivityManager = ActivityManager.getService(); + } + + @Override + protected List getCandidates() { + Context c = getContext(); + return Arrays.asList( + new ColorModeCandidateInfo(c.getString(R.string.color_mode_option_natural), + KEY_COLOR_MODE_NATURAL), + new ColorModeCandidateInfo(c.getString(R.string.color_mode_option_boosted), + KEY_COLOR_MODE_BOOSTED), + new ColorModeCandidateInfo(c.getString(R.string.color_mode_option_saturated), + KEY_COLOR_MODE_SATURATED) + ); + } + + @Override + protected String getDefaultKey() { + if (isNativeModeEnabled()) { + return KEY_COLOR_MODE_SATURATED; + } + if (getSaturationValue() > COLOR_SATURATION_NATURAL) { + return KEY_COLOR_MODE_BOOSTED; + } + return KEY_COLOR_MODE_NATURAL; + } + + @Override + protected boolean setDefaultKey(String key) { + switch (key) { + case KEY_COLOR_MODE_NATURAL: + applySaturation(COLOR_SATURATION_NATURAL); + setNativeMode(false); + break; + case KEY_COLOR_MODE_BOOSTED: + applySaturation(COLOR_SATURATION_BOOSTED); + setNativeMode(false); + break; + case KEY_COLOR_MODE_SATURATED: + applySaturation(COLOR_SATURATION_NATURAL); + setNativeMode(true); + break; + } + + updateConfiguration(); + + return true; + } + + @VisibleForTesting + void updateConfiguration() { + try { + mActivityManager.updateConfiguration(null); + } catch (RemoteException e) { + Log.d(TAG, "Could not update configuration", e); + } + } + + @Override + public int getMetricsCategory() { + return MetricsProto.MetricsEvent.COLOR_MODE_SETTINGS; + } + + /** + * Propagates the provided saturation to the SurfaceFlinger. + */ + private void applySaturation(float saturation) { + SystemProperties.set(PERSISTENT_PROPERTY_SATURATION, Float.toString(saturation)); + if (mSurfaceFlinger != null) { + final Parcel data = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + data.writeFloat(saturation); + try { + mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_SATURATION, data, null, 0); + } catch (RemoteException ex) { + Log.e(TAG, "Failed to set saturation", ex); + } finally { + data.recycle(); + } + } + } + + private static float getSaturationValue() { + try { + return Float.parseFloat(SystemProperties.get( + PERSISTENT_PROPERTY_SATURATION, Float.toString(COLOR_SATURATION_NATURAL))); + } catch (NumberFormatException e) { + return COLOR_SATURATION_NATURAL; + } + } + + /** + * Toggles native mode on/off in SurfaceFlinger. + */ + private void setNativeMode(boolean enabled) { + SystemProperties.set(PERSISTENT_PROPERTY_NATIVE_MODE, enabled ? "1" : "0"); + if (mSurfaceFlinger != null) { + final Parcel data = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + data.writeInt(enabled ? 1 : 0); + try { + mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_NATIVE_MODE, data, null, 0); + } catch (RemoteException ex) { + Log.e(TAG, "Failed to set native mode", ex); + } finally { + data.recycle(); + } + } + } + + private static boolean isNativeModeEnabled() { + return SystemProperties.getBoolean(PERSISTENT_PROPERTY_NATIVE_MODE, false); + } + + @VisibleForTesting + static class ColorModeCandidateInfo extends CandidateInfo { + private final CharSequence mLabel; + private final String mKey; + + ColorModeCandidateInfo(CharSequence label, String key) { + super(true); + mLabel = label; + mKey = key; + } + + @Override + public CharSequence loadLabel() { + return mLabel; + } + + @Override + public Drawable loadIcon() { + return null; + } + + @Override + public String getKey() { + return mKey; + } + } +} diff --git a/tests/robotests/src/com/android/settings/display/ColorModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/ColorModePreferenceControllerTest.java deleted file mode 100644 index 00d910eb82c..00000000000 --- a/tests/robotests/src/com/android/settings/display/ColorModePreferenceControllerTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2017 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.display; - -import android.content.Context; -import android.os.IBinder; -import android.support.v14.preference.SwitchPreference; -import android.support.v7.preference.PreferenceScreen; -import com.android.settings.testutils.SettingsRobolectricTestRunner; -import com.android.settings.TestConfig; -import com.android.settings.testutils.shadow.SettingsShadowSystemProperties; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.annotation.Config; -import org.robolectric.util.ReflectionHelpers; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(SettingsRobolectricTestRunner.class) -@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) -public class ColorModePreferenceControllerTest { - @Mock - private ColorModePreferenceController.ConfigurationWrapper mConfigWrapper; - @Mock - private SwitchPreference mPreference; - @Mock - private PreferenceScreen mScreen; - @Mock - private Context mContext; - @Mock - private IBinder mSurfaceFlinger; - - private ColorModePreferenceController mController; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - SettingsShadowSystemProperties.clear(); - - mController = new ColorModePreferenceController(mContext); - ReflectionHelpers.setField(mController, "mSurfaceFlinger", mSurfaceFlinger); - ReflectionHelpers.setField(mController, "mConfigWrapper", mConfigWrapper); - - when(mConfigWrapper.isScreenWideColorGamut()).thenReturn(true); - - when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); - when(mPreference.getKey()).thenReturn(mController.getPreferenceKey()); - } - - @Config(shadows = {SettingsShadowSystemProperties.class}) - @Test - public void shouldCheckPreference() { - SettingsShadowSystemProperties.set( - ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION, - Float.toString(ColorModePreferenceController.COLOR_SATURATION_VIVID)); - - mController.updateState(mPreference); - - verify(mPreference).setChecked(true); - } - - @Config(shadows = {SettingsShadowSystemProperties.class}) - @Test - public void shouldUncheckPreference() { - SettingsShadowSystemProperties.set( - ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION, - Float.toString(ColorModePreferenceController.COLOR_SATURATION_DEFAULT)); - - mController.updateState(mPreference); - - verify(mPreference).setChecked(false); - } - - @Config(shadows = {SettingsShadowSystemProperties.class}) - @Test - public void shouldBoostSaturationOnCheck() { - mController.onPreferenceChange(mPreference, true); - - String saturation = SettingsShadowSystemProperties - .get(ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION); - assertThat(saturation) - .isEqualTo(Float.toString(ColorModePreferenceController.COLOR_SATURATION_VIVID)); - } - - @Config(shadows = {SettingsShadowSystemProperties.class}) - @Test - public void shouldResetSaturationOnUncheck() { - mController.onPreferenceChange(mPreference, false); - - String saturation = SettingsShadowSystemProperties - .get(ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION); - assertThat(saturation) - .isEqualTo(Float.toString(ColorModePreferenceController.COLOR_SATURATION_DEFAULT)); - } -} diff --git a/tests/robotests/src/com/android/settings/display/ColorModePreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/display/ColorModePreferenceFragmentTest.java new file mode 100644 index 00000000000..435f4f21a67 --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/ColorModePreferenceFragmentTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2017 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.display; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.IActivityManager; +import android.content.res.Configuration; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.internal.logging.nano.MetricsProto; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.SettingsShadowSystemProperties; +import com.android.settings.widget.RadioButtonPickerFragment; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class ColorModePreferenceFragmentTest { + @Mock + private IBinder mSurfaceFlinger; + @Mock + private IActivityManager mActivityManager; + + private ColorModePreferenceFragment mFragment; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + SettingsShadowSystemProperties.clear(); + + mFragment = spy(new ColorModePreferenceFragment()); + doReturn(RuntimeEnvironment.application).when(mFragment).getContext(); + doNothing().when(mFragment).updateConfiguration(); + + ReflectionHelpers.setField(mFragment, "mSurfaceFlinger", mSurfaceFlinger); + ReflectionHelpers.setField(mFragment, "mActivityManager", mActivityManager); + } + + @Test + public void verifyMetricsConstant() { + assertThat(mFragment.getMetricsCategory()) + .isEqualTo(MetricsProto.MetricsEvent.COLOR_MODE_SETTINGS); + } + + @Test + public void getCandidates() { + List candidates = + mFragment.getCandidates(); + + assertThat(candidates.size()).isEqualTo(3); + assertThat(candidates.get(0).getKey()) + .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_NATURAL); + assertThat(candidates.get(1).getKey()) + .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_BOOSTED); + assertThat(candidates.get(2).getKey()) + .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_SATURATED); + } + + @Config(shadows = {SettingsShadowSystemProperties.class}) + @Test + public void getKey_natural() { + SettingsShadowSystemProperties.set( + ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION, + Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_NATURAL)); + SettingsShadowSystemProperties.set( + ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE, "0"); + + assertThat(mFragment.getDefaultKey()) + .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_NATURAL); + } + + @Config(shadows = {SettingsShadowSystemProperties.class}) + @Test + public void getKey_boosted() { + SettingsShadowSystemProperties.set( + ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION, + Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_BOOSTED)); + SettingsShadowSystemProperties.set( + ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE, "0"); + + assertThat(mFragment.getDefaultKey()) + .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_BOOSTED); + } + + @Config(shadows = {SettingsShadowSystemProperties.class}) + @Test + public void getKey_saturated() { + SettingsShadowSystemProperties.set( + ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE, "1"); + + assertThat(mFragment.getDefaultKey()) + .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_SATURATED); + } + + @Config(shadows = {SettingsShadowSystemProperties.class}) + @Test + public void setKey_natural() { + mFragment.setDefaultKey(ColorModePreferenceFragment.KEY_COLOR_MODE_NATURAL); + + String saturation = SettingsShadowSystemProperties + .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION); + assertThat(saturation) + .isEqualTo(Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_NATURAL)); + + String nativeMode = SettingsShadowSystemProperties + .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE); + assertThat(nativeMode).isEqualTo("0"); + } + + @Config(shadows = {SettingsShadowSystemProperties.class}) + @Test + public void setKey_boosted() { + mFragment.setDefaultKey(ColorModePreferenceFragment.KEY_COLOR_MODE_BOOSTED); + + String saturation = SettingsShadowSystemProperties + .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION); + assertThat(saturation) + .isEqualTo(Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_BOOSTED)); + + String nativeMode = SettingsShadowSystemProperties + .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE); + assertThat(nativeMode).isEqualTo("0"); + } + + @Config(shadows = {SettingsShadowSystemProperties.class}) + @Test + public void setKey_saturated() { + mFragment.setDefaultKey(ColorModePreferenceFragment.KEY_COLOR_MODE_SATURATED); + + String saturation = SettingsShadowSystemProperties + .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION); + assertThat(saturation) + .isEqualTo(Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_NATURAL)); + + String nativeMode = SettingsShadowSystemProperties + .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE); + assertThat(nativeMode).isEqualTo("1"); + } +}