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 }