Add an API for setting a new brightness curve.

In addition, this also provides multiple strategies for mapping from
ambient room brightness in lux to display brightness. The default one is
the classic strategy, where we map directly from lux to backlight
brightness in an arbitrary unit. The newer and preferred strategy is to
use the physical brightness of the display, but requires that the
brightness properties of the display are appropriately configured.

Bug: 69406783
Test: atest com.android.server.display.BrightnessMappingStrategyTest &&
      atest android.hardware.display.BrightnessConfigurationTest &&
      atest android.hardware.display.PersistentDataStoreTeset

Change-Id: I60227bdb6c299d0fa92686cbf3e5994b336a3a79
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 9a6e609..6c5bfc7 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -24,6 +24,7 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.hardware.display.BrightnessConfiguration;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
@@ -34,7 +35,6 @@
 import android.util.EventLog;
 import android.util.MathUtils;
 import android.util.Slog;
-import android.util.Spline;
 import android.util.TimeUtils;
 
 import java.io.PrintWriter;
@@ -76,9 +76,8 @@
     // The light sensor, or null if not available or needed.
     private final Sensor mLightSensor;
 
-    // The auto-brightness spline adjustment.
-    // The brightness values have been scaled to a range of 0..1.
-    private final Spline mScreenAutoBrightnessSpline;
+    // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
+    private final BrightnessMappingStrategy mBrightnessMapper;
 
     // The minimum and maximum screen brightnesses.
     private final int mScreenBrightnessRangeMinimum;
@@ -186,7 +185,7 @@
     private float mBrightnessAdjustmentSampleOldGamma;
 
     public AutomaticBrightnessController(Callbacks callbacks, Looper looper,
-            SensorManager sensorManager, Spline autoBrightnessSpline, int lightSensorWarmUpTime,
+            SensorManager sensorManager, BrightnessMappingStrategy mapper, int lightSensorWarmUpTime,
             int brightnessMin, int brightnessMax, float dozeScaleFactor,
             int lightSensorRate, int initialLightSensorRate, long brighteningLightDebounceConfig,
             long darkeningLightDebounceConfig, boolean resetAmbientLuxAfterWarmUpConfig,
@@ -194,7 +193,7 @@
             HysteresisLevels dynamicHysteresis) {
         mCallbacks = callbacks;
         mSensorManager = sensorManager;
-        mScreenAutoBrightnessSpline = autoBrightnessSpline;
+        mBrightnessMapper = mapper;
         mScreenBrightnessRangeMinimum = brightnessMin;
         mScreenBrightnessRangeMaximum = brightnessMax;
         mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
@@ -228,15 +227,16 @@
         return mScreenAutoBrightness;
     }
 
-    public void configure(boolean enable, float adjustment, boolean dozing,
-            boolean userInitiatedChange) {
+    public void configure(boolean enable, @Nullable BrightnessConfiguration configuration,
+            float adjustment, boolean dozing, boolean userInitiatedChange) {
         // While dozing, the application processor may be suspended which will prevent us from
         // receiving new information from the light sensor. On some devices, we may be able to
         // switch to a wake-up light sensor instead but for now we will simply disable the sensor
         // and hold onto the last computed screen auto brightness.  We save the dozing flag for
         // debugging purposes.
         mDozing = dozing;
-        boolean changed = setLightSensorEnabled(enable && !dozing);
+        boolean changed = setBrightnessConfiguration(configuration);
+        changed |= setLightSensorEnabled(enable && !dozing);
         if (enable && !dozing && userInitiatedChange) {
             prepareBrightnessAdjustmentSample();
         }
@@ -246,10 +246,13 @@
         }
     }
 
