content: /*
   * Copyright 2017 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.app.ActivityManager;
  import android.app.ActivityTaskManager;
  import android.content.BroadcastReceiver;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.ParceledListSlice;
  import android.database.ContentObserver;
  import android.graphics.PixelFormat;
  import android.hardware.Sensor;
  import android.hardware.SensorEvent;
  import android.hardware.SensorEventListener;
  import android.hardware.SensorManager;
  import android.hardware.display.AmbientBrightnessDayStats;
  import android.hardware.display.BrightnessChangeEvent;
  import android.hardware.display.ColorDisplayManager;
  import android.hardware.display.DisplayManager;
  import android.hardware.display.DisplayManagerInternal;
  import android.hardware.display.DisplayedContentSample;
  import android.hardware.display.DisplayedContentSamplingAttributes;
  import android.net.Uri;
  import android.os.BatteryManager;
  import android.os.Environment;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.PowerManager;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.Settings;
  import android.util.AtomicFile;
  import android.util.Slog;
  import android.util.Xml;
  import android.view.Display;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.os.BackgroundThread;
  import com.android.internal.util.FastXmlSerializer;
  import com.android.internal.util.RingBuffer;
  import com.android.server.LocalServices;
  
  import libcore.io.IoUtils;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlSerializer;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.io.PrintWriter;
  import java.nio.charset.StandardCharsets;
  import java.text.SimpleDateFormat;
  import java.util.ArrayDeque;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.Deque;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.concurrent.TimeUnit;
  
  /**
   * Class that tracks recent brightness settings changes and stores
   * associated information such as light sensor readings.
   */
  public class BrightnessTracker {
  
      static final String TAG = "BrightnessTracker";
      static final boolean DEBUG = false;
      @VisibleForTesting
      static final boolean ENABLE_COLOR_SAMPLING = false;
  
      private static final String EVENTS_FILE = "brightness_events.xml";
      private static final String AMBIENT_BRIGHTNESS_STATS_FILE = "ambient_brightness_stats.xml";
      private static final int MAX_EVENTS = 100;
      // Discard events when reading or writing that are older than this.
      private static final long MAX_EVENT_AGE = TimeUnit.DAYS.toMillis(30);
      // Time over which we keep lux sensor readings.
      private static final long LUX_EVENT_HORIZON = TimeUnit.SECONDS.toNanos(10);
  
      private static final String TAG_EVENTS = "events";
      private static final String TAG_EVENT = "event";
      private static final String ATTR_NITS = "nits";
      private static final String ATTR_TIMESTAMP = "timestamp";
      private static final String ATTR_PACKAGE_NAME = "packageName";
      private static final String ATTR_USER = "user";
      private static final String ATTR_LUX = "lux";
      private static final String ATTR_LUX_TIMESTAMPS = "luxTimestamps";
      private static final String ATTR_BATTERY_LEVEL = "batteryLevel";
      private static final String ATTR_NIGHT_MODE = "nightMode";
      private static final String ATTR_COLOR_TEMPERATURE = "colorTemperature";
      private static final String ATTR_LAST_NITS = "lastNits";
      private static final String ATTR_DEFAULT_CONFIG = "defaultConfig";
      private static final String ATTR_POWER_SAVE = "powerSaveFactor";
      private static final String ATTR_USER_POINT = "userPoint";
      private static final String ATTR_COLOR_SAMPLE_DURATION = "colorSampleDuration";
      private static final String ATTR_COLOR_VALUE_BUCKETS = "colorValueBuckets";
  
      private static final int MSG_BACKGROUND_START = 0;
      private static final int MSG_BRIGHTNESS_CHANGED = 1;
      private static final int MSG_STOP_SENSOR_LISTENER = 2;
      private static final int MSG_START_SENSOR_LISTENER = 3;
  
      private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
  
      private static final long COLOR_SAMPLE_DURATION = TimeUnit.SECONDS.toSeconds(10);
      // Sample chanel 2 of HSV which is the Value component.
      private static final int COLOR_SAMPLE_COMPONENT_MASK = 0x1 << 2;
  
      // Lock held while accessing mEvents, is held while writing events to flash.
      private final Object mEventsLock = new Object();
      @GuardedBy("mEventsLock")
      private RingBuffer<BrightnessChangeEvent> mEvents
              = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS);
      @GuardedBy("mEventsLock")
      private boolean mEventsDirty;
  
      private volatile boolean mWriteBrightnessTrackerStateScheduled;
  
      private AmbientBrightnessStatsTracker mAmbientBrightnessStatsTracker;
  
      private final UserManager mUserManager;
      private final Context mContext;
      private final ContentResolver mContentResolver;
      private final Handler mBgHandler;
  
      // These members should only be accessed on the mBgHandler thread.
      private BroadcastReceiver mBroadcastReceiver;
      private SensorListener mSensorListener;
      private SettingsObserver mSettingsObserver;
      private DisplayListener mDisplayListener;
      private boolean mSensorRegistered;
      private boolean mColorSamplingEnabled;
      private int mNoFramesToSample;
      private float mFrameRate;
      // End of block of members that should only be accessed on the mBgHandler thread.
  
      private @UserIdInt int mCurrentUserId = UserHandle.USER_NULL;
  
      // Lock held while collecting data related to brightness changes.
      private final Object mDataCollectionLock = new Object();
      @GuardedBy("mDataCollectionLock")
      private Deque<LightData> mLastSensorReadings = new ArrayDeque<>();
      @GuardedBy("mDataCollectionLock")
      private float mLastBatteryLevel = Float.NaN;
      @GuardedBy("mDataCollectionLock")
      private float mLastBrightness = -1;
      @GuardedBy("mDataCollectionLock")
      private boolean mStarted;
  
      private final Injector mInjector;
  
      public BrightnessTracker(Context context, @Nullable Injector injector) {
          // Note this will be called very early in boot, other system
          // services may not be present.
          mContext = context;
          mContentResolver = context.getContentResolver();
          if (injector != null) {
              mInjector = injector;
          } else {
              mInjector = new Injector();
          }
          mBgHandler = new TrackerHandler(mInjector.getBackgroundHandler().getLooper());
          mUserManager = mContext.getSystemService(UserManager.class);
      }
  
      /**
       * Start listening for brightness slider events
       *
       * @param initialBrightness the initial screen brightness
       */
      public void start(float initialBrightness) {
          if (DEBUG) {
              Slog.d(TAG, "Start");
          }
          mCurrentUserId = ActivityManager.getCurrentUser();
          mBgHandler.obtainMessage(MSG_BACKGROUND_START, (Float) initialBrightness).sendToTarget();
      }
  
      private void backgroundStart(float initialBrightness) {
          readEvents();
          readAmbientBrightnessStats();
  
          mSensorListener = new SensorListener();
  
          mSettingsObserver = new SettingsObserver(mBgHandler);
          mInjector.registerBrightnessModeObserver(mContentResolver, mSettingsObserver);
          startSensorListener();
  
          final IntentFilter intentFilter = new IntentFilter();
          intentFilter.addAction(Intent.ACTION_SHUTDOWN);
          intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
          intentFilter.addAction(Intent.ACTION_SCREEN_ON);
          intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
          mBroadcastReceiver = new Receiver();
          mInjector.registerReceiver(mContext, mBroadcastReceiver, intentFilter);
  
          mInjector.scheduleIdleJob(mContext);
          synchronized (mDataCollectionLock) {
              mLastBrightness = initialBrightness;
              mStarted = true;
          }
          enableColorSampling();
      }
  
      /** Stop listening for events */
      @VisibleForTesting
      void stop() {
          if (DEBUG) {
              Slog.d(TAG, "Stop");
          }
          mBgHandler.removeMessages(MSG_BACKGROUND_START);
          stopSensorListener();
          mInjector.unregisterSensorListener(mContext, mSensorListener);
          mInjector.unregisterBrightnessModeObserver(mContext, mSettingsObserver);
          mInjector.unregisterReceiver(mContext, mBroadcastReceiver);
          mInjector.cancelIdleJob(mContext);
  
          synchronized (mDataCollectionLock) {
              mStarted = false;
          }
          disableColorSampling();
      }
  
      public void onSwitchUser(@UserIdInt int newUserId) {
          if (DEBUG) {
              Slog.d(TAG, "Used id updated from " + mCurrentUserId + " to " + newUserId);
          }
          mCurrentUserId = newUserId;
      }
  
      /**
       * @param userId userId to fetch data for.
       * @param includePackage if false we will null out BrightnessChangeEvent.packageName
       * @return List of recent {@link BrightnessChangeEvent}s
       */
      public ParceledListSlice<BrightnessChangeEvent> getEvents(int userId, boolean includePackage) {
          BrightnessChangeEvent[] events;
          synchronized (mEventsLock) {
              events = mEvents.toArray();
          }
          int[] profiles = mInjector.getProfileIds(mUserManager, userId);
          Map<Integer, Boolean> toRedact = new HashMap<>();
          for (int i = 0; i < profiles.length; ++i) {
              int profileId = profiles[i];
              // Include slider interactions when a managed profile app is in the
              // foreground but always redact the package name.
              boolean redact = (!includePackage) || profileId != userId;
              toRedact.put(profiles[i], redact);
          }
          ArrayList<BrightnessChangeEvent> out = new ArrayList<>(events.length);
          for (int i = 0; i < events.length; ++i) {
              Boolean redact = toRedact.get(events[i].userId);
              if (redact != null) {
                  if (!redact) {
                      out.add(events[i]);
                  } else {
                      BrightnessChangeEvent event = new BrightnessChangeEvent((events[i]),
                              /* redactPackage */ true);
                      out.add(event);
                  }
              }
          }
          return new ParceledListSlice<>(out);
      }
  
      public void persistBrightnessTrackerState() {
          scheduleWriteBrightnessTrackerState();
      }
  
      /**
       * Notify the BrightnessTracker that the user has changed the brightness of the display.
       */
      public void notifyBrightnessChanged(float brightness, boolean userInitiated,
              float powerBrightnessFactor, boolean isUserSetBrightness,
              boolean isDefaultBrightnessConfig) {
          if (DEBUG) {
              Slog.d(TAG, String.format("notifyBrightnessChanged(brightness=%f, userInitiated=%b)",
                          brightness, userInitiated));
          }
          Message m = mBgHandler.obtainMessage(MSG_BRIGHTNESS_CHANGED,
                  userInitiated ? 1 : 0, 0 /*unused*/, new BrightnessChangeValues(brightness,
                          powerBrightnessFactor, isUserSetBrightness, isDefaultBrightnessConfig,
                          mInjector.currentTimeMillis()));
          m.sendToTarget();
      }
  
      private void handleBrightnessChanged(float brightness, boolean userInitiated,
              float powerBrightnessFactor, boolean isUserSetBrightness,
              boolean isDefaultBrightnessConfig, long timestamp) {
          BrightnessChangeEvent.Builder builder;
  
          synchronized (mDataCollectionLock) {
              if (!mStarted) {
                  // Not currently gathering brightness change information
                  return;
              }
  
              float previousBrightness = mLastBrightness;
              mLastBrightness = brightness;
  
              if (!userInitiated) {
                  // We want to record what current brightness is so that we know what the user
                  // changed it from, but if it wasn't user initiated then we don't want to record it
                  // as a BrightnessChangeEvent.
                  return;
              }
  
              builder = new BrightnessChangeEvent.Builder();
              builder.setBrightness(brightness);
              builder.setTimeStamp(timestamp);
              builder.setPowerBrightnessFactor(powerBrightnessFactor);
              builder.setUserBrightnessPoint(isUserSetBrightness);
              builder.setIsDefaultBrightnessConfig(isDefaultBrightnessConfig);
  
              final int readingCount = mLastSensorReadings.size();
              if (readingCount == 0) {
                  // No sensor data so ignore this.
                  return;
              }
  
              float[] luxValues = new float[readingCount];
              long[] luxTimestamps = new long[readingCount];
  
              int pos = 0;
  
              // Convert sensor timestamp in elapsed time nanos to current time millis.
              long currentTimeMillis = mInjector.currentTimeMillis();
              long elapsedTimeNanos = mInjector.elapsedRealtimeNanos();
              for (LightData reading : mLastSensorReadings) {
                  luxValues[pos] = reading.lux;
                  luxTimestamps[pos] = currentTimeMillis -
                          TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanos - reading.timestamp);
                  ++pos;
              }
              builder.setLuxValues(luxValues);
              builder.setLuxTimestamps(luxTimestamps);
  
              builder.setBatteryLevel(mLastBatteryLevel);
              builder.setLastBrightness(previousBrightness);
          }
  
          try {
              final ActivityManager.StackInfo focusedStack = mInjector.getFocusedStack();
              if (focusedStack != null && focusedStack.topActivity != null) {
                  builder.setUserId(focusedStack.userId);
                  builder.setPackageName(focusedStack.topActivity.getPackageName());
              } else {
                  // Ignore the event because we can't determine user / package.
                  if (DEBUG) {
                      Slog.d(TAG, "Ignoring event due to null focusedStack.");
                  }
                  return;
              }
          } catch (RemoteException e) {
              // Really shouldn't be possible.
              return;
          }
  
          builder.setNightMode(mInjector.isNightDisplayActivated(mContext));
          builder.setColorTemperature(mInjector.getNightDisplayColorTemperature(mContext));
  
          if (mColorSamplingEnabled) {
              DisplayedContentSample sample = mInjector.sampleColor(mNoFramesToSample);
              if (sample != null && sample.getSampleComponent(
                      DisplayedContentSample.ColorComponent.CHANNEL2) != null) {
                  float numMillis = (sample.getNumFrames() / mFrameRate) * 1000.0f;
                  builder.setColorValues(
                          sample.getSampleComponent(DisplayedContentSample.ColorComponent.CHANNEL2),
                          Math.round(numMillis));
              }
          }
  
          BrightnessChangeEvent event = builder.build();
          if (DEBUG) {
              Slog.d(TAG, "Event " + event.brightness + " " + event.packageName);
          }
          synchronized (mEventsLock) {
              mEventsDirty = true;
              mEvents.append(event);
          }
      }
  
      private void startSensorListener() {
          if (!mSensorRegistered
                  && mInjector.isInteractive(mContext)
                  && mInjector.isBrightnessModeAutomatic(mContentResolver)) {
              mAmbientBrightnessStatsTracker.start();
              mSensorRegistered = true;
              mInjector.registerSensorListener(mContext, mSensorListener,
                      mInjector.getBackgroundHandler());
          }
      }
  
      private void stopSensorListener() {
          if (mSensorRegistered) {
              mAmbientBrightnessStatsTracker.stop();
              mInjector.unregisterSensorListener(mContext, mSensorListener);
              mSensorRegistered = false;
          }
      }
  
      private void scheduleWriteBrightnessTrackerState() {
          if (!mWriteBrightnessTrackerStateScheduled) {
              mBgHandler.post(() -> {
                  mWriteBrightnessTrackerStateScheduled = false;
                  writeEvents();
                  writeAmbientBrightnessStats();
              });
              mWriteBrightnessTrackerStateScheduled = true;
          }
      }
  
      private void writeEvents() {
          synchronized (mEventsLock) {
              if (!mEventsDirty) {
                  // Nothing to write
                  return;
              }
  
              final AtomicFile writeTo = mInjector.getFile(EVENTS_FILE);
              if (writeTo == null) {
                  return;
              }
              if (mEvents.isEmpty()) {
                  if (writeTo.exists()) {
                      writeTo.delete();
                  }
                  mEventsDirty = false;
              } else {
                  FileOutputStream output = null;
                  try {
                      output = writeTo.startWrite();
                      writeEventsLocked(output);
                      writeTo.finishWrite(output);
                      mEventsDirty = false;
                  } catch (IOException e) {
                      writeTo.failWrite(output);
                      Slog.e(TAG, "Failed to write change mEvents.", e);
                  }
              }
          }
      }
  
      private void writeAmbientBrightnessStats() {
          final AtomicFile writeTo = mInjector.getFile(AMBIENT_BRIGHTNESS_STATS_FILE);
          if (writeTo == null) {
              return;
          }
          FileOutputStream output = null;
          try {
              output = writeTo.startWrite();
              mAmbientBrightnessStatsTracker.writeStats(output);
              writeTo.finishWrite(output);
          } catch (IOException e) {
              writeTo.failWrite(output);
              Slog.e(TAG, "Failed to write ambient brightness stats.", e);
          }
      }
  
      private void readEvents() {
          synchronized (mEventsLock) {
              // Read might prune events so mark as dirty.
              mEventsDirty = true;
              mEvents.clear();
              final AtomicFile readFrom = mInjector.getFile(EVENTS_FILE);
              if (readFrom != null && readFrom.exists()) {
                  FileInputStream input = null;
                  try {
                      input = readFrom.openRead();
                      readEventsLocked(input);
                  } catch (IOException e) {
                      readFrom.delete();
                      Slog.e(TAG, "Failed to read change mEvents.", e);
                  } finally {
                      IoUtils.closeQuietly(input);
                  }
              }
          }
      }
  
      private void readAmbientBrightnessStats() {
          mAmbientBrightnessStatsTracker = new AmbientBrightnessStatsTracker(mUserManager, null);
          final AtomicFile readFrom = mInjector.getFile(AMBIENT_BRIGHTNESS_STATS_FILE);
          if (readFrom != null && readFrom.exists()) {
              FileInputStream input = null;
              try {
                  input = readFrom.openRead();
                  mAmbientBrightnessStatsTracker.readStats(input);
              } catch (IOException e) {
                  readFrom.delete();
                  Slog.e(TAG, "Failed to read ambient brightness stats.", e);
              } finally {
                  IoUtils.closeQuietly(input);
              }
          }
      }
  
      @VisibleForTesting
      @GuardedBy("mEventsLock")
      void writeEventsLocked(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);
  
          out.startTag(null, TAG_EVENTS);
          BrightnessChangeEvent[] toWrite = mEvents.toArray();
          // Clear events, code below will add back the ones that are still within the time window.
          mEvents.clear();
          if (DEBUG) {
              Slog.d(TAG, "Writing events " + toWrite.length);
          }
          final long timeCutOff = mInjector.currentTimeMillis() - MAX_EVENT_AGE;
          for (int i = 0; i < toWrite.length; ++i) {
              int userSerialNo = mInjector.getUserSerialNumber(mUserManager, toWrite[i].userId);
              if (userSerialNo != -1 && toWrite[i].timeStamp > timeCutOff) {
                  mEvents.append(toWrite[i]);
                  out.startTag(null, TAG_EVENT);
                  out.attribute(null, ATTR_NITS, Float.toString(toWrite[i].brightness));
                  out.attribute(null, ATTR_TIMESTAMP, Long.toString(toWrite[i].timeStamp));
                  out.attribute(null, ATTR_PACKAGE_NAME, toWrite[i].packageName);
                  out.attribute(null, ATTR_USER, Integer.toString(userSerialNo));
                  out.attribute(null, ATTR_BATTERY_LEVEL, Float.toString(toWrite[i].batteryLevel));
                  out.attribute(null, ATTR_NIGHT_MODE, Boolean.toString(toWrite[i].nightMode));
                  out.attribute(null, ATTR_COLOR_TEMPERATURE, Integer.toString(
                          toWrite[i].colorTemperature));
                  out.attribute(null, ATTR_LAST_NITS,
                          Float.toString(toWrite[i].lastBrightness));
                  out.attribute(null, ATTR_DEFAULT_CONFIG,
                          Boolean.toString(toWrite[i].isDefaultBrightnessConfig));
                  out.attribute(null, ATTR_POWER_SAVE,
                          Float.toString(toWrite[i].powerBrightnessFactor));
                  out.attribute(null, ATTR_USER_POINT,
                          Boolean.toString(toWrite[i].isUserSetBrightness));
                  StringBuilder luxValues = new StringBuilder();
                  StringBuilder luxTimestamps = new StringBuilder();
                  for (int j = 0; j < toWrite[i].luxValues.length; ++j) {
                      if (j > 0) {
                          luxValues.append(',');
                          luxTimestamps.append(',');
                      }
                      luxValues.append(Float.toString(toWrite[i].luxValues[j]));
                      luxTimestamps.append(Long.toString(toWrite[i].luxTimestamps[j]));
                  }
                  out.attribute(null, ATTR_LUX, luxValues.toString());
                  out.attribute(null, ATTR_LUX_TIMESTAMPS, luxTimestamps.toString());
                  if (toWrite[i].colorValueBuckets != null
                          && toWrite[i].colorValueBuckets.length > 0) {
                      out.attribute(null, ATTR_COLOR_SAMPLE_DURATION,
                              Long.toString(toWrite[i].colorSampleDuration));
                      StringBuilder buckets = new StringBuilder();
                      for (int j = 0; j < toWrite[i].colorValueBuckets.length; ++j) {
                          if (j > 0) {
                              buckets.append(',');
                          }
                          buckets.append(Long.toString(toWrite[i].colorValueBuckets[j]));
                      }
                      out.attribute(null, ATTR_COLOR_VALUE_BUCKETS, buckets.toString());
                  }
                  out.endTag(null, TAG_EVENT);
              }
          }
          out.endTag(null, TAG_EVENTS);
          out.endDocument();
          stream.flush();
      }
  
      @VisibleForTesting
      @GuardedBy("mEventsLock")
      void readEventsLocked(InputStream stream) throws IOException {
          try {
              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_EVENTS.equals(tag)) {
                  throw new XmlPullParserException(
                          "Events not found in brightness tracker file " + tag);
              }
  
              final long timeCutOff = mInjector.currentTimeMillis() - MAX_EVENT_AGE;
  
              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_EVENT.equals(tag)) {
                      BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder();
  
                      String brightness = parser.getAttributeValue(null, ATTR_NITS);
                      builder.setBrightness(Float.parseFloat(brightness));
                      String timestamp = parser.getAttributeValue(null, ATTR_TIMESTAMP);
                      builder.setTimeStamp(Long.parseLong(timestamp));
                      builder.setPackageName(parser.getAttributeValue(null, ATTR_PACKAGE_NAME));
                      String user = parser.getAttributeValue(null, ATTR_USER);
                      builder.setUserId(mInjector.getUserId(mUserManager, Integer.parseInt(user)));
                      String batteryLevel = parser.getAttributeValue(null, ATTR_BATTERY_LEVEL);
                      builder.setBatteryLevel(Float.parseFloat(batteryLevel));
                      String nightMode = parser.getAttributeValue(null, ATTR_NIGHT_MODE);
                      builder.setNightMode(Boolean.parseBoolean(nightMode));
                      String colorTemperature =
                              parser.getAttributeValue(null, ATTR_COLOR_TEMPERATURE);
                      builder.setColorTemperature(Integer.parseInt(colorTemperature));
                      String lastBrightness = parser.getAttributeValue(null, ATTR_LAST_NITS);
                      builder.setLastBrightness(Float.parseFloat(lastBrightness));
  
                      String luxValue = parser.getAttributeValue(null, ATTR_LUX);
                      String luxTimestamp = parser.getAttributeValue(null, ATTR_LUX_TIMESTAMPS);
  
                      String[] luxValuesStrings = luxValue.split(",");
                      String[] luxTimestampsStrings = luxTimestamp.split(",");
                      if (luxValuesStrings.length != luxTimestampsStrings.length) {
                          continue;
                      }
                      float[] luxValues = new float[luxValuesStrings.length];
                      long[] luxTimestamps = new long[luxValuesStrings.length];
                      for (int i = 0; i < luxValues.length; ++i) {
                          luxValues[i] = Float.parseFloat(luxValuesStrings[i]);
                          luxTimestamps[i] = Long.parseLong(luxTimestampsStrings[i]);
                      }
                      builder.setLuxValues(luxValues);
                      builder.setLuxTimestamps(luxTimestamps);
  
                      String defaultConfig = parser.getAttributeValue(null, ATTR_DEFAULT_CONFIG);
                      if (defaultConfig != null) {
                          builder.setIsDefaultBrightnessConfig(Boolean.parseBoolean(defaultConfig));
                      }
                      String powerSave = parser.getAttributeValue(null, ATTR_POWER_SAVE);
                      if (powerSave != null) {
                          builder.setPowerBrightnessFactor(Float.parseFloat(powerSave));
                      } else {
                          builder.setPowerBrightnessFactor(1.0f);
                      }
                      String userPoint = parser.getAttributeValue(null, ATTR_USER_POINT);
                      if (userPoint != null) {
                          builder.setUserBrightnessPoint(Boolean.parseBoolean(userPoint));
                      }
  
                      String colorSampleDurationString =
                              parser.getAttributeValue(null, ATTR_COLOR_SAMPLE_DURATION);
                      String colorValueBucketsString =
                              parser.getAttributeValue(null, ATTR_COLOR_VALUE_BUCKETS);
                      if (colorSampleDurationString != null && colorValueBucketsString != null) {
                          long colorSampleDuration = Long.parseLong(colorSampleDurationString);
                          String[] buckets = colorValueBucketsString.split(",");
                          long[] bucketValues = new long[buckets.length];
                          for (int i = 0; i < bucketValues.length; ++i) {
                              bucketValues[i] = Long.parseLong(buckets[i]);
                          }
                          builder.setColorValues(bucketValues, colorSampleDuration);
                      }
  
                      BrightnessChangeEvent event = builder.build();
                      if (DEBUG) {
                          Slog.i(TAG, "Read event " + event.brightness
                                  + " " + event.packageName);
                      }
  
                      if (event.userId != -1 && event.timeStamp > timeCutOff
                              && event.luxValues.length > 0) {
                          mEvents.append(event);
                      }
                  }
              }
          } catch (NullPointerException | NumberFormatException | XmlPullParserException
                  | IOException e) {
              // Failed to parse something, just start with an empty event log.
              mEvents = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS);
              Slog.e(TAG, "Failed to parse brightness event", e);
              // Re-throw so we will delete the bad file.
              throw new IOException("failed to parse file", e);
          }
      }
  
      public void dump(final PrintWriter pw) {
          pw.println("BrightnessTracker state:");
          synchronized (mDataCollectionLock) {
              pw.println("  mStarted=" + mStarted);
              pw.println("  mLastBatteryLevel=" + mLastBatteryLevel);
              pw.println("  mLastBrightness=" + mLastBrightness);
              pw.println("  mLastSensorReadings.size=" + mLastSensorReadings.size());
              if (!mLastSensorReadings.isEmpty()) {
                  pw.println("  mLastSensorReadings time span "
                          + mLastSensorReadings.peekFirst().timestamp + "->"
                          + mLastSensorReadings.peekLast().timestamp);
              }
          }
          synchronized (mEventsLock) {
              pw.println("  mEventsDirty=" + mEventsDirty);
              pw.println("  mEvents.size=" + mEvents.size());
              BrightnessChangeEvent[] events = mEvents.toArray();
              for (int i = 0; i < events.length; ++i) {
                  pw.print("    " + FORMAT.format(new Date(events[i].timeStamp)));
                  pw.print(", userId=" + events[i].userId);
                  pw.print(", " + events[i].lastBrightness + "->" + events[i].brightness);
                  pw.print(", isUserSetBrightness=" + events[i].isUserSetBrightness);
                  pw.print(", powerBrightnessFactor=" + events[i].powerBrightnessFactor);
                  pw.print(", isDefaultBrightnessConfig=" + events[i].isDefaultBrightnessConfig);
                  pw.print(" {");
                  for (int j = 0; j < events[i].luxValues.length; ++j){
                      if (j != 0) {
                          pw.print(", ");
                      }
                      pw.print("(" + events[i].luxValues[j] + "," + events[i].luxTimestamps[j] + ")");
                  }
                  pw.println("}");
              }
          }
          pw.println("  mWriteBrightnessTrackerStateScheduled="
                  + mWriteBrightnessTrackerStateScheduled);
          mBgHandler.runWithScissors(() -> dumpLocal(pw), 1000);
          if (mAmbientBrightnessStatsTracker != null) {
              pw.println();
              mAmbientBrightnessStatsTracker.dump(pw);
          }
      }
  
      private void dumpLocal(PrintWriter pw) {
          pw.println("  mSensorRegistered=" + mSensorRegistered);
          pw.println("  mColorSamplingEnabled=" + mColorSamplingEnabled);
          pw.println("  mNoFramesToSample=" + mNoFramesToSample);
          pw.println("  mFrameRate=" + mFrameRate);
      }
  
      private void enableColorSampling() {
          if (!ENABLE_COLOR_SAMPLING
                  || !mInjector.isBrightnessModeAutomatic(mContentResolver)
                  || !mInjector.isInteractive(mContext)
                  || mColorSamplingEnabled) {
              return;
          }
  
          mFrameRate = mInjector.getFrameRate(mContext);
          if (mFrameRate <= 0) {
              Slog.wtf(TAG, "Default display has a zero or negative framerate.");
              return;
          }
          mNoFramesToSample = (int) (mFrameRate * COLOR_SAMPLE_DURATION);
  
          DisplayedContentSamplingAttributes attributes = mInjector.getSamplingAttributes();
          if (DEBUG && attributes != null) {
              Slog.d(TAG, "Color sampling"
                      + " mask=0x" + Integer.toHexString(attributes.getComponentMask())
                      + " dataSpace=0x" + Integer.toHexString(attributes.getDataspace())
                      + " pixelFormat=0x" + Integer.toHexString(attributes.getPixelFormat()));
          }
          // Do we support sampling the Value component of HSV
          if (attributes != null && attributes.getPixelFormat() == PixelFormat.HSV_888
                  && (attributes.getComponentMask() & COLOR_SAMPLE_COMPONENT_MASK) != 0) {
  
              mColorSamplingEnabled = mInjector.enableColorSampling(/* enable= */true,
                      mNoFramesToSample);
              if (DEBUG) {
                  Slog.i(TAG, "turning on color sampling for "
                          + mNoFramesToSample + " frames, success=" + mColorSamplingEnabled);
              }
          }
          if (mColorSamplingEnabled && mDisplayListener == null) {
              mDisplayListener = new DisplayListener();
              mInjector.registerDisplayListener(mContext, mDisplayListener, mBgHandler);
          }
      }
  
      private void disableColorSampling() {
          if (!mColorSamplingEnabled) {
              return;
          }
          mInjector.enableColorSampling(/* enable= */ false, /* noFrames= */ 0);
          mColorSamplingEnabled = false;
          if (mDisplayListener != null) {
              mInjector.unRegisterDisplayListener(mContext, mDisplayListener);
              mDisplayListener = null;
          }
          if (DEBUG) {
              Slog.i(TAG, "turning off color sampling");
          }
      }
  
      private void updateColorSampling() {
          if (!mColorSamplingEnabled) {
              return;
          }
          float frameRate = mInjector.getFrameRate(mContext);
          if (frameRate != mFrameRate) {
              disableColorSampling();
              enableColorSampling();
          }
      }
  
      public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(int userId) {
          if (mAmbientBrightnessStatsTracker != null) {
              ArrayList<AmbientBrightnessDayStats> stats =
                      mAmbientBrightnessStatsTracker.getUserStats(userId);
              if (stats != null) {
                  return new ParceledListSlice<>(stats);
              }
          }
          return ParceledListSlice.emptyList();
      }
  
      // Not allowed to keep the SensorEvent so used to copy the data we care about.
      private static class LightData {
          public float lux;
          // Time in elapsedRealtimeNanos
          public long timestamp;
      }
  
      private void recordSensorEvent(SensorEvent event) {
          long horizon = mInjector.elapsedRealtimeNanos() - LUX_EVENT_HORIZON;
          synchronized (mDataCollectionLock) {
              if (DEBUG) {
                  Slog.v(TAG, "Sensor event " + event);
              }
              if (!mLastSensorReadings.isEmpty()
                      && event.timestamp < mLastSensorReadings.getLast().timestamp) {
                  // Ignore event that came out of order.
                  return;
              }
              LightData data = null;
              while (!mLastSensorReadings.isEmpty()
                      && mLastSensorReadings.getFirst().timestamp < horizon) {
                  // Remove data that has fallen out of the window.
                  data = mLastSensorReadings.removeFirst();
              }
              // We put back the last one we removed so we know how long
              // the first sensor reading was valid for.
              if (data != null) {
                  mLastSensorReadings.addFirst(data);
              }
  
              data = new LightData();
              data.timestamp = event.timestamp;
              data.lux = event.values[0];
              mLastSensorReadings.addLast(data);
          }
      }
  
      private void recordAmbientBrightnessStats(SensorEvent event) {
          mAmbientBrightnessStatsTracker.add(mCurrentUserId, event.values[0]);
      }
  
      private void batteryLevelChanged(int level, int scale) {
          synchronized (mDataCollectionLock) {
              mLastBatteryLevel = (float) level / (float) scale;
          }
      }
  
      private final class SensorListener implements SensorEventListener {
          @Override
          public void onSensorChanged(SensorEvent event) {
              recordSensorEvent(event);
              recordAmbientBrightnessStats(event);
          }
  
          @Override
          public void onAccuracyChanged(Sensor sensor, int accuracy) {
  
          }
      }
  
      private final class DisplayListener implements DisplayManager.DisplayListener {
  
          @Override
          public void onDisplayAdded(int displayId) {
              // Ignore
          }
  
          @Override
          public void onDisplayRemoved(int displayId) {
              // Ignore
          }
  
          @Override
          public void onDisplayChanged(int displayId) {
              if (displayId == Display.DEFAULT_DISPLAY) {
                  updateColorSampling();
              }
          }
      }
  
      private final class SettingsObserver extends ContentObserver {
          public SettingsObserver(Handler handler) {
              super(handler);
          }
  
          @Override
          public void onChange(boolean selfChange, Uri uri) {
              if (DEBUG) {
                  Slog.v(TAG, "settings change " + uri);
              }
              if (mInjector.isBrightnessModeAutomatic(mContentResolver)) {
                  mBgHandler.obtainMessage(MSG_START_SENSOR_LISTENER).sendToTarget();
              } else {
                  mBgHandler.obtainMessage(MSG_STOP_SENSOR_LISTENER).sendToTarget();
              }
          }
      }
  
      private final class Receiver extends BroadcastReceiver {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (DEBUG) {
                  Slog.d(TAG, "Received " + intent.getAction());
              }
              String action = intent.getAction();
              if (Intent.ACTION_SHUTDOWN.equals(action)) {
                  stop();
                  scheduleWriteBrightnessTrackerState();
              } else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
                  int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
                  int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
                  if (level != -1 && scale != 0) {
                      batteryLevelChanged(level, scale);
                  }
              } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
                  mBgHandler.obtainMessage(MSG_STOP_SENSOR_LISTENER).sendToTarget();
              } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
                  mBgHandler.obtainMessage(MSG_START_SENSOR_LISTENER).sendToTarget();
              }
          }
      }
  
      private final class TrackerHandler extends Handler {
          public TrackerHandler(Looper looper) {
              super(looper, null, true /*async*/);
          }
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case MSG_BACKGROUND_START:
                      backgroundStart((float)msg.obj /*initial brightness*/);
                      break;
                  case MSG_BRIGHTNESS_CHANGED:
                      BrightnessChangeValues values = (BrightnessChangeValues) msg.obj;
                      boolean userInitiatedChange = (msg.arg1 == 1);
                      handleBrightnessChanged(values.brightness, userInitiatedChange,
                              values.powerBrightnessFactor, values.isUserSetBrightness,
                              values.isDefaultBrightnessConfig, values.timestamp);
                      break;
                  case MSG_START_SENSOR_LISTENER:
                      startSensorListener();
                      enableColorSampling();
                      break;
                  case MSG_STOP_SENSOR_LISTENER:
                      stopSensorListener();
                      disableColorSampling();
                      break;
              }
          }
      }
  
      private static class BrightnessChangeValues {
          final float brightness;
          final float powerBrightnessFactor;
          final boolean isUserSetBrightness;
          final boolean isDefaultBrightnessConfig;
          final long timestamp;
  
          BrightnessChangeValues(float brightness, float powerBrightnessFactor,
                  boolean isUserSetBrightness, boolean isDefaultBrightnessConfig,
                  long timestamp) {
              this.brightness = brightness;
              this.powerBrightnessFactor = powerBrightnessFactor;
              this.isUserSetBrightness = isUserSetBrightness;
              this.isDefaultBrightnessConfig = isDefaultBrightnessConfig;
              this.timestamp = timestamp;
          }
      }
  
      @VisibleForTesting
      static class Injector {
          public void registerSensorListener(Context context,
                  SensorEventListener sensorListener, Handler handler) {
              SensorManager sensorManager = context.getSystemService(SensorManager.class);
              Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
              sensorManager.registerListener(sensorListener,
                      lightSensor, SensorManager.SENSOR_DELAY_NORMAL, handler);
          }
  
          public void unregisterSensorListener(Context context, SensorEventListener sensorListener) {
              SensorManager sensorManager = context.getSystemService(SensorManager.class);
              sensorManager.unregisterListener(sensorListener);
          }
  
          public void registerBrightnessModeObserver(ContentResolver resolver,
                  ContentObserver settingsObserver) {
              resolver.registerContentObserver(Settings.System.getUriFor(
                      Settings.System.SCREEN_BRIGHTNESS_MODE),
                      false, settingsObserver, UserHandle.USER_ALL);
          }
  
          public void unregisterBrightnessModeObserver(Context context,
                  ContentObserver settingsObserver) {
              context.getContentResolver().unregisterContentObserver(settingsObserver);
          }
  
          public void registerReceiver(Context context,
                  BroadcastReceiver receiver, IntentFilter filter) {
              context.registerReceiver(receiver, filter);
          }
  
          public void unregisterReceiver(Context context,
                  BroadcastReceiver receiver) {
              context.unregisterReceiver(receiver);
          }
  
          public Handler getBackgroundHandler() {
              return BackgroundThread.getHandler();
          }
  
          public boolean isBrightnessModeAutomatic(ContentResolver resolver) {
              return Settings.System.getIntForUser(resolver, Settings.System.SCREEN_BRIGHTNESS_MODE,
                      Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT)
                      == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC;
          }
  
          public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue,
                  int userId) {
              return Settings.Secure.getIntForUser(resolver, setting, defaultValue, userId);
          }
  
          public AtomicFile getFile(String filename) {
              return new AtomicFile(new File(Environment.getDataSystemDeDirectory(), filename));
          }
  
          public long currentTimeMillis() {
              return System.currentTimeMillis();
          }
  
          public long elapsedRealtimeNanos() {
              return SystemClock.elapsedRealtimeNanos();
          }
  
          public int getUserSerialNumber(UserManager userManager, int userId) {
              return userManager.getUserSerialNumber(userId);
          }
  
          public int getUserId(UserManager userManager, int userSerialNumber) {
              return userManager.getUserHandle(userSerialNumber);
          }
  
          public int[] getProfileIds(UserManager userManager, int userId) {
              if (userManager != null) {
                  return userManager.getProfileIds(userId, false);
              } else {
                  return new int[]{userId};
              }
          }
  
          public ActivityManager.StackInfo getFocusedStack() throws RemoteException {
              return ActivityTaskManager.getService().getFocusedStackInfo();
          }
  
          public void scheduleIdleJob(Context context) {
              BrightnessIdleJob.scheduleJob(context);
          }
  
          public void cancelIdleJob(Context context) {
              BrightnessIdleJob.cancelJob(context);
          }
  
          public boolean isInteractive(Context context) {
              return context.getSystemService(PowerManager.class).isInteractive();
          }
  
          public int getNightDisplayColorTemperature(Context context) {
              return context.getSystemService(ColorDisplayManager.class)
                      .getNightDisplayColorTemperature();
          }
  
          public boolean isNightDisplayActivated(Context context) {
              return context.getSystemService(ColorDisplayManager.class).isNightDisplayActivated();
          }
  
          public DisplayedContentSample sampleColor(int noFramesToSample) {
              final DisplayManagerInternal displayManagerInternal =
                      LocalServices.getService(DisplayManagerInternal.class);
              return displayManagerInternal.getDisplayedContentSample(
                     Display.DEFAULT_DISPLAY, noFramesToSample, 0);
          }
  
          public float getFrameRate(Context context) {
              final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
              Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
              return display.getRefreshRate();
          }
  
          public DisplayedContentSamplingAttributes getSamplingAttributes() {
              final DisplayManagerInternal displayManagerInternal =
                      LocalServices.getService(DisplayManagerInternal.class);
              return displayManagerInternal.getDisplayedContentSamplingAttributes(
                      Display.DEFAULT_DISPLAY);
          }
  
          public boolean enableColorSampling(boolean enable, int noFrames) {
              final DisplayManagerInternal displayManagerInternal =
                      LocalServices.getService(DisplayManagerInternal.class);
              return displayManagerInternal.setDisplayedContentSamplingEnabled(
                      Display.DEFAULT_DISPLAY, enable, COLOR_SAMPLE_COMPONENT_MASK, noFrames);
          }
  
          public void registerDisplayListener(Context context,
                  DisplayManager.DisplayListener listener, Handler handler) {
              final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
              displayManager.registerDisplayListener(listener, handler);
          }
  
          public void unRegisterDisplayListener(Context context,
                  DisplayManager.DisplayListener listener) {
              final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
              displayManager.unregisterDisplayListener(listener);
          }
      }
  }
  