1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.display; 18 19 import android.annotation.Nullable; 20 import android.annotation.UserIdInt; 21 import android.hardware.display.AmbientBrightnessDayStats; 22 import android.os.SystemClock; 23 import android.os.UserManager; 24 import android.util.Slog; 25 import android.util.Xml; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.internal.util.FastXmlSerializer; 29 30 import org.xmlpull.v1.XmlPullParser; 31 import org.xmlpull.v1.XmlPullParserException; 32 import org.xmlpull.v1.XmlSerializer; 33 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.OutputStream; 37 import java.io.PrintWriter; 38 import java.nio.charset.StandardCharsets; 39 import java.time.LocalDate; 40 import java.time.format.DateTimeParseException; 41 import java.util.ArrayDeque; 42 import java.util.ArrayList; 43 import java.util.Deque; 44 import java.util.HashMap; 45 import java.util.Map; 46 47 /** 48 * Class that stores stats of ambient brightness regions as histogram. 49 */ 50 public class AmbientBrightnessStatsTracker { 51 52 private static final String TAG = "AmbientBrightnessStatsTracker"; 53 private static final boolean DEBUG = false; 54 55 @VisibleForTesting 56 static final float[] BUCKET_BOUNDARIES_FOR_NEW_STATS = 57 {0, 0.1f, 0.3f, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000}; 58 @VisibleForTesting 59 static final int MAX_DAYS_TO_TRACK = 7; 60 61 private final AmbientBrightnessStats mAmbientBrightnessStats; 62 private final Timer mTimer; 63 private final Injector mInjector; 64 private final UserManager mUserManager; 65 private float mCurrentAmbientBrightness; 66 private @UserIdInt int mCurrentUserId; 67 AmbientBrightnessStatsTracker(UserManager userManager, @Nullable Injector injector)68 public AmbientBrightnessStatsTracker(UserManager userManager, @Nullable Injector injector) { 69 mUserManager = userManager; 70 if (injector != null) { 71 mInjector = injector; 72 } else { 73 mInjector = new Injector(); 74 } 75 mAmbientBrightnessStats = new AmbientBrightnessStats(); 76 mTimer = new Timer(() -> mInjector.elapsedRealtimeMillis()); 77 mCurrentAmbientBrightness = -1; 78 } 79 start()80 public synchronized void start() { 81 mTimer.reset(); 82 mTimer.start(); 83 } 84 stop()85 public synchronized void stop() { 86 if (mTimer.isRunning()) { 87 mAmbientBrightnessStats.log(mCurrentUserId, mInjector.getLocalDate(), 88 mCurrentAmbientBrightness, mTimer.totalDurationSec()); 89 } 90 mTimer.reset(); 91 mCurrentAmbientBrightness = -1; 92 } 93 add(@serIdInt int userId, float newAmbientBrightness)94 public synchronized void add(@UserIdInt int userId, float newAmbientBrightness) { 95 if (mTimer.isRunning()) { 96 if (userId == mCurrentUserId) { 97 mAmbientBrightnessStats.log(mCurrentUserId, mInjector.getLocalDate(), 98 mCurrentAmbientBrightness, mTimer.totalDurationSec()); 99 } else { 100 if (DEBUG) { 101 Slog.v(TAG, "User switched since last sensor event."); 102 } 103 mCurrentUserId = userId; 104 } 105 mTimer.reset(); 106 mTimer.start(); 107 mCurrentAmbientBrightness = newAmbientBrightness; 108 } else { 109 if (DEBUG) { 110 Slog.e(TAG, "Timer not running while trying to add brightness stats."); 111 } 112 } 113 } 114 writeStats(OutputStream stream)115 public synchronized void writeStats(OutputStream stream) throws IOException { 116 mAmbientBrightnessStats.writeToXML(stream); 117 } 118 readStats(InputStream stream)119 public synchronized void readStats(InputStream stream) throws IOException { 120 mAmbientBrightnessStats.readFromXML(stream); 121 } 122 getUserStats(int userId)123 public synchronized ArrayList<AmbientBrightnessDayStats> getUserStats(int userId) { 124 return mAmbientBrightnessStats.getUserStats(userId); 125 } 126 dump(PrintWriter pw)127 public synchronized void dump(PrintWriter pw) { 128 pw.println("AmbientBrightnessStats:"); 129 pw.print(mAmbientBrightnessStats); 130 } 131 132 /** 133 * AmbientBrightnessStats tracks ambient brightness stats across users over multiple days. 134 * This class is not ThreadSafe. 135 */ 136 class AmbientBrightnessStats { 137 138 private static final String TAG_AMBIENT_BRIGHTNESS_STATS = "ambient-brightness-stats"; 139 private static final String TAG_AMBIENT_BRIGHTNESS_DAY_STATS = 140 "ambient-brightness-day-stats"; 141 private static final String ATTR_USER = "user"; 142 private static final String ATTR_LOCAL_DATE = "local-date"; 143 private static final String ATTR_BUCKET_BOUNDARIES = "bucket-boundaries"; 144 private static final String ATTR_BUCKET_STATS = "bucket-stats"; 145 146 private Map<Integer, Deque<AmbientBrightnessDayStats>> mStats; 147 AmbientBrightnessStats()148 public AmbientBrightnessStats() { 149 mStats = new HashMap<>(); 150 } 151 log(@serIdInt int userId, LocalDate localDate, float ambientBrightness, float durationSec)152 public void log(@UserIdInt int userId, LocalDate localDate, float ambientBrightness, 153 float durationSec) { 154 Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats(mStats, userId); 155 AmbientBrightnessDayStats dayStats = getOrCreateDayStats(userStats, localDate); 156 dayStats.log(ambientBrightness, durationSec); 157 } 158 getUserStats(@serIdInt int userId)159 public ArrayList<AmbientBrightnessDayStats> getUserStats(@UserIdInt int userId) { 160 if (mStats.containsKey(userId)) { 161 return new ArrayList<>(mStats.get(userId)); 162 } else { 163 return null; 164 } 165 } 166 writeToXML(OutputStream stream)167 public void writeToXML(OutputStream stream) throws IOException { 168 XmlSerializer out = new FastXmlSerializer(); 169 out.setOutput(stream, StandardCharsets.UTF_8.name()); 170 out.startDocument(null, true); 171 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 172 173 final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK); 174 out.startTag(null, TAG_AMBIENT_BRIGHTNESS_STATS); 175 for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) { 176 for (AmbientBrightnessDayStats userDayStats : entry.getValue()) { 177 int userSerialNumber = mInjector.getUserSerialNumber(mUserManager, 178 entry.getKey()); 179 if (userSerialNumber != -1 && userDayStats.getLocalDate().isAfter(cutOffDate)) { 180 out.startTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS); 181 out.attribute(null, ATTR_USER, Integer.toString(userSerialNumber)); 182 out.attribute(null, ATTR_LOCAL_DATE, 183 userDayStats.getLocalDate().toString()); 184 StringBuilder bucketBoundariesValues = new StringBuilder(); 185 StringBuilder timeSpentValues = new StringBuilder(); 186 for (int i = 0; i < userDayStats.getBucketBoundaries().length; i++) { 187 if (i > 0) { 188 bucketBoundariesValues.append(","); 189 timeSpentValues.append(","); 190 } 191 bucketBoundariesValues.append(userDayStats.getBucketBoundaries()[i]); 192 timeSpentValues.append(userDayStats.getStats()[i]); 193 } 194 out.attribute(null, ATTR_BUCKET_BOUNDARIES, 195 bucketBoundariesValues.toString()); 196 out.attribute(null, ATTR_BUCKET_STATS, timeSpentValues.toString()); 197 out.endTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS); 198 } 199 } 200 } 201 out.endTag(null, TAG_AMBIENT_BRIGHTNESS_STATS); 202 out.endDocument(); 203 stream.flush(); 204 } 205 readFromXML(InputStream stream)206 public void readFromXML(InputStream stream) throws IOException { 207 try { 208 Map<Integer, Deque<AmbientBrightnessDayStats>> parsedStats = new HashMap<>(); 209 XmlPullParser parser = Xml.newPullParser(); 210 parser.setInput(stream, StandardCharsets.UTF_8.name()); 211 212 int type; 213 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 214 && type != XmlPullParser.START_TAG) { 215 } 216 String tag = parser.getName(); 217 if (!TAG_AMBIENT_BRIGHTNESS_STATS.equals(tag)) { 218 throw new XmlPullParserException( 219 "Ambient brightness stats not found in tracker file " + tag); 220 } 221 222 final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK); 223 parser.next(); 224 int outerDepth = parser.getDepth(); 225 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 226 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 227 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 228 continue; 229 } 230 tag = parser.getName(); 231 if (TAG_AMBIENT_BRIGHTNESS_DAY_STATS.equals(tag)) { 232 String userSerialNumber = parser.getAttributeValue(null, ATTR_USER); 233 LocalDate localDate = LocalDate.parse( 234 parser.getAttributeValue(null, ATTR_LOCAL_DATE)); 235 String[] bucketBoundaries = parser.getAttributeValue(null, 236 ATTR_BUCKET_BOUNDARIES).split(","); 237 String[] bucketStats = parser.getAttributeValue(null, 238 ATTR_BUCKET_STATS).split(","); 239 if (bucketBoundaries.length != bucketStats.length 240 || bucketBoundaries.length < 1) { 241 throw new IOException("Invalid brightness stats string."); 242 } 243 float[] parsedBucketBoundaries = new float[bucketBoundaries.length]; 244 float[] parsedBucketStats = new float[bucketStats.length]; 245 for (int i = 0; i < bucketBoundaries.length; i++) { 246 parsedBucketBoundaries[i] = Float.parseFloat(bucketBoundaries[i]); 247 parsedBucketStats[i] = Float.parseFloat(bucketStats[i]); 248 } 249 int userId = mInjector.getUserId(mUserManager, 250 Integer.parseInt(userSerialNumber)); 251 if (userId != -1 && localDate.isAfter(cutOffDate)) { 252 Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats( 253 parsedStats, userId); 254 userStats.offer( 255 new AmbientBrightnessDayStats(localDate, 256 parsedBucketBoundaries, parsedBucketStats)); 257 } 258 } 259 } 260 mStats = parsedStats; 261 } catch (NullPointerException | NumberFormatException | XmlPullParserException | 262 DateTimeParseException | IOException e) { 263 throw new IOException("Failed to parse brightness stats file.", e); 264 } 265 } 266 267 @Override toString()268 public String toString() { 269 StringBuilder builder = new StringBuilder(); 270 for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) { 271 for (AmbientBrightnessDayStats dayStats : entry.getValue()) { 272 builder.append(" "); 273 builder.append(entry.getKey()).append(" "); 274 builder.append(dayStats).append("\n"); 275 } 276 } 277 return builder.toString(); 278 } 279 getOrCreateUserStats( Map<Integer, Deque<AmbientBrightnessDayStats>> stats, @UserIdInt int userId)280 private Deque<AmbientBrightnessDayStats> getOrCreateUserStats( 281 Map<Integer, Deque<AmbientBrightnessDayStats>> stats, @UserIdInt int userId) { 282 if (!stats.containsKey(userId)) { 283 stats.put(userId, new ArrayDeque<>()); 284 } 285 return stats.get(userId); 286 } 287 getOrCreateDayStats( Deque<AmbientBrightnessDayStats> userStats, LocalDate localDate)288 private AmbientBrightnessDayStats getOrCreateDayStats( 289 Deque<AmbientBrightnessDayStats> userStats, LocalDate localDate) { 290 AmbientBrightnessDayStats lastBrightnessStats = userStats.peekLast(); 291 if (lastBrightnessStats != null && lastBrightnessStats.getLocalDate().equals( 292 localDate)) { 293 return lastBrightnessStats; 294 } else { 295 AmbientBrightnessDayStats dayStats = new AmbientBrightnessDayStats(localDate, 296 BUCKET_BOUNDARIES_FOR_NEW_STATS); 297 if (userStats.size() == MAX_DAYS_TO_TRACK) { 298 userStats.poll(); 299 } 300 userStats.offer(dayStats); 301 return dayStats; 302 } 303 } 304 } 305 306 @VisibleForTesting 307 interface Clock { elapsedTimeMillis()308 long elapsedTimeMillis(); 309 } 310 311 @VisibleForTesting 312 static class Timer { 313 314 private final Clock clock; 315 private long startTimeMillis; 316 private boolean started; 317 Timer(Clock clock)318 public Timer(Clock clock) { 319 this.clock = clock; 320 } 321 reset()322 public void reset() { 323 started = false; 324 } 325 start()326 public void start() { 327 if (!started) { 328 startTimeMillis = clock.elapsedTimeMillis(); 329 started = true; 330 } 331 } 332 isRunning()333 public boolean isRunning() { 334 return started; 335 } 336 totalDurationSec()337 public float totalDurationSec() { 338 if (started) { 339 return (float) ((clock.elapsedTimeMillis() - startTimeMillis) / 1000.0); 340 } 341 return 0; 342 } 343 } 344 345 @VisibleForTesting 346 static class Injector { elapsedRealtimeMillis()347 public long elapsedRealtimeMillis() { 348 return SystemClock.elapsedRealtime(); 349 } 350 getUserSerialNumber(UserManager userManager, int userId)351 public int getUserSerialNumber(UserManager userManager, int userId) { 352 return userManager.getUserSerialNumber(userId); 353 } 354 getUserId(UserManager userManager, int userSerialNumber)355 public int getUserId(UserManager userManager, int userSerialNumber) { 356 return userManager.getUserHandle(userSerialNumber); 357 } 358 getLocalDate()359 public LocalDate getLocalDate() { 360 return LocalDate.now(); 361 } 362 } 363 }