content: /*
   * Copyright (C) 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.internal.os;
  
  import android.os.BatteryStats;
  import android.os.Parcel;
  import android.os.StatFs;
  import android.os.SystemClock;
  import android.util.ArraySet;
  import android.util.Slog;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.ParseUtils;
  
  import java.io.File;
  import java.io.FilenameFilter;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Set;
  
  /**
   * BatteryStatsHistory encapsulates battery history files.
   * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
   * {@link #mActiveFile}.
   * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
   * current mActiveFile is closed and a new mActiveFile is open.
   * History files are under directory /data/system/battery-history/.
   * History files have name battery-history-<num>.bin. The file number <num> starts from zero and
   * grows sequentially.
   * The mActiveFile is always the highest numbered history file.
   * The lowest number file is always the oldest file.
   * The highest number file is always the newest file.
   * The file number grows sequentially and we never skip number.
   * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
   * the lowest numbered file is deleted and a new file is open.
   *
   * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
   * locks on BatteryStatsImpl object.
   */
  @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
  public class BatteryStatsHistory {
      private static final boolean DEBUG = false;
      private static final String TAG = "BatteryStatsHistory";
      public static final String HISTORY_DIR = "battery-history";
      public static final String FILE_SUFFIX = ".bin";
      private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;
  
      private final BatteryStatsImpl mStats;
      private final Parcel mHistoryBuffer;
      private final File mHistoryDir;
      /**
       * The active history file that the history buffer is backed up into.
       */
      private AtomicFile mActiveFile;
      /**
       * A list of history files with incremental indexes.
       */
      private final List<Integer> mFileNumbers = new ArrayList<>();
  
      /**
       * A list of small history parcels, used when BatteryStatsImpl object is created from
       * deserialization of a parcel, such as Settings app or checkin file.
       */
      private List<Parcel> mHistoryParcels = null;
  
      /**
       * When iterating history files, the current file index.
       */
      private int mCurrentFileIndex;
      /**
       * When iterating history files, the current file parcel.
       */
      private Parcel mCurrentParcel;
      /**
       * When iterating history file, the current parcel's Parcel.dataSize().
       */
      private int mCurrentParcelEnd;
      /**
       * When iterating history files, the current record count.
       */
      private int mRecordCount = 0;
      /**
       * Used when BatteryStatsImpl object is created from deserialization of a parcel,
       * such as Settings app or checkin file, to iterate over history parcels.
       */
      private int mParcelIndex = 0;
  
      /**
       * Constructor
       * @param stats BatteryStatsImpl object.
       * @param systemDir typically /data/system
       * @param historyBuffer The in-memory history buffer.
       */
      public BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer) {
          mStats = stats;
          mHistoryBuffer = historyBuffer;
          mHistoryDir = new File(systemDir, HISTORY_DIR);
          mHistoryDir.mkdirs();
          if (!mHistoryDir.exists()) {
              Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
          }
  
          final Set<Integer> dedup = new ArraySet<>();
          // scan directory, fill mFileNumbers and mActiveFile.
          mHistoryDir.listFiles(new FilenameFilter() {
              @Override
              public boolean accept(File dir, String name) {
                  final int b = name.lastIndexOf(FILE_SUFFIX);
                  if (b <= 0) {
                      return false;
                  }
                  final Integer c =
                          ParseUtils.parseInt(name.substring(0, b), -1);
                  if (c != -1) {
                      dedup.add(c);
                      return true;
                  } else {
                      return false;
                  }
              }
          });
          if (!dedup.isEmpty()) {
              mFileNumbers.addAll(dedup);
              Collections.sort(mFileNumbers);
              setActiveFile(mFileNumbers.get(mFileNumbers.size() - 1));
          } else {
              // No file found, default to have file 0.
              mFileNumbers.add(0);
              setActiveFile(0);
          }
      }
  
      /**
       * Used when BatteryStatsImpl object is created from deserialization of a parcel,
       * such as Settings app or checkin file.
       * @param stats BatteryStatsImpl object.
       * @param historyBuffer the history buffer inside BatteryStatsImpl
       */
      public BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer) {
          mStats = stats;
          mHistoryDir = null;
          mHistoryBuffer = historyBuffer;
      }
      /**
       * Set the active file that mHistoryBuffer is backed up into.
       *
       * @param fileNumber the history file that mHistoryBuffer is backed up into.
       */
      private void setActiveFile(int fileNumber) {
          mActiveFile = getFile(fileNumber);
          if (DEBUG) {
              Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath());
          }
      }
  
      /**
       * Create history AtomicFile from file number.
       * @param num file number.
       * @return AtomicFile object.
       */
      private AtomicFile getFile(int num) {
          return new AtomicFile(
                  new File(mHistoryDir,  num + FILE_SUFFIX));
      }
  
      /**
       * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
       * create next history file.
       */
      public void startNextFile() {
          if (mFileNumbers.isEmpty()) {
              Slog.wtf(TAG, "mFileNumbers should never be empty");
              return;
          }
          // The last number in mFileNumbers is the highest number. The next file number is highest
          // number plus one.
          final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1;
          mFileNumbers.add(next);
          setActiveFile(next);
  
          // if free disk space is less than 100MB, delete oldest history file.
          if (!hasFreeDiskSpace()) {
              int oldest = mFileNumbers.remove(0);
              getFile(oldest).delete();
          }
  
          // if there are more history files than allowed, delete oldest history files.
          // MAX_HISTORY_FILES can be updated by GService config at run time.
          while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) {
              int oldest = mFileNumbers.get(0);
              getFile(oldest).delete();
              mFileNumbers.remove(0);
          }
      }
  
      /**
       * Delete all existing history files. Active history file start from number 0 again.
       */
      public void resetAllFiles() {
          for (Integer i : mFileNumbers) {
              getFile(i).delete();
          }
          mFileNumbers.clear();
          mFileNumbers.add(0);
          setActiveFile(0);
      }
  
      /**
       * Start iterating history files and history buffer.
       * @return always return true.
       */
      public boolean startIteratingHistory() {
          mRecordCount = 0;
          mCurrentFileIndex = 0;
          mCurrentParcel = null;
          mCurrentParcelEnd = 0;
          mParcelIndex = 0;
          return true;
      }
  
      /**
       * Finish iterating history files and history buffer.
       */
      public void finishIteratingHistory() {
          // setDataPosition so mHistoryBuffer Parcel can be written.
          mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
          if (DEBUG) {
              Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
          }
      }
  
      /**
       * When iterating history files and history buffer, always start from the lowest numbered
       * history file, when reached the mActiveFile (highest numbered history file), do not read from
       * mActiveFile, read from history buffer instead because the buffer has more updated data.
       * @param out a history item.
       * @return The parcel that has next record. null if finished all history files and history
       *         buffer
       */
      public Parcel getNextParcel(BatteryStats.HistoryItem out) {
          if (mRecordCount == 0) {
              // reset out if it is the first record.
              out.clear();
          }
          ++mRecordCount;
  
          // First iterate through all records in current parcel.
          if (mCurrentParcel != null)
          {
              if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
                  // There are more records in current parcel.
                  return mCurrentParcel;
              } else if (mHistoryBuffer == mCurrentParcel) {
                  // finished iterate through all history files and history buffer.
                  return null;
              } else if (mHistoryParcels == null
                      || !mHistoryParcels.contains(mCurrentParcel)) {
                  // current parcel is from history file.
                  mCurrentParcel.recycle();
              }
          }
  
          // Try next available history file.
          // skip the last file because its data is in history buffer.
          while (mCurrentFileIndex < mFileNumbers.size() - 1) {
              mCurrentParcel = null;
              mCurrentParcelEnd = 0;
              final Parcel p = Parcel.obtain();
              AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++));
              if (readFileToParcel(p, file)) {
                  int bufSize = p.readInt();
                  int curPos = p.dataPosition();
                  mCurrentParcelEnd = curPos + bufSize;
                  mCurrentParcel = p;
                  if (curPos < mCurrentParcelEnd) {
                      return mCurrentParcel;
                  }
              } else {
                  p.recycle();
              }
          }
  
          // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
          // of a parcel, such as Settings app or checkin file.
          if (mHistoryParcels != null) {
              while (mParcelIndex < mHistoryParcels.size()) {
                  final Parcel p = mHistoryParcels.get(mParcelIndex++);
                  if (!skipHead(p)) {
                      continue;
                  }
                  final int bufSize = p.readInt();
                  final int curPos = p.dataPosition();
                  mCurrentParcelEnd = curPos + bufSize;
                  mCurrentParcel = p;
                  if (curPos < mCurrentParcelEnd) {
                      return mCurrentParcel;
                  }
              }
          }
  
          // finished iterator through history files (except the last one), now history buffer.
          if (mHistoryBuffer.dataSize() <= 0) {
              // buffer is empty.
              return null;
          }
          mHistoryBuffer.setDataPosition(0);
          mCurrentParcel = mHistoryBuffer;
          mCurrentParcelEnd = mCurrentParcel.dataSize();
          return mCurrentParcel;
      }
  
      /**
       * Read history file into a parcel.
       * @param out the Parcel read into.
       * @param file the File to read from.
       * @return true if success, false otherwise.
       */
      public boolean readFileToParcel(Parcel out, AtomicFile file) {
          byte[] raw = null;
          try {
              final long start = SystemClock.uptimeMillis();
              raw = file.readFully();
              if (DEBUG) {
                  Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
                          + " duration ms:" + (SystemClock.uptimeMillis() - start));
              }
          } catch(Exception e) {
              Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
              return false;
          }
          out.unmarshall(raw, 0, raw.length);
          out.setDataPosition(0);
          return skipHead(out);
      }
  
      /**
       * Skip the header part of history parcel.
       * @param p history parcel to skip head.
       * @return true if version match, false if not.
       */
      private boolean skipHead(Parcel p) {
          p.setDataPosition(0);
          final int version = p.readInt();
          if (version != mStats.VERSION) {
              return false;
          }
          // skip historyBaseTime field.
          p.readLong();
          return true;
      }
  
      /**
       * Read all history files and serialize into a big Parcel. This is to send history files to
       * Settings app since Settings app can not access /data/system directory.
       * Checkin file also call this method.
       * @param out the output parcel
       */
      public void writeToParcel(Parcel out) {
          final long start = SystemClock.uptimeMillis();
          out.writeInt(mFileNumbers.size() - 1);
          for(int i = 0;  i < mFileNumbers.size() - 1; i++) {
              AtomicFile file = getFile(mFileNumbers.get(i));
              byte[] raw = new byte[0];
              try {
                  raw = file.readFully();
              } catch(Exception e) {
                  Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
              }
              out.writeByteArray(raw);
          }
          if (DEBUG) {
              Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
          }
      }
  
      /**
       * This is for Settings app, when Settings app receives big history parcel, it call
       * this method to parse it into list of parcels.
       * Checkin file also call this method.
       * @param in the input parcel.
       */
      public void readFromParcel(Parcel in) {
          final long start = SystemClock.uptimeMillis();
          mHistoryParcels = new ArrayList<>();
          final int count = in.readInt();
          for(int i = 0; i < count; i++) {
              byte[] temp = in.createByteArray();
              if (temp.length == 0) {
                  continue;
              }
              Parcel p = Parcel.obtain();
              p.unmarshall(temp, 0, temp.length);
              p.setDataPosition(0);
              mHistoryParcels.add(p);
          }
          if (DEBUG) {
              Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start));
          }
      }
  
      /**
       * @return true if there is more than 100MB free disk space left.
       */
      private boolean hasFreeDiskSpace() {
          final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
          return stats.getAvailableBytes() > MIN_FREE_SPACE;
      }
  
      public List<Integer> getFilesNumbers() {
          return mFileNumbers;
      }
  
      public AtomicFile getActiveFile() {
          return mActiveFile;
      }
  
      /**
       * @return the total size of all history files and history buffer.
       */
      public int getHistoryUsedSize() {
          int ret = 0;
          for(int i = 0; i < mFileNumbers.size() - 1; i++) {
              ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
          }
          ret += mHistoryBuffer.dataSize();
          if (mHistoryParcels != null) {
              for(int i = 0; i < mHistoryParcels.size(); i++) {
                  ret += mHistoryParcels.get(i).dataSize();
              }
          }
          return ret;
      }
  }
  