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