Add Ambient Brightness tracker API
Test: atest com.android.server.display.AmbientBrightnessStatsTrackerTest
&& atest android.hardware.display.AmbientBrightnessDayStatsTest
Bug: 69406079
Change-Id: I4b13c6bdd3e9fdded8086371f46dba0fd3102b98
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl b/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl
new file mode 100644
index 0000000..9070777
--- /dev/null
+++ b/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2018 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 AmbientBrightnessDayStats;
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.java b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
new file mode 100644
index 0000000..41be397
--- /dev/null
+++ b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2018 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.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+
+/**
+ * AmbientBrightnessDayStats stores and manipulates brightness stats over a single day.
+ * {@see DisplayManager.getAmbientBrightnessStats()}
+ * TODO: Make this system API
+ *
+ * @hide
+ */
+public class AmbientBrightnessDayStats implements Parcelable {
+
+ /** The localdate for which brightness stats are being tracked */
+ private final LocalDate mLocalDate;
+
+ /** Ambient brightness values for creating bucket boundaries from */
+ private final float[] mBucketBoundaries;
+
+ /** Stats of how much time (in seconds) was spent in each of the buckets */
+ private final float[] mStats;
+
+ /**
+ * @hide
+ */
+ public AmbientBrightnessDayStats(@NonNull LocalDate localDate,
+ @NonNull float[] bucketBoundaries) {
+ Preconditions.checkNotNull(localDate);
+ Preconditions.checkNotNull(bucketBoundaries);
+ int numBuckets = bucketBoundaries.length;
+ if (numBuckets < 1) {
+ throw new IllegalArgumentException("Bucket boundaries must contain at least 1 value");
+ }
+ mLocalDate = localDate;
+ mBucketBoundaries = bucketBoundaries;
+ mStats = new float[numBuckets];
+ }
+
+ /**
+ * @hide
+ */
+ public AmbientBrightnessDayStats(@NonNull LocalDate localDate,
+ @NonNull float[] bucketBoundaries, @NonNull float[] stats) {
+ Preconditions.checkNotNull(localDate);
+ Preconditions.checkNotNull(bucketBoundaries);
+ Preconditions.checkNotNull(stats);
+ if (bucketBoundaries.length < 1) {
+ throw new IllegalArgumentException("Bucket boundaries must contain at least 1 value");
+ }
+ if (bucketBoundaries.length != stats.length) {
+ throw new IllegalArgumentException("Bucket boundaries and stats must be of same size.");
+ }
+ mLocalDate = localDate;
+ mBucketBoundaries = bucketBoundaries;
+ mStats = stats;
+ }
+
+ public LocalDate getLocalDate() {
+ return mLocalDate;
+ }
+
+ public float[] getStats() {
+ return mStats;
+ }
+
+ public float[] getBucketBoundaries() {
+ return mBucketBoundaries;
+ }
+
+ private AmbientBrightnessDayStats(Parcel source) {
+ mLocalDate = LocalDate.parse(source.readString());
+ mBucketBoundaries = source.createFloatArray();
+ mStats = source.createFloatArray();
+ }
+
+ public static final Creator<AmbientBrightnessDayStats> CREATOR =
+ new Creator<AmbientBrightnessDayStats>() {
+
+ @Override
+ public AmbientBrightnessDayStats createFromParcel(Parcel source) {
+ return new AmbientBrightnessDayStats(source);
+ }
+
+ @Override
+ public AmbientBrightnessDayStats[] newArray(int size) {
+ return new AmbientBrightnessDayStats[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ AmbientBrightnessDayStats other = (AmbientBrightnessDayStats) obj;
+ return mLocalDate.equals(other.mLocalDate) && Arrays.equals(mBucketBoundaries,
+ other.mBucketBoundaries) && Arrays.equals(mStats, other.mStats);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = result * prime + mLocalDate.hashCode();
+ result = result * prime + Arrays.hashCode(mBucketBoundaries);
+ result = result * prime + Arrays.hashCode(mStats);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder bucketBoundariesString = new StringBuilder();
+ StringBuilder statsString = new StringBuilder();
+ for (int i = 0; i < mBucketBoundaries.length; i++) {
+ if (i != 0) {
+ bucketBoundariesString.append(", ");
+ statsString.append(", ");
+ }
+ bucketBoundariesString.append(mBucketBoundaries[i]);
+ statsString.append(mStats[i]);
+ }
+ return new StringBuilder()
+ .append(mLocalDate).append(" ")
+ .append("{").append(bucketBoundariesString).append("} ")
+ .append("{").append(statsString).append("}").toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mLocalDate.toString());
+ dest.writeFloatArray(mBucketBoundaries);
+ dest.writeFloatArray(mStats);
+ }
+
+ /** @hide */
+ public void log(float ambientBrightness, float durationSec) {
+ int bucketIndex = getBucketIndex(ambientBrightness);
+ if (bucketIndex >= 0) {
+ mStats[bucketIndex] += durationSec;
+ }
+ }
+
+ private int getBucketIndex(float ambientBrightness) {
+ if (ambientBrightness < mBucketBoundaries[0]) {
+ return -1;
+ }
+ int low = 0;
+ int high = mBucketBoundaries.length - 1;
+ while (low < high) {
+ int mid = (low + high) / 2;
+ if (mBucketBoundaries[mid] <= ambientBrightness
+ && ambientBrightness < mBucketBoundaries[mid + 1]) {
+ return mid;
+ } else if (mBucketBoundaries[mid] < ambientBrightness) {
+ low = mid + 1;
+ } else if (mBucketBoundaries[mid] > ambientBrightness) {
+ high = mid - 1;
+ }
+ }
+ return low;
+ }
+}
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 4de4880..22fb8e7 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -631,6 +631,16 @@
}
/**
+ * Fetch {@link AmbientBrightnessDayStats}s.
+ *
+ * @hide until we make it a system api
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_LIGHT_STATS)
+ public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
+ return mGlobal.getAmbientBrightnessStats();
+ }
+
+ /**
* Sets the global display brightness configuration.
*
* @hide
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 2d5f5e0..d7f7c86 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -525,6 +525,21 @@
}
}
+ /**
+ * Retrieves ambient brightness stats.
+ */
+ public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
+ try {
+ ParceledListSlice<AmbientBrightnessDayStats> stats = mDm.getAmbientBrightnessStats();
+ if (stats == null) {
+ return Collections.emptyList();
+ }
+ return stats.getList();
+ } 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/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 1cfad4f..f468942 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -174,9 +174,9 @@
public abstract boolean isUidPresentOnDisplay(int uid, int displayId);
/**
- * Persist brightness slider events.
+ * Persist brightness slider events and ambient brightness stats.
*/
- public abstract void persistBrightnessSliderEvents();
+ public abstract void persistBrightnessTrackerState();
/**
* Notifies the display manager that resource overlays have changed.
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 13599cf..0571ae1 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -87,6 +87,9 @@
// Requires BRIGHTNESS_SLIDER_USAGE permission.
ParceledListSlice getBrightnessEvents(String callingPackage);
+ // Requires ACCESS_AMBIENT_LIGHT_STATS permission.
+ ParceledListSlice getAmbientBrightnessStats();
+
// Sets the global brightness configuration for a given user. Requires
// CONFIGURE_DISPLAY_BRIGHTNESS, and INTERACT_ACROSS_USER if the user being configured is not
// the same as the calling user.
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b23a64b..4598b38 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3012,6 +3012,13 @@
<permission android:name="android.permission.BRIGHTNESS_SLIDER_USAGE"
android:protectionLevel="signature|privileged|development" />
+ <!-- Allows an application to collect ambient light stats.
+ <p>Not for use by third party applications.</p>
+ TODO: Make a system API
+ @hide -->
+ <permission android:name="android.permission.ACCESS_AMBIENT_LIGHT_STATS"
+ android:protectionLevel="signature|privileged|development" />
+
<!-- Allows an application to modify the display brightness configuration
@hide
@SystemApi -->
diff --git a/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java b/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
new file mode 100644
index 0000000..84409d4
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2018 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.LocalDate;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientBrightnessDayStatsTest {
+
+ @Test
+ public void testAmbientBrightnessDayStatsAdd() {
+ AmbientBrightnessDayStats dayStats = new AmbientBrightnessDayStats(LocalDate.now(),
+ new float[]{0, 1, 10, 100});
+ dayStats.log(0, 1);
+ dayStats.log(0.5f, 1.5f);
+ dayStats.log(50, 12.5f);
+ dayStats.log(2000, 1.24f);
+ dayStats.log(-10, 0.5f);
+ assertEquals(2.5f, dayStats.getStats()[0], 0);
+ assertEquals(0, dayStats.getStats()[1], 0);
+ assertEquals(12.5f, dayStats.getStats()[2], 0);
+ assertEquals(1.24f, dayStats.getStats()[3], 0);
+ }
+
+ @Test
+ public void testAmbientBrightnessDayStatsEquals() {
+ LocalDate today = LocalDate.now();
+ AmbientBrightnessDayStats dayStats1 = new AmbientBrightnessDayStats(today,
+ new float[]{0, 1, 10, 100});
+ AmbientBrightnessDayStats dayStats2 = new AmbientBrightnessDayStats(today,
+ new float[]{0, 1, 10, 100}, new float[4]);
+ AmbientBrightnessDayStats dayStats3 = new AmbientBrightnessDayStats(today,
+ new float[]{0, 1, 10, 100}, new float[]{1, 3, 5, 7});
+ AmbientBrightnessDayStats dayStats4 = new AmbientBrightnessDayStats(today,
+ new float[]{0, 1, 10, 100}, new float[]{1, 3, 5, 0});
+ assertEquals(dayStats1, dayStats2);
+ assertEquals(dayStats1.hashCode(), dayStats2.hashCode());
+ assertNotEquals(dayStats1, dayStats3);
+ assertNotEquals(dayStats1.hashCode(), dayStats3.hashCode());
+ dayStats4.log(100, 7);
+ assertEquals(dayStats3, dayStats4);
+ assertEquals(dayStats3.hashCode(), dayStats4.hashCode());
+ }
+
+ @Test
+ public void testAmbientBrightnessDayStatsIncorrectInit() {
+ try {
+ new AmbientBrightnessDayStats(LocalDate.now(), new float[]{1, 10, 100},
+ new float[]{1, 5, 6, 7});
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ try {
+ new AmbientBrightnessDayStats(LocalDate.now(), new float[]{});
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testParcelUnparcelAmbientBrightnessDayStats() {
+ LocalDate today = LocalDate.now();
+ AmbientBrightnessDayStats stats = new AmbientBrightnessDayStats(today,
+ new float[]{0, 1, 10, 100}, new float[]{1.3f, 2.6f, 5.8f, 10});
+ // Parcel the data
+ Parcel parcel = Parcel.obtain();
+ stats.writeToParcel(parcel, 0);
+ byte[] parceled = parcel.marshall();
+ parcel.recycle();
+ // Unparcel and check that it has not changed
+ parcel = Parcel.obtain();
+ parcel.unmarshall(parceled, 0, parceled.length);
+ parcel.setDataPosition(0);
+ AmbientBrightnessDayStats statsAgain = AmbientBrightnessDayStats.CREATOR.createFromParcel(
+ parcel);
+ assertEquals(stats, statsAgain);
+ }
+}
diff --git a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
new file mode 100644
index 0000000..6e571bd
--- /dev/null
+++ b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2018 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.annotation.UserIdInt;
+import android.hardware.display.AmbientBrightnessDayStats;
+import android.os.SystemClock;
+import android.os.UserManager;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Class that stores stats of ambient brightness regions as histogram.
+ */
+public class AmbientBrightnessStatsTracker {
+
+ private static final String TAG = "AmbientBrightnessStatsTracker";
+ private static final boolean DEBUG = false;
+
+ @VisibleForTesting
+ static final float[] BUCKET_BOUNDARIES_FOR_NEW_STATS =
+ {0, 0.1f, 0.3f, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000};
+ @VisibleForTesting
+ static final int MAX_DAYS_TO_TRACK = 7;
+
+ private final AmbientBrightnessStats mAmbientBrightnessStats;
+ private final Timer mTimer;
+ private final Injector mInjector;
+ private final UserManager mUserManager;
+ private float mCurrentAmbientBrightness;
+ private @UserIdInt int mCurrentUserId;
+
+ public AmbientBrightnessStatsTracker(UserManager userManager, @Nullable Injector injector) {
+ mUserManager = userManager;
+ if (injector != null) {
+ mInjector = injector;
+ } else {
+ mInjector = new Injector();
+ }
+ mAmbientBrightnessStats = new AmbientBrightnessStats();
+ mTimer = new Timer(() -> mInjector.elapsedRealtimeMillis());
+ mCurrentAmbientBrightness = -1;
+ }
+
+ public synchronized void start() {
+ mTimer.reset();
+ mTimer.start();
+ }
+
+ public synchronized void stop() {
+ if (mTimer.isRunning()) {
+ mAmbientBrightnessStats.log(mCurrentUserId, mInjector.getLocalDate(),
+ mCurrentAmbientBrightness, mTimer.totalDurationSec());
+ }
+ mTimer.reset();
+ mCurrentAmbientBrightness = -1;
+ }
+
+ public synchronized void add(@UserIdInt int userId, float newAmbientBrightness) {
+ if (mTimer.isRunning()) {
+ if (userId == mCurrentUserId) {
+ mAmbientBrightnessStats.log(mCurrentUserId, mInjector.getLocalDate(),
+ mCurrentAmbientBrightness, mTimer.totalDurationSec());
+ } else {
+ if (DEBUG) {
+ Slog.v(TAG, "User switched since last sensor event.");
+ }
+ mCurrentUserId = userId;
+ }
+ mTimer.reset();
+ mTimer.start();
+ mCurrentAmbientBrightness = newAmbientBrightness;
+ } else {
+ if (DEBUG) {
+ Slog.e(TAG, "Timer not running while trying to add brightness stats.");
+ }
+ }
+ }
+
+ public synchronized void writeStats(OutputStream stream) throws IOException {
+ mAmbientBrightnessStats.writeToXML(stream);
+ }
+
+ public synchronized void readStats(InputStream stream) throws IOException {
+ mAmbientBrightnessStats.readFromXML(stream);
+ }
+
+ public synchronized ArrayList<AmbientBrightnessDayStats> getUserStats(int userId) {
+ return mAmbientBrightnessStats.getUserStats(userId);
+ }
+
+ public synchronized void dump(PrintWriter pw) {
+ pw.println("AmbientBrightnessStats:");
+ pw.print(mAmbientBrightnessStats);
+ }
+
+ /**
+ * AmbientBrightnessStats tracks ambient brightness stats across users over multiple days.
+ * This class is not ThreadSafe.
+ */
+ class AmbientBrightnessStats {
+
+ private static final String TAG_AMBIENT_BRIGHTNESS_STATS = "ambient-brightness-stats";
+ private static final String TAG_AMBIENT_BRIGHTNESS_DAY_STATS =
+ "ambient-brightness-day-stats";
+ private static final String ATTR_USER = "user";
+ private static final String ATTR_LOCAL_DATE = "local-date";
+ private static final String ATTR_BUCKET_BOUNDARIES = "bucket-boundaries";
+ private static final String ATTR_BUCKET_STATS = "bucket-stats";
+
+ private Map<Integer, Deque<AmbientBrightnessDayStats>> mStats;
+
+ public AmbientBrightnessStats() {
+ mStats = new HashMap<>();
+ }
+
+ public void log(@UserIdInt int userId, LocalDate localDate, float ambientBrightness,
+ float durationSec) {
+ Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats(mStats, userId);
+ AmbientBrightnessDayStats dayStats = getOrCreateDayStats(userStats, localDate);
+ dayStats.log(ambientBrightness, durationSec);
+ }
+
+ public ArrayList<AmbientBrightnessDayStats> getUserStats(@UserIdInt int userId) {
+ if (mStats.containsKey(userId)) {
+ return new ArrayList<>(mStats.get(userId));
+ } else {
+ return null;
+ }
+ }
+
+ public void writeToXML(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);
+
+ final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK);
+ out.startTag(null, TAG_AMBIENT_BRIGHTNESS_STATS);
+ for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) {
+ for (AmbientBrightnessDayStats userDayStats : entry.getValue()) {
+ int userSerialNumber = mInjector.getUserSerialNumber(mUserManager,
+ entry.getKey());
+ if (userSerialNumber != -1 && userDayStats.getLocalDate().isAfter(cutOffDate)) {
+ out.startTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS);
+ out.attribute(null, ATTR_USER, Integer.toString(userSerialNumber));
+ out.attribute(null, ATTR_LOCAL_DATE,
+ userDayStats.getLocalDate().toString());
+ StringBuilder bucketBoundariesValues = new StringBuilder();
+ StringBuilder timeSpentValues = new StringBuilder();
+ for (int i = 0; i < userDayStats.getBucketBoundaries().length; i++) {
+ if (i > 0) {
+ bucketBoundariesValues.append(",");
+ timeSpentValues.append(",");
+ }
+ bucketBoundariesValues.append(userDayStats.getBucketBoundaries()[i]);
+ timeSpentValues.append(userDayStats.getStats()[i]);
+ }
+ out.attribute(null, ATTR_BUCKET_BOUNDARIES,
+ bucketBoundariesValues.toString());
+ out.attribute(null, ATTR_BUCKET_STATS, timeSpentValues.toString());
+ out.endTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS);
+ }
+ }
+ }
+ out.endTag(null, TAG_AMBIENT_BRIGHTNESS_STATS);
+ out.endDocument();
+ stream.flush();
+ }
+
+ public void readFromXML(InputStream stream) throws IOException {
+ try {
+ Map<Integer, Deque<AmbientBrightnessDayStats>> parsedStats = new HashMap<>();
+ 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_AMBIENT_BRIGHTNESS_STATS.equals(tag)) {
+ throw new XmlPullParserException(
+ "Ambient brightness stats not found in tracker file " + tag);
+ }
+
+ final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK);
+ 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_AMBIENT_BRIGHTNESS_DAY_STATS.equals(tag)) {
+ String userSerialNumber = parser.getAttributeValue(null, ATTR_USER);
+ LocalDate localDate = LocalDate.parse(
+ parser.getAttributeValue(null, ATTR_LOCAL_DATE));
+ String[] bucketBoundaries = parser.getAttributeValue(null,
+ ATTR_BUCKET_BOUNDARIES).split(",");
+ String[] bucketStats = parser.getAttributeValue(null,
+ ATTR_BUCKET_STATS).split(",");
+ if (bucketBoundaries.length != bucketStats.length
+ || bucketBoundaries.length < 1) {
+ throw new IOException("Invalid brightness stats string.");
+ }
+ float[] parsedBucketBoundaries = new float[bucketBoundaries.length];
+ float[] parsedBucketStats = new float[bucketStats.length];
+ for (int i = 0; i < bucketBoundaries.length; i++) {
+ parsedBucketBoundaries[i] = Float.parseFloat(bucketBoundaries[i]);
+ parsedBucketStats[i] = Float.parseFloat(bucketStats[i]);
+ }
+ int userId = mInjector.getUserId(mUserManager,
+ Integer.parseInt(userSerialNumber));
+ if (userId != -1 && localDate.isAfter(cutOffDate)) {
+ Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats(
+ parsedStats, userId);
+ userStats.offer(
+ new AmbientBrightnessDayStats(localDate,
+ parsedBucketBoundaries, parsedBucketStats));
+ }
+ }
+ }
+ mStats = parsedStats;
+ } catch (NullPointerException | NumberFormatException | XmlPullParserException |
+ DateTimeParseException | IOException e) {
+ throw new IOException("Failed to parse brightness stats file.", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) {
+ for (AmbientBrightnessDayStats dayStats : entry.getValue()) {
+ builder.append(" ");
+ builder.append(entry.getKey()).append(" ");
+ builder.append(dayStats).append("\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ private Deque<AmbientBrightnessDayStats> getOrCreateUserStats(
+ Map<Integer, Deque<AmbientBrightnessDayStats>> stats, @UserIdInt int userId) {
+ if (!stats.containsKey(userId)) {
+ stats.put(userId, new ArrayDeque<>());
+ }
+ return stats.get(userId);
+ }
+
+ private AmbientBrightnessDayStats getOrCreateDayStats(
+ Deque<AmbientBrightnessDayStats> userStats, LocalDate localDate) {
+ AmbientBrightnessDayStats lastBrightnessStats = userStats.peekLast();
+ if (lastBrightnessStats != null && lastBrightnessStats.getLocalDate().equals(
+ localDate)) {
+ return lastBrightnessStats;
+ } else {
+ AmbientBrightnessDayStats dayStats = new AmbientBrightnessDayStats(localDate,
+ BUCKET_BOUNDARIES_FOR_NEW_STATS);
+ if (userStats.size() == MAX_DAYS_TO_TRACK) {
+ userStats.poll();
+ }
+ userStats.offer(dayStats);
+ return dayStats;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ interface Clock {
+ long elapsedTimeMillis();
+ }
+
+ @VisibleForTesting
+ static class Timer {
+
+ private final Clock clock;
+ private long startTimeMillis;
+ private boolean started;
+
+ public Timer(Clock clock) {
+ this.clock = clock;
+ }
+
+ public void reset() {
+ started = false;
+ }
+
+ public void start() {
+ if (!started) {
+ startTimeMillis = clock.elapsedTimeMillis();
+ started = true;
+ }
+ }
+
+ public boolean isRunning() {
+ return started;
+ }
+
+ public float totalDurationSec() {
+ if (started) {
+ return (float) ((clock.elapsedTimeMillis() - startTimeMillis) / 1000.0);
+ }
+ return 0;
+ }
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ public long elapsedRealtimeMillis() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ public int getUserSerialNumber(UserManager userManager, int userId) {
+ return userManager.getUserSerialNumber(userId);
+ }
+
+ public int getUserId(UserManager userManager, int userSerialNumber) {
+ return userManager.getUserHandle(userSerialNumber);
+ }
+
+ public LocalDate getLocalDate() {
+ return LocalDate.now();
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/BrightnessIdleJob.java b/services/core/java/com/android/server/display/BrightnessIdleJob.java
index 876acf4..b0a41cb 100644
--- a/services/core/java/com/android/server/display/BrightnessIdleJob.java
+++ b/services/core/java/com/android/server/display/BrightnessIdleJob.java
@@ -70,7 +70,7 @@
Slog.d(BrightnessTracker.TAG, "Scheduled write of brightness events");
}
DisplayManagerInternal dmi = LocalServices.getService(DisplayManagerInternal.class);
- dmi.persistBrightnessSliderEvents();
+ dmi.persistBrightnessTrackerState();
return false;
}
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index bcf8bfe..ac76fae 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -17,6 +17,7 @@
package com.android.server.display;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
@@ -28,8 +29,8 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.display.AmbientBrightnessDayStats;
import android.hardware.display.BrightnessChangeEvent;
-import android.net.Uri;
import android.os.BatteryManager;
import android.os.Environment;
import android.os.Handler;
@@ -81,6 +82,7 @@
static final boolean DEBUG = false;
private static final String EVENTS_FILE = "brightness_events.xml";
+ private static final String AMBIENT_BRIGHTNESS_STATS_FILE = "ambient_brightness_stats.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);
@@ -113,6 +115,10 @@
private final Runnable mEventsWriter = () -> writeEvents();
private volatile boolean mWriteEventsScheduled;
+ private AmbientBrightnessStatsTracker mAmbientBrightnessStatsTracker;
+ private final Runnable mAmbientBrightnessStatsWriter = () -> writeAmbientBrightnessStats();
+ private volatile boolean mWriteBrightnessStatsScheduled;
+
private UserManager mUserManager;
private final Context mContext;
private final ContentResolver mContentResolver;
@@ -120,6 +126,7 @@
// mBroadcastReceiver and mSensorListener should only be used on the mBgHandler thread.
private BroadcastReceiver mBroadcastReceiver;
private SensorListener mSensorListener;
+ private @UserIdInt int mCurrentUserId = UserHandle.USER_NULL;
// Lock held while collecting data related to brightness changes.
private final Object mDataCollectionLock = new Object();
@@ -157,12 +164,19 @@
}
mBgHandler = new TrackerHandler(mInjector.getBackgroundHandler().getLooper());
mUserManager = mContext.getSystemService(UserManager.class);
-
+ try {
+ final ActivityManager.StackInfo focusedStack = mInjector.getFocusedStack();
+ mCurrentUserId = focusedStack.userId;
+ } catch (RemoteException e) {
+ // Really shouldn't be possible.
+ return;
+ }
mBgHandler.obtainMessage(MSG_BACKGROUND_START, (Float) initialBrightness).sendToTarget();
}
private void backgroundStart(float initialBrightness) {
readEvents();
+ readAmbientBrightnessStats();
mSensorListener = new SensorListener();
@@ -196,12 +210,20 @@
mInjector.unregisterSensorListener(mContext, mSensorListener);
mInjector.unregisterReceiver(mContext, mBroadcastReceiver);
mInjector.cancelIdleJob(mContext);
+ mAmbientBrightnessStatsTracker.stop();
synchronized (mDataCollectionLock) {
mStarted = false;
}
}
+ public void onSwitchUser(@UserIdInt int newUserId) {
+ if (DEBUG) {
+ Slog.d(TAG, "Used id updated from " + mCurrentUserId + " to " + newUserId);
+ }
+ mCurrentUserId = newUserId;
+ }
+
/**
* @param userId userId to fetch data for.
* @param includePackage if false we will null out BrightnessChangeEvent.packageName
@@ -228,8 +250,8 @@
return new ParceledListSlice<>(out);
}
- public void persistEvents() {
- scheduleWriteEvents();
+ public void persistBrightnessTrackerState() {
+ scheduleWriteBrightnessTrackerState();
}
/**
@@ -321,11 +343,15 @@
}
}
- private void scheduleWriteEvents() {
+ private void scheduleWriteBrightnessTrackerState() {
if (!mWriteEventsScheduled) {
mBgHandler.post(mEventsWriter);
mWriteEventsScheduled = true;
}
+ if (!mWriteBrightnessStatsScheduled) {
+ mBgHandler.post(mAmbientBrightnessStatsWriter);
+ mWriteBrightnessStatsScheduled = true;
+ }
}
private void writeEvents() {
@@ -336,7 +362,7 @@
return;
}
- final AtomicFile writeTo = mInjector.getFile();
+ final AtomicFile writeTo = mInjector.getFile(EVENTS_FILE);
if (writeTo == null) {
return;
}
@@ -360,12 +386,29 @@
}
}
+ private void writeAmbientBrightnessStats() {
+ mWriteBrightnessStatsScheduled = false;
+ final AtomicFile writeTo = mInjector.getFile(AMBIENT_BRIGHTNESS_STATS_FILE);
+ if (writeTo == null) {
+ return;
+ }
+ FileOutputStream output = null;
+ try {
+ output = writeTo.startWrite();
+ mAmbientBrightnessStatsTracker.writeStats(output);
+ writeTo.finishWrite(output);
+ } catch (IOException e) {
+ writeTo.failWrite(output);
+ Slog.e(TAG, "Failed to write ambient brightness stats.", e);
+ }
+ }
+
private void readEvents() {
synchronized (mEventsLock) {
// Read might prune events so mark as dirty.
mEventsDirty = true;
mEvents.clear();
- final AtomicFile readFrom = mInjector.getFile();
+ final AtomicFile readFrom = mInjector.getFile(EVENTS_FILE);
if (readFrom != null && readFrom.exists()) {
FileInputStream input = null;
try {
@@ -381,6 +424,23 @@
}
}
+ private void readAmbientBrightnessStats() {
+ mAmbientBrightnessStatsTracker = new AmbientBrightnessStatsTracker(mUserManager, null);
+ final AtomicFile readFrom = mInjector.getFile(AMBIENT_BRIGHTNESS_STATS_FILE);
+ if (readFrom != null && readFrom.exists()) {
+ FileInputStream input = null;
+ try {
+ input = readFrom.openRead();
+ mAmbientBrightnessStatsTracker.readStats(input);
+ } catch (IOException e) {
+ readFrom.delete();
+ Slog.e(TAG, "Failed to read ambient brightness stats.", e);
+ } finally {
+ IoUtils.closeQuietly(input);
+ }
+ }
+ }
+
@VisibleForTesting
@GuardedBy("mEventsLock")
void writeEventsLocked(OutputStream stream) throws IOException {
@@ -545,6 +605,13 @@
pw.println("}");
}
}
+ if (mAmbientBrightnessStatsTracker != null) {
+ mAmbientBrightnessStatsTracker.dump(pw);
+ }
+ }
+
+ public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(int userId) {
+ return new ParceledListSlice<>(mAmbientBrightnessStatsTracker.getUserStats(userId));
}
// Not allowed to keep the SensorEvent so used to copy the data we care about.
@@ -584,6 +651,10 @@
}
}
+ private void recordAmbientBrightnessStats(SensorEvent event) {
+ mAmbientBrightnessStatsTracker.add(mCurrentUserId, event.values[0]);
+ }
+
private void batteryLevelChanged(int level, int scale) {
synchronized (mDataCollectionLock) {
mLastBatteryLevel = (float) level / (float) scale;
@@ -594,6 +665,7 @@
@Override
public void onSensorChanged(SensorEvent event) {
recordSensorEvent(event);
+ recordAmbientBrightnessStats(event);
}
@Override
@@ -611,7 +683,7 @@
String action = intent.getAction();
if (Intent.ACTION_SHUTDOWN.equals(action)) {
stop();
- scheduleWriteEvents();
+ scheduleWriteBrightnessTrackerState();
} else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
@@ -619,8 +691,10 @@
batteryLevelChanged(level, scale);
}
} else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ mAmbientBrightnessStatsTracker.stop();
mInjector.unregisterSensorListener(mContext, mSensorListener);
} else if (Intent.ACTION_SCREEN_ON.equals(action)) {
+ mAmbientBrightnessStatsTracker.start();
mInjector.registerSensorListener(mContext, mSensorListener,
mInjector.getBackgroundHandler());
}
@@ -679,8 +753,8 @@
return Settings.Secure.getIntForUser(resolver, setting, defaultValue, userId);
}
- public AtomicFile getFile() {
- return new AtomicFile(new File(Environment.getDataSystemDeDirectory(), EVENTS_FILE));
+ public AtomicFile getFile(String filename) {
+ return new AtomicFile(new File(Environment.getDataSystemDeDirectory(), filename));
}
public long currentTimeMillis() {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 0c2ff05..a5c1fe2 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -38,6 +38,7 @@
import android.content.res.Resources;
import android.graphics.Point;
import android.hardware.SensorManager;
+import android.hardware.display.AmbientBrightnessDayStats;
import android.hardware.display.BrightnessChangeEvent;
import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.DisplayManagerGlobal;
@@ -359,6 +360,7 @@
mPersistentDataStore.getBrightnessConfiguration(userSerial);
mDisplayPowerController.setBrightnessConfiguration(config);
}
+ mDisplayPowerController.onSwitchUser(newUserId);
}
}
@@ -1835,6 +1837,23 @@
}
@Override // Binder call
+ public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.ACCESS_AMBIENT_LIGHT_STATS,
+ "Permission required to to access ambient light stats.");
+ final int callingUid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(callingUid);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mSyncRoot) {
+ return mDisplayPowerController.getAmbientBrightnessStats(userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override // Binder call
public void setBrightnessConfigurationForUser(
BrightnessConfiguration c, @UserIdInt int userId, String packageName) {
mContext.enforceCallingOrSelfPermission(
@@ -2039,9 +2058,9 @@
}
@Override
- public void persistBrightnessSliderEvents() {
+ public void persistBrightnessTrackerState() {
synchronized (mSyncRoot) {
- mDisplayPowerController.persistBrightnessSliderEvents();
+ mDisplayPowerController.persistBrightnessTrackerState();
}
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 056c3e6..f2a7d81 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -34,6 +34,7 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.display.AmbientBrightnessDayStats;
import android.hardware.display.BrightnessChangeEvent;
import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
@@ -498,11 +499,20 @@
return mBrightnessTracker.getEvents(userId, includePackage);
}
+ public void onSwitchUser(@UserIdInt int newUserId) {
+ mBrightnessTracker.onSwitchUser(newUserId);
+ }
+
+ public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
+ @UserIdInt int userId) {
+ return mBrightnessTracker.getAmbientBrightnessStats(userId);
+ }
+
/**
- * Persist the brightness slider events to disk.
+ * Persist the brightness slider events and ambient brightness stats to disk.
*/
- public void persistBrightnessSliderEvents() {
- mBrightnessTracker.persistEvents();
+ public void persistBrightnessTrackerState() {
+ mBrightnessTracker.persistBrightnessTrackerState();
}
/**
diff --git a/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
new file mode 100644
index 0000000..8502e69
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/AmbientBrightnessStatsTrackerTest.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2018 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.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.hardware.display.AmbientBrightnessDayStats;
+import android.os.SystemClock;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+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.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientBrightnessStatsTrackerTest {
+
+ private TestInjector mTestInjector;
+
+ @Before
+ public void setUp() {
+ mTestInjector = new TestInjector();
+ }
+
+ @Test
+ public void testBrightnessStatsTrackerOverSingleDay() {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ ArrayList<AmbientBrightnessDayStats> userStats;
+ float[] expectedStats;
+ // Test case where no user data
+ userStats = statsTracker.getUserStats(0);
+ assertNull(userStats);
+ // Test after adding some user data
+ statsTracker.start();
+ statsTracker.add(0, 0);
+ mTestInjector.incrementTime(1000);
+ statsTracker.stop();
+ userStats = statsTracker.getUserStats(0);
+ assertEquals(1, userStats.size());
+ assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 1;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ // Test after adding some more user data
+ statsTracker.start();
+ statsTracker.add(0, 0.05f);
+ mTestInjector.incrementTime(1000);
+ statsTracker.add(0, 0.2f);
+ mTestInjector.incrementTime(1500);
+ statsTracker.add(0, 50000);
+ mTestInjector.incrementTime(2500);
+ statsTracker.stop();
+ userStats = statsTracker.getUserStats(0);
+ assertEquals(1, userStats.size());
+ assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 2;
+ expectedStats[1] = 1.5f;
+ expectedStats[11] = 2.5f;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ }
+
+ @Test
+ public void testBrightnessStatsTrackerOverMultipleDays() {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ ArrayList<AmbientBrightnessDayStats> userStats;
+ float[] expectedStats;
+ // Add data for day 1
+ statsTracker.start();
+ statsTracker.add(0, 0.05f);
+ mTestInjector.incrementTime(1000);
+ statsTracker.add(0, 0.2f);
+ mTestInjector.incrementTime(1500);
+ statsTracker.add(0, 1);
+ mTestInjector.incrementTime(2500);
+ statsTracker.stop();
+ // Add data for day 2
+ mTestInjector.incrementDate(1);
+ statsTracker.start();
+ statsTracker.add(0, 0);
+ mTestInjector.incrementTime(3500);
+ statsTracker.add(0, 5);
+ mTestInjector.incrementTime(5000);
+ statsTracker.stop();
+ // Test that the data is tracked as expected
+ userStats = statsTracker.getUserStats(0);
+ assertEquals(2, userStats.size());
+ assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 1;
+ expectedStats[1] = 1.5f;
+ expectedStats[3] = 2.5f;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ assertEquals(mTestInjector.getLocalDate(), userStats.get(1).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 3.5f;
+ expectedStats[4] = 5;
+ assertArrayEquals(expectedStats, userStats.get(1).getStats(), 0);
+ }
+
+ @Test
+ public void testBrightnessStatsTrackerOverMultipleUsers() {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ ArrayList<AmbientBrightnessDayStats> userStats;
+ float[] expectedStats;
+ // Add data for user 1
+ statsTracker.start();
+ statsTracker.add(0, 0.05f);
+ mTestInjector.incrementTime(1000);
+ statsTracker.add(0, 0.2f);
+ mTestInjector.incrementTime(1500);
+ statsTracker.add(0, 1);
+ mTestInjector.incrementTime(2500);
+ statsTracker.stop();
+ // Add data for user 2
+ mTestInjector.incrementDate(1);
+ statsTracker.start();
+ statsTracker.add(1, 0);
+ mTestInjector.incrementTime(3500);
+ statsTracker.add(1, 5);
+ mTestInjector.incrementTime(5000);
+ statsTracker.stop();
+ // Test that the data is tracked as expected
+ userStats = statsTracker.getUserStats(0);
+ assertEquals(1, userStats.size());
+ assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 1;
+ expectedStats[1] = 1.5f;
+ expectedStats[3] = 2.5f;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ userStats = statsTracker.getUserStats(1);
+ assertEquals(1, userStats.size());
+ assertEquals(mTestInjector.getLocalDate(), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 3.5f;
+ expectedStats[4] = 5;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ }
+
+ @Test
+ public void testBrightnessStatsTrackerOverMaxDays() {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ ArrayList<AmbientBrightnessDayStats> userStats;
+ // Add 10 extra days of data over the buffer limit
+ for (int i = 0; i < AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK + 10; i++) {
+ mTestInjector.incrementDate(1);
+ statsTracker.start();
+ statsTracker.add(0, 10);
+ mTestInjector.incrementTime(1000);
+ statsTracker.add(0, 20);
+ mTestInjector.incrementTime(1000);
+ statsTracker.stop();
+ }
+ // Assert that we are only tracking last "MAX_DAYS_TO_TRACK"
+ userStats = statsTracker.getUserStats(0);
+ assertEquals(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK, userStats.size());
+ LocalDate runningDate = mTestInjector.getLocalDate();
+ for (int i = AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK - 1; i >= 0; i--) {
+ assertEquals(runningDate, userStats.get(i).getLocalDate());
+ runningDate = runningDate.minusDays(1);
+ }
+ }
+
+ @Test
+ public void testReadAmbientBrightnessStats() throws IOException {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ LocalDate date = mTestInjector.getLocalDate();
+ ArrayList<AmbientBrightnessDayStats> userStats;
+ String statsFile =
+ "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+ + "<ambient-brightness-stats>\r\n"
+ // Old stats that shouldn't be read
+ + "<ambient-brightness-day-stats user=\"10\" local-date=\""
+ + date.minusDays(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK)
+ + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+ + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+ + "0.0,0.0,0.0\" />\r\n"
+ // Valid stats that should get read
+ + "<ambient-brightness-day-stats user=\"10\" local-date=\""
+ + date.minusDays(1)
+ + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+ + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+ + "0.0,0.0,0.0\" />\r\n"
+ // Valid stats that should get read
+ + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
+ + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+ + "3000.0,10000.0\" bucket-stats=\"0.0,0.0,0.0,0.0,4.482,0.0,0.0,0.0,0.0,"
+ + "0.0\" />\r\n"
+ + "</ambient-brightness-stats>";
+ statsTracker.readStats(getInputStream(statsFile));
+ userStats = statsTracker.getUserStats(0);
+ assertEquals(2, userStats.size());
+ assertEquals(new AmbientBrightnessDayStats(date.minusDays(1),
+ new float[]{0, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000},
+ new float[]{1.088f, 0, 0.726f, 0, 25.868f, 0, 0, 0, 0, 0}), userStats.get(0));
+ assertEquals(new AmbientBrightnessDayStats(date,
+ new float[]{0, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000},
+ new float[]{0, 0, 0, 0, 4.482f, 0, 0, 0, 0, 0}), userStats.get(1));
+ }
+
+ @Test
+ public void testFailedReadAmbientBrightnessStatsWithException() {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ LocalDate date = mTestInjector.getLocalDate();
+ String statsFile;
+ // Test with parse error
+ statsFile =
+ "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+ + "<ambient-brightness-stats>\r\n"
+ // Incorrect since bucket boundaries not parsable
+ + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
+ + "\" bucket-boundaries=\"asdf,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+ + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+ + "0.0,0.0,0.0\" />\r\n"
+ + "</ambient-brightness-stats>";
+ try {
+ statsTracker.readStats(getInputStream(statsFile));
+ } catch (IOException e) {
+ // Expected
+ }
+ assertNull(statsTracker.getUserStats(0));
+ // Test with incorrect data (bucket boundaries length not equal to stats length)
+ statsFile =
+ "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+ + "<ambient-brightness-stats>\r\n"
+ // Correct data
+ + "<ambient-brightness-day-stats user=\"10\" local-date=\""
+ + date.minusDays(1)
+ + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+ + "3000.0,10000.0\" bucket-stats=\"0.0,0.0,0.0,0.0,4.482,0.0,0.0,0.0,0.0,"
+ + "0.0\" />\r\n"
+ // Incorrect data
+ + "<ambient-brightness-day-stats user=\"10\" local-date=\"" + date
+ + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,1000.0,"
+ + "3000.0,10000.0\" bucket-stats=\"1.088,0.0,0.726,0.0,25.868,0.0,0.0,"
+ + "0.0,0.0,0.0\" />\r\n"
+ + "</ambient-brightness-stats>";
+ try {
+ statsTracker.readStats(getInputStream(statsFile));
+ } catch (Exception e) {
+ // Expected
+ }
+ assertNull(statsTracker.getUserStats(0));
+ // Test with missing attribute
+ statsFile =
+ "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\r\n"
+ + "<ambient-brightness-stats>\r\n"
+ + "<ambientBrightnessDayStats user=\"10\" local-date=\"" + date
+ + "\" bucket-boundaries=\"0.0,1.0,3.0,10.0,30.0,100.0,300.0,1000.0,"
+ + "3000.0,10000.0\" />\r\n"
+ + "</ambient-brightness-stats>";
+ try {
+ statsTracker.readStats(getInputStream(statsFile));
+ } catch (Exception e) {
+ // Expected
+ }
+ assertNull(statsTracker.getUserStats(0));
+ }
+
+ @Test
+ public void testWriteThenReadAmbientBrightnessStats() throws IOException {
+ AmbientBrightnessStatsTracker statsTracker = getTestStatsTracker();
+ ArrayList<AmbientBrightnessDayStats> userStats;
+ float[] expectedStats;
+ // Generate some dummy data
+ // Data: very old which should not be read
+ statsTracker.start();
+ statsTracker.add(0, 0.05f);
+ mTestInjector.incrementTime(1000);
+ statsTracker.add(0, 0.2f);
+ mTestInjector.incrementTime(1500);
+ statsTracker.add(0, 1);
+ mTestInjector.incrementTime(2500);
+ statsTracker.stop();
+ // Data: day 1 user 1
+ mTestInjector.incrementDate(AmbientBrightnessStatsTracker.MAX_DAYS_TO_TRACK - 1);
+ statsTracker.start();
+ statsTracker.add(0, 0.05f);
+ mTestInjector.incrementTime(1000);
+ statsTracker.add(0, 0.2f);
+ mTestInjector.incrementTime(1500);
+ statsTracker.add(0, 1);
+ mTestInjector.incrementTime(2500);
+ statsTracker.stop();
+ // Data: day 1 user 2
+ statsTracker.start();
+ statsTracker.add(1, 0);
+ mTestInjector.incrementTime(3500);
+ statsTracker.add(1, 5);
+ mTestInjector.incrementTime(5000);
+ statsTracker.stop();
+ // Data: day 2 user 1
+ mTestInjector.incrementDate(1);
+ statsTracker.start();
+ statsTracker.add(0, 0);
+ mTestInjector.incrementTime(3500);
+ statsTracker.add(0, 50000);
+ mTestInjector.incrementTime(5000);
+ statsTracker.stop();
+ // Write them
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ statsTracker.writeStats(baos);
+ baos.flush();
+ // Read them back and assert that it's the same
+ ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
+ AmbientBrightnessStatsTracker newStatsTracker = getTestStatsTracker();
+ newStatsTracker.readStats(input);
+ userStats = newStatsTracker.getUserStats(0);
+ assertEquals(2, userStats.size());
+ // Check day 1 user 1
+ assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 1;
+ expectedStats[1] = 1.5f;
+ expectedStats[3] = 2.5f;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ // Check day 2 user 1
+ assertEquals(mTestInjector.getLocalDate(), userStats.get(1).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 3.5f;
+ expectedStats[11] = 5;
+ assertArrayEquals(expectedStats, userStats.get(1).getStats(), 0);
+ userStats = newStatsTracker.getUserStats(1);
+ assertEquals(1, userStats.size());
+ // Check day 1 user 2
+ assertEquals(mTestInjector.getLocalDate().minusDays(1), userStats.get(0).getLocalDate());
+ expectedStats = getEmptyStatsArray();
+ expectedStats[0] = 3.5f;
+ expectedStats[4] = 5;
+ assertArrayEquals(expectedStats, userStats.get(0).getStats(), 0);
+ }
+
+ @Test
+ public void testTimer() {
+ AmbientBrightnessStatsTracker.Timer timer = new AmbientBrightnessStatsTracker.Timer(
+ () -> mTestInjector.elapsedRealtimeMillis());
+ assertEquals(0, timer.totalDurationSec(), 0);
+ mTestInjector.incrementTime(1000);
+ assertEquals(0, timer.totalDurationSec(), 0);
+ assertFalse(timer.isRunning());
+ // Start timer
+ timer.start();
+ assertTrue(timer.isRunning());
+ assertEquals(0, timer.totalDurationSec(), 0);
+ mTestInjector.incrementTime(1000);
+ assertTrue(timer.isRunning());
+ assertEquals(1, timer.totalDurationSec(), 0);
+ // Reset timer
+ timer.reset();
+ assertEquals(0, timer.totalDurationSec(), 0);
+ assertFalse(timer.isRunning());
+ // Start again
+ timer.start();
+ assertTrue(timer.isRunning());
+ assertEquals(0, timer.totalDurationSec(), 0);
+ mTestInjector.incrementTime(2000);
+ assertTrue(timer.isRunning());
+ assertEquals(2, timer.totalDurationSec(), 0);
+ // Reset again
+ timer.reset();
+ assertEquals(0, timer.totalDurationSec(), 0);
+ assertFalse(timer.isRunning());
+ }
+
+ private class TestInjector extends AmbientBrightnessStatsTracker.Injector {
+
+ private long mElapsedRealtimeMillis = SystemClock.elapsedRealtime();
+ private LocalDate mLocalDate = LocalDate.now();
+
+ public void incrementTime(long timeMillis) {
+ mElapsedRealtimeMillis += timeMillis;
+ }
+
+ public void incrementDate(int numDays) {
+ mLocalDate = mLocalDate.plusDays(numDays);
+ }
+
+ @Override
+ public long elapsedRealtimeMillis() {
+ return mElapsedRealtimeMillis;
+ }
+
+ @Override
+ public int getUserSerialNumber(UserManager userManager, int userId) {
+ return userId + 10;
+ }
+
+ @Override
+ public int getUserId(UserManager userManager, int userSerialNumber) {
+ return userSerialNumber - 10;
+ }
+
+ @Override
+ public LocalDate getLocalDate() {
+ return LocalDate.from(mLocalDate);
+ }
+ }
+
+ private AmbientBrightnessStatsTracker getTestStatsTracker() {
+ return new AmbientBrightnessStatsTracker(
+ InstrumentationRegistry.getContext().getSystemService(UserManager.class),
+ mTestInjector);
+ }
+
+ private float[] getEmptyStatsArray() {
+ return new float[AmbientBrightnessStatsTracker.BUCKET_BOUNDARIES_FOR_NEW_STATS.length];
+ }
+
+ private InputStream getInputStream(String data) {
+ return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
index edc7d74..8277184 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
@@ -639,7 +639,7 @@
}
@Override
- public AtomicFile getFile() {
+ public AtomicFile getFile(String filename) {
// Don't have the test write / read from anywhere.
return null;
}