New class to track brightness slider interactions.

Provides system apps with access to the users
brightness slider interactions and context from
when the brightness level was changed.

Test: runtest -c com.android.server.display.BrightnessTrackerTest frameworks-services

Change-Id: Ibdb3c78cb1d11887cb38b24c30754ff2e6f3bda8
diff --git a/core/java/android/hardware/display/BrightnessChangeEvent.aidl b/core/java/android/hardware/display/BrightnessChangeEvent.aidl
new file mode 100644
index 0000000..942e0db
--- /dev/null
+++ b/core/java/android/hardware/display/BrightnessChangeEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.hardware.display;
+
+parcelable BrightnessChangeEvent;
diff --git a/core/java/android/hardware/display/BrightnessChangeEvent.java b/core/java/android/hardware/display/BrightnessChangeEvent.java
new file mode 100644
index 0000000..fe24e32
--- /dev/null
+++ b/core/java/android/hardware/display/BrightnessChangeEvent.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 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 android.hardware.display;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data about a brightness settings change.
+ * TODO make this SystemAPI
+ * @hide
+ */
+public final class BrightnessChangeEvent implements Parcelable {
+    /** Brightness in nits */
+    public int brightness;
+
+    /** Timestamp of the change {@see System.currentTimeMillis()} */
+    public long timeStamp;
+
+    /** Package name of focused activity when brightness was changed. */
+    public String packageName;
+
+    /** User id of of the user running when brightness was changed.
+     * @hide */
+    public int userId;
+
+    /** Lux values of recent sensor data */
+    public float[] luxValues;
+
+    /** Timestamps of the lux sensor readings {@see System.currentTimeMillis()} */
+    public long[] luxTimestamps;
+
+    /** Most recent battery level when brightness was changed or Float.NaN */
+    public float batteryLevel;
+
+    /** Color filter active to provide night mode */
+    public boolean nightMode;
+
+    /** If night mode color filter is active this will be the temperature in kelvin */
+    public int colorTemperature;
+
+    /** Brightness level before slider adjustment */
+    public int lastBrightness;
+
+    public BrightnessChangeEvent() {
+    }
+
+    private BrightnessChangeEvent(Parcel source) {
+        brightness = source.readInt();
+        timeStamp = source.readLong();
+        packageName = source.readString();
+        userId = source.readInt();
+        luxValues = source.createFloatArray();
+        luxTimestamps = source.createLongArray();
+        batteryLevel = source.readFloat();
+        nightMode = source.readBoolean();
+        colorTemperature = source.readInt();
+        lastBrightness = source.readInt();
+    }
+
+    public static final Creator<BrightnessChangeEvent> CREATOR =
+            new Creator<BrightnessChangeEvent>() {
+                public BrightnessChangeEvent createFromParcel(Parcel source) {
+                    return new BrightnessChangeEvent(source);
+                }
+                public BrightnessChangeEvent[] newArray(int size) {
+                    return new BrightnessChangeEvent[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(brightness);
+        dest.writeLong(timeStamp);
+        dest.writeString(packageName);
+        dest.writeInt(userId);
+        dest.writeFloatArray(luxValues);
+        dest.writeLongArray(luxTimestamps);
+        dest.writeFloat(batteryLevel);
+        dest.writeBoolean(nightMode);
+        dest.writeInt(colorTemperature);
+        dest.writeInt(lastBrightness);
+    }
+}
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index b2af44e..ef77d6e 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -30,6 +30,7 @@
 import android.view.WindowManagerPolicy;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Manages the properties of attached displays.
@@ -615,6 +616,21 @@
     }
 
     /**
+     * Fetch {@link BrightnessChangeEvent}s.
+     * @hide until we make it a system api.
+     */
+    public List<BrightnessChangeEvent> getBrightnessEvents() {
+        return mGlobal.getBrightnessEvents();
+    }
+
+    /**
+     * @hide STOPSHIP - remove when adaptive brightness accepts curves.
+     */
+    public void setBrightness(int brightness) {
+        mGlobal.setBrightness(brightness);
+    }
+
+    /**
      * Listens for changes in available display devices.
      */
     public interface DisplayListener {
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index a8a4eb6..d93d0e4 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -17,6 +17,7 @@
 package android.hardware.display;
 
 import android.content.Context;
+import android.content.pm.ParceledListSlice;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.hardware.display.DisplayManager.DisplayListener;
@@ -37,6 +38,8 @@
 import android.view.Surface;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Manager communication with the display manager service on behalf of
@@ -456,6 +459,33 @@
         }
     }
 
