Add support for setting color transforms

Bug: 24038268
Change-Id: I05275c906e02eb9e67331f6f909166eb08ad5536
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 21ba7bd..121a187 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -359,6 +359,14 @@
         }
     }
 
+    public void requestColorTransform(int displayId, int colorTransformId) {
+        try {
+            mDm.requestColorTransform(displayId, colorTransformId);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to request color transform.", ex);
+        }
+    }
+
     public VirtualDisplay createVirtualDisplay(Context context, MediaProjection projection,
             String name, int width, int height, int densityDpi, Surface surface, int flags,
             VirtualDisplay.Callback callback, Handler handler) {
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 4486dd4..8a1abf1 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -59,6 +59,9 @@
     // No permissions required.
     WifiDisplayStatus getWifiDisplayStatus();
 
+    // Requires CONFIGURE_DISPLAY_COLOR_TRANSFORM
+    void requestColorTransform(int displayId, int colorTransformId);
+
     // Requires CAPTURE_VIDEO_OUTPUT, CAPTURE_SECURE_VIDEO_OUTPUT, or an appropriate
     // MediaProjection token for certain combinations of flags.
     int createVirtualDisplay(in IVirtualDisplayCallback callback,
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 35c4192..1269ad9 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -16,7 +16,10 @@
 
 package android.view;
 
+import android.annotation.RequiresPermission;
+import android.content.Context;
 import android.content.res.CompatibilityInfo;
+import android.content.res.Resources;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -30,6 +33,8 @@
 
 import java.util.Arrays;
 
+import static android.Manifest.permission.CONFIGURE_DISPLAY_COLOR_TRANSFORM;
+
 /**
  * Provides information about the size and density of a logical display.
  * <p>
@@ -679,6 +684,49 @@
     }
 
     /**
+     * Request the display applies a color transform.
+     * @hide
+     */
+    @RequiresPermission(CONFIGURE_DISPLAY_COLOR_TRANSFORM)
+    public void requestColorTransform(ColorTransform colorTransform) {
+        mGlobal.requestColorTransform(mDisplayId, colorTransform.getId());
+    }
+
+    /**
+     * Returns the active color transform of this display
+     * @hide
+     */
+    public ColorTransform getColorTransform() {
+        synchronized (this) {
+            updateDisplayInfoLocked();
+            return mDisplayInfo.getColorTransform();
+        }
+    }
+
+    /**
+     * Returns the default color transform of this display
+     * @hide
+     */
+    public ColorTransform getDefaultColorTransform() {
+        synchronized (this) {
+            updateDisplayInfoLocked();
+            return mDisplayInfo.getDefaultColorTransform();
+        }
+    }
+
+    /**
+     * Gets the supported color transforms of this device.
+     * @hide
+     */
+    public ColorTransform[] getSupportedColorTransforms() {
+        synchronized (this) {
+            updateDisplayInfoLocked();
+            ColorTransform[] transforms = mDisplayInfo.supportedColorTransforms;
+            return Arrays.copyOf(transforms, transforms.length);
+        }
+    }
+
+    /**
      * Gets the app VSYNC offset, in nanoseconds.  This is a positive value indicating
      * the phase offset of the VSYNC events provided by Choreographer relative to the
      * display refresh.  For example, if Choreographer reports that the refresh occurred
@@ -1054,4 +1102,89 @@
             }
         };
     }
+
+    /**
+     * A color transform supported by a given display.
+     *
+     * @see Display#getSupportedColorTransforms()
+     * @hide
+     */
+    public static final class ColorTransform implements Parcelable {
+        public static final ColorTransform[] EMPTY_ARRAY = new ColorTransform[0];
+
+        private final int mId;
+        private final int mColorTransform;
+
+        public ColorTransform(int id, int colorTransform) {
+            mId = id;
+            mColorTransform = colorTransform;
+        }
+
+        public int getId() {
+            return mId;
+        }
+
+        public int getColorTransform() {
+            return mColorTransform;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof ColorTransform)) {
+                return false;
+            }
+            ColorTransform that = (ColorTransform) other;
+            return mId == that.mId
+                && mColorTransform == that.mColorTransform;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 1;
+            hash = hash * 17 + mId;
+            hash = hash * 17 + mColorTransform;
+            return hash;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder("{")
+                    .append("id=").append(mId)
+                    .append(", colorTransform=").append(mColorTransform)
+                    .append("}")
+                    .toString();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        private ColorTransform(Parcel in) {
+            this(in.readInt(), in.readInt());
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int parcelableFlags) {
+            out.writeInt(mId);
+            out.writeInt(mColorTransform);
+        }
+
+        @SuppressWarnings("hiding")
+        public static final Parcelable.Creator<ColorTransform> CREATOR
+                = new Parcelable.Creator<ColorTransform>() {
+            @Override
+            public ColorTransform createFromParcel(Parcel in) {
+                return new ColorTransform(in);
+            }
+
+            @Override
+            public ColorTransform[] newArray(int size) {
+                return new ColorTransform[size];
+            }
+        };
+    }
 }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index cf17990..ee76274 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -169,6 +169,15 @@
      */
     public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
 