+    public boolean setBrightnessConfiguration(BrightnessConfiguration configuration) {
+        return mBrightnessMapper.setBrightnessConfiguration(configuration);
+    }
+
     public void dump(PrintWriter pw) {
         pw.println();
         pw.println("Automatic Brightness Controller Configuration:");
-        pw.println("  mScreenAutoBrightnessSpline=" + mScreenAutoBrightnessSpline);
         pw.println("  mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
         pw.println("  mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
         pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
@@ -274,9 +277,13 @@
                 mInitialHorizonAmbientLightRingBuffer);
         pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
         pw.println("  mScreenAutoBrightnessAdjustment=" + mScreenAutoBrightnessAdjustment);
-        pw.println("  mScreenAutoBrightnessAdjustmentMaxGamma=" + mScreenAutoBrightnessAdjustmentMaxGamma);
+        pw.println("  mScreenAutoBrightnessAdjustmentMaxGamma="
+                + mScreenAutoBrightnessAdjustmentMaxGamma);
         pw.println("  mLastScreenAutoBrightnessGamma=" + mLastScreenAutoBrightnessGamma);
         pw.println("  mDozing=" + mDozing);
+
+        pw.println();
+        mBrightnessMapper.dump(pw);
     }
 
     private boolean setLightSensorEnabled(boolean enable) {
@@ -533,7 +540,7 @@
             return;
         }
 
-        float value = mScreenAutoBrightnessSpline.interpolate(mAmbientLux);
+        float value = mBrightnessMapper.getBrightness(mAmbientLux);
         float gamma = 1.0f;
 
         if (USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
new file mode 100644
index 0000000..3b9d40f
--- /dev/null
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -0,0 +1,289 @@
+/*
+ * 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.server.display;
+
+import android.annotation.Nullable;
+import android.hardware.display.BrightnessConfiguration;
+import android.os.PowerManager;
+import android.util.MathUtils;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.Spline;
+
+import com.android.internal.util.Preconditions;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * A utility to map from an ambient brightness to a display's "backlight" brightness based on the
+ * available display information and brightness configuration.
+ *
+ * Note that without a mapping from the nits to a display backlight level, any
+ * {@link BrightnessConfiguration}s that are set are just ignored.
+ */
+public abstract class BrightnessMappingStrategy {
+    private static final String TAG = "BrightnessMappingStrategy";
+    private static final boolean DEBUG = false;
+
+    @Nullable
+    public static BrightnessMappingStrategy create(
+            float[] luxLevels, int[] brightnessLevelsBacklight, float[] brightnessLevelsNits,
+            float[] nitsRange, int[] backlightRange) {
+        if (isValidMapping(nitsRange, backlightRange)
+                && isValidMapping(luxLevels, brightnessLevelsNits)) {
+            BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+            builder.setCurve(luxLevels, brightnessLevelsNits);
+            return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange);
+        } else if (isValidMapping(luxLevels, brightnessLevelsBacklight)) {
+            return new SimpleMappingStrategy(luxLevels, brightnessLevelsBacklight);
+        } else {
+            return null;
+        }
+    }
+
+    private static boolean isValidMapping(float[] x, float[] y) {
+        if (x == null || y == null || x.length == 0 || y.length == 0) {
+            return false;
+        }
+        if (x.length != y.length) {
+            return false;
+        }
+        final int N = x.length;
+        float prevX = x[0];
+        float prevY = y[0];
+        if (prevX < 0 || prevY < 0 || Float.isNaN(prevX) || Float.isNaN(prevY)) {
+            return false;
+        }
+        for (int i = 1; i < N; i++) {
+            if (prevX >= x[i] || prevY > y[i]) {
+                return false;
+            }
+            if (Float.isNaN(x[i]) || Float.isNaN(y[i])) {
+                return false;
+            }
+            prevX = x[i];
+            prevY = y[i];
+        }
+        return true;
+    }
+
+    private static boolean isValidMapping(float[] x, int[] y) {
+        if (x == null || y == null || x.length == 0 || y.length == 0) {
+            return false;
+        }
+        if (x.length != y.length) {
+            return false;
+        }
+        final int N = x.length;
+        float prevX = x[0];
+        int prevY = y[0];
+        if (prevX < 0 || prevY < 0 || Float.isNaN(prevX)) {
+            return false;
+        }
+        for (int i = 1; i < N; i++) {
+            if (prevX >= x[i] || prevY > y[i]) {
+                return false;
+            }
+            if (Float.isNaN(x[i])) {
+                return false;
+            }
+            prevX = x[i];
+            prevY = y[i];
+        }
+        return true;
+    }
+
+    /**
+     * Sets the {@link BrightnessConfiguration}.
+     *
+     * @param config The new configuration. If {@code null} is passed, the default configuration is
+     *               used.
+     * @return Whether the brightness configuration has changed.
+     */
+    public abstract boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config);
+
+    /**
+     * Returns the desired brightness of the display based on the current ambient lux.
+     *
+     * The returned brightness will be in the range [0, 1.0], where 1.0 is the display at max
+     * brightness and 0 is the display at minimum brightness.
+     *
+     * @param lux The current ambient brightness in lux.
+     * @return The desired brightness of the display compressed to the range [0, 1.0].
+     */
+    public abstract float getBrightness(float lux);
+
+    public abstract void dump(PrintWriter pw);
+
+    private static float normalizeAbsoluteBrightness(int brightness) {
+        brightness = MathUtils.constrain(brightness,
+                PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
+        return (float) brightness / PowerManager.BRIGHTNESS_ON;
+    }
+
+
+    /**
+     * A {@link BrightnessMappingStrategy} that maps from ambient room brightness directly to the
+     * backlight of the display.
+     *
+     * Since we don't have information about the display's physical brightness, any brightness
+     * configurations that are set are just ignored.
+     */
+    private static class SimpleMappingStrategy extends BrightnessMappingStrategy {
+        private final Spline mSpline;
+
+        public SimpleMappingStrategy(float[] lux, int[] brightness) {
+            Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
+                    "Lux and brightness arrays must not be empty!");
+            Preconditions.checkArgument(lux.length == brightness.length,
+                    "Lux and brightness arrays must be the same length!");
+            Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux");
+            Preconditions.checkArrayElementsInRange(brightness,
+                    0, Integer.MAX_VALUE, "brightness");
+
+            final int N = brightness.length;
+            float[] x = new float[N];
+            float[] y = new float[N];
+            for (int i = 0; i < N; i++) {
+                x[i] = lux[i];
+                y[i] = normalizeAbsoluteBrightness(brightness[i]);
+            }
+
+            mSpline = Spline.createSpline(x, y);
+            if (DEBUG) {
+                Slog.d(TAG, "Auto-brightness spline: " + mSpline);
+                for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
+                    Slog.d(TAG, String.format("  %7.1f: %7.1f", v, mSpline.interpolate(v)));
+                }
+            }
+        }
+
+        @Override
+        public boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config) {
+            Slog.e(TAG,
+                    "setBrightnessConfiguration called on device without display information.");
+            return false;
+        }
+
+        @Override
+        public float getBrightness(float lux) {
+            return mSpline.interpolate(lux);
+        }
+
+        @Override
+        public void dump(PrintWriter pw) {
+            pw.println("SimpleMappingStrategy");
+            pw.println("  mSpline=" + mSpline);
+        }
+    }
+
+    /** A {@link BrightnessMappingStrategy} that maps from ambient room brightness to the physical
+     * range of the display, rather than to the range of the backlight control (typically 0-255).
+     *
+     * By mapping through the physical brightness, the curve becomes portable across devices and
+     * gives us more resolution in the resulting mapping.
+     */
+    @VisibleForTesting
+    static class PhysicalMappingStrategy extends BrightnessMappingStrategy {
+        // The current brightness configuration.
+        private BrightnessConfiguration mConfig;
+
+        // A spline mapping from the current ambient light in lux to the desired display brightness
+        // in nits.
+        private Spline mBrightnessSpline;
+
+        // A spline mapping from nits to the corresponding backlight value, normalized to the range
+        // [0, 1.0].
+        private final Spline mBacklightSpline;
+
+        // The default brightness configuration.
+        private final BrightnessConfiguration mDefaultConfig;
+
+        public PhysicalMappingStrategy(BrightnessConfiguration config,
+                float[] nits, int[] backlight) {
+            Preconditions.checkArgument(nits.length != 0 && backlight.length != 0,
+                    "Nits and backlight arrays must not be empty!");
+            Preconditions.checkArgument(nits.length == backlight.length,
+                    "Nits and backlight arrays must be the same length!");
+            Preconditions.checkNotNull(config);
+            Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits");
+            Preconditions.checkArrayElementsInRange(backlight,
+                    PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON, "backlight");
+
+            // Setup the backlight spline
+            final int N = nits.length;
+            float[] x = new float[N];
+            float[] y = new float[N];
+            for (int i = 0; i < N; i++) {
+                x[i] = nits[i];
+                y[i] = normalizeAbsoluteBrightness(backlight[i]);
+            }
+
+            mBacklightSpline = Spline.createSpline(x, y);
+            if (DEBUG) {
+                Slog.d(TAG, "Backlight spline: " + mBacklightSpline);
+                for (float v = 1f; v < nits[nits.length - 1] * 1.25f; v *= 1.25f) {
+                    Slog.d(TAG, String.format(
+                                "  %7.1f: %7.1f", v, mBacklightSpline.interpolate(v)));
+                }
+            }
+
+            mDefaultConfig = config;
+            setBrightnessConfiguration(config);
+        }
+
+        @Override
+        public boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config) {
+            if (config == null) {
+                config = mDefaultConfig;
+            }
+            if (config.equals(mConfig)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Tried to set an identical brightness config, ignoring");
+                }
+                return false;
+            }
+
+            Pair<float[], float[]> curve = config.getCurve();
+            mBrightnessSpline = Spline.createSpline(curve.first /*lux*/, curve.second /*nits*/);
+            if (DEBUG) {
+                Slog.d(TAG, "Brightness spline: " + mBrightnessSpline);
+                final float[] lux = curve.first;
+                for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
+                    Slog.d(TAG, String.format(
+                                "  %7.1f: %7.1f", v, mBrightnessSpline.interpolate(v)));
+                }
+            }
+            mConfig = config;
+            return true;
+        }
+
+        @Override
+        public float getBrightness(float lux) {
+            return mBacklightSpline.interpolate(mBrightnessSpline.interpolate(lux));
+        }
+
+        @Override
+        public void dump(PrintWriter pw) {
+            pw.println("PhysicalMappingStrategy");
+            pw.println("  mConfig=" + mConfig);
+            pw.println("  mBrightnessSpline=" + mBrightnessSpline);
+            pw.println("  mBacklightSpline=" + mBacklightSpline);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 19a74d7..c32e220 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -29,6 +29,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -37,6 +38,7 @@
 import android.graphics.Point;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessChangeEvent;
+import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayViewport;
@@ -62,6 +64,7 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.IntArray;
 import android.util.Slog;
@@ -70,6 +73,7 @@
 import android.view.DisplayInfo;
 import android.view.Surface;
 
+import com.android.internal.util.Preconditions;
 import com.android.server.AnimationThread;
 import com.android.server.DisplayThread;
 import com.android.server.LocalServices;
@@ -144,6 +148,7 @@
     private static final int MSG_REQUEST_TRAVERSAL = 4;
     private static final int MSG_UPDATE_VIEWPORT = 5;
     private static final int MSG_REGISTER_BRIGHTNESS_TRACKER = 6;
+    private static final int MSG_LOAD_BRIGHTNESS_CONFIGURATION = 7;
 
     private final Context mContext;
     private final DisplayManagerHandler mHandler;
@@ -218,6 +223,9 @@
     // The virtual display adapter, or null if not registered.
     private VirtualDisplayAdapter mVirtualDisplayAdapter;
 
+    // The User ID of the current user
+    private @UserIdInt int mCurrentUserId;
+
     // The stable device screen height and width. These are not tied to a specific display, even
     // the default display, because they need to be stable over the course of the device's entire
     // life, even if the default display changes (e.g. a new monitor is plugged into a PC-like
@@ -277,17 +285,18 @@
         mDisplayAdapterListener = new DisplayAdapterListener();
         mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
         mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger(
-            com.android.internal.R.integer.config_defaultDisplayDefaultColorMode);
+                com.android.internal.R.integer.config_defaultDisplayDefaultColorMode);
 
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
         mBrightnessTracker = new BrightnessTracker(context, null);
