content: /*
   * Copyright 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.display;
  
  import android.annotation.Nullable;
  import android.annotation.UserIdInt;
  import android.hardware.display.AmbientBrightnessDayStats;
  import android.os.SystemClock;
  import android.os.UserManager;
  import android.util.Slog;
  import android.util.Xml;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.FastXmlSerializer;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlSerializer;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.io.PrintWriter;
  import java.nio.charset.StandardCharsets;
  import java.time.LocalDate;
  import java.time.format.DateTimeParseException;
  import java.util.ArrayDeque;
  import java.util.ArrayList;
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.Map;
  
  /**
   * Class that stores stats of ambient brightness regions as histogram.
   */
  public class AmbientBrightnessStatsTracker {
  
      private static final String TAG = "AmbientBrightnessStatsTracker";
      private static final boolean DEBUG = false;
  
      @VisibleForTesting
      static final float[] BUCKET_BOUNDARIES_FOR_NEW_STATS =
              {0, 0.1f, 0.3f, 1, 3, 10, 30, 100, 300, 1000, 3000, 10000};
      @VisibleForTesting
      static final int MAX_DAYS_TO_TRACK = 7;
  
      private final AmbientBrightnessStats mAmbientBrightnessStats;
      private final Timer mTimer;
      private final Injector mInjector;
      private final UserManager mUserManager;
      private float mCurrentAmbientBrightness;
      private @UserIdInt int mCurrentUserId;
  
      public AmbientBrightnessStatsTracker(UserManager userManager, @Nullable Injector injector) {
          mUserManager = userManager;
          if (injector != null) {
              mInjector = injector;
          } else {
              mInjector = new Injector();
          }
          mAmbientBrightnessStats = new AmbientBrightnessStats();
          mTimer = new Timer(() -> mInjector.elapsedRealtimeMillis());
          mCurrentAmbientBrightness = -1;
      }
  
      public synchronized void start() {
          mTimer.reset();
          mTimer.start();
      }
  
      public synchronized void stop() {
          if (mTimer.isRunning()) {
              mAmbientBrightnessStats.log(mCurrentUserId, mInjector.getLocalDate(),
                      mCurrentAmbientBrightness, mTimer.totalDurationSec());
          }
          mTimer.reset();
          mCurrentAmbientBrightness = -1;
      }
  
      public synchronized void add(@UserIdInt int userId, float newAmbientBrightness) {
          if (mTimer.isRunning()) {
              if (userId == mCurrentUserId) {
                  mAmbientBrightnessStats.log(mCurrentUserId, mInjector.getLocalDate(),
                          mCurrentAmbientBrightness, mTimer.totalDurationSec());
              } else {
                  if (DEBUG) {
                      Slog.v(TAG, "User switched since last sensor event.");
                  }
                  mCurrentUserId = userId;
              }
              mTimer.reset();
              mTimer.start();
              mCurrentAmbientBrightness = newAmbientBrightness;
          } else {
              if (DEBUG) {
                  Slog.e(TAG, "Timer not running while trying to add brightness stats.");
              }
          }
      }
  
      public synchronized void writeStats(OutputStream stream) throws IOException {
          mAmbientBrightnessStats.writeToXML(stream);
      }
  
      public synchronized void readStats(InputStream stream) throws IOException {
          mAmbientBrightnessStats.readFromXML(stream);
      }
  
      public synchronized ArrayList<AmbientBrightnessDayStats> getUserStats(int userId) {
          return mAmbientBrightnessStats.getUserStats(userId);
      }
  
      public synchronized void dump(PrintWriter pw) {
          pw.println("AmbientBrightnessStats:");
          pw.print(mAmbientBrightnessStats);
      }
  
      /**
       * AmbientBrightnessStats tracks ambient brightness stats across users over multiple days.
       * This class is not ThreadSafe.
       */
      class AmbientBrightnessStats {
  
          private static final String TAG_AMBIENT_BRIGHTNESS_STATS = "ambient-brightness-stats";
          private static final String TAG_AMBIENT_BRIGHTNESS_DAY_STATS =
                  "ambient-brightness-day-stats";
          private static final String ATTR_USER = "user";
          private static final String ATTR_LOCAL_DATE = "local-date";
          private static final String ATTR_BUCKET_BOUNDARIES = "bucket-boundaries";
          private static final String ATTR_BUCKET_STATS = "bucket-stats";
  
          private Map<Integer, Deque<AmbientBrightnessDayStats>> mStats;
  
          public AmbientBrightnessStats() {
              mStats = new HashMap<>();
          }
  
          public void log(@UserIdInt int userId, LocalDate localDate, float ambientBrightness,
                  float durationSec) {
              Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats(mStats, userId);
              AmbientBrightnessDayStats dayStats = getOrCreateDayStats(userStats, localDate);
              dayStats.log(ambientBrightness, durationSec);
          }
  
          public ArrayList<AmbientBrightnessDayStats> getUserStats(@UserIdInt int userId) {
              if (mStats.containsKey(userId)) {
                  return new ArrayList<>(mStats.get(userId));
              } else {
                  return null;
              }
          }
  
          public void writeToXML(OutputStream stream) throws IOException {
              XmlSerializer out = new FastXmlSerializer();
              out.setOutput(stream, StandardCharsets.UTF_8.name());
              out.startDocument(null, true);
              out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
  
              final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK);
              out.startTag(null, TAG_AMBIENT_BRIGHTNESS_STATS);
              for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) {
                  for (AmbientBrightnessDayStats userDayStats : entry.getValue()) {
                      int userSerialNumber = mInjector.getUserSerialNumber(mUserManager,
                              entry.getKey());
                      if (userSerialNumber != -1 && userDayStats.getLocalDate().isAfter(cutOffDate)) {
                          out.startTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS);
                          out.attribute(null, ATTR_USER, Integer.toString(userSerialNumber));
                          out.attribute(null, ATTR_LOCAL_DATE,
                                  userDayStats.getLocalDate().toString());
                          StringBuilder bucketBoundariesValues = new StringBuilder();
                          StringBuilder timeSpentValues = new StringBuilder();
                          for (int i = 0; i < userDayStats.getBucketBoundaries().length; i++) {
                              if (i > 0) {
                                  bucketBoundariesValues.append(",");
                                  timeSpentValues.append(",");
                              }
                              bucketBoundariesValues.append(userDayStats.getBucketBoundaries()[i]);
                              timeSpentValues.append(userDayStats.getStats()[i]);
                          }
                          out.attribute(null, ATTR_BUCKET_BOUNDARIES,
                                  bucketBoundariesValues.toString());
                          out.attribute(null, ATTR_BUCKET_STATS, timeSpentValues.toString());
                          out.endTag(null, TAG_AMBIENT_BRIGHTNESS_DAY_STATS);
                      }
                  }
              }
              out.endTag(null, TAG_AMBIENT_BRIGHTNESS_STATS);
              out.endDocument();
              stream.flush();
          }
  
          public void readFromXML(InputStream stream) throws IOException {
              try {
                  Map<Integer, Deque<AmbientBrightnessDayStats>> parsedStats = new HashMap<>();
                  XmlPullParser parser = Xml.newPullParser();
                  parser.setInput(stream, StandardCharsets.UTF_8.name());
  
                  int type;
                  while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                          && type != XmlPullParser.START_TAG) {
                  }
                  String tag = parser.getName();
                  if (!TAG_AMBIENT_BRIGHTNESS_STATS.equals(tag)) {
                      throw new XmlPullParserException(
                              "Ambient brightness stats not found in tracker file " + tag);
                  }
  
                  final LocalDate cutOffDate = mInjector.getLocalDate().minusDays(MAX_DAYS_TO_TRACK);
                  parser.next();
                  int outerDepth = parser.getDepth();
                  while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                          && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                      if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                          continue;
                      }
                      tag = parser.getName();
                      if (TAG_AMBIENT_BRIGHTNESS_DAY_STATS.equals(tag)) {
                          String userSerialNumber = parser.getAttributeValue(null, ATTR_USER);
                          LocalDate localDate = LocalDate.parse(
                                  parser.getAttributeValue(null, ATTR_LOCAL_DATE));
                          String[] bucketBoundaries = parser.getAttributeValue(null,
                                  ATTR_BUCKET_BOUNDARIES).split(",");
                          String[] bucketStats = parser.getAttributeValue(null,
                                  ATTR_BUCKET_STATS).split(",");
                          if (bucketBoundaries.length != bucketStats.length
                                  || bucketBoundaries.length < 1) {
                              throw new IOException("Invalid brightness stats string.");
                          }
                          float[] parsedBucketBoundaries = new float[bucketBoundaries.length];
                          float[] parsedBucketStats = new float[bucketStats.length];
                          for (int i = 0; i < bucketBoundaries.length; i++) {
                              parsedBucketBoundaries[i] = Float.parseFloat(bucketBoundaries[i]);
                              parsedBucketStats[i] = Float.parseFloat(bucketStats[i]);
                          }
                          int userId = mInjector.getUserId(mUserManager,
                                  Integer.parseInt(userSerialNumber));
                          if (userId != -1 && localDate.isAfter(cutOffDate)) {
                              Deque<AmbientBrightnessDayStats> userStats = getOrCreateUserStats(
                                      parsedStats, userId);
                              userStats.offer(
                                      new AmbientBrightnessDayStats(localDate,
                                              parsedBucketBoundaries, parsedBucketStats));
                          }
                      }
                  }
                  mStats = parsedStats;
              } catch (NullPointerException | NumberFormatException | XmlPullParserException |
                      DateTimeParseException | IOException e) {
                  throw new IOException("Failed to parse brightness stats file.", e);
              }
          }
  
          @Override
          public String toString() {
              StringBuilder builder = new StringBuilder();
              for (Map.Entry<Integer, Deque<AmbientBrightnessDayStats>> entry : mStats.entrySet()) {
                  for (AmbientBrightnessDayStats dayStats : entry.getValue()) {
                      builder.append("  ");
                      builder.append(entry.getKey()).append(" ");
                      builder.append(dayStats).append("\n");
                  }
              }
              return builder.toString();
          }
  
          private Deque<AmbientBrightnessDayStats> getOrCreateUserStats(
                  Map<Integer, Deque<AmbientBrightnessDayStats>> stats, @UserIdInt int userId) {
              if (!stats.containsKey(userId)) {
                  stats.put(userId, new ArrayDeque<>());
              }
              return stats.get(userId);
          }
  
          private AmbientBrightnessDayStats getOrCreateDayStats(
                  Deque<AmbientBrightnessDayStats> userStats, LocalDate localDate) {
              AmbientBrightnessDayStats lastBrightnessStats = userStats.peekLast();
              if (lastBrightnessStats != null && lastBrightnessStats.getLocalDate().equals(
                      localDate)) {
                  return lastBrightnessStats;
              } else {
                  AmbientBrightnessDayStats dayStats = new AmbientBrightnessDayStats(localDate,
                          BUCKET_BOUNDARIES_FOR_NEW_STATS);
                  if (userStats.size() == MAX_DAYS_TO_TRACK) {
                      userStats.poll();
                  }
                  userStats.offer(dayStats);
                  return dayStats;
              }
          }
      }
  
      @VisibleForTesting
      interface Clock {
          long elapsedTimeMillis();
      }
  
      @VisibleForTesting
      static class Timer {
  
          private final Clock clock;
          private long startTimeMillis;
          private boolean started;
  
          public Timer(Clock clock) {
              this.clock = clock;
          }
  
          public void reset() {
              started = false;
          }
  
          public void start() {
              if (!started) {
                  startTimeMillis = clock.elapsedTimeMillis();
                  started = true;
              }
          }
  
          public boolean isRunning() {
              return started;
          }
  
          public float totalDurationSec() {
              if (started) {
                  return (float) ((clock.elapsedTimeMillis() - startTimeMillis) / 1000.0);
              }
              return 0;
          }
      }
  
      @VisibleForTesting
      static class Injector {
          public long elapsedRealtimeMillis() {
              return SystemClock.elapsedRealtime();
          }
  
          public int getUserSerialNumber(UserManager userManager, int userId) {
              return userManager.getUserSerialNumber(userId);
          }
  
          public int getUserId(UserManager userManager, int userSerialNumber) {
              return userManager.getUserHandle(userSerialNumber);
          }
  
          public LocalDate getLocalDate() {
              return LocalDate.now();
          }
      }
  }