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 }