Peeyush Agarwal | cc155dd | 2018-01-10 11:51:33 +0000 | [diff] [blame] | 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 | |
| 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 | |
| 80 | public synchronized void start() { |
| 81 | mTimer.reset(); |
| 82 | mTimer.start(); |
| 83 | } |
| 84 | |
| 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 | |
| 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 | |
| 115 | public synchronized void writeStats(OutputStream stream) throws IOException { |
| 116 | mAmbientBrightnessStats.writeToXML(stream); |
| 117 | } |
| 118 | |
| 119 | public synchronized void readStats(InputStream stream) throws IOException { |
| 120 | mAmbientBrightnessStats.readFromXML(stream); |
| 121 | } |
| 122 | |
| 123 | public synchronized ArrayList<AmbientBrightnessDayStats> getUserStats(int userId) { |
| 124 | return mAmbientBrightnessStats.getUserStats(userId); |
| 125 | } |
| 126 | |
| 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 | |
| 148 | public AmbientBrightnessStats() { |
| 149 | mStats = new HashMap<>(); |
| 150 | } |
| 151 | |
| 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 | |
| 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 | |
| 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 | |
| 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 |
| 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 | |
| 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 | |
| 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 { |
| 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 | |
| 318 | public Timer(Clock clock) { |
| 319 | this.clock = clock; |
| 320 | } |
| 321 | |
| 322 | public void reset() { |
| 323 | started = false; |
| 324 | } |
| 325 | |
| 326 | public void start() { |
| 327 | if (!started) { |
| 328 | startTimeMillis = clock.elapsedTimeMillis(); |
| 329 | started = true; |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | public boolean isRunning() { |
| 334 | return started; |
| 335 | } |
| 336 | |
| 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 { |
| 347 | public long elapsedRealtimeMillis() { |
| 348 | return SystemClock.elapsedRealtime(); |
| 349 | } |
| 350 | |
| 351 | public int getUserSerialNumber(UserManager userManager, int userId) { |
| 352 | return userManager.getUserSerialNumber(userId); |
| 353 | } |
| 354 | |
| 355 | public int getUserId(UserManager userManager, int userSerialNumber) { |
| 356 | return userManager.getUserHandle(userSerialNumber); |
| 357 | } |
| 358 | |
| 359 | public LocalDate getLocalDate() { |
| 360 | return LocalDate.now(); |
| 361 | } |
| 362 | } |
| 363 | } |