Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9143fb26 authored by Romain Guy's avatar Romain Guy Committed by Chia-I Wu
Browse files

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
Merged-In: I7009723e11ee164c93719f0e42fed9d1b3cf6e01
Change-Id: I7009723e11ee164c93719f0e42fed9d1b3cf6e01
(cherry picked from commit 05fa18f8)
parent 16776b64
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -64,9 +64,11 @@
        android:key="auto_rotate"
        android:title="@string/accelerometer_title" />

    <SwitchPreference
    <Preference
        android:key="color_mode"
        android:title="@string/color_mode_title" />
        android:title="@string/color_mode_title"
        android:fragment="com.android.settings.display.ColorModePreferenceFragment"
        settings:keywords="@string/keywords_color_mode" />

    <Preference
        android:key="font_size"
+23 −65
Original line number Diff line number Diff line
@@ -18,35 +18,22 @@ import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.support.v7.preference.Preference;
import android.support.v7.preference.TwoStatePreference;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.core.PreferenceController;

public class ColorModePreferenceController extends PreferenceController implements
        Preference.OnPreferenceChangeListener {
public class ColorModePreferenceController extends PreferenceController {
    private static final String TAG = "ColorModePreference";

    private static final String KEY_COLOR_MODE = "color_mode";

    @VisibleForTesting
    static final float COLOR_SATURATION_DEFAULT = 1.0f;
    @VisibleForTesting
    static final float COLOR_SATURATION_VIVID = 1.1f;

    private static final int SURFACE_FLINGER_TRANSACTION_SATURATION = 1022;
    @VisibleForTesting
    static final String PERSISTENT_PROPERTY_SATURATION = "persist.sys.sf.color_saturation";
    private static final int SURFACE_FLINGER_TRANSACTION_QUERY_WIDE_COLOR = 1024;

    private final IBinder mSurfaceFlinger;
    private final ConfigurationWrapper mConfigWrapper;

    public ColorModePreferenceController(Context context) {
        super(context);
        mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger");
        mConfigWrapper = new ConfigurationWrapper(context);
        mConfigWrapper = new ConfigurationWrapper();
    }

    @Override
@@ -54,65 +41,36 @@ public class ColorModePreferenceController extends PreferenceController implemen
        return KEY_COLOR_MODE;
    }

    @Override
    public void updateState(Preference preference) {
        TwoStatePreference colorMode = (TwoStatePreference) preference;
        colorMode.setChecked(getSaturationValue() > 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);
    @VisibleForTesting
    static class ConfigurationWrapper {
        private final IBinder mSurfaceFlinger;

        return true;
        ConfigurationWrapper() {
            mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger");
        }

    /**
     * Propagates the provided saturation to the SurfaceFlinger.
     */
    private void applySaturation(float saturation) {
        boolean isScreenWideColorGamut() {
            if (mSurfaceFlinger != null) {
                final Parcel data = Parcel.obtain();
                final Parcel reply = Parcel.obtain();
                data.writeInterfaceToken("android.ui.ISurfaceComposer");
            data.writeFloat(saturation);
                try {
                mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_SATURATION, data, null, 0);
                    mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_QUERY_WIDE_COLOR,
                            data, reply, 0);
                    return reply.readBoolean();
                } catch (RemoteException ex) {
                Log.e(TAG, "Failed to set saturation", ex);
                    Log.e(TAG, "Failed to query wide color support", ex);
                } finally {
                    data.recycle();
                    reply.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;

        ConfigurationWrapper(Context context) {
            mContext = context;
        }

        boolean isScreenWideColorGamut() {
            return mContext.getResources().getConfiguration().isScreenWideColorGamut();
            return false;
        }
    }
}
+207 −0
Original line number Diff line number Diff line
/*
 * 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<? extends CandidateInfo> 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;
        }
    }
}
+170 −0
Original line number Diff line number Diff line
@@ -15,99 +15,156 @@
 */
package com.android.settings.display;

import android.content.Context;
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.support.v14.preference.SwitchPreference;
import android.support.v7.preference.PreferenceScreen;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
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 static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;

@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;
public class ColorModePreferenceFragmentTest {
    @Mock
    private IBinder mSurfaceFlinger;
    @Mock
    private IActivityManager mActivityManager;

    private ColorModePreferenceController mController;
    private ColorModePreferenceFragment mFragment;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        SettingsShadowSystemProperties.clear();

        mController = new ColorModePreferenceController(mContext);
        ReflectionHelpers.setField(mController, "mSurfaceFlinger", mSurfaceFlinger);
        ReflectionHelpers.setField(mController, "mConfigWrapper", mConfigWrapper);
        mFragment = spy(new ColorModePreferenceFragment());
        doReturn(RuntimeEnvironment.application).when(mFragment).getContext();
        doNothing().when(mFragment).updateConfiguration();

        when(mConfigWrapper.isScreenWideColorGamut()).thenReturn(true);
        ReflectionHelpers.setField(mFragment, "mSurfaceFlinger", mSurfaceFlinger);
        ReflectionHelpers.setField(mFragment, "mActivityManager", mActivityManager);
    }

        when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference);
        when(mPreference.getKey()).thenReturn(mController.getPreferenceKey());
    @Test
    public void verifyMetricsConstant() {
        assertThat(mFragment.getMetricsCategory())
                .isEqualTo(MetricsProto.MetricsEvent.COLOR_MODE_SETTINGS);
    }

    @Test
    public void getCandidates() {
        List<? extends RadioButtonPickerFragment.CandidateInfo> 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 shouldCheckPreference() {
    public void getKey_natural() {
        SettingsShadowSystemProperties.set(
                ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION,
                Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_NATURAL));
        SettingsShadowSystemProperties.set(
                ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION,
                Float.toString(ColorModePreferenceController.COLOR_SATURATION_VIVID));
                ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE, "0");

        mController.updateState(mPreference);
        assertThat(mFragment.getDefaultKey())
                .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_NATURAL);
    }

        verify(mPreference).setChecked(true);
    @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 shouldUncheckPreference() {
    public void getKey_saturated() {
        SettingsShadowSystemProperties.set(
                ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION,
                Float.toString(ColorModePreferenceController.COLOR_SATURATION_DEFAULT));
                ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE, "1");

        mController.updateState(mPreference);
        assertThat(mFragment.getDefaultKey())
                .isEqualTo(ColorModePreferenceFragment.KEY_COLOR_MODE_SATURATED);
    }

        verify(mPreference).setChecked(false);
    @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 shouldBoostSaturationOnCheck() {
        mController.onPreferenceChange(mPreference, true);
    public void setKey_boosted() {
        mFragment.setDefaultKey(ColorModePreferenceFragment.KEY_COLOR_MODE_BOOSTED);

        String saturation = SettingsShadowSystemProperties
                .get(ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION);
                .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION);
        assertThat(saturation)
                .isEqualTo(Float.toString(ColorModePreferenceController.COLOR_SATURATION_VIVID));
                .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 shouldResetSaturationOnUncheck() {
        mController.onPreferenceChange(mPreference, false);
    public void setKey_saturated() {
        mFragment.setDefaultKey(ColorModePreferenceFragment.KEY_COLOR_MODE_SATURATED);

        String saturation = SettingsShadowSystemProperties
                .get(ColorModePreferenceController.PERSISTENT_PROPERTY_SATURATION);
                .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_SATURATION);
        assertThat(saturation)
                .isEqualTo(Float.toString(ColorModePreferenceController.COLOR_SATURATION_DEFAULT));
                .isEqualTo(Float.toString(ColorModePreferenceFragment.COLOR_SATURATION_NATURAL));

        String nativeMode = SettingsShadowSystemProperties
                .get(ColorModePreferenceFragment.PERSISTENT_PROPERTY_NATIVE_MODE);
        assertThat(nativeMode).isEqualTo("1");
    }
}