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 }