+    /**
+     * Retrieves brightness change events.
+     */
+    public List<BrightnessChangeEvent> getBrightnessEvents() {
+        try {
+            ParceledListSlice<BrightnessChangeEvent> events = mDm.getBrightnessEvents();
+            if (events == null) {
+                return Collections.emptyList();
+            }
+            return events.getList();
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set brightness but don't add a BrightnessChangeEvent
+     * STOPSHIP remove when adaptive brightness accepts curves.
+     */
+    public void setBrightness(int brightness) {
+        try {
+            mDm.setBrightness(brightness);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
     private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub {
         @Override
         public void onDisplayEvent(int displayId, int event) {
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 5053884..b796cf9 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -16,6 +16,7 @@
 
 package android.hardware.display;
 
+import android.content.pm.ParceledListSlice;
 import android.graphics.Point;
 import android.hardware.display.IDisplayManagerCallback;
 import android.hardware.display.IVirtualDisplayCallback;
@@ -81,4 +82,11 @@
 
     // Get a stable metric for the device's display size. No permissions required.
     Point getStableDisplaySize();
+
+    // Requires BRIGHTNESS_SLIDER_USAGE permission.
+    ParceledListSlice getBrightnessEvents();
+
+    // STOPSHIP remove when adaptive brightness code is updated to accept curves.
+    // Requires BRIGHTNESS_SLIDER_USAGE permission.
+    void setBrightness(int brightness);
 }
diff --git a/core/java/com/android/internal/util/RingBuffer.java b/core/java/com/android/internal/util/RingBuffer.java
index ad84353..c22be2c 100644
--- a/core/java/com/android/internal/util/RingBuffer.java
+++ b/core/java/com/android/internal/util/RingBuffer.java
@@ -45,6 +45,17 @@
         return (int) Math.min(mBuffer.length, (long) mCursor);
     }
 
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    public void clear() {
+        for (int i = 0; i < size(); ++i) {
+            mBuffer[i] = null;
+        }
+        mCursor = 0;
+    }
+
     public void append(T t) {
         mBuffer[indexOf(mCursor++)] = t;
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7af1b46..fd17dcb 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2907,6 +2907,13 @@
     <permission android:name="android.permission.CONFIGURE_DISPLAY_COLOR_MODE"
         android:protectionLevel="signature" />
 
+    <!-- Allows an application to collect usage infomation about brightness slider changes.
+         <p>Not for use by third-party applications.</p>
+         TODO: make a System API
+         @hide -->
+    <permission android:name="android.permission.BRIGHTNESS_SLIDER_USAGE"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi Allows an application to control VPN.
          <p>Not for use by third-party applications.</p>
          @hide -->
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
new file mode 100644
index 0000000..361d928
--- /dev/null
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -0,0 +1,639 @@
+/*
+ * Copyright 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.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ParceledListSlice;
+import android.database.ContentObserver;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.BrightnessChangeEvent;
+import android.net.Uri;
+import android.os.BatteryManager;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.RingBuffer;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+import java.util.Deque;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class that tracks recent brightness settings changes and stores
+ * associated information such as light sensor readings.
+ */
+public class BrightnessTracker {
+
+    private static final String TAG = "BrightnessTracker";
+    private static final boolean DEBUG = false;
+
+    private static final String EVENTS_FILE = "brightness_events.xml";
+    private static final int MAX_EVENTS = 100;
+    // Discard events when reading or writing that are older than this.
+    private static final long MAX_EVENT_AGE = TimeUnit.DAYS.toMillis(30);
+    // Time over which we keep lux sensor readings.
+    private static final long LUX_EVENT_HORIZON = TimeUnit.SECONDS.toNanos(10);
+
+    private static final String TAG_EVENTS = "events";
+    private static final String TAG_EVENT = "event";
+    private static final String ATTR_BRIGHTNESS = "brightness";
+    private static final String ATTR_TIMESTAMP = "timestamp";
+    private static final String ATTR_PACKAGE_NAME = "packageName";
+    private static final String ATTR_USER = "user";
+    private static final String ATTR_LUX = "lux";
+    private static final String ATTR_LUX_TIMESTAMPS = "luxTimestamps";
+    private static final String ATTR_BATTERY_LEVEL = "batteryLevel";
+    private static final String ATTR_NIGHT_MODE = "nightMode";
+    private static final String ATTR_COLOR_TEMPERATURE = "colorTemperature";
+    private static final String ATTR_LAST_BRIGHTNESS = "lastBrightness";
+
+    // Lock held while accessing mEvents, is held while writing events to flash.
+    private final Object mEventsLock = new Object();
+    @GuardedBy("mEventsLock")
+    private RingBuffer<BrightnessChangeEvent> mEvents
+            = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS);
+    private final Runnable mEventsWriter = () -> writeEvents();
+    private volatile boolean mWriteEventsScheduled;
+
+    private UserManager mUserManager;
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private Handler mBgHandler;
+    // mSettingsObserver, mBroadcastReceiver and mSensorListener should only be used on
+    // the mBgHandler thread.
+    private SettingsObserver mSettingsObserver;
+    private BroadcastReceiver mBroadcastReceiver;
+    private SensorListener mSensorListener;
+
+    // Lock held while collecting data related to brightness changes.
+    private final Object mDataCollectionLock = new Object();
+    @GuardedBy("mDataCollectionLock")
+    private Deque<LightData> mLastSensorReadings = new ArrayDeque<>();
+    @GuardedBy("mDataCollectionLock")
+    private float mLastBatteryLevel = Float.NaN;
+    @GuardedBy("mDataCollectionLock")
+    private int mIgnoreBrightness = -1;
+    @GuardedBy("mDataCollectionLock")
+    private int mLastBrightness = -1;
+
+    private final Injector mInjector;
+
+    public BrightnessTracker(Context context, @Nullable Injector injector) {
+        // Note this will be called very early in boot, other system
+        // services may not be present.
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        if (injector != null) {
+            mInjector = injector;
+        } else {
+            mInjector = new Injector();
+        }
+    }
+
+    /** Start listening for brightness slider events */
+    public void start() {
+        if (DEBUG) {
+            Slog.d(TAG, "Start");
+        }
+        mBgHandler = mInjector.getBackgroundHandler();
+        mUserManager = mContext.getSystemService(UserManager.class);
+
+        mBgHandler.post(() -> backgroundStart());
+    }
+
+    private void backgroundStart() {
+        readEvents();
+
+        mLastBrightness = mInjector.getSystemIntForUser(mContentResolver,
+                Settings.System.SCREEN_BRIGHTNESS, -1,
+                UserHandle.USER_CURRENT);
+
+        mSensorListener = new SensorListener();
+        mInjector.registerSensorListener(mContext, mSensorListener);
+
+        mSettingsObserver = new SettingsObserver(mBgHandler);
+        mInjector.registerBrightnessObserver(mContentResolver, mSettingsObserver);
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_SHUTDOWN);
+        intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
+        mBroadcastReceiver = new Receiver();
+        mInjector.registerReceiver(mContext, mBroadcastReceiver, intentFilter);
+    }
+
+    /** Stop listening for events */
+    @VisibleForTesting
+    void stop() {
+        if (DEBUG) {
+            Slog.d(TAG, "Stop");
+        }
+        mInjector.unregisterSensorListener(mContext, mSensorListener);
+        mInjector.unregisterReceiver(mContext, mBroadcastReceiver);
+        mInjector.unregisterBrightnessObserver(mContext, mSettingsObserver);
+    }
+
+    /**
+     * @param userId userId to fetch data for.
+     * @return List of recent {@link BrightnessChangeEvent}s
+     */
+    public ParceledListSlice<BrightnessChangeEvent> getEvents(int userId) {
+        // TODO include apps from any managed profiles in the brightness information.
+        BrightnessChangeEvent[] events;
+        synchronized (mEventsLock) {
+            events = mEvents.toArray();
+        }
+        ArrayList<BrightnessChangeEvent> out = new ArrayList<>(events.length);
+        for (int i = 0; i < events.length; ++i) {
+            if (events[i].userId == userId) {
+                out.add(events[i]);
+            }
+        }
+        return new ParceledListSlice<>(out);
+    }
+
+    /** Sets brightness without logging the brightness change event */
+    public void setBrightness(int brightness, int userId) {
+        synchronized (mDataCollectionLock) {
+            mIgnoreBrightness = brightness;
+        }
+        mInjector.putSystemIntForUser(mContentResolver, Settings.System.SCREEN_BRIGHTNESS,
+                brightness, userId);
+    }
+
+    private void handleBrightnessChanged() {
+        if (DEBUG) {
+            Slog.d(TAG, "Brightness change");
+        }
+        final BrightnessChangeEvent event = new BrightnessChangeEvent();
+        event.timeStamp = mInjector.currentTimeMillis();
+
+        int brightness = mInjector.getSystemIntForUser(mContentResolver,
+                Settings.System.SCREEN_BRIGHTNESS, -1,
+                UserHandle.USER_CURRENT);
+
+        synchronized (mDataCollectionLock) {
+            int previousBrightness = mLastBrightness;
+            mLastBrightness = brightness;
+
+            if (brightness == -1 || brightness == mIgnoreBrightness) {
+                // Notified of brightness change but no setting or self change so ignore.
+                mIgnoreBrightness = -1;
+                return;
+            }
+
+            final int readingCount = mLastSensorReadings.size();
+            if (readingCount == 0) {
+                // No sensor data so ignore this.
+                return;
+            }
+
+            event.luxValues = new float[readingCount];
+            event.luxTimestamps = new long[readingCount];
+
+            int pos = 0;
+
+            // Convert sensor timestamp in elapsed time nanos to current time millis.
+            long currentTimeMillis = mInjector.currentTimeMillis();
+            long elapsedTimeNanos = mInjector.elapsedRealtimeNanos();
+            for (LightData reading : mLastSensorReadings) {
+                event.luxValues[pos] = reading.lux;
+                event.luxTimestamps[pos] = currentTimeMillis -
+                        TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanos - reading.timestamp);
+                ++pos;
+            }
+
+            event.batteryLevel = mLastBatteryLevel;
+            event.lastBrightness = previousBrightness;
+        }
+
+        event.brightness = brightness;
+
+        try {
+            final ActivityManager.StackInfo focusedStack = mInjector.getFocusedStack();
+            event.userId = focusedStack.userId;
+            event.packageName = focusedStack.topActivity.getPackageName();
+        } catch (RemoteException e) {
+            // Really shouldn't be possible.
+        }
+
+        event.nightMode = mInjector.getSecureIntForUser(mContentResolver,
+                Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 0, UserHandle.USER_CURRENT)
+                == 1;
+        event.colorTemperature = mInjector.getSecureIntForUser(mContentResolver,
+                Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE,
+                0, UserHandle.USER_CURRENT);
+
+        if (DEBUG) {
+            Slog.d(TAG, "Event " + event.brightness + " " + event.packageName);
+        }
+        synchronized (mEventsLock) {
+            mEvents.append(event);
+        }
+    }
+
+    private void scheduleWriteEvents() {
+        if (!mWriteEventsScheduled) {
+            mBgHandler.post(mEventsWriter);
+            mWriteEventsScheduled = true;
+        }
+    }
+
+    private void writeEvents() {
+        mWriteEventsScheduled = false;
+        // TODO kick off write on handler thread e.g. every 24 hours.
+        synchronized (mEventsLock) {
+            final AtomicFile writeTo = mInjector.getFile();
+            if (writeTo == null) {
+                return;
+            }
+            if (mEvents.isEmpty()) {
+                if (writeTo.exists()) {
+                    writeTo.delete();
+                }
+            } else {
+                FileOutputStream output = null;
+                try {
+                    output = writeTo.startWrite();
+                    writeEventsLocked(output);
+                    writeTo.finishWrite(output);
+                } catch (IOException e) {
+                    writeTo.failWrite(output);
+                    Slog.e(TAG, "Failed to write change mEvents.", e);
+                }
+            }
+        }
+    }
+
+    private void readEvents() {
+        synchronized (mEventsLock) {
+            mEvents.clear();
+            final AtomicFile readFrom = mInjector.getFile();
+            if (readFrom != null && readFrom.exists()) {
+                FileInputStream input = null;
+                try {
+                    input = readFrom.openRead();
+                    readEventsLocked(input);
+                } catch (IOException e) {
+                    readFrom.delete();
+                    Slog.e(TAG, "Failed to read change mEvents.", e);
+                } finally {
+                    IoUtils.closeQuietly(input);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mEventsLock")
+    void writeEventsLocked(OutputStream stream) throws IOException {
+        XmlSerializer out = new FastXmlSerializer();
+        out.setOutput(stream, StandardCharsets.UTF_8.name());
+        out.startDocument(null, true);
+        out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+        out.startTag(null, TAG_EVENTS);
+        BrightnessChangeEvent[] toWrite = mEvents.toArray();
+        if (DEBUG) {
+            Slog.d(TAG, "Writing events " + toWrite.length);
+        }
+        final long timeCutOff = System.currentTimeMillis() - MAX_EVENT_AGE;
+        for (int i = 0; i < toWrite.length; ++i) {
+            int userSerialNo = mInjector.getUserSerialNumber(mUserManager, toWrite[i].userId);
+            if (userSerialNo != -1 && toWrite[i].timeStamp > timeCutOff) {
+                out.startTag(null, TAG_EVENT);
+                out.attribute(null, ATTR_BRIGHTNESS, Integer.toString(toWrite[i].brightness));
+                out.attribute(null, ATTR_TIMESTAMP, Long.toString(toWrite[i].timeStamp));
+                out.attribute(null, ATTR_PACKAGE_NAME, toWrite[i].packageName);
+                out.attribute(null, ATTR_USER, Integer.toString(userSerialNo));
+                out.attribute(null, ATTR_BATTERY_LEVEL, Float.toString(toWrite[i].batteryLevel));
+                out.attribute(null, ATTR_NIGHT_MODE, Boolean.toString(toWrite[i].nightMode));
+                out.attribute(null, ATTR_COLOR_TEMPERATURE, Integer.toString(
+                        toWrite[i].colorTemperature));
+                out.attribute(null, ATTR_LAST_BRIGHTNESS,
+                        Integer.toString(toWrite[i].lastBrightness));
+                StringBuilder luxValues = new StringBuilder();
+                StringBuilder luxTimestamps = new StringBuilder();
+                for (int j = 0; j < toWrite[i].luxValues.length; ++j) {
+                    if (j > 0) {
+                        luxValues.append(',');
+                        luxTimestamps.append(',');
+                    }
+                    luxValues.append(Float.toString(toWrite[i].luxValues[j]));
+                    luxTimestamps.append(Long.toString(toWrite[i].luxTimestamps[j]));
+                }
+                out.attribute(null, ATTR_LUX, luxValues.toString());
+                out.attribute(null, ATTR_LUX_TIMESTAMPS, luxTimestamps.toString());
+                out.endTag(null, TAG_EVENT);
+            }
+        }
+        out.endTag(null, TAG_EVENTS);
+        out.endDocument();
+        stream.flush();
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mEventsLock")
+    void readEventsLocked(InputStream stream) throws IOException {
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(stream, StandardCharsets.UTF_8.name());
+
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+            }
+            String tag = parser.getName();
+            if (!TAG_EVENTS.equals(tag)) {
+                throw new XmlPullParserException(
+                        "Events not found in brightness tracker file " + tag);
+            }
+
+            final long timeCutOff = mInjector.currentTimeMillis() - MAX_EVENT_AGE;
+
+            parser.next();
+            int outerDepth = parser.getDepth();
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+                tag = parser.getName();
+                if (TAG_EVENT.equals(tag)) {
+                    BrightnessChangeEvent event = new BrightnessChangeEvent();
+
+                    String brightness = parser.getAttributeValue(null, ATTR_BRIGHTNESS);
+                    event.brightness = Integer.parseInt(brightness);
+                    String timestamp = parser.getAttributeValue(null, ATTR_TIMESTAMP);
+                    event.timeStamp = Long.parseLong(timestamp);
+                    event.packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
+                    String user = parser.getAttributeValue(null, ATTR_USER);
+                    event.userId = mInjector.getUserId(mUserManager, Integer.parseInt(user));
+                    String batteryLevel = parser.getAttributeValue(null, ATTR_BATTERY_LEVEL);
+                    event.batteryLevel = Float.parseFloat(batteryLevel);
+                    String nightMode = parser.getAttributeValue(null, ATTR_NIGHT_MODE);
+                    event.nightMode = Boolean.parseBoolean(nightMode);
+                    String colorTemperature =
+                            parser.getAttributeValue(null, ATTR_COLOR_TEMPERATURE);
+                    event.colorTemperature = Integer.parseInt(colorTemperature);
+                    String lastBrightness = parser.getAttributeValue(null, ATTR_LAST_BRIGHTNESS);
+                    event.lastBrightness = Integer.parseInt(lastBrightness);
+
+                    String luxValue = parser.getAttributeValue(null, ATTR_LUX);
+                    String luxTimestamp = parser.getAttributeValue(null, ATTR_LUX_TIMESTAMPS);
+
+                    String[] luxValues = luxValue.split(",");
+                    String[] luxTimestamps = luxTimestamp.split(",");
+                    if (luxValues.length != luxTimestamps.length) {
+                        continue;
+                    }
+                    event.luxValues = new float[luxValues.length];
+                    event.luxTimestamps = new long[luxValues.length];
+                    for (int i = 0; i < luxValues.length; ++i) {
+                        event.luxValues[i] = Float.parseFloat(luxValues[i]);
+                        event.luxTimestamps[i] = Long.parseLong(luxTimestamps[i]);
+                    }
+
+                    if (DEBUG) {
+                        Slog.i(TAG, "Read event " + event.brightness
+                                + " " + event.packageName);
+                    }
+
+                    if (event.userId != -1 && event.timeStamp > timeCutOff
+                            && event.luxValues.length > 0) {
+                        mEvents.append(event);
+                    }
+                }
+            }
+        } catch (NullPointerException | NumberFormatException | XmlPullParserException
+                | IOException e) {
+            // Failed to parse something, just start with an empty event log.
+            mEvents = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS);
+            Slog.e(TAG, "Failed to parse brightness event", e);
+            // Re-throw so we will delete the bad file.
+            throw new IOException("failed to parse file", e);
+        }
+    }
+
+    // Not allowed to keep the SensorEvent so used to copy the data we care about.
+    private static class LightData {
+        public float lux;
+        // Time in elapsedRealtimeNanos
+        public long timestamp;
+    }
+
+    private void recordSensorEvent(SensorEvent event) {
+        long horizon = mInjector.elapsedRealtimeNanos() - LUX_EVENT_HORIZON;
+        synchronized (mDataCollectionLock) {
+            if (DEBUG) {
+                Slog.v(TAG, "Sensor event " + event);
+            }
+            if (!mLastSensorReadings.isEmpty()
+                    && event.timestamp < mLastSensorReadings.getLast().timestamp) {
+                // Ignore event that came out of order.
+                return;
+            }
+            LightData data = null;
+            while (!mLastSensorReadings.isEmpty()
+                    && mLastSensorReadings.getFirst().timestamp < horizon) {
+                // Remove data that has fallen out of the window.
+                data = mLastSensorReadings.removeFirst();
+            }
+            // We put back the last one we removed so we know how long
+            // the first sensor reading was valid for.
+            if (data != null) {
+                mLastSensorReadings.addFirst(data);
+            }
+
+            data = new LightData();
+            data.timestamp = event.timestamp;
+            data.lux = event.values[0];
+            mLastSensorReadings.addLast(data);
+        }
+    }
+
+    private void batteryLevelChanged(int level, int scale) {
+        synchronized (mDataCollectionLock) {
+            mLastBatteryLevel = (float) level / (float) scale;
+        }
+    }
+
+    private final class SensorListener implements SensorEventListener {
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            recordSensorEvent(event);
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+
+        }
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        public SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (DEBUG) {
+                Slog.v(TAG, "settings change " + uri);
+            }
+            // Self change is based on observer passed to notifyObserver, SettingsProvider
+            // passes null so no changes are self changes.
+            handleBrightnessChanged();
+        }
+    }
+
+    private final class Receiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (DEBUG) {
+                Slog.d(TAG, "Received " + intent.getAction());
+            }
+            String action = intent.getAction();
+            if (Intent.ACTION_SHUTDOWN.equals(action)) {
+                stop();
+                scheduleWriteEvents();
+            } else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+                int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+                int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
+                if (level != -1 && scale != 0) {
+                    batteryLevelChanged(level, scale);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static class Injector {
+        public void registerSensorListener(Context context,
+                SensorEventListener sensorListener) {
+            SensorManager sensorManager = context.getSystemService(SensorManager.class);
+            Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
+            sensorManager.registerListener(sensorListener,
+                    lightSensor, SensorManager.SENSOR_DELAY_NORMAL);
+        }
+
+        public void unregisterSensorListener(Context context, SensorEventListener sensorListener) {
+            SensorManager sensorManager = context.getSystemService(SensorManager.class);
+            sensorManager.unregisterListener(sensorListener);
+        }
+
+        public void registerBrightnessObserver(ContentResolver resolver,
+                ContentObserver settingsObserver) {
+            resolver.registerContentObserver(Settings.System.getUriFor(
+                    Settings.System.SCREEN_BRIGHTNESS),
+                    false, settingsObserver, UserHandle.USER_ALL);
+        }
+
+        public void unregisterBrightnessObserver(Context context,
+                ContentObserver settingsObserver) {
+            context.getContentResolver().unregisterContentObserver(settingsObserver);
+        }
+
+        public void registerReceiver(Context context,
+                BroadcastReceiver receiver, IntentFilter filter) {
+            context.registerReceiver(receiver, filter);
+        }
+
+        public void unregisterReceiver(Context context,
+                BroadcastReceiver receiver) {
+            context.unregisterReceiver(receiver);
+        }
+
+        public Handler getBackgroundHandler() {
+            return BackgroundThread.getHandler();
+        }
+
+        public int getSystemIntForUser(ContentResolver resolver, String setting, int defaultValue,
+                int userId) {
+            return Settings.System.getIntForUser(resolver, setting, defaultValue, userId);
+        }
+
+        public void putSystemIntForUser(ContentResolver resolver, String setting, int value,
+                int userId) {
+            Settings.System.putIntForUser(resolver, setting, value, userId);
+        }
+
+        public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue,
+                int userId) {
+            return Settings.Secure.getIntForUser(resolver, setting, defaultValue, userId);
+        }
+
+        public AtomicFile getFile() {
+            return new AtomicFile(new File(Environment.getDataSystemDeDirectory(), EVENTS_FILE));
+        }
+
+        public long currentTimeMillis() {
+            return System.currentTimeMillis();
+        }
+
+        public long elapsedRealtimeNanos() {
+            return SystemClock.elapsedRealtimeNanos();
+        }
+
+        public int getUserSerialNumber(UserManager userManager, int userId) {
+            return userManager.getUserSerialNumber(userId);
+        }
+
+        public int getUserId(UserManager userManager, int userSerialNumber) {
+            return userManager.getUserHandle(userSerialNumber);
+        }
+
+        public ActivityManager.StackInfo getFocusedStack() throws RemoteException {
+            return ActivityManager.getService().getFocusedStackInfo();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index d0a1d9e..f1e2011 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -31,9 +31,11 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.hardware.SensorManager;
+import android.hardware.display.BrightnessChangeEvent;
 import android.hardware.display.DisplayManagerGlobal;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayViewport;
@@ -58,6 +60,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.IntArray;
 import android.util.Slog;
@@ -139,6 +142,7 @@
     private static final int MSG_DELIVER_DISPLAY_EVENT = 3;
     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 final Context mContext;
     private final DisplayManagerHandler mHandler;
@@ -256,6 +260,8 @@
 
     private final Injector mInjector;
 
+    private final BrightnessTracker mBrightnessTracker;
+
     public DisplayManagerService(Context context) {
         this(context, new Injector());
     }
@@ -274,6 +280,7 @@
 
         PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
         mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
+        mBrightnessTracker = new BrightnessTracker(context, null);
     }
 
     public void setupSchedulerPolicies() {
@@ -350,6 +357,7 @@
         }
 
         mHandler.sendEmptyMessage(MSG_REGISTER_ADDITIONAL_DISPLAY_ADAPTERS);
+        mHandler.sendEmptyMessage(MSG_REGISTER_BRIGHTNESS_TRACKER);
     }
 
     @VisibleForTesting
@@ -1352,6 +1360,10 @@
                             mTempExternalTouchViewport, mTempVirtualTouchViewports);
                     break;
                 }
