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.FrameworkStatsLog; 29 import com.android.modules.utils.TypedXmlPullParser; 30 import com.android.modules.utils.TypedXmlSerializer; 31 32 import org.xmlpull.v1.XmlPullParser; 33 import org.xmlpull.v1.XmlPullParserException; 34 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.io.PrintWriter; 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 TypedXmlSerializer out = Xml.resolveSerializer(stream); 169 out.startDocument(null, true); 170 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 171 172 final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK); 173 out.startTag(null, TAG_AMBIENT_BRIGHTNESS_STATS); 174 for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) { 175 for (AmbientBrightnessDayStats userDayStats : entry.getValue()) { 176 int userSerialNumber = mInjector.getUserSerialNumber(mUserManager, 177 entry.getKey()); 178 if (userSerialNumber != -1 && userDayStats.getLocalDate().isAfter(cutOffDate)) { 179 out.startTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS); 180 out.attributeInt(null, ATTR_USER, userSerialNumber); 181 out.attribute(null, ATTR_LOCAL_DATE, 182 userDayStats.getLocalDate().toString()); 183 StringBuilder bucketBoundariesValues = new StringBuilder(); 184 StringBuilder timeSpentValues = new StringBuilder(); 185 for (int i = 0; i < userDayStats.getBucketBoundaries().length; i++) { 186 if (i > 0) { 187 bucketBoundariesValues.append(","); 188 timeSpentValues.append(","); 189 } 190 bucketBoundariesValues.append(userDayStats.getBucketBoundaries()[i]); 191 timeSpentValues.append(userDayStats.getStats()[i]); 192 } 193 out.attribute(null, ATTR_BUCKET_BOUNDARIES, 194 bucketBoundariesValues.toString()); 195 out.attribute(null, ATTR_BUCKET_STATS, timeSpentValues.toString()); 196 out.endTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS); 197 } 198 } 199 } 200 out.endTag(null, TAG_AMBIENT_BRIGHTNESS_STATS); 201 out.endDocument(); 202 stream.flush(); 203 } 204 readFromXML(InputStream stream)205 public void readFromXML(InputStream stream) throws IOException { 206 try { 207 Map<Integer, Deque<AmbientBrightnessDayStats>> parsedStats = new HashMap<>(); 208 TypedXmlPullParser parser = Xml.resolvePullParser(stream); 209 210 int type; 211 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 212 && type != XmlPullParser.START_TAG) { 213 } 214 String tag = parser.getName(); 215 if (!TAG_AMBIENT_BRIGHTNESS_STATS.equals(tag)) { 216 throw new XmlPullParserException( 217 "Ambient brightness stats not found in tracker file " + tag); 218 } 219 220 final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK); 221 int outerDepth = parser.getDepth(); 222 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 223 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 224 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 225 continue; 226 } 227 tag = parser.getName(); 228 if (TAG_AMBIENT_BRIGHTNESS_DAY_STATS.equals(tag)) { 229 int userSerialNumber = parser.getAttributeInt(null, ATTR_USER); 230 LocalDate localDate = LocalDate.parse( 231 parser.getAttributeValue(null, ATTR_LOCAL_DATE)); 232 String[] bucketBoundaries = parser.getAttributeValue(null, 233 ATTR_BUCKET_BOUNDARIES).split(","); 234 String[] bucketStats = parser.getAttributeValue(null, 235 ATTR_BUCKET_STATS).split(","); 236 if (bucketBoundaries.length != bucketStats.length 237 || bucketBoundaries.length < 1) { 238 throw new IOException("Invalid brightness stats string."); 239 } 240 float[] parsedBucketBoundaries = new float[bucketBoundaries.length]; 241 float[] parsedBucketStats = new float[bucketStats.length]; 242 for (int i = 0; i < bucketBoundaries.length; i++) { 243 parsedBucketBoundaries[i] = Float.parseFloat(bucketBoundaries[i]); 244 parsedBucketStats[i] = Float.parseFloat(bucketStats[i]); 245 } 246 int userId = mInjector.getUserId(mUserManager, userSerialNumber); 247 if (userId != -1 && localDate.isAfter(cutOffDate)) { 248 Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats( 249 parsedStats, userId); 250 userStats.offer( 251 new AmbientBrightnessDayStats(localDate, 252 parsedBucketBoundaries, parsedBucketStats)); 253 } 254 } 255 } 256 mStats = parsedStats; 257 } catch (NullPointerException | NumberFormatException | XmlPullParserException | 258 DateTimeParseException | IOException e) { 259 throw new IOException("Failed to parse brightness stats file.", e); 260 } 261 } 262 263 @Override toString()264 public String toString() { 265 StringBuilder builder = new StringBuilder(); 266 for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) { 267 for (AmbientBrightnessDayStats dayStats : entry.getValue()) { 268 builder.append(" "); 269 builder.append(entry.getKey()).append(" "); 270 builder.append(dayStats).append("\n"); 271 } 272 } 273 return builder.toString(); 274 } 275 getOrCreateUserStats( Map<Integer, Deque<AmbientBrightnessDayStats>> stats, @UserIdInt int userId)276 private Deque<AmbientBrightnessDayStats> getOrCreateUserStats( 277 Map<Integer, Deque<AmbientBrightnessDayStats>> stats, @UserIdInt int userId) { 278 if (!stats.containsKey(userId)) { 279 stats.put(userId, new ArrayDeque<>()); 280 } 281 return stats.get(userId); 282 } 283 getOrCreateDayStats( Deque<AmbientBrightnessDayStats> userStats, LocalDate localDate)284 private AmbientBrightnessDayStats getOrCreateDayStats( 285 Deque<AmbientBrightnessDayStats> userStats, LocalDate localDate) { 286 AmbientBrightnessDayStats lastBrightnessStats = userStats.peekLast(); 287 if (lastBrightnessStats != null && lastBrightnessStats.getLocalDate().equals( 288 localDate)) { 289 return lastBrightnessStats; 290 } else { 291 // It is a new day, and we have available data, so log data. The daily boundary 292 // might not be right if the user changes timezones but that is fine, since it 293 // won't be that frequent. 294 if (lastBrightnessStats != null) { 295 FrameworkStatsLog.write(FrameworkStatsLog.AMBIENT_BRIGHTNESS_STATS_REPORTED, 296 lastBrightnessStats.getStats(), 297 lastBrightnessStats.getBucketBoundaries()); 298 } 299 AmbientBrightnessDayStats dayStats = new AmbientBrightnessDayStats(localDate, 300 BUCKET_BOUNDARIES_FOR_NEW_STATS); 301 if (userStats.size() == MAX_DAYS_TO_TRACK) { 302 userStats.poll(); 303 } 304 userStats.offer(dayStats); 305 return dayStats; 306 } 307 } 308 } 309 310 @VisibleForTesting 311 interface Clock { elapsedTimeMillis()312 long elapsedTimeMillis(); 313 } 314 315 @VisibleForTesting 316 static class Timer { 317 318 private final Clock clock; 319 private long startTimeMillis; 320 private boolean started; 321 Timer(Clock clock)322 public Timer(Clock clock) { 323 this.clock = clock; 324 } 325 reset()326 public void reset() { 327 started = false; 328 } 329 start()330 public void start() { 331 if (!started) { 332 startTimeMillis = clock.elapsedTimeMillis(); 333 started = true; 334 } 335 } 336 isRunning()337 public boolean isRunning() { 338 return started; 339 } 340 totalDurationSec()341 public float totalDurationSec() { 342 if (started) { 343 return (float) ((clock.elapsedTimeMillis() - startTimeMillis) / 1000.0); 344 } 345 return 0; 346 } 347 } 348 349 @VisibleForTesting 350 static class Injector { elapsedRealtimeMillis()351 public long elapsedRealtimeMillis() { 352 return SystemClock.elapsedRealtime(); 353 } 354 getUserSerialNumber(UserManager userManager, int userId)355 public int getUserSerialNumber(UserManager userManager, int userId) { 356 return userManager.getUserSerialNumber(userId); 357 } 358 getUserId(UserManager userManager, int userSerialNumber)359 public int getUserId(UserManager userManager, int userSerialNumber) { 360 return userManager.getUserHandle(userSerialNumber); 361 } 362 getLocalDate()363 public LocalDate getLocalDate() { 364 return LocalDate.now(); 365 } 366 } 367 }