+        mCurrentUserId = UserHandle.USER_SYSTEM;
     }
 
     public void setupSchedulerPolicies() {
         // android.display and android.anim is critical to user experience and we should make sure
-        // it is not in the default foregroup groups, add it to top-app to make sure it uses all the
-        // cores and scheduling settings for top-app when it runs.
+        // it is not in the default foregroup groups, add it to top-app to make sure it uses all
+        // the cores and scheduling settings for top-app when it runs.
         Process.setThreadGroupAndCpuset(DisplayThread.get().getThreadId(),
                 Process.THREAD_GROUP_TOP_APP);
         Process.setThreadGroupAndCpuset(AnimationThread.get().getThreadId(),
@@ -339,6 +348,19 @@
         }
     }
 
+    @Override
+    public void onSwitchUser(@UserIdInt int newUserId) {
+        final int userSerial = getUserManager().getUserSerialNumber(newUserId);
+        synchronized (mSyncRoot) {
+            if (mCurrentUserId != newUserId) {
+                mCurrentUserId = newUserId;
+                BrightnessConfiguration config =
+                        mPersistentDataStore.getBrightnessConfiguration(userSerial);
+                mDisplayPowerController.setBrightnessConfiguration(config);
+            }
+        }
+    }
+
     // TODO: Use dependencies or a boot phase
     public void windowManagerAndInputReady() {
         synchronized (mSyncRoot) {
@@ -982,6 +1004,30 @@
         }
     }
 
+    private void setBrightnessConfigurationForUserInternal(
+            @NonNull BrightnessConfiguration c, @UserIdInt int userId) {
+        final int userSerial = getUserManager().getUserSerialNumber(userId);
+        synchronized (mSyncRoot) {
+            try {
+                mPersistentDataStore.setBrightnessConfigurationForUser(c, userSerial);
+            } finally {
+                mPersistentDataStore.saveIfNeeded();
+            }
+            if (userId == mCurrentUserId) {
+                mDisplayPowerController.setBrightnessConfiguration(c);
+            }
+        }
+    }
+
+    private void loadBrightnessConfiguration() {
+        synchronized (mSyncRoot) {
+            final int userSerial = getUserManager().getUserSerialNumber(mCurrentUserId);
+            BrightnessConfiguration config =
+                    mPersistentDataStore.getBrightnessConfiguration(userSerial);
+            mDisplayPowerController.setBrightnessConfiguration(config);
+        }
+    }
+
     // Updates all existing logical displays given the current set of display devices.
     // Removes invalid logical displays.
     // Sends notifications if needed.
@@ -1226,6 +1272,10 @@
         return mProjectionService;
     }
 
+    private UserManager getUserManager() {
+        return mContext.getSystemService(UserManager.class);
+    }
+
     private void dumpInternal(PrintWriter pw) {
         pw.println("DISPLAY MANAGER (dumpsys display)");
 
@@ -1368,6 +1418,10 @@
                 case MSG_REGISTER_BRIGHTNESS_TRACKER:
                     mBrightnessTracker.start();
                     break;
+
+                case MSG_LOAD_BRIGHTNESS_CONFIGURATION:
+                    loadBrightnessConfiguration();
+                    break;
             }
         }
     }
@@ -1797,6 +1851,27 @@
             }
         }
 
