content: /**
   * Copyright (C) 2014 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.usage;
  
  import static android.app.usage.UsageEvents.Event.DEVICE_SHUTDOWN;
  import static android.app.usage.UsageEvents.Event.DEVICE_STARTUP;
  import static android.app.usage.UsageStatsManager.INTERVAL_BEST;
  import static android.app.usage.UsageStatsManager.INTERVAL_COUNT;
  import static android.app.usage.UsageStatsManager.INTERVAL_DAILY;
  import static android.app.usage.UsageStatsManager.INTERVAL_MONTHLY;
  import static android.app.usage.UsageStatsManager.INTERVAL_WEEKLY;
  import static android.app.usage.UsageStatsManager.INTERVAL_YEARLY;
  
  import android.app.usage.ConfigurationStats;
  import android.app.usage.EventList;
  import android.app.usage.EventStats;
  import android.app.usage.TimeSparseArray;
  import android.app.usage.UsageEvents;
  import android.app.usage.UsageEvents.Event;
  import android.app.usage.UsageStats;
  import android.app.usage.UsageStatsManager;
  import android.content.Context;
  import android.content.res.Configuration;
  import android.os.SystemClock;
  import android.text.format.DateUtils;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.AtomicFile;
  import android.util.Slog;
  import android.util.SparseIntArray;
  
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.server.usage.UsageStatsDatabase.StatCombiner;
  
  import java.io.File;
  import java.io.IOException;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  
  /**
   * A per-user UsageStatsService. All methods are meant to be called with the main lock held
   * in UsageStatsService.
   */
  class UserUsageStatsService {
      private static final String TAG = "UsageStatsService";
      private static final boolean DEBUG = UsageStatsService.DEBUG;
      private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      private static final int sDateFormatFlags =
              DateUtils.FORMAT_SHOW_DATE
              | DateUtils.FORMAT_SHOW_TIME
              | DateUtils.FORMAT_SHOW_YEAR
              | DateUtils.FORMAT_NUMERIC_DATE;
  
      private final Context mContext;
      private final UsageStatsDatabase mDatabase;
      private final IntervalStats[] mCurrentStats;
      private boolean mStatsChanged = false;
      private final UnixCalendar mDailyExpiryDate;
      private final StatsUpdatedListener mListener;
      private final String mLogPrefix;
      private String mLastBackgroundedPackage;
      private final int mUserId;
  
      private static final long[] INTERVAL_LENGTH = new long[] {
              UnixCalendar.DAY_IN_MILLIS, UnixCalendar.WEEK_IN_MILLIS,
              UnixCalendar.MONTH_IN_MILLIS, UnixCalendar.YEAR_IN_MILLIS
      };
  
      interface StatsUpdatedListener {
          void onStatsUpdated();
          void onStatsReloaded();
          /**
           * Callback that a system update was detected
           * @param mUserId user that needs to be initialized
           */
          void onNewUpdate(int mUserId);
      }
  
      UserUsageStatsService(Context context, int userId, File usageStatsDir,
              StatsUpdatedListener listener) {
          mContext = context;
          mDailyExpiryDate = new UnixCalendar(0);
          mDatabase = new UsageStatsDatabase(usageStatsDir);
          mCurrentStats = new IntervalStats[INTERVAL_COUNT];
          mListener = listener;
          mLogPrefix = "User[" + Integer.toString(userId) + "] ";
          mUserId = userId;
      }
  
      void init(final long currentTimeMillis) {
          mDatabase.init(currentTimeMillis);
  
          int nullCount = 0;
          for (int i = 0; i < mCurrentStats.length; i++) {
              mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
              if (mCurrentStats[i] == null) {
                  // Find out how many intervals we don't have data for.
                  // Ideally it should be all or none.
                  nullCount++;
              }
          }
  
          if (nullCount > 0) {
              if (nullCount != mCurrentStats.length) {
                  // This is weird, but we shouldn't fail if something like this
                  // happens.
                  Slog.w(TAG, mLogPrefix + "Some stats have no latest available");
              } else {
                  // This must be first boot.
              }
  
              // By calling loadActiveStats, we will
              // generate new stats for each bucket.
              loadActiveStats(currentTimeMillis);
          } else {
              // Set up the expiry date to be one day from the latest daily stat.
              // This may actually be today and we will rollover on the first event
              // that is reported.
              updateRolloverDeadline();
          }
  
          // During system reboot, add a DEVICE_SHUTDOWN event to the end of event list, the timestamp
          // is last time UsageStatsDatabase is persisted to disk or the last event's time whichever
          // is higher (because the file system timestamp is round down to integral seconds).
          // Also add a DEVICE_STARTUP event with current system timestamp.
          final IntervalStats currentDailyStats = mCurrentStats[INTERVAL_DAILY];
          if (currentDailyStats != null) {
              final Event shutdownEvent = new Event(DEVICE_SHUTDOWN,
                      Math.max(currentDailyStats.lastTimeSaved, currentDailyStats.endTime));
              shutdownEvent.mPackage = Event.DEVICE_EVENT_PACKAGE_NAME;
              currentDailyStats.addEvent(shutdownEvent);
              final Event startupEvent = new Event(DEVICE_STARTUP, System.currentTimeMillis());
              startupEvent.mPackage = Event.DEVICE_EVENT_PACKAGE_NAME;
              currentDailyStats.addEvent(startupEvent);
          }
  
          if (mDatabase.isNewUpdate()) {
              notifyNewUpdate();
          }
      }
  
      void onTimeChanged(long oldTime, long newTime) {
          persistActiveStats();
          mDatabase.onTimeChanged(newTime - oldTime);
          loadActiveStats(newTime);
      }
  
      void reportEvent(Event event) {
          if (DEBUG) {
              Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
                      + "[" + event.mTimeStamp + "]: "
                      + eventToString(event.mEventType));
          }
  
          if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
              // Need to rollover
              rolloverStats(event.mTimeStamp);
          }
  
          final IntervalStats currentDailyStats = mCurrentStats[INTERVAL_DAILY];
  
          final Configuration newFullConfig = event.mConfiguration;
          if (event.mEventType == Event.CONFIGURATION_CHANGE
                  && currentDailyStats.activeConfiguration != null) {
              // Make the event configuration a delta.
              event.mConfiguration = Configuration.generateDelta(
                      currentDailyStats.activeConfiguration, newFullConfig);
          }
  
          if (event.mEventType != Event.SYSTEM_INTERACTION
                  // ACTIVITY_DESTROYED is a private event. If there is preceding ACTIVITY_STOPPED
                  // ACTIVITY_DESTROYED will be dropped. Otherwise it will be converted to
                  // ACTIVITY_STOPPED.
                  && event.mEventType != Event.ACTIVITY_DESTROYED
                  // FLUSH_TO_DISK is a private event.
                  && event.mEventType != Event.FLUSH_TO_DISK
                  // DEVICE_SHUTDOWN is added to event list after reboot.
                  && event.mEventType != Event.DEVICE_SHUTDOWN) {
              currentDailyStats.addEvent(event);
          }
  
          boolean incrementAppLaunch = false;
          if (event.mEventType == Event.ACTIVITY_RESUMED) {
              if (event.mPackage != null && !event.mPackage.equals(mLastBackgroundedPackage)) {
                  incrementAppLaunch = true;
              }
          } else if (event.mEventType == Event.ACTIVITY_PAUSED) {
              if (event.mPackage != null) {
                  mLastBackgroundedPackage = event.mPackage;
              }
          }
  
          for (IntervalStats stats : mCurrentStats) {
              switch (event.mEventType) {
                  case Event.CONFIGURATION_CHANGE: {
                      stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
                  } break;
                  case Event.CHOOSER_ACTION: {
                      stats.updateChooserCounts(event.mPackage, event.mContentType, event.mAction);
                      String[] annotations = event.mContentAnnotations;
                      if (annotations != null) {
                          for (String annotation : annotations) {
                              stats.updateChooserCounts(event.mPackage, annotation, event.mAction);
                          }
                      }
                  } break;
                  case Event.SCREEN_INTERACTIVE: {
                      stats.updateScreenInteractive(event.mTimeStamp);
                  } break;
                  case Event.SCREEN_NON_INTERACTIVE: {
                      stats.updateScreenNonInteractive(event.mTimeStamp);
                  } break;
                  case Event.KEYGUARD_SHOWN: {
                      stats.updateKeyguardShown(event.mTimeStamp);
                  } break;
                  case Event.KEYGUARD_HIDDEN: {
                      stats.updateKeyguardHidden(event.mTimeStamp);
                  } break;
                  default: {
                      stats.update(event.mPackage, event.getClassName(),
                              event.mTimeStamp, event.mEventType, event.mInstanceId);
                      if (incrementAppLaunch) {
                          stats.incrementAppLaunchCount(event.mPackage);
                      }
                  } break;
              }
          }
  
          notifyStatsChanged();
      }
  
      private static final StatCombiner<UsageStats> sUsageStatsCombiner =
              new StatCombiner<UsageStats>() {
                  @Override
                  public void combine(IntervalStats stats, boolean mutable,
                                      List<UsageStats> accResult) {
                      if (!mutable) {
                          accResult.addAll(stats.packageStats.values());
                          return;
                      }
  
                      final int statCount = stats.packageStats.size();
                      for (int i = 0; i < statCount; i++) {
                          accResult.add(new UsageStats(stats.packageStats.valueAt(i)));
                      }
                  }
              };
  
      private static final StatCombiner<ConfigurationStats> sConfigStatsCombiner =
              new StatCombiner<ConfigurationStats>() {
                  @Override
                  public void combine(IntervalStats stats, boolean mutable,
                                      List<ConfigurationStats> accResult) {
                      if (!mutable) {
                          accResult.addAll(stats.configurations.values());
                          return;
                      }
  
                      final int configCount = stats.configurations.size();
                      for (int i = 0; i < configCount; i++) {
                          accResult.add(new ConfigurationStats(stats.configurations.valueAt(i)));
                      }
                  }
              };
  
      private static final StatCombiner<EventStats> sEventStatsCombiner =
              new StatCombiner<EventStats>() {
                  @Override
                  public void combine(IntervalStats stats, boolean mutable,
                          List<EventStats> accResult) {
                      stats.addEventStatsTo(accResult);
                  }
              };
  
      /**
       * Generic query method that selects the appropriate IntervalStats for the specified time range
       * and bucket, then calls the {@link com.android.server.usage.UsageStatsDatabase.StatCombiner}
       * provided to select the stats to use from the IntervalStats object.
       */
      private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime,
              StatCombiner<T> combiner) {
          if (intervalType == INTERVAL_BEST) {
              intervalType = mDatabase.findBestFitBucket(beginTime, endTime);
              if (intervalType < 0) {
                  // Nothing saved to disk yet, so every stat is just as equal (no rollover has
                  // occurred.
                  intervalType = INTERVAL_DAILY;
              }
          }
  
          if (intervalType < 0 || intervalType >= mCurrentStats.length) {
              if (DEBUG) {
                  Slog.d(TAG, mLogPrefix + "Bad intervalType used " + intervalType);
              }
              return null;
          }
  
          final IntervalStats currentStats = mCurrentStats[intervalType];
  
          if (DEBUG) {
              Slog.d(TAG, mLogPrefix + "SELECT * FROM " + intervalType + " WHERE beginTime >= "
                      + beginTime + " AND endTime < " + endTime);
          }
  
          if (beginTime >= currentStats.endTime) {
              if (DEBUG) {
                  Slog.d(TAG, mLogPrefix + "Requesting stats after " + beginTime + " but latest is "
                          + currentStats.endTime);
              }
              // Nothing newer available.
              return null;
          }
  
          // Truncate the endTime to just before the in-memory stats. Then, we'll append the
          // in-memory stats to the results (if necessary) so as to avoid writing to disk too
          // often.
          final long truncatedEndTime = Math.min(currentStats.beginTime, endTime);
  
          // Get the stats from disk.
          List<T> results = mDatabase.queryUsageStats(intervalType, beginTime,
                  truncatedEndTime, combiner);
          if (DEBUG) {
              Slog.d(TAG, "Got " + (results != null ? results.size() : 0) + " results from disk");
              Slog.d(TAG, "Current stats beginTime=" + currentStats.beginTime +
                      " endTime=" + currentStats.endTime);
          }
  
          // Now check if the in-memory stats match the range and add them if they do.
          if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
              if (DEBUG) {
                  Slog.d(TAG, mLogPrefix + "Returning in-memory stats");
              }
  
              if (results == null) {
                  results = new ArrayList<>();
              }
              combiner.combine(currentStats, true, results);
          }
  
          if (DEBUG) {
              Slog.d(TAG, mLogPrefix + "Results: " + (results != null ? results.size() : 0));
          }
          return results;
      }
  
      List<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime) {
          return queryStats(bucketType, beginTime, endTime, sUsageStatsCombiner);
      }
  
      List<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime) {
          return queryStats(bucketType, beginTime, endTime, sConfigStatsCombiner);
      }
  
      List<EventStats> queryEventStats(int bucketType, long beginTime, long endTime) {
          return queryStats(bucketType, beginTime, endTime, sEventStatsCombiner);
      }
  
      UsageEvents queryEvents(final long beginTime, final long endTime,
                              boolean obfuscateInstantApps) {
          final ArraySet<String> names = new ArraySet<>();
          List<Event> results = queryStats(INTERVAL_DAILY,
                  beginTime, endTime, new StatCombiner<Event>() {
                      @Override
                      public void combine(IntervalStats stats, boolean mutable,
                              List<Event> accumulatedResult) {
                          final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
                          final int size = stats.events.size();
                          for (int i = startIndex; i < size; i++) {
                              if (stats.events.get(i).mTimeStamp >= endTime) {
                                  return;
                              }
  
                              Event event = stats.events.get(i);
                              if (obfuscateInstantApps) {
                                  event = event.getObfuscatedIfInstantApp();
                              }
                              if (event.mPackage != null) {
                                  names.add(event.mPackage);
                              }
                              if (event.mClass != null) {
                                  names.add(event.mClass);
                              }
                              if (event.mTaskRootPackage != null) {
                                  names.add(event.mTaskRootPackage);
                              }
                              if (event.mTaskRootClass != null) {
                                  names.add(event.mTaskRootClass);
                              }
                              accumulatedResult.add(event);
                          }
                      }
                  });
  
          if (results == null || results.isEmpty()) {
              return null;
          }
  
          String[] table = names.toArray(new String[names.size()]);
          Arrays.sort(table);
          return new UsageEvents(results, table, true);
      }
  
      UsageEvents queryEventsForPackage(final long beginTime, final long endTime,
              final String packageName, boolean includeTaskRoot) {
          final ArraySet<String> names = new ArraySet<>();
          names.add(packageName);
          final List<Event> results = queryStats(INTERVAL_DAILY,
                  beginTime, endTime, (stats, mutable, accumulatedResult) -> {
                      final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
                      final int size = stats.events.size();
                      for (int i = startIndex; i < size; i++) {
                          if (stats.events.get(i).mTimeStamp >= endTime) {
                              return;
                          }
  
                          final Event event = stats.events.get(i);
                          if (!packageName.equals(event.mPackage)) {
                              continue;
                          }
                          if (event.mClass != null) {
                              names.add(event.mClass);
                          }
                          if (includeTaskRoot && event.mTaskRootPackage != null) {
                              names.add(event.mTaskRootPackage);
                          }
                          if (includeTaskRoot && event.mTaskRootClass != null) {
                              names.add(event.mTaskRootClass);
                          }
                          accumulatedResult.add(event);
                      }
                  });
  
          if (results == null || results.isEmpty()) {
              return null;
          }
  
          final String[] table = names.toArray(new String[names.size()]);
          Arrays.sort(table);
          return new UsageEvents(results, table, includeTaskRoot);
      }
  
      void persistActiveStats() {
          if (mStatsChanged) {
              Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
              try {
                  for (int i = 0; i < mCurrentStats.length; i++) {
                      mDatabase.putUsageStats(i, mCurrentStats[i]);
                  }
                  mStatsChanged = false;
              } catch (IOException e) {
                  Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
              }
          }
      }
  
      private void rolloverStats(final long currentTimeMillis) {
          final long startTime = SystemClock.elapsedRealtime();
          Slog.i(TAG, mLogPrefix + "Rolling over usage stats");
  
          // Finish any ongoing events with an END_OF_DAY or ROLLOVER_FOREGROUND_SERVICE event.
          // Make a note of which components need a new CONTINUE_PREVIOUS_DAY or
          // CONTINUING_FOREGROUND_SERVICE entry.
          final Configuration previousConfig =
                  mCurrentStats[INTERVAL_DAILY].activeConfiguration;
          ArraySet<String> continuePkgs = new ArraySet<>();
          ArrayMap<String, SparseIntArray> continueActivity =
                  new ArrayMap<>();
          ArrayMap<String, ArrayMap<String, Integer>> continueForegroundService =
                  new ArrayMap<>();
          for (IntervalStats stat : mCurrentStats) {
              final int pkgCount = stat.packageStats.size();
              for (int i = 0; i < pkgCount; i++) {
                  final UsageStats pkgStats = stat.packageStats.valueAt(i);
                  if (pkgStats.mActivities.size() > 0
                          || !pkgStats.mForegroundServices.isEmpty()) {
                      if (pkgStats.mActivities.size() > 0) {
                          continueActivity.put(pkgStats.mPackageName,
                                  pkgStats.mActivities);
                          stat.update(pkgStats.mPackageName, null,
                                  mDailyExpiryDate.getTimeInMillis() - 1,
                                  Event.END_OF_DAY, 0);
                      }
                      if (!pkgStats.mForegroundServices.isEmpty()) {
                          continueForegroundService.put(pkgStats.mPackageName,
                                  pkgStats.mForegroundServices);
                          stat.update(pkgStats.mPackageName, null,
                                  mDailyExpiryDate.getTimeInMillis() - 1,
                                  Event.ROLLOVER_FOREGROUND_SERVICE, 0);
                      }
                      continuePkgs.add(pkgStats.mPackageName);
                      notifyStatsChanged();
                  }
              }
  
              stat.updateConfigurationStats(null,
                      mDailyExpiryDate.getTimeInMillis() - 1);
              stat.commitTime(mDailyExpiryDate.getTimeInMillis() - 1);
          }
  
          persistActiveStats();
          mDatabase.prune(currentTimeMillis);
          loadActiveStats(currentTimeMillis);
  
          final int continueCount = continuePkgs.size();
          for (int i = 0; i < continueCount; i++) {
              String pkgName = continuePkgs.valueAt(i);
              final long beginTime = mCurrentStats[INTERVAL_DAILY].beginTime;
              for (IntervalStats stat : mCurrentStats) {
                  if (continueActivity.containsKey(pkgName)) {
                      final SparseIntArray eventMap =
                              continueActivity.get(pkgName);
                      final int size = eventMap.size();
                      for (int j = 0; j < size; j++) {
                          stat.update(pkgName, null, beginTime,
                                  eventMap.valueAt(j), eventMap.keyAt(j));
                      }
                  }
                  if (continueForegroundService.containsKey(pkgName)) {
                      final ArrayMap<String, Integer> eventMap =
                              continueForegroundService.get(pkgName);
                      final int size = eventMap.size();
                      for (int j = 0; j < size; j++) {
                          stat.update(pkgName, eventMap.keyAt(j), beginTime,
                                  eventMap.valueAt(j), 0);
                      }
                  }
                  stat.updateConfigurationStats(previousConfig, beginTime);
                  notifyStatsChanged();
              }
          }
          persistActiveStats();
  
          final long totalTime = SystemClock.elapsedRealtime() - startTime;
          Slog.i(TAG, mLogPrefix + "Rolling over usage stats complete. Took " + totalTime
                  + " milliseconds");
      }
  
      private void notifyStatsChanged() {
          if (!mStatsChanged) {
              mStatsChanged = true;
              mListener.onStatsUpdated();
          }
      }
  
      private void notifyNewUpdate() {
          mListener.onNewUpdate(mUserId);
      }
  
      private void loadActiveStats(final long currentTimeMillis) {
          for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
              final IntervalStats stats = mDatabase.getLatestUsageStats(intervalType);
              if (stats != null
                      && currentTimeMillis < stats.beginTime + INTERVAL_LENGTH[intervalType]) {
                  if (DEBUG) {
                      Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
                              sDateFormat.format(stats.beginTime) + "(" + stats.beginTime +
                              ") for interval " + intervalType);
                  }
                  mCurrentStats[intervalType] = stats;
              } else {
                  // No good fit remains.
                  if (DEBUG) {
                      Slog.d(TAG, "Creating new stats @ " +
                              sDateFormat.format(currentTimeMillis) + "(" +
                              currentTimeMillis + ") for interval " + intervalType);
                  }
  
                  mCurrentStats[intervalType] = new IntervalStats();
                  mCurrentStats[intervalType].beginTime = currentTimeMillis;
                  mCurrentStats[intervalType].endTime = currentTimeMillis + 1;
              }
          }
  
          mStatsChanged = false;
          updateRolloverDeadline();
  
          // Tell the listener that the stats reloaded, which may have changed idle states.
          mListener.onStatsReloaded();
      }
  
      private void updateRolloverDeadline() {
          mDailyExpiryDate.setTimeInMillis(
                  mCurrentStats[INTERVAL_DAILY].beginTime);
          mDailyExpiryDate.addDays(1);
          Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
                  sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
                  mDailyExpiryDate.getTimeInMillis() + ")");
      }
  
      //
      // -- DUMP related methods --
      //
  
      void checkin(final IndentingPrintWriter pw) {
          mDatabase.checkinDailyFiles(new UsageStatsDatabase.CheckinAction() {
              @Override
              public boolean checkin(IntervalStats stats) {
                  printIntervalStats(pw, stats, false, false, null);
                  return true;
              }
          });
      }
  
      void dump(IndentingPrintWriter pw, String pkg) {
          dump(pw, pkg, false);
      }
      void dump(IndentingPrintWriter pw, String pkg, boolean compact) {
          printLast24HrEvents(pw, !compact, pkg);
          for (int interval = 0; interval < mCurrentStats.length; interval++) {
              pw.print("In-memory ");
              pw.print(intervalToString(interval));
              pw.println(" stats");
              printIntervalStats(pw, mCurrentStats[interval], !compact, true, pkg);
          }
          mDatabase.dump(pw, compact);
      }
  
      void dumpDatabaseInfo(IndentingPrintWriter ipw) {
          mDatabase.dump(ipw, false);
      }
  
      void dumpFile(IndentingPrintWriter ipw, String[] args) {
          if (args == null || args.length == 0) {
              // dump all files for every interval for specified user
              final int numIntervals = mDatabase.mSortedStatFiles.length;
              for (int interval = 0; interval < numIntervals; interval++) {
                  ipw.println("interval=" + intervalToString(interval));
                  ipw.increaseIndent();
                  dumpFileDetailsForInterval(ipw, interval);
                  ipw.decreaseIndent();
              }
          } else {
              final int interval;
              try {
                  final int intervalValue = stringToInterval(args[0]);
                  if (intervalValue == -1) {
                      interval = Integer.valueOf(args[0]);
                  } else {
                      interval = intervalValue;
                  }
              } catch (NumberFormatException nfe) {
                  ipw.println("invalid interval specified.");
                  return;
              }
              if (interval < 0 || interval >= mDatabase.mSortedStatFiles.length) {
                  ipw.println("the specified interval does not exist.");
                  return;
              }
              if (args.length == 1) {
                  // dump all files in the specified interval
                  dumpFileDetailsForInterval(ipw, interval);
              } else {
                  // dump details only for the specified filename
                  final long filename;
                  try {
                      filename = Long.valueOf(args[1]);
                  } catch (NumberFormatException nfe) {
                      ipw.println("invalid filename specified.");
                      return;
                  }
                  final IntervalStats stats = mDatabase.readIntervalStatsForFile(interval, filename);
                  if (stats == null) {
                      ipw.println("the specified filename does not exist.");
                      return;
                  }
                  dumpFileDetails(ipw, stats, Long.valueOf(args[1]));
              }
          }
      }
  
      private void dumpFileDetailsForInterval(IndentingPrintWriter ipw, int interval) {
          final TimeSparseArray<AtomicFile> files = mDatabase.mSortedStatFiles[interval];
          final int numFiles = files.size();
          for (int i = 0; i < numFiles; i++) {
              final long filename = files.keyAt(i);
              final IntervalStats stats = mDatabase.readIntervalStatsForFile(interval, filename);
              dumpFileDetails(ipw, stats, filename);
              ipw.println();
          }
      }
  
      private void dumpFileDetails(IndentingPrintWriter ipw, IntervalStats stats, long filename) {
          ipw.println("file=" + filename);
          ipw.increaseIndent();
          printIntervalStats(ipw, stats, false, false, null);
          ipw.decreaseIndent();
      }
  
      static String formatDateTime(long dateTime, boolean pretty) {
          if (pretty) {
              return "\"" + sDateFormat.format(dateTime)+ "\"";
          }
          return Long.toString(dateTime);
      }
  
      private String formatElapsedTime(long elapsedTime, boolean pretty) {
          if (pretty) {
              return "\"" + DateUtils.formatElapsedTime(elapsedTime / 1000) + "\"";
          }
          return Long.toString(elapsedTime);
      }
  
  
      void printEvent(IndentingPrintWriter pw, Event event, boolean prettyDates) {
          pw.printPair("time", formatDateTime(event.mTimeStamp, prettyDates));
          pw.printPair("type", eventToString(event.mEventType));
          pw.printPair("package", event.mPackage);
          if (event.mClass != null) {
              pw.printPair("class", event.mClass);
          }
          if (event.mConfiguration != null) {
              pw.printPair("config", Configuration.resourceQualifierString(event.mConfiguration));
          }
          if (event.mShortcutId != null) {
              pw.printPair("shortcutId", event.mShortcutId);
          }
          if (event.mEventType == Event.STANDBY_BUCKET_CHANGED) {
              pw.printPair("standbyBucket", event.getStandbyBucket());
              pw.printPair("reason", UsageStatsManager.reasonToString(event.getStandbyReason()));
          } else if (event.mEventType == Event.ACTIVITY_RESUMED
                  || event.mEventType == Event.ACTIVITY_PAUSED
                  || event.mEventType == Event.ACTIVITY_STOPPED) {
              pw.printPair("instanceId", event.getInstanceId());
          }
  
          if (event.getTaskRootPackageName() != null) {
              pw.printPair("taskRootPackage", event.getTaskRootPackageName());
          }
  
          if (event.getTaskRootClassName() != null) {
              pw.printPair("taskRootClass", event.getTaskRootClassName());
          }
  
          if (event.mNotificationChannelId != null) {
              pw.printPair("channelId", event.mNotificationChannelId);
          }
          pw.printHexPair("flags", event.mFlags);
          pw.println();
      }
  
      void printLast24HrEvents(IndentingPrintWriter pw, boolean prettyDates, final String pkg) {
          final long endTime = System.currentTimeMillis();
          UnixCalendar yesterday = new UnixCalendar(endTime);
          yesterday.addDays(-1);
  
          final long beginTime = yesterday.getTimeInMillis();
  
          List<Event> events = queryStats(INTERVAL_DAILY,
                  beginTime, endTime, new StatCombiner<Event>() {
                      @Override
                      public void combine(IntervalStats stats, boolean mutable,
                              List<Event> accumulatedResult) {
                          final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
                          final int size = stats.events.size();
                          for (int i = startIndex; i < size; i++) {
                              if (stats.events.get(i).mTimeStamp >= endTime) {
                                  return;
                              }
  
                              Event event = stats.events.get(i);
                              if (pkg != null && !pkg.equals(event.mPackage)) {
                                  continue;
                              }
                              accumulatedResult.add(event);
                          }
                      }
                  });
  
          pw.print("Last 24 hour events (");
          if (prettyDates) {
              pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
                      beginTime, endTime, sDateFormatFlags) + "\"");
          } else {
              pw.printPair("beginTime", beginTime);
              pw.printPair("endTime", endTime);
          }
          pw.println(")");
          if (events != null) {
              pw.increaseIndent();
              for (Event event : events) {
                  printEvent(pw, event, prettyDates);
              }
              pw.decreaseIndent();
          }
      }
  
      void printEventAggregation(IndentingPrintWriter pw, String label,
              IntervalStats.EventTracker tracker, boolean prettyDates) {
          if (tracker.count != 0 || tracker.duration != 0) {
              pw.print(label);
              pw.print(": ");
              pw.print(tracker.count);
              pw.print("x for ");
              pw.print(formatElapsedTime(tracker.duration, prettyDates));
              if (tracker.curStartTime != 0) {
                  pw.print(" (now running, started at ");
                  formatDateTime(tracker.curStartTime, prettyDates);
                  pw.print(")");
              }
              pw.println();
          }
      }
  
      void printIntervalStats(IndentingPrintWriter pw, IntervalStats stats,
              boolean prettyDates, boolean skipEvents, String pkg) {
          if (prettyDates) {
              pw.printPair("timeRange", "\"" + DateUtils.formatDateRange(mContext,
                      stats.beginTime, stats.endTime, sDateFormatFlags) + "\"");
          } else {
              pw.printPair("beginTime", stats.beginTime);
              pw.printPair("endTime", stats.endTime);
          }
          pw.println();
          pw.increaseIndent();
          pw.println("packages");
          pw.increaseIndent();
          final ArrayMap<String, UsageStats> pkgStats = stats.packageStats;
          final int pkgCount = pkgStats.size();
          for (int i = 0; i < pkgCount; i++) {
              final UsageStats usageStats = pkgStats.valueAt(i);
              if (pkg != null && !pkg.equals(usageStats.mPackageName)) {
                  continue;
              }
              pw.printPair("package", usageStats.mPackageName);
              pw.printPair("totalTimeUsed",
                      formatElapsedTime(usageStats.mTotalTimeInForeground, prettyDates));
              pw.printPair("lastTimeUsed", formatDateTime(usageStats.mLastTimeUsed, prettyDates));
              pw.printPair("totalTimeVisible",
                      formatElapsedTime(usageStats.mTotalTimeVisible, prettyDates));
              pw.printPair("lastTimeVisible",
                      formatDateTime(usageStats.mLastTimeVisible, prettyDates));
              pw.printPair("totalTimeFS",
                      formatElapsedTime(usageStats.mTotalTimeForegroundServiceUsed, prettyDates));
              pw.printPair("lastTimeFS",
                      formatDateTime(usageStats.mLastTimeForegroundServiceUsed, prettyDates));
              pw.printPair("appLaunchCount", usageStats.mAppLaunchCount);
              pw.println();
          }
          pw.decreaseIndent();
  
          pw.println();
          pw.println("ChooserCounts");
          pw.increaseIndent();
          for (UsageStats usageStats : pkgStats.values()) {
              if (pkg != null && !pkg.equals(usageStats.mPackageName)) {
                  continue;
              }
              pw.printPair("package", usageStats.mPackageName);
              if (usageStats.mChooserCounts != null) {
                  final int chooserCountSize = usageStats.mChooserCounts.size();
                  for (int i = 0; i < chooserCountSize; i++) {
                      final String action = usageStats.mChooserCounts.keyAt(i);
                      final ArrayMap<String, Integer> counts = usageStats.mChooserCounts.valueAt(i);
                      final int annotationSize = counts.size();
                      for (int j = 0; j < annotationSize; j++) {
                          final String key = counts.keyAt(j);
                          final int count = counts.valueAt(j);
                          if (count != 0) {
                              pw.printPair("ChooserCounts", action + ":" + key + " is " +
                                      Integer.toString(count));
                              pw.println();
                          }
                      }
                  }
              }
              pw.println();
          }
          pw.decreaseIndent();
  
          if (pkg == null) {
              pw.println("configurations");
              pw.increaseIndent();
              final ArrayMap<Configuration, ConfigurationStats> configStats = stats.configurations;
              final int configCount = configStats.size();
              for (int i = 0; i < configCount; i++) {
                  final ConfigurationStats config = configStats.valueAt(i);
                  pw.printPair("config", Configuration.resourceQualifierString(
                          config.mConfiguration));
                  pw.printPair("totalTime", formatElapsedTime(config.mTotalTimeActive, prettyDates));
                  pw.printPair("lastTime", formatDateTime(config.mLastTimeActive, prettyDates));
                  pw.printPair("count", config.mActivationCount);
                  pw.println();
              }
              pw.decreaseIndent();
              pw.println("event aggregations");
              pw.increaseIndent();
              printEventAggregation(pw, "screen-interactive", stats.interactiveTracker,
                      prettyDates);
              printEventAggregation(pw, "screen-non-interactive", stats.nonInteractiveTracker,
                      prettyDates);
              printEventAggregation(pw, "keyguard-shown", stats.keyguardShownTracker,
                      prettyDates);
              printEventAggregation(pw, "keyguard-hidden", stats.keyguardHiddenTracker,
                      prettyDates);
              pw.decreaseIndent();
          }
  
          // The last 24 hours of events is already printed in the non checkin dump
          // No need to repeat here.
          if (!skipEvents) {
              pw.println("events");
              pw.increaseIndent();
              final EventList events = stats.events;
              final int eventCount = events != null ? events.size() : 0;
              for (int i = 0; i < eventCount; i++) {
                  final Event event = events.get(i);
                  if (pkg != null && !pkg.equals(event.mPackage)) {
                      continue;
                  }
                  printEvent(pw, event, prettyDates);
              }
              pw.decreaseIndent();
          }
          pw.decreaseIndent();
      }
  
      public static String intervalToString(int interval) {
          switch (interval) {
              case INTERVAL_DAILY:
                  return "daily";
              case INTERVAL_WEEKLY:
                  return "weekly";
              case INTERVAL_MONTHLY:
                  return "monthly";
              case INTERVAL_YEARLY:
                  return "yearly";
              default:
                  return "?";
          }
      }
  
      private static int stringToInterval(String interval) {
          switch (interval.toLowerCase()) {
              case "daily":
                  return INTERVAL_DAILY;
              case "weekly":
                  return INTERVAL_WEEKLY;
              case "monthly":
                  return INTERVAL_MONTHLY;
              case "yearly":
                  return INTERVAL_YEARLY;
              default:
                  return -1;
          }
      }
  
      private static String eventToString(int eventType) {
          switch (eventType) {
              case Event.NONE:
                  return "NONE";
              case Event.ACTIVITY_PAUSED:
                  return "ACTIVITY_PAUSED";
              case Event.ACTIVITY_RESUMED:
                  return "ACTIVITY_RESUMED";
              case Event.FOREGROUND_SERVICE_START:
                  return "FOREGROUND_SERVICE_START";
              case Event.FOREGROUND_SERVICE_STOP:
                  return "FOREGROUND_SERVICE_STOP";
              case Event.ACTIVITY_STOPPED:
                  return "ACTIVITY_STOPPED";
              case Event.END_OF_DAY:
                  return "END_OF_DAY";
              case Event.ROLLOVER_FOREGROUND_SERVICE:
                  return "ROLLOVER_FOREGROUND_SERVICE";
              case Event.CONTINUE_PREVIOUS_DAY:
                  return "CONTINUE_PREVIOUS_DAY";
              case Event.CONTINUING_FOREGROUND_SERVICE:
                  return "CONTINUING_FOREGROUND_SERVICE";
              case Event.CONFIGURATION_CHANGE:
                  return "CONFIGURATION_CHANGE";
              case Event.SYSTEM_INTERACTION:
                  return "SYSTEM_INTERACTION";
              case Event.USER_INTERACTION:
                  return "USER_INTERACTION";
              case Event.SHORTCUT_INVOCATION:
                  return "SHORTCUT_INVOCATION";
              case Event.CHOOSER_ACTION:
                  return "CHOOSER_ACTION";
              case Event.NOTIFICATION_SEEN:
                  return "NOTIFICATION_SEEN";
              case Event.STANDBY_BUCKET_CHANGED:
                  return "STANDBY_BUCKET_CHANGED";
              case Event.NOTIFICATION_INTERRUPTION:
                  return "NOTIFICATION_INTERRUPTION";
              case Event.SLICE_PINNED:
                  return "SLICE_PINNED";
              case Event.SLICE_PINNED_PRIV:
                  return "SLICE_PINNED_PRIV";
              case Event.SCREEN_INTERACTIVE:
                  return "SCREEN_INTERACTIVE";
              case Event.SCREEN_NON_INTERACTIVE:
                  return "SCREEN_NON_INTERACTIVE";
              case Event.KEYGUARD_SHOWN:
                  return "KEYGUARD_SHOWN";
              case Event.KEYGUARD_HIDDEN:
                  return "KEYGUARD_HIDDEN";
              case Event.DEVICE_SHUTDOWN:
                  return "DEVICE_SHUTDOWN";
              case Event.DEVICE_STARTUP:
                  return "DEVICE_STARTUP";
              default:
                  return "UNKNOWN_TYPE_" + eventType;
          }
      }
  
      byte[] getBackupPayload(String key){
          return mDatabase.getBackupPayload(key);
      }
  
      void applyRestoredPayload(String key, byte[] payload){
          mDatabase.applyRestoredPayload(key, payload);
      }
  }
  