Home | History | Annotate | Download | only in display
      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 }