+        @Override // Binder call
+        public void setBrightnessConfigurationForUser(
+                BrightnessConfiguration c, @UserIdInt int userId) {
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS,
+                    "Permission required to change the display's brightness configuration");
+            if (userId != UserHandle.getCallingUserId()) {
+                mContext.enforceCallingOrSelfPermission(
+                        Manifest.permission.INTERACT_ACROSS_USERS,
+                        "Permission required to change the display brightness"
+                        + " configuration of another user");
+            }
+            Preconditions.checkNotNull(c);
+            final long token = Binder.clearCallingIdentity();
+            try {
+                setBrightnessConfigurationForUserInternal(c, userId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
         private boolean validatePackageName(int uid, String packageName) {
             if (packageName != null) {
                 String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
@@ -1868,18 +1943,24 @@
                 mDisplayPowerController = new DisplayPowerController(
                         mContext, callbacks, handler, sensorManager, blanker);
             }
+
+            mHandler.sendEmptyMessage(MSG_LOAD_BRIGHTNESS_CONFIGURATION);
         }
 
         @Override
         public boolean requestPowerState(DisplayPowerRequest request,
                 boolean waitForNegativeProximity) {
-            return mDisplayPowerController.requestPowerState(request,
-                    waitForNegativeProximity);
+            synchronized (mSyncRoot) {
+                return mDisplayPowerController.requestPowerState(request,
+                        waitForNegativeProximity);
+            }
         }
 
         @Override
         public boolean isProximitySensorAvailable() {
-            return mDisplayPowerController.isProximitySensorAvailable();
+            synchronized (mSyncRoot) {
+                return mDisplayPowerController.isProximitySensorAvailable();
+            }
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 29a007a..a2d9548 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -26,10 +26,12 @@
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
@@ -93,6 +95,8 @@
     private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 2;
     private static final int MSG_SCREEN_ON_UNBLOCKED = 3;
     private static final int MSG_SCREEN_OFF_UNBLOCKED = 4;
+    private static final int MSG_CONFIGURE_BRIGHTNESS = 5;
+    private static final int MSG_USER_SWITCH = 6;
 
     private static final int PROXIMITY_UNKNOWN = -1;
     private static final int PROXIMITY_NEGATIVE = 0;
@@ -285,6 +289,14 @@
     // The controller for the automatic brightness level.
     private AutomaticBrightnessController mAutomaticBrightnessController;
 
+    // The default brightness configuration. Used for whenever we don't have a valid brightness
+    // configuration set. This is typically seen with users that don't have a brightness
+    // configuration that's different from the default.
+    private BrightnessConfiguration mDefaultBrightnessConfiguration;
+
+    // The current brightness configuration.
+    private BrightnessConfiguration mBrightnessConfiguration;
+
     // Animators.
     private ObjectAnimator mColorFadeOnAnimator;
     private ObjectAnimator mColorFadeOffAnimator;
@@ -333,7 +345,8 @@
                 screenBrightnessSettingMinimum, mScreenBrightnessDimConfig),
                 mScreenBrightnessDarkConfig);
 
-        mScreenBrightnessRangeMaximum = PowerManager.BRIGHTNESS_ON;
+        mScreenBrightnessRangeMaximum = clampAbsoluteBrightness(resources.getInteger(
+                    com.android.internal.R.integer.config_screenBrightnessSettingMaximum));
 
         mUseSoftwareAutoBrightnessConfig = resources.getBoolean(
                 com.android.internal.R.bool.config_automatic_brightness_available);
@@ -348,60 +361,60 @@
         mSkipScreenOnBrightnessRamp = resources.getBoolean(
                 com.android.internal.R.bool.config_skipScreenOnBrightnessRamp);
 
-        int lightSensorRate = resources.getInteger(
-                com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
-        int initialLightSensorRate = resources.getInteger(
-                com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
-        if (initialLightSensorRate == -1) {
-          initialLightSensorRate = lightSensorRate;
-        } else if (initialLightSensorRate > lightSensorRate) {
-          Slog.w(TAG, "Expected config_autoBrightnessInitialLightSensorRate ("
-                  + initialLightSensorRate + ") to be less than or equal to "
-                  + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
-        }
-        long brighteningLightDebounce = resources.getInteger(
-                com.android.internal.R.integer.config_autoBrightnessBrighteningLightDebounce);
-        long darkeningLightDebounce = resources.getInteger(
-                com.android.internal.R.integer.config_autoBrightnessDarkeningLightDebounce);
-        boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
-                com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
-        int ambientLightHorizon = resources.getInteger(
-                com.android.internal.R.integer.config_autoBrightnessAmbientLightHorizon);
-        float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
-                com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
-                1, 1);
-
-        int[] brightLevels = resources.getIntArray(
-                com.android.internal.R.array.config_dynamicHysteresisBrightLevels);
-        int[] darkLevels = resources.getIntArray(
-                com.android.internal.R.array.config_dynamicHysteresisDarkLevels);
-        int[] luxLevels = resources.getIntArray(
-                com.android.internal.R.array.config_dynamicHysteresisLuxLevels);
-        HysteresisLevels dynamicHysteresis = new HysteresisLevels(
-                brightLevels, darkLevels, luxLevels);
-
         if (mUseSoftwareAutoBrightnessConfig) {
-            int[] lux = resources.getIntArray(
-                    com.android.internal.R.array.config_autoBrightnessLevels);
-            int[] screenBrightness = resources.getIntArray(
+            float[] luxLevels = getLuxLevels(resources.getIntArray(
+                    com.android.internal.R.array.config_autoBrightnessLevels));
+            int[] backlightValues = resources.getIntArray(
                     com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
-            int lightSensorWarmUpTimeConfig = resources.getInteger(
-                    com.android.internal.R.integer.config_lightSensorWarmupTime);
+            float[] brightnessValuesNits = getFloatArray(resources.obtainTypedArray(
+                    com.android.internal.R.array.config_autoBrightnessDisplayValuesNits));
+
+            final float screenMinimumNits = resources.getFloat(
+                    com.android.internal.R.dimen.config_screenBrightnessMinimumNits);
+            final float screenMaximumNits = resources.getFloat(
+                    com.android.internal.R.dimen.config_screenBrightnessMaximumNits);
+
             final float dozeScaleFactor = resources.getFraction(
                     com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
                     1, 1);
 
-            Spline screenAutoBrightnessSpline = createAutoBrightnessSpline(lux, screenBrightness);
-            if (screenAutoBrightnessSpline == null) {
-                Slog.e(TAG, "Error in config.xml.  config_autoBrightnessLcdBacklightValues "
-                        + "(size " + screenBrightness.length + ") "
-                        + "must be monotic and have exactly one more entry than "
-                        + "config_autoBrightnessLevels (size " + lux.length + ") "
-                        + "which must be strictly increasing.  "
-                        + "Auto-brightness will be disabled.");
-                mUseSoftwareAutoBrightnessConfig = false;
-            } else {
-                int bottom = clampAbsoluteBrightness(screenBrightness[0]);
+            int[] brightLevels = resources.getIntArray(
+                    com.android.internal.R.array.config_dynamicHysteresisBrightLevels);
+            int[] darkLevels = resources.getIntArray(
+                    com.android.internal.R.array.config_dynamicHysteresisDarkLevels);
+            int[] luxHysteresisLevels = resources.getIntArray(
+                    com.android.internal.R.array.config_dynamicHysteresisLuxLevels);
+            HysteresisLevels dynamicHysteresis = new HysteresisLevels(
+                    brightLevels, darkLevels, luxHysteresisLevels);
+
+            long brighteningLightDebounce = resources.getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessBrighteningLightDebounce);
+            long darkeningLightDebounce = resources.getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessDarkeningLightDebounce);
+            boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
+                    com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+            int ambientLightHorizon = resources.getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessAmbientLightHorizon);
+            float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
+                    com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
+                    1, 1);
+
+            int lightSensorWarmUpTimeConfig = resources.getInteger(
+                    com.android.internal.R.integer.config_lightSensorWarmupTime);
+            int lightSensorRate = resources.getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
+            int initialLightSensorRate = resources.getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
+            if (initialLightSensorRate == -1) {
+                initialLightSensorRate = lightSensorRate;
+            } else if (initialLightSensorRate > lightSensorRate) {
+                Slog.w(TAG, "Expected config_autoBrightnessInitialLightSensorRate ("
+                        + initialLightSensorRate + ") to be less than or equal to "
+                        + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
+            }
+
+            if (backlightValues != null && backlightValues.length > 0) {
+                final int bottom = backlightValues[0];
                 if (mScreenBrightnessDarkConfig > bottom) {
                     Slog.w(TAG, "config_screenBrightnessDark (" + mScreenBrightnessDarkConfig
                             + ") should be less than or equal to the first value of "
@@ -411,13 +424,24 @@
                 if (bottom < screenBrightnessRangeMinimum) {
                     screenBrightnessRangeMinimum = bottom;
                 }
+            }
+
+            float[] nitsRange = { screenMinimumNits, screenMaximumNits };
+            int[] backlightRange = { screenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum };
+
+            BrightnessMappingStrategy mapper = BrightnessMappingStrategy.create(
+                    luxLevels, backlightValues, brightnessValuesNits,
+                    nitsRange, backlightRange);
+            if (mapper != null) {
                 mAutomaticBrightnessController = new AutomaticBrightnessController(this,
-                        handler.getLooper(), sensorManager, screenAutoBrightnessSpline,
-                        lightSensorWarmUpTimeConfig, screenBrightnessRangeMinimum,
-                        mScreenBrightnessRangeMaximum, dozeScaleFactor, lightSensorRate,
-                        initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
+                        handler.getLooper(), sensorManager, mapper, lightSensorWarmUpTimeConfig,
+                        screenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum,
+                        dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                        brighteningLightDebounce, darkeningLightDebounce,
                         autoBrightnessResetAmbientLuxAfterWarmUp, ambientLightHorizon,
                         autoBrightnessAdjustmentMaxGamma, dynamicHysteresis);
+            } else {
+                mUseSoftwareAutoBrightnessConfig = false;
             }
         }
 
@@ -444,6 +468,25 @@
 
     }
 
+    private static float[] getLuxLevels(int[] lux) {
+        // The first control point is implicit and always at 0 lux.
+        float[] levels = new float[lux.length + 1];
+        for (int i = 0; i < lux.length; i++) {
+            levels[i + 1] = (float) lux[i];
+        }
+        return levels;
+    }
+
+    private static float[] getFloatArray(TypedArray array) {
+        final int N = array.length();
+        float[] vals = new float[N];
+        for (int i = 0; i < N; i++) {
+            vals[i] = array.getFloat(i, -1.0f);
+        }
+        array.recycle();
+        return vals;
+    }
+
     /**
      * Returns true if the proximity sensor screen-off function is available.
      */
@@ -512,7 +555,6 @@
         if (!mPendingUpdatePowerStateLocked) {
             mPendingUpdatePowerStateLocked = true;
             Message msg = mHandler.obtainMessage(MSG_UPDATE_POWER_STATE);
-            msg.setAsynchronous(true);
             mHandler.sendMessage(msg);
         }
     }
@@ -691,8 +733,8 @@
             final boolean userInitiatedChange = autoBrightnessAdjustmentChanged
                     && mPowerRequest.brightnessSetByUser;
             mAutomaticBrightnessController.configure(autoBrightnessEnabled,
-                    mPowerRequest.screenAutoBrightnessAdjustment, state != Display.STATE_ON,
-                    userInitiatedChange);
+                    mBrightnessConfiguration, mPowerRequest.screenAutoBrightnessAdjustment,
+                    state != Display.STATE_ON, userInitiatedChange);
         }
 
         // Apply brightness boost.
@@ -874,6 +916,11 @@
         sendUpdatePowerState();
     }
 
