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