+    /** The active color transform. */
+    public int colorTransformId;
+
+    /** The default color transform. */
+    public int defaultColorTransformId;
+
+    /** The list of supported color transforms */
+    public Display.ColorTransform[] supportedColorTransforms = Display.ColorTransform.EMPTY_ARRAY;
+
     /**
      * The logical display density which is the basis for density-independent
      * pixels.
@@ -279,6 +288,8 @@
                 && rotation == other.rotation
                 && modeId == other.modeId
                 && defaultModeId == other.defaultModeId
+                && colorTransformId == other.colorTransformId
+                && defaultColorTransformId == other.defaultColorTransformId
                 && logicalDensityDpi == other.logicalDensityDpi
                 && physicalXDpi == other.physicalXDpi
                 && physicalYDpi == other.physicalYDpi
@@ -317,6 +328,10 @@
         modeId = other.modeId;
         defaultModeId = other.defaultModeId;
         supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
+        colorTransformId = other.colorTransformId;
+        defaultColorTransformId = other.defaultColorTransformId;
+        supportedColorTransforms = Arrays.copyOf(
+                other.supportedColorTransforms, other.supportedColorTransforms.length);
         logicalDensityDpi = other.logicalDensityDpi;
         physicalXDpi = other.physicalXDpi;
         physicalYDpi = other.physicalYDpi;
@@ -353,6 +368,13 @@
         for (int i = 0; i < nModes; i++) {
             supportedModes[i] = Display.Mode.CREATOR.createFromParcel(source);
         }
+        colorTransformId = source.readInt();
+        defaultColorTransformId = source.readInt();
+        int nColorTransforms = source.readInt();
+        supportedColorTransforms = new Display.ColorTransform[nColorTransforms];
+        for (int i = 0; i < nColorTransforms; i++) {
+            supportedColorTransforms[i] = Display.ColorTransform.CREATOR.createFromParcel(source);
+        }
         logicalDensityDpi = source.readInt();
         physicalXDpi = source.readFloat();
         physicalYDpi = source.readFloat();
@@ -390,6 +412,12 @@
         for (int i = 0; i < supportedModes.length; i++) {
             supportedModes[i].writeToParcel(dest, flags);
         }
+        dest.writeInt(colorTransformId);
+        dest.writeInt(defaultColorTransformId);
+        dest.writeInt(supportedColorTransforms.length);
+        for (int i = 0; i < supportedColorTransforms.length; i++) {
+            supportedColorTransforms[i].writeToParcel(dest, flags);
+        }
         dest.writeInt(logicalDensityDpi);
         dest.writeFloat(physicalXDpi);
         dest.writeFloat(physicalYDpi);
@@ -461,6 +489,24 @@
         return result;
     }
 
+    public Display.ColorTransform getColorTransform() {
+        return findColorTransform(colorTransformId);
+    }
+
+    public Display.ColorTransform getDefaultColorTransform() {
+        return findColorTransform(defaultColorTransformId);
+    }
+
+    private Display.ColorTransform findColorTransform(int colorTransformId) {
+        for (int i = 0; i < supportedColorTransforms.length; i++) {
+            Display.ColorTransform colorTransform = supportedColorTransforms[i];
+            if (colorTransform.getId() == colorTransformId) {
+                return colorTransform;
+            }
+        }
+        throw new IllegalStateException("Unable to locate color transform: " + colorTransformId);
+    }
+
     public void getAppMetrics(DisplayMetrics outMetrics) {
         getAppMetrics(outMetrics, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
     }
@@ -562,6 +608,12 @@
         sb.append(defaultModeId);
         sb.append(", modes ");
         sb.append(Arrays.toString(supportedModes));
+        sb.append(", colorTransformId ");
+        sb.append(colorTransformId);
+        sb.append(", defaultColorTransformId ");
+        sb.append(defaultColorTransformId);
+        sb.append(", supportedColorTransforms ");
+        sb.append(Arrays.toString(supportedColorTransforms));
         sb.append(", rotation ");
         sb.append(rotation);
         sb.append(", density ");
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b8b6444..6afc544 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2160,6 +2160,13 @@
     <permission android:name="android.permission.CONTROL_WIFI_DISPLAY"
         android:protectionLevel="signature" />
 
+    <!-- Allows an application to control the color transforms applied to
+         displays system-wide.
+         <p>Not for use by third-party applications.</p>
+         @hide -->
+    <permission android:name="android.permission.CONFIGURE_DISPLAY_COLOR_TRANSFORM"
+        android:protectionLevel="signature" />
+
     <!-- @SystemApi Allows an application to control VPN.
          <p>Not for use by third-party applications.</p>
          @hide -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index fd600e3..dbd1b8d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1740,6 +1740,28 @@
     -->
     <bool name="config_enableWifiDisplay">false</bool>
 
+    <!-- The color transform values that correspond to each respective configuration mode for the
+         built-in display, or -1 if the mode is unsupported by the device. The possible
+         configuration modes are:
+            1. Wide-gamut ("Vibrant")
+            2. Adobe RGB ("Natural")
+            3. sRGB ("Standard")
+
+        For example, if a device had Wide-gamut as color transform mode 1, sRGB mode as color
+        transform mode 7, and did not support Adobe RGB at all this would look like:
+
+            <integer-array name="config_colorTransforms">
+                <item>1</item>
+                <item>-1</item>
+                <item>7</item>
+            </integer-array>
+    -->
+    <integer-array name="config_colorTransforms">
+        <item>-1</item>
+        <item>-1</item>
+        <item>-1</item>
+    </integer-array>
+
     <!-- When true use the linux /dev/input/event subsystem to detect the switch changes
          on the headphone/microphone jack. When false use the older uevent framework. -->
     <bool name="config_useDevInputEventForAudioJack">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 357d4c3..e21aab7 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1130,6 +1130,7 @@
   <java-symbol type="array" name="config_telephonyHardware" />
   <java-symbol type="array" name="config_keySystemUuidMapping" />
   <java-symbol type="array" name="config_gpsParameters" />
+  <java-symbol type="array" name="config_colorTransforms" />
 
   <java-symbol type="drawable" name="default_wallpaper" />
   <java-symbol type="drawable" name="indicator_input_error" />
diff --git a/services/core/java/com/android/server/display/DisplayAdapter.java b/services/core/java/com/android/server/display/DisplayAdapter.java
index 6ba25a5..701b9f1 100644
--- a/services/core/java/com/android/server/display/DisplayAdapter.java
+++ b/services/core/java/com/android/server/display/DisplayAdapter.java
@@ -49,6 +49,13 @@
      */
     private static final AtomicInteger NEXT_DISPLAY_MODE_ID = new AtomicInteger(1);  // 0 = no mode.
 
