diff options
15 files changed, 1281 insertions, 19 deletions
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl b/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl new file mode 100644 index 000000000000..9070777bab63 --- /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 000000000000..41be397cabc6 --- /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 4de4880b7c17..22fb8e75289a 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -631,6 +631,16 @@ public final class DisplayManager { } /** + * 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 2d5f5e041486..d7f7c865b8fb 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -525,6 +525,21 @@ public final class DisplayManagerGlobal { } } + /** + * 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 1cfad4f0168f..f468942cc951 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -174,9 +174,9 @@ public abstract class DisplayManagerInternal { 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 13599cfa0b7d..0571ae1fe825 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -87,6 +87,9 @@ interface IDisplayManager { // 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 b23a64b9f38f..4598b388cd65 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 000000000000..84409d48a4dc --- /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 000000000000..6e571bd75946 --- /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 876acf45fda8..b0a41cb589eb 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 @@ public class BrightnessIdleJob extends JobService { 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 bcf8bfe6aaad..ac76faef3cc0 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.Sensor; 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { // 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 @@ public class BrightnessTracker { } 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { return new ParceledListSlice<>(out); } - public void persistEvents() { - scheduleWriteEvents(); + public void persistBrightnessTrackerState() { + scheduleWriteBrightnessTrackerState(); } /** @@ -321,11 +343,15 @@ public class BrightnessTracker { } } - 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 @@ public class BrightnessTracker { return; } - final AtomicFile writeTo = mInjector.getFile(); + final AtomicFile writeTo = mInjector.getFile(EVENTS_FILE); if (writeTo == null) { return; } @@ -360,12 +386,29 @@ public class BrightnessTracker { } } + 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 @@ public class BrightnessTracker { } } + 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { } } + 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 @@ public class BrightnessTracker { @Override public void onSensorChanged(SensorEvent event) { recordSensorEvent(event); + recordAmbientBrightnessStats(event); } @Override @@ -611,7 +683,7 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { 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 0c2ff0519615..a5c1fe299e4e 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.pm.ParceledListSlice; 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 @@ public final class DisplayManagerService extends SystemService { mPersistentDataStore.getBrightnessConfiguration(userSerial); mDisplayPowerController.setBrightnessConfiguration(config); } + mDisplayPowerController.onSwitchUser(newUserId); } } @@ -1835,6 +1837,23 @@ public final class DisplayManagerService extends SystemService { } @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 @@ public final class DisplayManagerService extends SystemService { } @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 056c3e641c4a..f2a7d8171b63 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.Sensor; 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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call 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 000000000000..8502e69dd060 --- /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 edc7d74b47cb..82771843e6e7 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 @@ public class BrightnessTrackerTest { } @Override - public AtomicFile getFile() { + public AtomicFile getFile(String filename) { // Don't have the test write / read from anywhere. return null; } |