+
+                case MSG_REGISTER_BRIGHTNESS_TRACKER:
+                    mBrightnessTracker.start();
+                    break;
             }
         }
     }
@@ -1736,6 +1748,35 @@
             }
         }
 
+        @Override // Binder call
+        public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents() {
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.BRIGHTNESS_SLIDER_USAGE,
+                    "Permission to read brightness events.");
+            int userId = UserHandle.getUserId(Binder.getCallingUid());
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return mBrightnessTracker.getEvents(userId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
+        public void setBrightness(int brightness) {
+            // STOPSHIP - remove when adaptive brightness controller accepts curves.
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.BRIGHTNESS_SLIDER_USAGE,
+                    "Permission to set brightness.");
+            int userId = UserHandle.getUserId(Binder.getCallingUid());
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mBrightnessTracker.setBrightness(brightness, userId);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
         private boolean validatePackageName(int uid, String packageName) {
             if (packageName != null) {
                 String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
new file mode 100644
index 0000000..d9fac87
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
@@ -0,0 +1,640 @@
+/*
+ * Copyright 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.display.BrightnessChangeEvent;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.MessageQueue;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.AtomicFile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BrightnessTrackerTest {
+
+    private BrightnessTracker mTracker;
+    private TestInjector mInjector;
+
+    private static Object sHandlerLock = new Object();
+    private static Handler sHandler;
+    private static HandlerThread sThread =
+            new HandlerThread("brightness.test", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+
+    private static Handler ensureHandler() {
+        synchronized (sHandlerLock) {
+            if (sHandler == null) {
+                sThread.start();
+                sHandler = new Handler(sThread.getLooper());
+            }
+            return sHandler;
+        }
+    }
+
+
+    @Before
+    public void setUp() throws Exception {
+        mInjector = new TestInjector(ensureHandler());
+
+        mTracker = new BrightnessTracker(InstrumentationRegistry.getContext(), mInjector);
+    }
+
+    @Test
+    public void testStartStopTracker() {
+        startTracker(mTracker);
+        assertNotNull(mInjector.mSensorListener);
+        assertNotNull(mInjector.mSettingsObserver);
+        assertNotNull(mInjector.mBroadcastReceiver);
+        mTracker.stop();
+        assertNull(mInjector.mSensorListener);
+        assertNull(mInjector.mSettingsObserver);
+        assertNull(mInjector.mBroadcastReceiver);
+    }
+
+    @Test
+    public void testBrightnessEvent() {
+        final int brightness = 20;
+
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, brightness);
+        startTracker(mTracker);
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0).getList();
+        mTracker.stop();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(mInjector.currentTimeMillis(), event.timeStamp);
+        assertEquals(1, event.luxValues.length);
+        assertEquals(1.0f, event.luxValues[0], 0.1f);
+        assertEquals(mInjector.currentTimeMillis() - TimeUnit.SECONDS.toMillis(2),
+                event.luxTimestamps[0]);
+        assertEquals(brightness, event.brightness);
+
+        // System had no data so these should all be at defaults.
+        assertEquals(Float.NaN, event.batteryLevel, 0.0);
+        assertFalse(event.nightMode);
+        assertEquals(0, event.colorTemperature);
+    }
+
+    @Test
+    public void testBrightnessFullPopulatedEvent() {
+        final int lastBrightness = 230;
+        final int brightness = 130;
+
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, lastBrightness);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3333);
+
+        startTracker(mTracker);
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, brightness);
+        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
+                batteryChangeEvent(30, 60));
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f));
+        final long sensorTime = mInjector.currentTimeMillis();
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0).getList();
+        mTracker.stop();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(event.timeStamp, mInjector.currentTimeMillis());
+        assertArrayEquals(new float[] {1000.0f}, event.luxValues, 0.01f);
+        assertArrayEquals(new long[] {sensorTime}, event.luxTimestamps);
+        assertEquals(brightness, event.brightness);
+        assertEquals(lastBrightness, event.lastBrightness);
+        assertEquals(0.5, event.batteryLevel, 0.01);
+        assertTrue(event.nightMode);
+        assertEquals(3333, event.colorTemperature);
+        assertEquals("a.package", event.packageName);
+        assertEquals(0, event.userId);
+    }
+
+    @Test
+    public void testIgnoreSelfChange() {
+        final int initialBrightness = 30;
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, initialBrightness);
+        startTracker(mTracker);
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
+
+        final int systemUpdatedBrightness = 20;
+        mTracker.setBrightness(systemUpdatedBrightness, 0);
+        assertEquals(systemUpdatedBrightness,
+                (int) mInjector.mSystemIntSettings.get(Settings.System.SCREEN_BRIGHTNESS));
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0).getList();
+        // No events because we filtered out our change.
+        assertEquals(0, events.size());
+
+        final int firstUserUpdateBrightness = 20;
+        // Then change comes from somewhere else so we shouldn't filter.
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS,
+                firstUserUpdateBrightness);
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+
+        // and with a different brightness value.
+        final int secondUserUpdateBrightness = 34;
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS,
+                secondUserUpdateBrightness);
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        events = mTracker.getEvents(0).getList();
+
+        assertEquals(2, events.size());
+        // First event is change from system update (20) to first user update (20)
+        assertEquals(systemUpdatedBrightness, events.get(0).lastBrightness);
+        assertEquals(firstUserUpdateBrightness, events.get(0).brightness);
+        // Second event is from first to second user update.
+        assertEquals(firstUserUpdateBrightness, events.get(1).lastBrightness);
+        assertEquals(secondUserUpdateBrightness, events.get(1).brightness);
+
+        mTracker.stop();
+    }
+
+    @Test
+    public void testLimitedBufferSize() {
+        startTracker(mTracker);
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
+
+        for (int brightness = 0; brightness <= 255; ++brightness) {
+            mInjector.mSensorListener.onSensorChanged(createSensorEvent(1.0f));
+            mInjector.incrementTime(TimeUnit.SECONDS.toNanos(1));
+            mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, brightness);
+            mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                    Settings.System.SCREEN_BRIGHTNESS));
+        }
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0).getList();
+        mTracker.stop();
+
+        // Should be capped at 100 events, and they should be the most recent 100.
+        assertEquals(100, events.size());
+        for (int i = 0; i < events.size(); i++) {
+            BrightnessChangeEvent event = events.get(i);
+            assertEquals(156 + i, event.brightness);
+        }
+    }
+
+    @Test
+    public void testLimitedSensorEvents() {
+        final int brightness = 20;
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, brightness);
+
+        startTracker(mTracker);
+        // 20 Sensor events 1 second apart.
+        for (int i = 0; i < 20; ++i) {
+            mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
+            mInjector.mSensorListener.onSensorChanged(createSensorEvent(i + 1.0f));
+        }
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0).getList();
+        mTracker.stop();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(mInjector.currentTimeMillis(), event.timeStamp);
+
+        // 12 sensor events, 11 for 0->10 seconds + 1 previous event.
+        assertEquals(12, event.luxValues.length);
+        for (int i = 0; i < 12; ++i) {
+            assertEquals(event.luxTimestamps[11 - i],
+                    mInjector.currentTimeMillis() - i * TimeUnit.SECONDS.toMillis(1));
+        }
+        assertEquals(brightness, event.brightness);
+    }
+
+    @Test
+    public void testReadEvents() throws Exception {
+        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
+                mInjector);
+        mInjector.mCurrentTimeMillis = System.currentTimeMillis();
+        long someTimeAgo = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12);
+        long twoMonthsAgo = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60);
+        // 3 Events in the file but one too old to read.
+        String eventFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                + "<events>\n"
+                + "<event brightness=\"194\" timestamp=\""
+                + Long.toString(someTimeAgo) + "\" packageName=\""
+                + "com.example.app\" user=\"10\" "
+                + "lastBrightness=\"32\" "
+                + "batteryLevel=\"1.0\" nightMode=\"false\" colorTemperature=\"0\"\n"
+                + "lux=\"32.2,31.1\" luxTimestamps=\""
+                + Long.toString(someTimeAgo) + "," + Long.toString(someTimeAgo) + "\"/>"
+                + "<event brightness=\"71\" timestamp=\""
+                + Long.toString(someTimeAgo) + "\" packageName=\""
+                + "com.android.anapp\" user=\"11\" "
+                + "lastBrightness=\"32\" "
+                + "batteryLevel=\"0.5\" nightMode=\"true\" colorTemperature=\"3235\"\n"
+                + "lux=\"132.2,131.1\" luxTimestamps=\""
+                + Long.toString(someTimeAgo) + "," + Long.toString(someTimeAgo) + "\"/>"
+                // Event that is too old so shouldn't show up.
+                + "<event brightness=\"142\" timestamp=\""
+                + Long.toString(twoMonthsAgo) + "\" packageName=\""
+                + "com.example.app\" user=\"10\" "
+                + "lastBrightness=\"32\" "
+                + "batteryLevel=\"1.0\" nightMode=\"false\" colorTemperature=\"0\"\n"
+                + "lux=\"32.2,31.1\" luxTimestamps=\""
+                + Long.toString(twoMonthsAgo) + "," + Long.toString(twoMonthsAgo) + "\"/>"
+                + "</events>";
+        tracker.readEventsLocked(getInputStream(eventFile));
+        List<BrightnessChangeEvent> events = tracker.getEvents(0).getList();
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(someTimeAgo, event.timeStamp);
+        assertEquals(194, event.brightness);
+        assertArrayEquals(new float[] {32.2f, 31.1f}, event.luxValues, 0.01f);
+        assertArrayEquals(new long[] {someTimeAgo, someTimeAgo}, event.luxTimestamps);
+        assertEquals(32, event.lastBrightness);
+        assertEquals(0, event.userId);
+        assertFalse(event.nightMode);
+        assertEquals(1.0f, event.batteryLevel, 0.01);
+        assertEquals("com.example.app", event.packageName);
+
+        events = tracker.getEvents(1).getList();
+        assertEquals(1, events.size());
+        event = events.get(0);
+        assertEquals(someTimeAgo, event.timeStamp);
+        assertEquals(71, event.brightness);
+        assertArrayEquals(new float[] {132.2f, 131.1f}, event.luxValues, 0.01f);
+        assertArrayEquals(new long[] {someTimeAgo, someTimeAgo}, event.luxTimestamps);
+        assertEquals(32, event.lastBrightness);
+        assertEquals(1, event.userId);
+        assertTrue(event.nightMode);
+        assertEquals(3235, event.colorTemperature);
+        assertEquals(0.5f, event.batteryLevel, 0.01);
+        assertEquals("com.android.anapp", event.packageName);
+    }
+
+    @Test
+    public void testFailedRead() {
+        String someTimeAgo =
+                Long.toString(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12));
+        mInjector.mCurrentTimeMillis = System.currentTimeMillis();
+
+        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
+                mInjector);
+        String eventFile = "junk in the file";
+        try {
+            tracker.readEventsLocked(getInputStream(eventFile));
+        } catch (IOException e) {
+            // Expected;
+        }
+        assertEquals(0, tracker.getEvents(0).getList().size());
+
+        // Missing lux value.
+        eventFile =
+                "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+                        + "<events>\n"
+                        + "<event brightness=\"194\" timestamp=\"" + someTimeAgo + "\" packageName=\""
+                        + "com.example.app\" user=\"10\" "
+                        + "batteryLevel=\"0.7\" nightMode=\"false\" colorTemperature=\"0\" />\n"
+                        + "</events>";
+        try {
+            tracker.readEventsLocked(getInputStream(eventFile));
+        } catch (IOException e) {
+            // Expected;
+        }
+        assertEquals(0, tracker.getEvents(0).getList().size());
+    }
+
+    @Test
+    public void testWriteThenRead() throws Exception {
+        final int brightness = 20;
+
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, brightness);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3339);
+
+        startTracker(mTracker);
+        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
+                batteryChangeEvent(30, 100));
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(2000.0f));
+        final long firstSensorTime = mInjector.currentTimeMillis();
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(2));
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(3000.0f));
+        final long secondSensorTime = mInjector.currentTimeMillis();
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(3));
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mTracker.writeEventsLocked(baos);
+        mTracker.stop();
+
+        baos.flush();
+        ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
+        BrightnessTracker tracker = new BrightnessTracker(InstrumentationRegistry.getContext(),
+                mInjector);
+        tracker.readEventsLocked(input);
+        List<BrightnessChangeEvent> events = tracker.getEvents(0).getList();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, 0.01f);
+        assertArrayEquals(new long[] {firstSensorTime, secondSensorTime}, event.luxTimestamps);
+        assertEquals(brightness, event.brightness);
+        assertEquals(0.3, event.batteryLevel, 0.01f);
+        assertTrue(event.nightMode);
+        assertEquals(3339, event.colorTemperature);
+    }
+
+    @Test
+    public void testParcelUnParcel() {
+        Parcel parcel = Parcel.obtain();
+        BrightnessChangeEvent event = new BrightnessChangeEvent();
+        event.brightness = 23;
+        event.timeStamp = 345L;
+        event.packageName = "com.example";
+        event.userId = 12;
+        event.luxValues = new float[2];
+        event.luxValues[0] = 3000.0f;
+        event.luxValues[1] = 4000.0f;
+        event.luxTimestamps = new long[2];
+        event.luxTimestamps[0] = 325L;
+        event.luxTimestamps[1] = 315L;
+        event.batteryLevel = 0.7f;
+        event.nightMode = false;
+        event.colorTemperature = 345;
+        event.lastBrightness = 50;
+
+        event.writeToParcel(parcel, 0);
+        byte[] parceled = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(parceled, 0, parceled.length);
+        parcel.setDataPosition(0);
+
+        BrightnessChangeEvent event2 = BrightnessChangeEvent.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        assertEquals(event.brightness, event2.brightness);
+        assertEquals(event.timeStamp, event2.timeStamp);
+        assertEquals(event.packageName, event2.packageName);
+        assertEquals(event.userId, event2.userId);
+        assertArrayEquals(event.luxValues, event2.luxValues, 0.01f);
+        assertArrayEquals(event.luxTimestamps, event2.luxTimestamps);
+        assertEquals(event.batteryLevel, event2.batteryLevel, 0.01f);
+        assertEquals(event.nightMode, event2.nightMode);
+        assertEquals(event.colorTemperature, event2.colorTemperature);
+        assertEquals(event.lastBrightness, event2.lastBrightness);
+
+        parcel = Parcel.obtain();
+        event.batteryLevel = Float.NaN;
+        event.writeToParcel(parcel, 0);
+        parceled = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(parceled, 0, parceled.length);
+        parcel.setDataPosition(0);
+        event2 = BrightnessChangeEvent.CREATOR.createFromParcel(parcel);
+        assertEquals(event.batteryLevel, event2.batteryLevel, 0.01f);
+    }
+
+    private InputStream getInputStream(String data) {
+        return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private Intent batteryChangeEvent(int level, int scale) {
+        Intent intent = new Intent();
+        intent.setAction(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_LEVEL, level);
+        intent.putExtra(BatteryManager.EXTRA_SCALE, scale);
+        return intent;
+    }
+
+    private SensorEvent createSensorEvent(float lux) {
+        SensorEvent event;
+        try {
+            Constructor<SensorEvent> constr =
+                    SensorEvent.class.getDeclaredConstructor(Integer.TYPE);
+            constr.setAccessible(true);
+            event = constr.newInstance(1);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        event.values[0] = lux;
+        event.timestamp = mInjector.mElapsedRealtimeNanos;
+
+        return event;
+    }
+
+    private void startTracker(BrightnessTracker tracker) {
+        tracker.start();
+        mInjector.waitForHandler();
+    }
+
+
+    private static final class Idle implements MessageQueue.IdleHandler {
+        private boolean mIdle;
+
+        @Override
+        public boolean queueIdle() {
+            synchronized (this) {
+                mIdle = true;
+                notifyAll();
+            }
+            return false;
+        }
+
+        public synchronized void waitForIdle() {
+            while (!mIdle) {
+                try {
+                    wait();
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+    private class TestInjector extends BrightnessTracker.Injector {
+        SensorEventListener mSensorListener;
+        ContentObserver mSettingsObserver;
+        BroadcastReceiver mBroadcastReceiver;
+        Map<String, Integer> mSystemIntSettings = new HashMap<>();
+        Map<String, Integer> mSecureIntSettings = new HashMap<>();
+        long mCurrentTimeMillis = System.currentTimeMillis();
+        long mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
+        Handler mHandler;
+
+        public TestInjector(Handler handler) {
+            mHandler = handler;
+        }
+
+        public void incrementTime(long timeMillis) {
+            mCurrentTimeMillis += timeMillis;
+            mElapsedRealtimeNanos += TimeUnit.MILLISECONDS.toNanos(timeMillis);
+        }
+
+        @Override
+        public void registerSensorListener(Context context,
+                SensorEventListener sensorListener) {
+            mSensorListener = sensorListener;
+        }
+
+        @Override
+        public void unregisterSensorListener(Context context,
+                SensorEventListener sensorListener) {
+            mSensorListener = null;
+        }
+
+        @Override
+        public void registerBrightnessObserver(ContentResolver resolver,
+                ContentObserver settingsObserver) {
+            mSettingsObserver = settingsObserver;
+        }
+
+        @Override
+        public void unregisterBrightnessObserver(Context context,
+                ContentObserver settingsObserver) {
+            mSettingsObserver = null;
+        }
+
+        @Override
+        public void registerReceiver(Context context,
+                BroadcastReceiver shutdownReceiver, IntentFilter shutdownFilter) {
+            mBroadcastReceiver = shutdownReceiver;
+        }
+
+        @Override
+        public void unregisterReceiver(Context context,
+                BroadcastReceiver broadcastReceiver) {
+            assertEquals(mBroadcastReceiver, broadcastReceiver);
+            mBroadcastReceiver = null;
+        }
+
+        @Override
+        public Handler getBackgroundHandler() {
+            return mHandler;
+        }
+
+        public void waitForHandler() {
+            Idle idle = new Idle();
+            mHandler.getLooper().getQueue().addIdleHandler(idle);
+            mHandler.post(() -> {});
+            idle.waitForIdle();
+        }
+
+        @Override
+        public int getSystemIntForUser(ContentResolver resolver, String setting, int defaultValue,
+                int userId) {
+            Integer value = mSystemIntSettings.get(setting);
+            if (value == null) {
+                return defaultValue;
+            } else {
+                return value;
+            }
+        }
+
+        @Override
+        public void putSystemIntForUser(ContentResolver resolver, String setting, int value,
+                int userId) {
+            mSystemIntSettings.put(setting, value);
+        }
+
+        @Override
+        public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue,
+                int userId) {
+            Integer value = mSecureIntSettings.get(setting);
+            if (value == null) {
+                return defaultValue;
+            } else {
+                return value;
+            }
+        }
+
+        @Override
+        public AtomicFile getFile() {
+            // Don't have the test write / read from anywhere.
+            return null;
+        }
+
+        @Override
+        public long currentTimeMillis() {
+            return mCurrentTimeMillis;
+        }
+
+        @Override
+        public long elapsedRealtimeNanos() {
+            return mElapsedRealtimeNanos;
+        }
+
+        @Override
+        public int getUserSerialNumber(UserManager userManager, int userId) {
+            return userId + 10;
+        }
+
+        @Override
+        public int getUserId(UserManager userManager, int userSerialNumber) {
+            return userSerialNumber - 10;
+        }
+
+        @Override
+        public ActivityManager.StackInfo getFocusedStack() throws RemoteException {
+            ActivityManager.StackInfo focusedStack = new ActivityManager.StackInfo();
+            focusedStack.userId = 0;
+            focusedStack.topActivity = new ComponentName("a.package", "a.class");
+            return focusedStack;
+        }
+    }
+}