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