+    /**
+     * Used to generate globally unique color transform ids.
+     *
+     * Valid IDs start at 1 with 0 as the sentinel value for the default mode.
+     */
+    private static final AtomicInteger NEXT_COLOR_TRANSFORM_ID = new AtomicInteger(1);
+
     // Called with SyncRoot lock held.
     public DisplayAdapter(DisplayManagerService.SyncRoot syncRoot,
             Context context, Handler handler, Listener listener, String name) {
@@ -134,6 +141,11 @@
                 NEXT_DISPLAY_MODE_ID.getAndIncrement(), width, height, refreshRate);
     }
 
+    public static Display.ColorTransform createColorTransform(int colorTransform) {
+        return new Display.ColorTransform(
+                NEXT_COLOR_TRANSFORM_ID.getAndIncrement(), colorTransform);
+    }
+
     public interface Listener {
         public void onDisplayDeviceEvent(DisplayDevice device, int event);
         public void onTraversalRequested();
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 93bda46..7af0bdb 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -135,7 +135,7 @@
     /**
      * Sets the mode, if supported.
      */
-    public void requestModeInTransactionLocked(int id) {
+    public void requestColorTransformAndModeInTransactionLocked(int colorTransformId, int modeId) {
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 97ada15..55ba302 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -155,6 +155,15 @@
      */
     public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
 
+    /** The active color transform of the display */
+    public int colorTransformId;
+
+    /** The default color transform of the display */
+    public int defaultColorTransformId;
+
+    /** The supported color transforms of the display */
+    public Display.ColorTransform[] supportedColorTransforms = Display.ColorTransform.EMPTY_ARRAY;
+
     /**
      * The nominal apparent density of the display in DPI used for layout calculations.
      * This density is sensitive to the viewing distance.  A big TV and a tablet may have
@@ -276,6 +285,9 @@
                 || modeId != other.modeId
                 || defaultModeId != other.defaultModeId
                 || !Arrays.equals(supportedModes, other.supportedModes)
+                || colorTransformId != other.colorTransformId
+                || defaultColorTransformId != other.defaultColorTransformId
+                || !Arrays.equals(supportedColorTransforms, other.supportedColorTransforms)
                 || densityDpi != other.densityDpi
                 || xDpi != other.xDpi
                 || yDpi != other.yDpi
@@ -306,6 +318,9 @@
         modeId = other.modeId;
         defaultModeId = other.defaultModeId;
         supportedModes = other.supportedModes;
+        colorTransformId = other.colorTransformId;
+        defaultColorTransformId = other.defaultColorTransformId;
+        supportedColorTransforms = other.supportedColorTransforms;
         densityDpi = other.densityDpi;
         xDpi = other.xDpi;
         yDpi = other.yDpi;
@@ -331,6 +346,9 @@
         sb.append(", modeId ").append(modeId);
         sb.append(", defaultModeId ").append(defaultModeId);
         sb.append(", supportedModes ").append(Arrays.toString(supportedModes));
+        sb.append(", colorTransformId ").append(colorTransformId);
+        sb.append(", defaultColorTransformId ").append(defaultColorTransformId);
+        sb.append(", supportedColorTransforms ").append(Arrays.toString(supportedColorTransforms));
         sb.append(", density ").append(densityDpi);
         sb.append(", ").append(xDpi).append(" x ").append(yDpi).append(" dpi");
         sb.append(", appVsyncOff ").append(appVsyncOffsetNanos);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index b2ab797..6a6570b 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -540,6 +540,17 @@
         }
     }
 
+    private void requestColorTransformInternal(int displayId, int colorTransformId) {
+        synchronized (mSyncRoot) {
+            LogicalDisplay display = mLogicalDisplays.get(displayId);
+            if (display != null &&
+                    display.getRequestedColorTransformIdLocked() != colorTransformId) {
+                display.setRequestedColorTransformIdLocked(colorTransformId);
+                scheduleTraversalLocked(false);
+            }
+        }
+    }
+
     private int createVirtualDisplayInternal(IVirtualDisplayCallback callback,
             IMediaProjection projection, int callingUid, String packageName,
             String name, int width, int height, int densityDpi, Surface surface, int flags) {
@@ -1340,6 +1351,19 @@
         }
 
         @Override // Binder call
+        public void requestColorTransform(int displayId, int colorTransformId) {
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.CONFIGURE_DISPLAY_COLOR_TRANSFORM,
+                    "Permission required to change the display color transform");
+            final long token = Binder.clearCallingIdentity();
+            try {
+                requestColorTransformInternal(displayId, colorTransformId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
         public int createVirtualDisplay(IVirtualDisplayCallback callback,
                 IMediaProjection projection, String packageName, String name,
                 int width, int height, int densityDpi, Surface surface, int flags) {
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 517a825..be37f52 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -31,6 +31,7 @@
 import android.os.Trace;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.Display;
 import android.view.DisplayEventReceiver;
 import android.view.Surface;
@@ -38,6 +39,7 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * A display adapter for the local displays managed by Surface Flinger.
@@ -143,14 +145,22 @@
         private final int mBuiltInDisplayId;
         private final Light mBacklight;
         private final SparseArray<DisplayModeRecord> mSupportedModes = new SparseArray<>();
+        private final SparseArray<Display.ColorTransform> mSupportedColorTransforms =
+                new SparseArray<>();
 
         private DisplayDeviceInfo mInfo;
         private boolean mHavePendingChanges;
         private int mState = Display.STATE_UNKNOWN;
         private int mBrightness = PowerManager.BRIGHTNESS_DEFAULT;
+        private int mActivePhysIndex;
         private int mDefaultModeId;
         private int mActiveModeId;
         private boolean mActiveModeInvalid;
+        private int mDefaultColorTransformId;
+        private int mActiveColorTransformId;
+        private boolean mActiveColorTransformInvalid;
+
+        private  SurfaceControl.PhysicalDisplayInfo mDisplayInfos[];
 
         public LocalDisplayDevice(IBinder displayToken, int builtInDisplayId,
                 SurfaceControl.PhysicalDisplayInfo[] physicalDisplayInfos, int activeDisplayInfo) {
@@ -167,22 +177,73 @@
 
         public boolean updatePhysicalDisplayInfoLocked(
                 SurfaceControl.PhysicalDisplayInfo[] physicalDisplayInfos, int activeDisplayInfo) {
-            // Build an updated list of all existing modes.
-            boolean modesAdded = false;
-            DisplayModeRecord activeRecord = null;
-            ArrayList<DisplayModeRecord> records = new ArrayList<DisplayModeRecord>();
+            mDisplayInfos = Arrays.copyOf(physicalDisplayInfos, physicalDisplayInfos.length);
+            mActivePhysIndex = activeDisplayInfo;
+            ArrayList<Display.ColorTransform> colorTransforms = new ArrayList<>();
+
+            // Build an updated list of all existing color transforms.
+            boolean colorTransformsAdded = false;
+            Display.ColorTransform activeColorTransform = null;
             for (int i = 0; i < physicalDisplayInfos.length; i++) {
                 SurfaceControl.PhysicalDisplayInfo info = physicalDisplayInfos[i];
+                // First check to see if we've already added this color transform
+                boolean existingMode = false;
+                for (int j = 0; j < colorTransforms.size(); j++) {
+                    if (colorTransforms.get(j).getColorTransform() == info.colorTransform) {
+                        existingMode = true;
+                        break;
+                    }
+                }
+                if (existingMode) {
+                    continue;
+                }
+                Display.ColorTransform colorTransform = findColorTransform(info);
+                if (colorTransform == null) {
+                    colorTransform = createColorTransform(info.colorTransform);
+                    colorTransformsAdded = true;
+                }
+                colorTransforms.add(colorTransform);
+                if (i == activeDisplayInfo) {
+                    activeColorTransform = colorTransform;
+                }
+            }
+
+            // Build an updated list of all existing modes.
+            ArrayList<DisplayModeRecord> records = new ArrayList<DisplayModeRecord>();
+            boolean modesAdded = false;
+            for (int i = 0; i < physicalDisplayInfos.length; i++) {
+                SurfaceControl.PhysicalDisplayInfo info = physicalDisplayInfos[i];
+                // First, check to see if we've already added a matching mode. Since not all
+                // configuration options are exposed via Display.Mode, it's possible that we have
+                // multiple PhysicalDisplayInfos that would generate the same Display.Mode.
+                boolean existingMode = false;
+                for (int j = 0; j < records.size(); j++) {
+                    if (records.get(j).hasMatchingMode(info)) {
+                        existingMode = true;
+                        break;
+                    }
+                }
+                if (existingMode) {
+                    continue;
+                }
+                // If we haven't already added a mode for this configuration to the new set of
+                // supported modes then check to see if we have one in the prior set of supported
+                // modes to reuse.
                 DisplayModeRecord record = findDisplayModeRecord(info);
-                if (record != null) {
-                    record.mPhysIndex = i;
-                } else {
-                    record = new DisplayModeRecord(info, i);
+                if (record == null) {
+                    record = new DisplayModeRecord(info);
                     modesAdded = true;
                 }
                 records.add(record);
-                if (i == activeDisplayInfo) {
+            }
+
+            // Get the currently active mode
+            DisplayModeRecord activeRecord = null;
+            for (int i = 0; i < records.size(); i++) {
+                DisplayModeRecord record = records.get(i);
+                if (record.hasMatchingMode(physicalDisplayInfos[activeDisplayInfo])){
                     activeRecord = record;
+                    break;
                 }
             }
             // Check whether surface flinger spontaneously changed modes out from under us. Schedule
@@ -192,25 +253,48 @@
                 mActiveModeInvalid = true;
                 sendTraversalRequestLocked();
             }
-            // If no modes were added and we have the same number of modes as before, then nothing
-            // actually changed except possibly the physical index (which we only care about when
-            // setting the mode) so we're done.
-            if (records.size() == mSupportedModes.size() && !modesAdded) {
+            // Check whether surface flinger spontaneously changed color transforms out from under
+            // us.
+            if (mActiveColorTransformId != 0
+                    && mActiveColorTransformId != activeColorTransform.getId()) {
+                mActiveColorTransformInvalid = true;
+                sendTraversalRequestLocked();
+            }
+
+            boolean colorTransformsChanged =
+                    colorTransforms.size() != mSupportedColorTransforms.size()
+                    || colorTransformsAdded;
+            boolean recordsChanged = records.size() != mSupportedModes.size() || modesAdded;
+            // If neither the records nor the supported color transforms have changed then we're
+            // done here.
+            if (!recordsChanged && !colorTransformsChanged) {
                 return false;
             }
             // Update the index of modes.
             mHavePendingChanges = true;
+
             mSupportedModes.clear();
             for (DisplayModeRecord record : records) {
                 mSupportedModes.put(record.mMode.getModeId(), record);
             }
-            // Update the default mode if needed.
-            if (mSupportedModes.indexOfKey(mDefaultModeId) < 0) {
+            mSupportedColorTransforms.clear();
+            for (Display.ColorTransform colorTransform : colorTransforms) {
+                mSupportedColorTransforms.put(colorTransform.getId(), colorTransform);
+            }
+
+            // Update the default mode and color transform if needed. This needs to be done in
+            // tandem so we always have a default state to fall back to.
+            if (findDisplayInfoIndexLocked(mDefaultColorTransformId, mDefaultModeId) < 0) {
                 if (mDefaultModeId != 0) {
-                    Slog.w(TAG, "Default display mode no longer available, using currently active"
-                            + " mode as default.");
+                    Slog.w(TAG, "Default display mode no longer available, using currently"
+                            + " active mode as default.");
                 }
                 mDefaultModeId = activeRecord.mMode.getModeId();
+                if (mDefaultColorTransformId != 0) {
+                    Slog.w(TAG, "Default color transform no longer available, using currently"
+                            + " active color transform as default");
+                }
+                mDefaultColorTransformId = activeColorTransform.getId();
             }
             // Determine whether the active mode is still there.
             if (mSupportedModes.indexOfKey(mActiveModeId) < 0) {
@@ -221,6 +305,16 @@
                 mActiveModeId = mDefaultModeId;
                 mActiveModeInvalid = true;
             }
+
+            // Determine whether the active color transform is still there.
+            if (mSupportedColorTransforms.indexOfKey(mActiveColorTransformId) < 0) {
+                if (mActiveColorTransformId != 0) {
+                    Slog.w(TAG, "Active color transform no longer available, reverting"
+                            + " to default transform.");
+                }
+                mActiveColorTransformId = mDefaultColorTransformId;
+                mActiveColorTransformInvalid = true;
+            }
             // Schedule traversals so that we apply pending changes.
             sendTraversalRequestLocked();
             return true;
@@ -229,13 +323,23 @@
         private DisplayModeRecord findDisplayModeRecord(SurfaceControl.PhysicalDisplayInfo info) {
             for (int i = 0; i < mSupportedModes.size(); i++) {
                 DisplayModeRecord record = mSupportedModes.valueAt(i);
-                if (record.mPhys.equals(info)) {
+                if (record.hasMatchingMode(info)) {
                     return record;
                 }
             }
             return null;
         }
 
+        private Display.ColorTransform findColorTransform(SurfaceControl.PhysicalDisplayInfo info) {
+            for (int i = 0; i < mSupportedColorTransforms.size(); i++) {
+                Display.ColorTransform transform = mSupportedColorTransforms.valueAt(i);
+                if (transform.getColorTransform() == info.colorTransform) {
+                    return transform;
+                }
+            }
+            return null;
+        }
+
         @Override
         public void applyPendingDisplayDeviceInfoChangesLocked() {
             if (mHavePendingChanges) {
@@ -247,7 +351,7 @@
         @Override
         public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
             if (mInfo == null) {
-                SurfaceControl.PhysicalDisplayInfo phys = mSupportedModes.get(mActiveModeId).mPhys;
+                SurfaceControl.PhysicalDisplayInfo phys = mDisplayInfos[mActivePhysIndex];
                 mInfo = new DisplayDeviceInfo();
                 mInfo.width = phys.width;
                 mInfo.height = phys.height;
@@ -258,6 +362,13 @@
                     DisplayModeRecord record = mSupportedModes.valueAt(i);
                     mInfo.supportedModes[i] = record.mMode;
                 }
+                mInfo.colorTransformId = mActiveColorTransformId;
+                mInfo.defaultColorTransformId = mDefaultColorTransformId;
+                mInfo.supportedColorTransforms =
+                        new Display.ColorTransform[mSupportedColorTransforms.size()];
+                for (int i = 0; i < mSupportedColorTransforms.size(); i++) {
+                    mInfo.supportedColorTransforms[i] = mSupportedColorTransforms.valueAt(i);
+                }
                 mInfo.appVsyncOffsetNanos = phys.appVsyncOffsetNanos;
                 mInfo.presentationDeadlineNanos = phys.presentationDeadlineNanos;
                 mInfo.state = mState;
@@ -402,7 +513,8 @@
         }
 
         @Override
-        public void requestModeInTransactionLocked(int modeId) {
+        public void requestColorTransformAndModeInTransactionLocked(
+                int colorTransformId, int modeId) {
             if (modeId == 0) {
                 modeId = mDefaultModeId;
             } else if (mSupportedModes.indexOfKey(modeId) < 0) {
@@ -410,13 +522,37 @@
                         + " reverting to default display mode.");
                 modeId = mDefaultModeId;
             }
-            if (mActiveModeId == modeId && !mActiveModeInvalid) {
+
+            if (colorTransformId == 0) {
+                colorTransformId = mDefaultColorTransformId;
+            } else if (mSupportedColorTransforms.indexOfKey(colorTransformId) < 0) {
+                Slog.w(TAG, "Requested color transform " + colorTransformId + " is not supported"
+                        + " by this display, reverting to the default color transform");
+                colorTransformId = mDefaultColorTransformId;
+            }
+            int physIndex = findDisplayInfoIndexLocked(colorTransformId, modeId);
+            if (physIndex < 0) {
+                Slog.w(TAG, "Requested color transform, mode ID pair (" + colorTransformId + ", "
+                        + modeId + ") not available, trying color transform with default mode ID");
+                modeId = mDefaultModeId;
+                physIndex = findDisplayInfoIndexLocked(colorTransformId, modeId);
+                if (physIndex < 0) {
+                    Slog.w(TAG, "Requested color transform with default mode ID still not"
+                            + " available, falling back to default color transform with default"
+                            + " mode.");
+                    colorTransformId = mDefaultColorTransformId;
+                    physIndex = findDisplayInfoIndexLocked(colorTransformId, modeId);
+                }
+            }
+            if (physIndex > 0 && mActivePhysIndex == physIndex) {
                 return;
             }
-            DisplayModeRecord record = mSupportedModes.get(modeId);
-            SurfaceControl.setActiveConfig(getDisplayTokenLocked(), record.mPhysIndex);
+            SurfaceControl.setActiveConfig(getDisplayTokenLocked(), physIndex);
+            mActivePhysIndex = physIndex;
             mActiveModeId = modeId;
             mActiveModeInvalid = false;
+            mActiveColorTransformId = colorTransformId;
+            mActiveColorTransformInvalid = false;
             updateDeviceInfoLocked();
         }
 
@@ -424,10 +560,43 @@
         public void dumpLocked(PrintWriter pw) {
             super.dumpLocked(pw);
             pw.println("mBuiltInDisplayId=" + mBuiltInDisplayId);
+            pw.println("mActivePhysIndex=" + mActivePhysIndex);
             pw.println("mActiveModeId=" + mActiveModeId);
+            pw.println("mActiveColorTransformId=" + mActiveColorTransformId);
             pw.println("mState=" + Display.stateToString(mState));
             pw.println("mBrightness=" + mBrightness);
             pw.println("mBacklight=" + mBacklight);
+            pw.println("mDisplayInfos=");
+            for (int i = 0; i < mDisplayInfos.length; i++) {
+                pw.println("  " + mDisplayInfos[i]);
+            }
+            pw.println("mSupportedModes=");
+            for (int i = 0; i < mSupportedModes.size(); i++) {
+                pw.println("  " + mSupportedModes.valueAt(i));
+            }
+            pw.println("mSupportedColorTransforms=[");
+            for (int i = 0; i < mSupportedColorTransforms.size(); i++) {
+                if (i != 0) {
+                    pw.print(", ");
+                }
+                pw.print(mSupportedColorTransforms.valueAt(i));
+            }
+            pw.println("]");
+        }
+
+        private int findDisplayInfoIndexLocked(int colorTransformId, int modeId) {
+            DisplayModeRecord record = mSupportedModes.get(modeId);
+            Display.ColorTransform transform = mSupportedColorTransforms.get(colorTransformId);
+            if (record != null && transform != null) {
+                for (int i = 0; i < mDisplayInfos.length; i++) {
+                    SurfaceControl.PhysicalDisplayInfo info = mDisplayInfos[i];
+                    if (info.colorTransform == transform.getColorTransform()
+                            && record.hasMatchingMode(info)){
+                        return i;
+                    }
+                }
+            }
+            return -1;
         }
 
         private void updateDeviceInfoLocked() {
@@ -441,13 +610,28 @@
      */
     private static final class DisplayModeRecord {
         public final Display.Mode mMode;
-        public final SurfaceControl.PhysicalDisplayInfo mPhys;
-        public int mPhysIndex;
 
-        public DisplayModeRecord(SurfaceControl.PhysicalDisplayInfo phys, int physIndex) {
+        public DisplayModeRecord(SurfaceControl.PhysicalDisplayInfo phys) {
             mMode = createMode(phys.width, phys.height, phys.refreshRate);
-            mPhys = phys;
-            mPhysIndex = physIndex;
+        }
+
+        /**
+         * Returns whether the mode generated by the given PhysicalDisplayInfo matches the mode
+         * contained by the record modulo mode ID.
+         *
+         * Note that this doesn't necessarily mean the the PhysicalDisplayInfos are identical, just
+         * that they generate identical modes.
+         */
+        public boolean hasMatchingMode(SurfaceControl.PhysicalDisplayInfo info) {
+            int modeRefreshRate = Float.floatToIntBits(mMode.getRefreshRate());
+            int displayInfoRefreshRate = Float.floatToIntBits(info.refreshRate);
+            return mMode.getPhysicalWidth() == info.width
+                    && mMode.getPhysicalHeight() == info.height
+                    && modeRefreshRate == displayInfoRefreshRate;
+        }
+
+        public String toString() {
+            return "DisplayModeRecord{mMode=" + mMode + "}";
         }
     }
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 6efc99a..6dae397 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -74,6 +74,7 @@
     private boolean mHasContent;
 
     private int mRequestedModeId;
+    private int mRequestedColorTransformId;
 
     // The display offsets to apply to the display projection.
     private int mDisplayOffsetX;
@@ -235,6 +236,11 @@
             mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId;
             mBaseDisplayInfo.supportedModes = Arrays.copyOf(
                     deviceInfo.supportedModes, deviceInfo.supportedModes.length);
+            mBaseDisplayInfo.colorTransformId = deviceInfo.colorTransformId;
+            mBaseDisplayInfo.defaultColorTransformId = deviceInfo.defaultColorTransformId;
+            mBaseDisplayInfo.supportedColorTransforms = Arrays.copyOf(
+                    deviceInfo.supportedColorTransforms,
+                    deviceInfo.supportedColorTransforms.length);
             mBaseDisplayInfo.logicalDensityDpi = deviceInfo.densityDpi;
             mBaseDisplayInfo.physicalXDpi = deviceInfo.xDpi;
             mBaseDisplayInfo.physicalYDpi = deviceInfo.yDpi;
@@ -275,11 +281,12 @@
         // Set the layer stack.
         device.setLayerStackInTransactionLocked(isBlanked ? BLANK_LAYER_STACK : mLayerStack);
 
-        // Set the mode.
+        // Set the color transform and mode.
         if (device == mPrimaryDisplayDevice) {
-            device.requestModeInTransactionLocked(mRequestedModeId);
+            device.requestColorTransformAndModeInTransactionLocked(
+                    mRequestedColorTransformId, mRequestedModeId);
         } else {
-            device.requestModeInTransactionLocked(0);  // Revert to default.
+            device.requestColorTransformAndModeInTransactionLocked(0, 0);  // Revert to default.
         }
 
         // Only grab the display info now as it may have been changed based on the requests above.
@@ -383,6 +390,18 @@
     }
 
     /**
+     * Requests the given color transform.
+     */
+    public void setRequestedColorTransformIdLocked(int colorTransformId) {
+        mRequestedColorTransformId = colorTransformId;
+    }
+
+    /** Returns the pending requested color transform. */
+    public int getRequestedColorTransformIdLocked() {
+        return mRequestedColorTransformId;
+    }
+
+    /**
      * Gets the burn-in offset in X.
      */
     public int getDisplayOffsetXLocked() {
@@ -409,6 +428,7 @@
         pw.println("mLayerStack=" + mLayerStack);
         pw.println("mHasContent=" + mHasContent);
         pw.println("mRequestedMode=" + mRequestedModeId);
+        pw.println("mRequestedColorTransformId=" + mRequestedColorTransformId);
         pw.println("mDisplayOffset=(" + mDisplayOffsetX + ", " + mDisplayOffsetY + ")");
         pw.println("mPrimaryDisplayDevice=" + (mPrimaryDisplayDevice != null ?
                 mPrimaryDisplayDevice.getNameLocked() : "null"));
diff --git a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
index 0bddff0..cf6264a 100644
--- a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
@@ -310,7 +310,7 @@
         }
 
         @Override
-        public void requestModeInTransactionLocked(int id) {
+        public void requestColorTransformAndModeInTransactionLocked(int color, int id) {
             int index = -1;
             if (id == 0) {
                 // Use the default.