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