+    public void setBrightnessConfiguration(BrightnessConfiguration c) {
+        Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS, c);
+        msg.sendToTarget();
+    }
+
     private void blockScreenOn() {
         if (mPendingScreenOnUnblocker == null) {
             Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
@@ -1241,7 +1288,6 @@
                 // Need to wait a little longer.
                 // Debounce again later.  We continue holding a wake lock while waiting.
                 Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
-                msg.setAsynchronous(true);
                 mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
             }
         }
@@ -1402,39 +1448,6 @@
         }
     }
 
-    private static Spline createAutoBrightnessSpline(int[] lux, int[] brightness) {
-        if (lux == null || lux.length == 0 || brightness == null || brightness.length == 0) {
-            Slog.e(TAG, "Could not create auto-brightness spline.");
-            return null;
-        }
-        try {
-            final int n = brightness.length;
-            float[] x = new float[n];
-            float[] y = new float[n];
-            y[0] = normalizeAbsoluteBrightness(brightness[0]);
-            for (int i = 1; i < n; i++) {
-                x[i] = lux[i - 1];
-                y[i] = normalizeAbsoluteBrightness(brightness[i]);
-            }
-
-            Spline spline = Spline.createSpline(x, y);
-            if (DEBUG) {
-                Slog.d(TAG, "Auto-brightness spline: " + spline);
-                for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
-                    Slog.d(TAG, String.format("  %7.1f: %7.1f", v, spline.interpolate(v)));
-                }
-            }
-            return spline;
-        } catch (IllegalArgumentException ex) {
-            Slog.e(TAG, "Could not create auto-brightness spline.", ex);
-            return null;
-        }
-    }
-
-    private static float normalizeAbsoluteBrightness(int value) {
-        return (float)clampAbsoluteBrightness(value) / PowerManager.BRIGHTNESS_ON;
-    }
-
     private static int clampAbsoluteBrightness(int value) {
         return MathUtils.constrain(value, PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
     }
@@ -1467,6 +1480,11 @@
                         updatePowerState();
                     }
                     break;
+                case MSG_CONFIGURE_BRIGHTNESS:
+                    BrightnessConfiguration c = (BrightnessConfiguration) msg.obj;
+                    mBrightnessConfiguration = c != null ? c : mDefaultBrightnessConfiguration;
+                    updatePowerState();
+                    break;
             }
         }
     }
