blob: 6e571bd75946dc2f8fe0b9d57c68c70c1f4dab52 [file] [log] [blame]
/*
* 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();
}
}
}