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.ACTIVITY_PAUSED;
  import static android.app.usage.UsageEvents.Event.ACTIVITY_RESUMED;
  import static android.app.usage.UsageEvents.Event.ACTIVITY_STOPPED;
  import static android.app.usage.UsageEvents.Event.CONFIGURATION_CHANGE;
  import static android.app.usage.UsageEvents.Event.CONTINUE_PREVIOUS_DAY;
  import static android.app.usage.UsageEvents.Event.CONTINUING_FOREGROUND_SERVICE;
  import static android.app.usage.UsageEvents.Event.DEVICE_SHUTDOWN;
  import static android.app.usage.UsageEvents.Event.END_OF_DAY;
  import static android.app.usage.UsageEvents.Event.FLUSH_TO_DISK;
  import static android.app.usage.UsageEvents.Event.FOREGROUND_SERVICE_START;
  import static android.app.usage.UsageEvents.Event.FOREGROUND_SERVICE_STOP;
  import static android.app.usage.UsageEvents.Event.KEYGUARD_HIDDEN;
  import static android.app.usage.UsageEvents.Event.KEYGUARD_SHOWN;
  import static android.app.usage.UsageEvents.Event.NOTIFICATION_INTERRUPTION;
  import static android.app.usage.UsageEvents.Event.ROLLOVER_FOREGROUND_SERVICE;
  import static android.app.usage.UsageEvents.Event.SCREEN_INTERACTIVE;
  import static android.app.usage.UsageEvents.Event.SCREEN_NON_INTERACTIVE;
  import static android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION;
  import static android.app.usage.UsageEvents.Event.STANDBY_BUCKET_CHANGED;
  import static android.app.usage.UsageEvents.Event.SYSTEM_INTERACTION;
  
  import android.app.usage.ConfigurationStats;
  import android.app.usage.EventList;
  import android.app.usage.EventStats;
  import android.app.usage.UsageEvents.Event;
  import android.app.usage.UsageStats;
  import android.content.res.Configuration;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.proto.ProtoInputStream;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import java.io.IOException;
  import java.util.List;
  
  public class IntervalStats {
      public static final int CURRENT_MAJOR_VERSION = 1;
      public static final int CURRENT_MINOR_VERSION = 1;
      public int majorVersion = CURRENT_MAJOR_VERSION;
      public int minorVersion = CURRENT_MINOR_VERSION;
      public long beginTime;
      public long endTime;
      public long lastTimeSaved;
      public final EventTracker interactiveTracker = new EventTracker();
      public final EventTracker nonInteractiveTracker = new EventTracker();
      public final EventTracker keyguardShownTracker = new EventTracker();
      public final EventTracker keyguardHiddenTracker = new EventTracker();
      public final ArrayMap<String, UsageStats> packageStats = new ArrayMap<>();
      public final ArrayMap<Configuration, ConfigurationStats> configurations = new ArrayMap<>();
      public Configuration activeConfiguration;
      public final EventList events = new EventList();
  
      // A string cache. This is important as when we're parsing XML files, we don't want to
      // keep hundreds of strings that have the same contents. We will read the string
      // and only keep it if it's not in the cache. The GC will take care of the
      // strings that had identical copies in the cache.
      public final ArraySet<String> mStringCache = new ArraySet<>();
  
      public static final class EventTracker {
          public long curStartTime;
          public long lastEventTime;
          public long duration;
          public int count;
  
          public void commitTime(long timeStamp) {
              if (curStartTime != 0) {
                  duration += timeStamp - curStartTime;
                  curStartTime = 0;
              }
          }
  
          public void update(long timeStamp) {
              if (curStartTime == 0) {
                  // If we aren't already running, time to bump the count.
                  count++;
              }
              commitTime(timeStamp);
              curStartTime = timeStamp;
              lastEventTime = timeStamp;
          }
  
          void addToEventStats(List<EventStats> out, int event, long beginTime, long endTime) {
              if (count != 0 || duration != 0) {
                  EventStats ev = new EventStats();
                  ev.mEventType = event;
                  ev.mCount = count;
                  ev.mTotalTime = duration;
                  ev.mLastEventTime = lastEventTime;
                  ev.mBeginTimeStamp = beginTime;
                  ev.mEndTimeStamp = endTime;
                  out.add(ev);
              }
          }
  
      }
  
      public IntervalStats() {
      }
  
      /**
       * Gets the UsageStats object for the given package, or creates one and adds it internally.
       */
      UsageStats getOrCreateUsageStats(String packageName) {
          UsageStats usageStats = packageStats.get(packageName);
          if (usageStats == null) {
              usageStats = new UsageStats();
              usageStats.mPackageName = getCachedStringRef(packageName);
              usageStats.mBeginTimeStamp = beginTime;
              usageStats.mEndTimeStamp = endTime;
              packageStats.put(usageStats.mPackageName, usageStats);
          }
          return usageStats;
      }
  
      /**
       * Gets the ConfigurationStats object for the given configuration, or creates one and adds it
       * internally.
       */
      ConfigurationStats getOrCreateConfigurationStats(Configuration config) {
          ConfigurationStats configStats = configurations.get(config);
          if (configStats == null) {
              configStats = new ConfigurationStats();
              configStats.mBeginTimeStamp = beginTime;
              configStats.mEndTimeStamp = endTime;
              configStats.mConfiguration = config;
              configurations.put(config, configStats);
          }
          return configStats;
      }
  
      /**
       * Builds a UsageEvents.Event, but does not add it internally.
       */
      Event buildEvent(String packageName, String className) {
          Event event = new Event();
          event.mPackage = getCachedStringRef(packageName);
          if (className != null) {
              event.mClass = getCachedStringRef(className);
          }
          return event;
      }
  
      /**
       * Builds a UsageEvents.Event from a proto, but does not add it internally.
       * Built here to take advantage of the cached String Refs
       */
      Event buildEvent(ProtoInputStream parser, List<String> stringPool)
              throws IOException {
          final Event event = new Event();
          while (true) {
              switch (parser.nextField()) {
                  case (int) IntervalStatsProto.Event.PACKAGE:
                      event.mPackage = getCachedStringRef(
                              parser.readString(IntervalStatsProto.Event.PACKAGE));
                      break;
                  case (int) IntervalStatsProto.Event.PACKAGE_INDEX:
                      event.mPackage = getCachedStringRef(stringPool.get(
                              parser.readInt(IntervalStatsProto.Event.PACKAGE_INDEX) - 1));
                      break;
                  case (int) IntervalStatsProto.Event.CLASS:
                      event.mClass = getCachedStringRef(
                              parser.readString(IntervalStatsProto.Event.CLASS));
                      break;
                  case (int) IntervalStatsProto.Event.CLASS_INDEX:
                      event.mClass = getCachedStringRef(stringPool.get(
                              parser.readInt(IntervalStatsProto.Event.CLASS_INDEX) - 1));
                      break;
                  case (int) IntervalStatsProto.Event.TIME_MS:
                      event.mTimeStamp = beginTime + parser.readLong(
                              IntervalStatsProto.Event.TIME_MS);
                      break;
                  case (int) IntervalStatsProto.Event.FLAGS:
                      event.mFlags = parser.readInt(IntervalStatsProto.Event.FLAGS);
                      break;
                  case (int) IntervalStatsProto.Event.TYPE:
                      event.mEventType = parser.readInt(IntervalStatsProto.Event.TYPE);
                      break;
                  case (int) IntervalStatsProto.Event.CONFIG:
                      event.mConfiguration = new Configuration();
                      event.mConfiguration.readFromProto(parser, IntervalStatsProto.Event.CONFIG);
                      break;
                  case (int) IntervalStatsProto.Event.SHORTCUT_ID:
                      event.mShortcutId = parser.readString(
                              IntervalStatsProto.Event.SHORTCUT_ID).intern();
                      break;
                  case (int) IntervalStatsProto.Event.STANDBY_BUCKET:
                      event.mBucketAndReason = parser.readInt(
                              IntervalStatsProto.Event.STANDBY_BUCKET);
                      break;
                  case (int) IntervalStatsProto.Event.NOTIFICATION_CHANNEL:
                      event.mNotificationChannelId = parser.readString(
                              IntervalStatsProto.Event.NOTIFICATION_CHANNEL);
                      break;
                  case (int) IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX:
                      event.mNotificationChannelId = getCachedStringRef(stringPool.get(
                              parser.readInt(IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX)
                                      - 1));
                      break;
                  case (int) IntervalStatsProto.Event.INSTANCE_ID:
                      event.mInstanceId = parser.readInt(IntervalStatsProto.Event.INSTANCE_ID);
                      break;
                  case (int) IntervalStatsProto.Event.TASK_ROOT_PACKAGE_INDEX:
                      event.mTaskRootPackage = getCachedStringRef(stringPool.get(
                              parser.readInt(IntervalStatsProto.Event.TASK_ROOT_PACKAGE_INDEX) - 1));
                      break;
                  case (int) IntervalStatsProto.Event.TASK_ROOT_CLASS_INDEX:
                      event.mTaskRootClass = getCachedStringRef(stringPool.get(
                              parser.readInt(IntervalStatsProto.Event.TASK_ROOT_CLASS_INDEX) - 1));
                      break;
                  case ProtoInputStream.NO_MORE_FIELDS:
                      // Handle default values for certain events types
                      switch (event.mEventType) {
                          case CONFIGURATION_CHANGE:
                              if (event.mConfiguration == null) {
                                  event.mConfiguration = new Configuration();
                              }
                              break;
                          case SHORTCUT_INVOCATION:
                              if (event.mShortcutId == null) {
                                  event.mShortcutId = "";
                              }
                              break;
                          case NOTIFICATION_INTERRUPTION:
                              if (event.mNotificationChannelId == null) {
                                  event.mNotificationChannelId = "";
                              }
                              break;
                      }
                      if (event.mTimeStamp == 0) {
                          //mTimestamp not set, assume default value 0 plus beginTime
                          event.mTimeStamp = beginTime;
                      }
                      return event;
              }
          }
      }
  
      private boolean isStatefulEvent(int eventType) {
          switch (eventType) {
              case ACTIVITY_RESUMED:
              case ACTIVITY_PAUSED:
              case ACTIVITY_STOPPED:
              case FOREGROUND_SERVICE_START:
              case FOREGROUND_SERVICE_STOP:
              case END_OF_DAY:
              case ROLLOVER_FOREGROUND_SERVICE:
              case CONTINUE_PREVIOUS_DAY:
              case CONTINUING_FOREGROUND_SERVICE:
              case DEVICE_SHUTDOWN:
                  return true;
          }
          return false;
      }
  
      /**
       * Returns whether the event type is one caused by user visible
       * interaction. Excludes those that are internally generated.
       */
      private boolean isUserVisibleEvent(int eventType) {
          return eventType != SYSTEM_INTERACTION
                  && eventType != STANDBY_BUCKET_CHANGED;
      }
  
      /**
       * Update the IntervalStats by a activity or foreground service event.
       * @param packageName package name of this event. Is null if event targets to all packages.
       * @param className class name of a activity or foreground service, could be null to if this
       *                  is sent to all activities/services in this package.
       * @param timeStamp Epoch timestamp in milliseconds.
       * @param eventType event type as in {@link Event}
       * @param instanceId if className is an activity, the hashCode of ActivityRecord's appToken.
       *                 if className is not an activity, instanceId is not used.
       * @hide
       */
      @VisibleForTesting
      public void update(String packageName, String className, long timeStamp, int eventType,
              int instanceId) {
          if (eventType == DEVICE_SHUTDOWN
                  || eventType == FLUSH_TO_DISK) {
              // DEVICE_SHUTDOWN and FLUSH_TO_DISK are sent to all packages.
              final int size = packageStats.size();
              for (int i = 0; i < size; i++) {
                  UsageStats usageStats = packageStats.valueAt(i);
                  usageStats.update(null, timeStamp, eventType, instanceId);
              }
          } else {
              UsageStats usageStats = getOrCreateUsageStats(packageName);
              usageStats.update(className, timeStamp, eventType, instanceId);
          }
          if (timeStamp > endTime) {
              endTime = timeStamp;
          }
      }
  
      /**
       * @hide
       */
      @VisibleForTesting
      public void addEvent(Event event) {
          // Cache common use strings
          event.mPackage = getCachedStringRef(event.mPackage);
          if (event.mClass != null) {
              event.mClass = getCachedStringRef(event.mClass);
          }
          if (event.mTaskRootPackage != null) {
              event.mTaskRootPackage = getCachedStringRef(event.mTaskRootPackage);
          }
          if (event.mTaskRootClass != null) {
              event.mTaskRootClass = getCachedStringRef(event.mTaskRootClass);
          }
          if (event.mEventType == NOTIFICATION_INTERRUPTION) {
              event.mNotificationChannelId = getCachedStringRef(event.mNotificationChannelId);
          }
          events.insert(event);
          if (event.mTimeStamp > endTime) {
              endTime = event.mTimeStamp;
          }
      }
  
      void updateChooserCounts(String packageName, String category, String action) {
          UsageStats usageStats = getOrCreateUsageStats(packageName);
          if (usageStats.mChooserCounts == null) {
              usageStats.mChooserCounts = new ArrayMap<>();
          }
          ArrayMap<String, Integer> chooserCounts;
          final int idx = usageStats.mChooserCounts.indexOfKey(action);
          if (idx < 0) {
              chooserCounts = new ArrayMap<>();
              usageStats.mChooserCounts.put(action, chooserCounts);
          } else {
              chooserCounts = usageStats.mChooserCounts.valueAt(idx);
          }
          int currentCount = chooserCounts.getOrDefault(category, 0);
          chooserCounts.put(category, currentCount + 1);
      }
  
      void updateConfigurationStats(Configuration config, long timeStamp) {
          if (activeConfiguration != null) {
              ConfigurationStats activeStats = configurations.get(activeConfiguration);
              activeStats.mTotalTimeActive += timeStamp - activeStats.mLastTimeActive;
              activeStats.mLastTimeActive = timeStamp - 1;
          }
  
          if (config != null) {
              ConfigurationStats configStats = getOrCreateConfigurationStats(config);
              configStats.mLastTimeActive = timeStamp;
              configStats.mActivationCount += 1;
              activeConfiguration = configStats.mConfiguration;
          }
          if (timeStamp > endTime) {
              endTime = timeStamp;
          }
      }
  
      void incrementAppLaunchCount(String packageName) {
          UsageStats usageStats = getOrCreateUsageStats(packageName);
          usageStats.mAppLaunchCount += 1;
      }
  
      void commitTime(long timeStamp) {
          interactiveTracker.commitTime(timeStamp);
          nonInteractiveTracker.commitTime(timeStamp);
          keyguardShownTracker.commitTime(timeStamp);
          keyguardHiddenTracker.commitTime(timeStamp);
      }
  
      void updateScreenInteractive(long timeStamp) {
          interactiveTracker.update(timeStamp);
          nonInteractiveTracker.commitTime(timeStamp);
      }
  
      void updateScreenNonInteractive(long timeStamp) {
          nonInteractiveTracker.update(timeStamp);
          interactiveTracker.commitTime(timeStamp);
      }
  
      void updateKeyguardShown(long timeStamp) {
          keyguardShownTracker.update(timeStamp);
          keyguardHiddenTracker.commitTime(timeStamp);
      }
  
      void updateKeyguardHidden(long timeStamp) {
          keyguardHiddenTracker.update(timeStamp);
          keyguardShownTracker.commitTime(timeStamp);
      }
  
      void addEventStatsTo(List<EventStats> out) {
          interactiveTracker.addToEventStats(out, SCREEN_INTERACTIVE,
                  beginTime, endTime);
          nonInteractiveTracker.addToEventStats(out, SCREEN_NON_INTERACTIVE,
                  beginTime, endTime);
          keyguardShownTracker.addToEventStats(out, KEYGUARD_SHOWN,
                  beginTime, endTime);
          keyguardHiddenTracker.addToEventStats(out, KEYGUARD_HIDDEN,
                  beginTime, endTime);
      }
  
      private String getCachedStringRef(String str) {
          final int index = mStringCache.indexOf(str);
          if (index < 0) {
              mStringCache.add(str);
              return str;
          }
          return mStringCache.valueAt(index);
      }
  
      /**
       * When an IntervalStats object is deserialized, if the object's version number
       * is lower than current version number, optionally perform a upgrade.
       */
      void upgradeIfNeeded() {
          // We only uprade on majorVersion change, no need to upgrade on minorVersion change.
          if (!(majorVersion < CURRENT_MAJOR_VERSION)) {
              return;
          }
          /*
            Optional upgrade code here.
          */
          majorVersion = CURRENT_MAJOR_VERSION;
      }
  }
  