@@ -1492,17 +1510,14 @@
         @Override
         public void onScreenOn() {
             Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
-            msg.setAsynchronous(true);
             mHandler.sendMessage(msg);
         }
     }
 
     private final class ScreenOffUnblocker implements WindowManagerPolicy.ScreenOffListener {
-
         @Override
         public void onScreenOff() {
             Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
-            msg.setAsynchronous(true);
             mHandler.sendMessage(msg);
         }
     }
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index 34c8e22..49b4465 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -24,12 +24,17 @@
 import org.xmlpull.v1.XmlSerializer;
 
 import android.graphics.Point;
+import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.WifiDisplay;
 import android.util.AtomicFile;
 import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Pair;
 import android.util.Xml;
 import android.view.Display;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.File;
@@ -37,10 +42,12 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import libcore.io.IoUtils;
@@ -57,14 +64,22 @@
  *     &lt;wifi-display deviceAddress="00:00:00:00:00:00" deviceName="XXXX" deviceAlias="YYYY" />
  *   &lt;remembered-wifi-displays>
  *   &lt;display-states>
- *      &lt;display>
+ *      &lt;display unique-id="XXXXXXX">
  *          &lt;color-mode>0&lt;/color-mode>
  *      &lt;/display>
  *  &lt;/display-states>
  *  &lt;stable-device-values>
- *      &lt;stable-display-height>1920&lt;stable-display-height>
- *      &lt;stable-display-width>1080&lt;stable-display-width>
+ *      &lt;stable-display-height>1920&lt;/stable-display-height>
+ *      &lt;stable-display-width>1080&lt;/stable-display-width>
  *  &lt;/stable-device-values>
+ *  &lt;brightness-configurations>
+ *      &lt;brightness-configuration user-id="0">
+ *          &lt;brightness-curve>
+ *              &lt;brightness-point lux="0" nits="13.25"/>
+ *              &lt;brightness-point lux="20" nits="35.94"/>
+ *          &lt;/brightness-curve>
+ *      &lt;/brightness-configuration>
+ *  &lt;/brightness-configurations>
  * &lt;/display-manager-state>
  * </code>
  *
@@ -73,6 +88,31 @@
 final class PersistentDataStore {
     static final String TAG = "DisplayManager";
 
+    private static final String TAG_DISPLAY_MANAGER_STATE = "display-manager-state";
+
+    private static final String TAG_REMEMBERED_WIFI_DISPLAYS = "remembered-wifi-displays";
+    private static final String TAG_WIFI_DISPLAY = "wifi-display";
+    private static final String ATTR_DEVICE_ADDRESS = "deviceAddress";
+    private static final String ATTR_DEVICE_NAME = "deviceName";
+    private static final String ATTR_DEVICE_ALIAS = "deviceAlias";
+
+    private static final String TAG_DISPLAY_STATES = "display-states";
+    private static final String TAG_DISPLAY = "display";
+    private static final String TAG_COLOR_MODE = "color-mode";
+    private static final String ATTR_UNIQUE_ID = "unique-id";
+
+    private static final String TAG_STABLE_DEVICE_VALUES = "stable-device-values";
+    private static final String TAG_STABLE_DISPLAY_HEIGHT = "stable-display-height";
+    private static final String TAG_STABLE_DISPLAY_WIDTH = "stable-display-width";
+
+    private static final String TAG_BRIGHTNESS_CONFIGURATIONS = "brightness-configurations";
+    private static final String TAG_BRIGHTNESS_CONFIGURATION = "brightness-configuration";
+    private static final String TAG_BRIGHTNESS_CURVE = "brightness-curve";
+    private static final String TAG_BRIGHTNESS_POINT = "brightness-point";
+    private static final String ATTR_USER_SERIAL = "user-serial";
+    private static final String ATTR_LUX = "lux";
+    private static final String ATTR_NITS = "nits";
+
     // Remembered Wifi display devices.
     private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>();
 
@@ -83,8 +123,8 @@
     // Display values which should be stable across the device's lifetime.
     private final StableDeviceValues mStableDeviceValues = new StableDeviceValues();
 
-    // The atomic file used to safely read or write the file.
-    private final AtomicFile mAtomicFile;
+    // Brightness configuration by user
+    private BrightnessConfigurations mBrightnessConfigurations = new BrightnessConfigurations();
 
     // True if the data has been loaded.
     private boolean mLoaded;
@@ -92,8 +132,16 @@
     // True if there are changes to be saved.
     private boolean mDirty;
 
+    // The interface for methods which should be replaced by the test harness.
+    private Injector mInjector;
+
     public PersistentDataStore() {
-        mAtomicFile = new AtomicFile(new File("/data/system/display-manager-state.xml"));
+        this(new Injector());
+    }
+
+    @VisibleForTesting
+    PersistentDataStore(Injector injector) {
+        mInjector = injector;
     }
 
     public void saveIfNeeded() {
@@ -225,6 +273,18 @@
 		}
 	}
 
