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