Merge "Add Ambient Brightness tracker API"

This commit is contained in:
TreeHugger Robot
2018-02-01 12:15:15 +00:00
committed by Android (Google) Code Review
15 changed files with 1281 additions and 19 deletions

View File

@@ -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;

View File

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

View File

@@ -630,6 +630,16 @@ public final class DisplayManager {
return mGlobal.getBrightnessEvents(mContext.getOpPackageName());
}
/**
* 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.
*

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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.

View File

@@ -3029,6 +3029,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 -->

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

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

View File

@@ -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);
@@ -116,6 +118,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;
@@ -123,6 +129,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();
@@ -160,12 +167,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();
@@ -199,12 +213,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
@@ -231,8 +253,8 @@ public class BrightnessTracker {
return new ParceledListSlice<>(out);
}
public void persistEvents() {
scheduleWriteEvents();
public void persistBrightnessTrackerState() {
scheduleWriteBrightnessTrackerState();
}
/**
@@ -332,11 +354,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() {
@@ -347,7 +373,7 @@ public class BrightnessTracker {
return;
}
final AtomicFile writeTo = mInjector.getFile();
final AtomicFile writeTo = mInjector.getFile(EVENTS_FILE);
if (writeTo == null) {
return;
}
@@ -371,12 +397,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 {
@@ -392,6 +435,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 {
@@ -581,6 +641,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.
@@ -620,6 +687,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;
@@ -630,6 +701,7 @@ public class BrightnessTracker {
@Override
public void onSensorChanged(SensorEvent event) {
recordSensorEvent(event);
recordAmbientBrightnessStats(event);
}
@Override
@@ -647,7 +719,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);
@@ -655,8 +727,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());
}
@@ -732,8 +806,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() {

View File

@@ -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);
}
}
@@ -1834,6 +1836,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) {
@@ -2039,9 +2058,9 @@ public final class DisplayManagerService extends SystemService {
}
@Override
public void persistBrightnessSliderEvents() {
public void persistBrightnessTrackerState() {
synchronized (mSyncRoot) {
mDisplayPowerController.persistBrightnessSliderEvents();
mDisplayPowerController.persistBrightnessTrackerState();
}
}

View File

@@ -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();
}
/**

View File

@@ -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));
}
}

View File

@@ -656,7 +656,7 @@ public class BrightnessTrackerTest {
}
@Override
public AtomicFile getFile() {
public AtomicFile getFile(String filename) {
// Don't have the test write / read from anywhere.
return null;
}