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 android.app.usage.ConfigurationStats;
  import android.app.usage.UsageEvents;
  import android.app.usage.UsageStats;
  import android.content.res.Configuration;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import com.android.internal.util.XmlUtils;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlSerializer;
  
  import java.io.IOException;
  import java.net.ProtocolException;
  
  /**
   * UsageStats reader/writer for version 1 of the XML format.
   */
  final class UsageStatsXmlV1 {
      private static final String TAG = "UsageStatsXmlV1";
  
      private static final String INTERACTIVE_TAG = "interactive";
      private static final String NON_INTERACTIVE_TAG = "non-interactive";
      private static final String KEYGUARD_SHOWN_TAG = "keyguard-shown";
      private static final String KEYGUARD_HIDDEN_TAG = "keyguard-hidden";
  
      private static final String PACKAGES_TAG = "packages";
      private static final String PACKAGE_TAG = "package";
  
      private static final String CHOOSER_COUNT_TAG = "chosen_action";
      private static final String CATEGORY_TAG = "category";
      private static final String NAME = "name";
      private static final String COUNT = "count";
  
      private static final String CONFIGURATIONS_TAG = "configurations";
      private static final String CONFIG_TAG = "config";
  
      private static final String EVENT_LOG_TAG = "event-log";
      private static final String EVENT_TAG = "event";
  
      // Attributes
      private static final String PACKAGE_ATTR = "package";
      private static final String FLAGS_ATTR = "flags";
      private static final String CLASS_ATTR = "class";
      private static final String TOTAL_TIME_ACTIVE_ATTR = "timeActive";
      private static final String TOTAL_TIME_VISIBLE_ATTR = "timeVisible";
      private static final String TOTAL_TIME_SERVICE_USED_ATTR = "timeServiceUsed";
      private static final String COUNT_ATTR = "count";
      private static final String ACTIVE_ATTR = "active";
      private static final String LAST_EVENT_ATTR = "lastEvent";
      private static final String TYPE_ATTR = "type";
      private static final String INSTANCE_ID_ATTR = "instanceId";
      private static final String SHORTCUT_ID_ATTR = "shortcutId";
      private static final String STANDBY_BUCKET_ATTR = "standbyBucket";
      private static final String APP_LAUNCH_COUNT_ATTR = "appLaunchCount";
      private static final String NOTIFICATION_CHANNEL_ATTR = "notificationChannel";
      private static final String MAJOR_VERSION_ATTR = "majorVersion";
      private static final String MINOR_VERSION_ATTR = "minorVersion";
  
      // Time attributes stored as an offset of the beginTime.
      private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
      private static final String LAST_TIME_VISIBLE_ATTR = "lastTimeVisible";
      private static final String LAST_TIME_SERVICE_USED_ATTR = "lastTimeServiceUsed";
      private static final String END_TIME_ATTR = "endTime";
      private static final String TIME_ATTR = "time";
  
      private static void loadUsageStats(XmlPullParser parser, IntervalStats statsOut)
              throws XmlPullParserException, IOException {
          final String pkg = parser.getAttributeValue(null, PACKAGE_ATTR);
          if (pkg == null) {
              throw new ProtocolException("no " + PACKAGE_ATTR + " attribute present");
          }
          final UsageStats stats = statsOut.getOrCreateUsageStats(pkg);
  
          // Apply the offset to the beginTime to find the absolute time.
          stats.mLastTimeUsed = statsOut.beginTime + XmlUtils.readLongAttribute(
                  parser, LAST_TIME_ACTIVE_ATTR);
  
          try {
              stats.mLastTimeVisible = statsOut.beginTime + XmlUtils.readLongAttribute(
                      parser, LAST_TIME_VISIBLE_ATTR);
          } catch (IOException e) {
              Log.i(TAG, "Failed to parse mLastTimeVisible");
          }
  
          try {
              stats.mLastTimeForegroundServiceUsed = statsOut.beginTime + XmlUtils.readLongAttribute(
                      parser, LAST_TIME_SERVICE_USED_ATTR);
          } catch (IOException e) {
              Log.i(TAG, "Failed to parse mLastTimeForegroundServiceUsed");
          }
  
          stats.mTotalTimeInForeground = XmlUtils.readLongAttribute(parser, TOTAL_TIME_ACTIVE_ATTR);
  
          try {
              stats.mTotalTimeVisible = XmlUtils.readLongAttribute(parser, TOTAL_TIME_VISIBLE_ATTR);
          } catch (IOException e) {
              Log.i(TAG, "Failed to parse mTotalTimeVisible");
          }
  
          try {
              stats.mTotalTimeForegroundServiceUsed = XmlUtils.readLongAttribute(parser,
                      TOTAL_TIME_SERVICE_USED_ATTR);
          } catch (IOException e) {
              Log.i(TAG, "Failed to parse mTotalTimeForegroundServiceUsed");
          }
  
          stats.mLastEvent = XmlUtils.readIntAttribute(parser, LAST_EVENT_ATTR);
          stats.mAppLaunchCount = XmlUtils.readIntAttribute(parser, APP_LAUNCH_COUNT_ATTR,
                  0);
          int eventCode;
          while ((eventCode = parser.next()) != XmlPullParser.END_DOCUMENT) {
              final String tag = parser.getName();
              if (eventCode == XmlPullParser.END_TAG && tag.equals(PACKAGE_TAG)) {
                  break;
              }
              if (eventCode != XmlPullParser.START_TAG) {
                  continue;
              }
              if (tag.equals(CHOOSER_COUNT_TAG)) {
                  String action = XmlUtils.readStringAttribute(parser, NAME);
                  loadChooserCounts(parser, stats, action);
              }
          }
      }
  
      private static void loadCountAndTime(XmlPullParser parser,
              IntervalStats.EventTracker tracker)
              throws IOException, XmlPullParserException {
          tracker.count = XmlUtils.readIntAttribute(parser, COUNT_ATTR, 0);
          tracker.duration = XmlUtils.readLongAttribute(parser, TIME_ATTR, 0);
          XmlUtils.skipCurrentTag(parser);
      }
  
      private static void loadChooserCounts(
              XmlPullParser parser, UsageStats usageStats, String action)
              throws XmlPullParserException, IOException {
          if (action == null) {
              return;
          }
          if (usageStats.mChooserCounts == null) {
              usageStats.mChooserCounts = new ArrayMap<>();
          }
          if (!usageStats.mChooserCounts.containsKey(action)) {
              ArrayMap<String, Integer> counts = new ArrayMap<>();
              usageStats.mChooserCounts.put(action, counts);
          }
  
          int eventCode;
          while ((eventCode = parser.next()) != XmlPullParser.END_DOCUMENT) {
              final String tag = parser.getName();
              if (eventCode == XmlPullParser.END_TAG && tag.equals(CHOOSER_COUNT_TAG)) {
                  break;
              }
              if (eventCode != XmlPullParser.START_TAG) {
                  continue;
              }
              if (tag.equals(CATEGORY_TAG)) {
                  String category = XmlUtils.readStringAttribute(parser, NAME);
                  int count = XmlUtils.readIntAttribute(parser, COUNT);
                  usageStats.mChooserCounts.get(action).put(category, count);
              }
          }
      }
  
      private static void loadConfigStats(XmlPullParser parser, IntervalStats statsOut)
              throws XmlPullParserException, IOException {
          final Configuration config = new Configuration();
          Configuration.readXmlAttrs(parser, config);
  
          final ConfigurationStats configStats = statsOut.getOrCreateConfigurationStats(config);
  
          // Apply the offset to the beginTime to find the absolute time.
          configStats.mLastTimeActive = statsOut.beginTime + XmlUtils.readLongAttribute(
                  parser, LAST_TIME_ACTIVE_ATTR);
  
          configStats.mTotalTimeActive = XmlUtils.readLongAttribute(parser, TOTAL_TIME_ACTIVE_ATTR);
          configStats.mActivationCount = XmlUtils.readIntAttribute(parser, COUNT_ATTR);
          if (XmlUtils.readBooleanAttribute(parser, ACTIVE_ATTR)) {
              statsOut.activeConfiguration = configStats.mConfiguration;
          }
      }
  
      private static void loadEvent(XmlPullParser parser, IntervalStats statsOut)
              throws XmlPullParserException, IOException {
          final String packageName = XmlUtils.readStringAttribute(parser, PACKAGE_ATTR);
          if (packageName == null) {
              throw new ProtocolException("no " + PACKAGE_ATTR + " attribute present");
          }
          final String className = XmlUtils.readStringAttribute(parser, CLASS_ATTR);
  
          final UsageEvents.Event event = statsOut.buildEvent(packageName, className);
  
          event.mFlags = XmlUtils.readIntAttribute(parser, FLAGS_ATTR, 0);
  
          // Apply the offset to the beginTime to find the absolute time of this event.
          event.mTimeStamp = statsOut.beginTime + XmlUtils.readLongAttribute(parser, TIME_ATTR);
  
          event.mEventType = XmlUtils.readIntAttribute(parser, TYPE_ATTR);
  
          try {
              event.mInstanceId = XmlUtils.readIntAttribute(parser, INSTANCE_ID_ATTR);
          } catch (IOException e) {
              Log.e(TAG, "Failed to parse mInstanceId", e);
          }
  
          switch (event.mEventType) {
              case UsageEvents.Event.CONFIGURATION_CHANGE:
                  event.mConfiguration = new Configuration();
                  Configuration.readXmlAttrs(parser, event.mConfiguration);
                  break;
              case UsageEvents.Event.SHORTCUT_INVOCATION:
                  final String id = XmlUtils.readStringAttribute(parser, SHORTCUT_ID_ATTR);
                  event.mShortcutId = (id != null) ? id.intern() : null;
                  break;
              case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
                  event.mBucketAndReason = XmlUtils.readIntAttribute(parser, STANDBY_BUCKET_ATTR, 0);
                  break;
              case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                  final String channelId =
                          XmlUtils.readStringAttribute(parser, NOTIFICATION_CHANNEL_ATTR);
                  event.mNotificationChannelId = (channelId != null) ? channelId.intern() : null;
                  break;
          }
          statsOut.addEvent(event);
      }
  
      private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
              final UsageStats usageStats) throws IOException {
          xml.startTag(null, PACKAGE_TAG);
  
          // Write the time offset.
          XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
                  usageStats.mLastTimeUsed - stats.beginTime);
          XmlUtils.writeLongAttribute(xml, LAST_TIME_VISIBLE_ATTR,
                  usageStats.mLastTimeVisible - stats.beginTime);
          XmlUtils.writeLongAttribute(xml, LAST_TIME_SERVICE_USED_ATTR,
                  usageStats.mLastTimeForegroundServiceUsed - stats.beginTime);
          XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
          XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
          XmlUtils.writeLongAttribute(xml, TOTAL_TIME_VISIBLE_ATTR, usageStats.mTotalTimeVisible);
          XmlUtils.writeLongAttribute(xml, TOTAL_TIME_SERVICE_USED_ATTR,
                  usageStats.mTotalTimeForegroundServiceUsed);
          XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
          if (usageStats.mAppLaunchCount > 0) {
              XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
          }
          writeChooserCounts(xml, usageStats);
          xml.endTag(null, PACKAGE_TAG);
      }
  
      private static void writeCountAndTime(XmlSerializer xml, String tag, int count, long time)
              throws IOException {
          xml.startTag(null, tag);
          XmlUtils.writeIntAttribute(xml, COUNT_ATTR, count);
          XmlUtils.writeLongAttribute(xml, TIME_ATTR, time);
          xml.endTag(null, tag);
      }
  
      private static void writeChooserCounts(XmlSerializer xml, final UsageStats usageStats)
              throws IOException {
          if (usageStats == null || usageStats.mChooserCounts == null ||
                  usageStats.mChooserCounts.keySet().isEmpty()) {
              return;
          }
          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);
              if (action == null || counts == null || counts.isEmpty()) {
                  continue;
              }
              xml.startTag(null, CHOOSER_COUNT_TAG);
              XmlUtils.writeStringAttribute(xml, NAME, action);
              writeCountsForAction(xml, counts);
              xml.endTag(null, CHOOSER_COUNT_TAG);
          }
      }
  
      private static void writeCountsForAction(XmlSerializer xml, ArrayMap<String, Integer> counts)
              throws IOException {
          final int countsSize = counts.size();
          for (int i = 0; i < countsSize; i++) {
              String key = counts.keyAt(i);
              int count = counts.valueAt(i);
              if (count > 0) {
                  xml.startTag(null, CATEGORY_TAG);
                  XmlUtils.writeStringAttribute(xml, NAME, key);
                  XmlUtils.writeIntAttribute(xml, COUNT, count);
                  xml.endTag(null, CATEGORY_TAG);
              }
          }
      }
  
      private static void writeConfigStats(XmlSerializer xml, final IntervalStats stats,
              final ConfigurationStats configStats, boolean isActive) throws IOException {
          xml.startTag(null, CONFIG_TAG);
  
          // Write the time offset.
          XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
                  configStats.mLastTimeActive - stats.beginTime);
  
          XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, configStats.mTotalTimeActive);
          XmlUtils.writeIntAttribute(xml, COUNT_ATTR, configStats.mActivationCount);
          if (isActive) {
              XmlUtils.writeBooleanAttribute(xml, ACTIVE_ATTR, true);
          }
  
          // Now write the attributes representing the configuration object.
          Configuration.writeXmlAttrs(xml, configStats.mConfiguration);
  
          xml.endTag(null, CONFIG_TAG);
      }
  
      private static void writeEvent(XmlSerializer xml, final IntervalStats stats,
              final UsageEvents.Event event) throws IOException {
          xml.startTag(null, EVENT_TAG);
  
          // Store the time offset.
          XmlUtils.writeLongAttribute(xml, TIME_ATTR, event.mTimeStamp - stats.beginTime);
  
          XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, event.mPackage);
          if (event.mClass != null) {
              XmlUtils.writeStringAttribute(xml, CLASS_ATTR, event.mClass);
          }
          XmlUtils.writeIntAttribute(xml, FLAGS_ATTR, event.mFlags);
          XmlUtils.writeIntAttribute(xml, TYPE_ATTR, event.mEventType);
          XmlUtils.writeIntAttribute(xml, INSTANCE_ID_ATTR, event.mInstanceId);
  
          switch (event.mEventType) {
              case UsageEvents.Event.CONFIGURATION_CHANGE:
                  if (event.mConfiguration != null) {
                      Configuration.writeXmlAttrs(xml, event.mConfiguration);
                  }
                  break;
              case UsageEvents.Event.SHORTCUT_INVOCATION:
                  if (event.mShortcutId != null) {
                      XmlUtils.writeStringAttribute(xml, SHORTCUT_ID_ATTR, event.mShortcutId);
                  }
                  break;
              case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
                  if (event.mBucketAndReason != 0) {
                      XmlUtils.writeIntAttribute(xml, STANDBY_BUCKET_ATTR, event.mBucketAndReason);
                  }
                  break;
              case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                  if (event.mNotificationChannelId != null) {
                      XmlUtils.writeStringAttribute(
                              xml, NOTIFICATION_CHANNEL_ATTR, event.mNotificationChannelId);
                  }
                  break;
          }
  
          xml.endTag(null, EVENT_TAG);
      }
  
      /**
       * Reads from the {@link XmlPullParser}, assuming that it is already on the
       * <code><usagestats></code> tag.
       *
       * @param parser The parser from which to read events.
       * @param statsOut The stats object to populate with the data from the XML file.
       */
      public static void read(XmlPullParser parser, IntervalStats statsOut)
              throws XmlPullParserException, IOException {
          statsOut.packageStats.clear();
          statsOut.configurations.clear();
          statsOut.activeConfiguration = null;
          statsOut.events.clear();
  
          statsOut.endTime = statsOut.beginTime + XmlUtils.readLongAttribute(parser, END_TIME_ATTR);
          try {
              statsOut.majorVersion = XmlUtils.readIntAttribute(parser, MAJOR_VERSION_ATTR);
          } catch (IOException e) {
              Log.e(TAG, "Failed to parse majorVersion", e);
          }
  
          try {
              statsOut.minorVersion = XmlUtils.readIntAttribute(parser, MINOR_VERSION_ATTR);
          } catch (IOException e) {
              Log.e(TAG, "Failed to parse minorVersion", e);
          }
  
          int eventCode;
          int outerDepth = parser.getDepth();
          while ((eventCode = parser.next()) != XmlPullParser.END_DOCUMENT
                  && (eventCode != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
              if (eventCode != XmlPullParser.START_TAG) {
                  continue;
              }
  
              final String tag = parser.getName();
              switch (tag) {
                  case INTERACTIVE_TAG:
                      loadCountAndTime(parser, statsOut.interactiveTracker);
                      break;
  
                  case NON_INTERACTIVE_TAG:
                      loadCountAndTime(parser, statsOut.nonInteractiveTracker);
                      break;
  
                  case KEYGUARD_SHOWN_TAG:
                      loadCountAndTime(parser, statsOut.keyguardShownTracker);
                      break;
  
                  case KEYGUARD_HIDDEN_TAG:
                      loadCountAndTime(parser, statsOut.keyguardHiddenTracker);
                      break;
  
                  case PACKAGE_TAG:
                      loadUsageStats(parser, statsOut);
                      break;
  
                  case CONFIG_TAG:
                      loadConfigStats(parser, statsOut);
                      break;
  
                  case EVENT_TAG:
                      loadEvent(parser, statsOut);
                      break;
              }
          }
      }
  
      /**
       * Writes the stats object to an XML file. The {@link XmlSerializer}
       * has already written the <code><usagestats></code> tag, but attributes may still
       * be added.
       *
       * @param xml The serializer to which to write the packageStats data.
       * @param stats The stats object to write to the XML file.
       */
      public static void write(XmlSerializer xml, IntervalStats stats) throws IOException {
          XmlUtils.writeLongAttribute(xml, END_TIME_ATTR, stats.endTime - stats.beginTime);
          XmlUtils.writeIntAttribute(xml, MAJOR_VERSION_ATTR, stats.majorVersion);
          XmlUtils.writeIntAttribute(xml, MINOR_VERSION_ATTR, stats.minorVersion);
  
          writeCountAndTime(xml, INTERACTIVE_TAG, stats.interactiveTracker.count,
                  stats.interactiveTracker.duration);
          writeCountAndTime(xml, NON_INTERACTIVE_TAG, stats.nonInteractiveTracker.count,
                  stats.nonInteractiveTracker.duration);
          writeCountAndTime(xml, KEYGUARD_SHOWN_TAG, stats.keyguardShownTracker.count,
                  stats.keyguardShownTracker.duration);
          writeCountAndTime(xml, KEYGUARD_HIDDEN_TAG, stats.keyguardHiddenTracker.count,
                  stats.keyguardHiddenTracker.duration);
  
          xml.startTag(null, PACKAGES_TAG);
          final int statsCount = stats.packageStats.size();
          for (int i = 0; i < statsCount; i++) {
              writeUsageStats(xml, stats, stats.packageStats.valueAt(i));
          }
          xml.endTag(null, PACKAGES_TAG);
  
          xml.startTag(null, CONFIGURATIONS_TAG);
          final int configCount = stats.configurations.size();
          for (int i = 0; i < configCount; i++) {
              boolean active = stats.activeConfiguration.equals(stats.configurations.keyAt(i));
              writeConfigStats(xml, stats, stats.configurations.valueAt(i), active);
          }
          xml.endTag(null, CONFIGURATIONS_TAG);
  
          xml.startTag(null, EVENT_LOG_TAG);
          final int eventCount = stats.events.size();
          for (int i = 0; i < eventCount; i++) {
              writeEvent(xml, stats, stats.events.get(i));
          }
          xml.endTag(null, EVENT_LOG_TAG);
      }
  
      private UsageStatsXmlV1() {
      }
  }
  