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"); + } +}