+    public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial) {
+        loadIfNeeded();
+        if (mBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial)) {
+            setDirty();
+        }
+    }
+
+    public BrightnessConfiguration getBrightnessConfiguration(int userSerial) {
+        loadIfNeeded();
+        return mBrightnessConfigurations.getBrightnessConfiguration(userSerial);
+    }
+
     private DisplayState getDisplayState(String uniqueId, boolean createIfAbsent) {
         loadIfNeeded();
         DisplayState state = mDisplayStates.get(uniqueId);
@@ -256,7 +316,7 @@
 
         final InputStream is;
         try {
-            is = mAtomicFile.openRead();
+            is = mInjector.openRead();
         } catch (FileNotFoundException ex) {
             return;
         }
@@ -278,9 +338,9 @@
     }
 
     private void save() {
-        final FileOutputStream os;
+        final OutputStream os;
         try {
-            os = mAtomicFile.startWrite();
+            os = mInjector.startWrite();
             boolean success = false;
             try {
                 XmlSerializer serializer = new FastXmlSerializer();
@@ -289,11 +349,7 @@
                 serializer.flush();
                 success = true;
             } finally {
-                if (success) {
-                    mAtomicFile.finishWrite(os);
-                } else {
-                    mAtomicFile.failWrite(os);
-                }
+                mInjector.finishWrite(os, success);
             }
         } catch (IOException ex) {
             Slog.w(TAG, "Failed to save display manager persistent store data.", ex);
@@ -302,18 +358,21 @@
 
     private void loadFromXml(XmlPullParser parser)
             throws IOException, XmlPullParserException {
-        XmlUtils.beginDocument(parser, "display-manager-state");
+        XmlUtils.beginDocument(parser, TAG_DISPLAY_MANAGER_STATE);
         final int outerDepth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-            if (parser.getName().equals("remembered-wifi-displays")) {
+            if (parser.getName().equals(TAG_REMEMBERED_WIFI_DISPLAYS)) {
                 loadRememberedWifiDisplaysFromXml(parser);
             }
-            if (parser.getName().equals("display-states")) {
+            if (parser.getName().equals(TAG_DISPLAY_STATES)) {
                 loadDisplaysFromXml(parser);
             }
-            if (parser.getName().equals("stable-device-values")) {
+            if (parser.getName().equals(TAG_STABLE_DEVICE_VALUES)) {
                 mStableDeviceValues.loadFromXml(parser);
             }
+            if (parser.getName().equals(TAG_BRIGHTNESS_CONFIGURATIONS)) {
+                mBrightnessConfigurations.loadFromXml(parser);
+            }
         }
     }
 
@@ -321,10 +380,10 @@
             throws IOException, XmlPullParserException {
         final int outerDepth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-            if (parser.getName().equals("wifi-display")) {
-                String deviceAddress = parser.getAttributeValue(null, "deviceAddress");
-                String deviceName = parser.getAttributeValue(null, "deviceName");
-                String deviceAlias = parser.getAttributeValue(null, "deviceAlias");
+            if (parser.getName().equals(TAG_WIFI_DISPLAY)) {
+                String deviceAddress = parser.getAttributeValue(null, ATTR_DEVICE_ADDRESS);
+                String deviceName = parser.getAttributeValue(null, ATTR_DEVICE_NAME);
+                String deviceAlias = parser.getAttributeValue(null, ATTR_DEVICE_ALIAS);
                 if (deviceAddress == null || deviceName == null) {
                     throw new XmlPullParserException(
                             "Missing deviceAddress or deviceName attribute on wifi-display.");
@@ -345,8 +404,8 @@
             throws IOException, XmlPullParserException {
         final int outerDepth = parser.getDepth();
         while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-            if (parser.getName().equals("display")) {
-                String uniqueId = parser.getAttributeValue(null, "unique-id");
+            if (parser.getName().equals(TAG_DISPLAY)) {
+                String uniqueId = parser.getAttributeValue(null, ATTR_UNIQUE_ID);
                 if (uniqueId == null) {
                     throw new XmlPullParserException(
                             "Missing unique-id attribute on display.");
@@ -365,32 +424,35 @@
     private void saveToXml(XmlSerializer serializer) throws IOException {
         serializer.startDocument(null, true);
         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
-        serializer.startTag(null, "display-manager-state");
-        serializer.startTag(null, "remembered-wifi-displays");
+        serializer.startTag(null, TAG_DISPLAY_MANAGER_STATE);
+        serializer.startTag(null, TAG_REMEMBERED_WIFI_DISPLAYS);
         for (WifiDisplay display : mRememberedWifiDisplays) {
-            serializer.startTag(null, "wifi-display");
-            serializer.attribute(null, "deviceAddress", display.getDeviceAddress());
-            serializer.attribute(null, "deviceName", display.getDeviceName());
+            serializer.startTag(null, TAG_WIFI_DISPLAY);
+            serializer.attribute(null, ATTR_DEVICE_ADDRESS, display.getDeviceAddress());
+            serializer.attribute(null, ATTR_DEVICE_NAME, display.getDeviceName());
             if (display.getDeviceAlias() != null) {
-                serializer.attribute(null, "deviceAlias", display.getDeviceAlias());
+                serializer.attribute(null, ATTR_DEVICE_ALIAS, display.getDeviceAlias());
             }
-            serializer.endTag(null, "wifi-display");
+            serializer.endTag(null, TAG_WIFI_DISPLAY);
         }
-        serializer.endTag(null, "remembered-wifi-displays");
-        serializer.startTag(null, "display-states");
+        serializer.endTag(null, TAG_REMEMBERED_WIFI_DISPLAYS);
+        serializer.startTag(null, TAG_DISPLAY_STATES);
         for (Map.Entry<String, DisplayState> entry : mDisplayStates.entrySet()) {
             final String uniqueId = entry.getKey();
             final DisplayState state = entry.getValue();
-            serializer.startTag(null, "display");
-            serializer.attribute(null, "unique-id", uniqueId);
+            serializer.startTag(null, TAG_DISPLAY);
+            serializer.attribute(null, ATTR_UNIQUE_ID, uniqueId);
             state.saveToXml(serializer);
-            serializer.endTag(null, "display");
+            serializer.endTag(null, TAG_DISPLAY);
         }
-        serializer.endTag(null, "display-states");
-        serializer.startTag(null, "stable-device-values");
+        serializer.endTag(null, TAG_DISPLAY_STATES);
+        serializer.startTag(null, TAG_STABLE_DEVICE_VALUES);
         mStableDeviceValues.saveToXml(serializer);
-        serializer.endTag(null, "stable-device-values");
-        serializer.endTag(null, "display-manager-state");
+        serializer.endTag(null, TAG_STABLE_DEVICE_VALUES);
+        serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
+        mBrightnessConfigurations.saveToXml(serializer);
+        serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
+        serializer.endTag(null, TAG_DISPLAY_MANAGER_STATE);
         serializer.endDocument();
     }
 
@@ -411,6 +473,8 @@
         }
         pw.println("  StableDeviceValues:");
         mStableDeviceValues.dump(pw, "      ");
+        pw.println("  BrightnessConfigurations:");
+        mBrightnessConfigurations.dump(pw, "      ");
     }
 
     private static final class DisplayState {
@@ -433,7 +497,7 @@
             final int outerDepth = parser.getDepth();
 
             while (XmlUtils.nextElementWithin(parser, outerDepth)) {
-                if (parser.getName().equals("color-mode")) {
+                if (parser.getName().equals(TAG_COLOR_MODE)) {
                     String value = parser.nextText();
                     mColorMode = Integer.parseInt(value);
                 }
@@ -441,9 +505,9 @@
         }
 
         public void saveToXml(XmlSerializer serializer) throws IOException {
-            serializer.startTag(null, "color-mode");
+            serializer.startTag(null, TAG_COLOR_MODE);
             serializer.text(Integer.toString(mColorMode));
-            serializer.endTag(null, "color-mode");
+            serializer.endTag(null, TAG_COLOR_MODE);
         }
 
         public void dump(final PrintWriter pw, final String prefix) {
@@ -472,10 +536,10 @@
             final int outerDepth = parser.getDepth();
             while (XmlUtils.nextElementWithin(parser, outerDepth)) {
                 switch (parser.getName()) {
-                    case "stable-display-width":
+                    case TAG_STABLE_DISPLAY_WIDTH:
                         mWidth = loadIntValue(parser);
                         break;
-                    case "stable-display-height":
+                    case TAG_STABLE_DISPLAY_HEIGHT:
                         mHeight = loadIntValue(parser);
                         break;
                 }
@@ -494,12 +558,12 @@
 
         public void saveToXml(XmlSerializer serializer) throws IOException {
             if (mWidth > 0 && mHeight > 0) {
-                serializer.startTag(null, "stable-display-width");
+                serializer.startTag(null, TAG_STABLE_DISPLAY_WIDTH);
                 serializer.text(Integer.toString(mWidth));
-                serializer.endTag(null, "stable-display-width");
-                serializer.startTag(null, "stable-display-height");
+                serializer.endTag(null, TAG_STABLE_DISPLAY_WIDTH);
+                serializer.startTag(null, TAG_STABLE_DISPLAY_HEIGHT);
                 serializer.text(Integer.toString(mHeight));
-                serializer.endTag(null, "stable-display-height");
+                serializer.endTag(null, TAG_STABLE_DISPLAY_HEIGHT);
             }
         }
 
@@ -508,4 +572,158 @@
             pw.println(prefix + "StableDisplayHeight=" + mHeight);
         }
     }
+
+    private static final class BrightnessConfigurations {
+        // Maps from a user ID to the users' given brightness configuration
+        private SparseArray<BrightnessConfiguration> mConfigurations;
+
+        public BrightnessConfigurations() {
+            mConfigurations = new SparseArray<>();
+        }
+
+        private boolean setBrightnessConfigurationForUser(BrightnessConfiguration c,
+                int userSerial) {
+            BrightnessConfiguration currentConfig = mConfigurations.get(userSerial);
+            if (currentConfig == null || !currentConfig.equals(c)) {
+                mConfigurations.put(userSerial, c);
+                return true;
+            }
+            return false;
+        }
+
+        public BrightnessConfiguration getBrightnessConfiguration(int userSerial) {
+            return mConfigurations.get(userSerial);
+        }
+
+        public void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException {
+            final int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                if (TAG_BRIGHTNESS_CONFIGURATION.equals(parser.getName())) {
+                    int userSerial;
+                    try {
+                        userSerial = Integer.parseInt(
+                                parser.getAttributeValue(null, ATTR_USER_SERIAL));
+                    } catch (NumberFormatException nfe) {
+                        userSerial= -1;
+                        Slog.e(TAG, "Failed to read in brightness configuration", nfe);
+                    }
+
+                    try {
+                        BrightnessConfiguration config = loadConfigurationFromXml(parser);
+                        if (userSerial>= 0 && config != null) {
+                            mConfigurations.put(userSerial, config);
+                        }
+                    } catch (IllegalArgumentException iae) {
+                        Slog.e(TAG, "Failed to load brightness configuration!", iae);
+                    }
+                }
+            }
+        }
+
+        private static BrightnessConfiguration loadConfigurationFromXml(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            final int outerDepth = parser.getDepth();
+            final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                if (TAG_BRIGHTNESS_CURVE.equals(parser.getName())) {
+                    Pair<float[], float[]> curve = loadCurveFromXml(parser, builder);
+                    builder.setCurve(curve.first /*lux*/, curve.second /*nits*/);
+                }
+            }
+            return builder.build();
+        }
+
+        private static Pair<float[], float[]> loadCurveFromXml(XmlPullParser parser,
+                BrightnessConfiguration.Builder builder)
+                throws IOException, XmlPullParserException {
+            final int outerDepth = parser.getDepth();
+            List<Float> luxLevels = new ArrayList<>();
+            List<Float> nitLevels = new ArrayList<>();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                if (TAG_BRIGHTNESS_POINT.equals(parser.getName())) {
+                    luxLevels.add(loadFloat(parser.getAttributeValue(null, ATTR_LUX)));
+                    nitLevels.add(loadFloat(parser.getAttributeValue(null, ATTR_NITS)));
+                }
+            }
+            final int N = luxLevels.size();
+            float[] lux = new float[N];
+            float[] nits = new float[N];
+            for (int i = 0; i < N; i++) {
+                lux[i] = luxLevels.get(i);
+                nits[i] = nitLevels.get(i);
+            }
+            return Pair.create(lux, nits);
+        }
+
+        private static float loadFloat(String val) {
+            try {
+                return Float.parseFloat(val);
+            } catch (NullPointerException | NumberFormatException e) {
+                Slog.e(TAG, "Failed to parse float loading brightness config", e);
+                return Float.NEGATIVE_INFINITY;
+            }
+        }
+
+        public void saveToXml(XmlSerializer serializer) throws IOException {
+            for (int i = 0; i < mConfigurations.size(); i++) {
+                final int userSerial= mConfigurations.keyAt(i);
+                final BrightnessConfiguration config = mConfigurations.valueAt(i);
+
+                serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATION);
+                serializer.attribute(null, ATTR_USER_SERIAL, Integer.toString(userSerial));
+                saveConfigurationToXml(serializer, config);
+                serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATION);
+            }
+        }
+
+        private static void saveConfigurationToXml(XmlSerializer serializer,
+                BrightnessConfiguration config) throws IOException {
+            serializer.startTag(null, TAG_BRIGHTNESS_CURVE);
+            final Pair<float[], float[]> curve = config.getCurve();
+            for (int i = 0; i < curve.first.length; i++) {
+                serializer.startTag(null, TAG_BRIGHTNESS_POINT);
+                serializer.attribute(null, ATTR_LUX, Float.toString(curve.first[i]));
+                serializer.attribute(null, ATTR_NITS, Float.toString(curve.second[i]));
+                serializer.endTag(null, TAG_BRIGHTNESS_POINT);
+            }
+            serializer.endTag(null, TAG_BRIGHTNESS_CURVE);
+        }
+
+        public void dump(final PrintWriter pw, final String prefix) {
+            for (int i = 0; i < mConfigurations.size(); i++) {
+                final int userSerial= mConfigurations.keyAt(i);
+                pw.println(prefix + "User " + userSerial + ":");
+                pw.println(prefix + "  " + mConfigurations.valueAt(i));
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static class Injector {
+        private final AtomicFile mAtomicFile;
+
+        public Injector() {
+            mAtomicFile = new AtomicFile(new File("/data/system/display-manager-state.xml"));
+        }
+
+        public InputStream openRead() throws FileNotFoundException {
+            return mAtomicFile.openRead();
+        }
+
+        public OutputStream startWrite() throws IOException {
+            return mAtomicFile.startWrite();
+        }
+
+        public void finishWrite(OutputStream os, boolean success) {
+            if (!(os instanceof FileOutputStream)) {
+                throw new IllegalArgumentException("Unexpected OutputStream as argument: " + os);
+            }
+            FileOutputStream fos = (FileOutputStream) os;
+            if (success) {
+                mAtomicFile.finishWrite(fos);
+            } else {
+                mAtomicFile.failWrite(fos);
+            }
+        }
+    }
 }