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.powermodel;
  
  import java.io.InputStream;
  import java.io.IOException;
  import java.util.regex.Pattern;
  import java.util.regex.Matcher;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import javax.xml.parsers.ParserConfigurationException;
  import javax.xml.parsers.SAXParser;
  import javax.xml.parsers.SAXParserFactory;
  import org.xml.sax.Attributes;
  import org.xml.sax.Locator;
  import org.xml.sax.SAXException;
  import org.xml.sax.SAXParseException;
  import org.xml.sax.helpers.DefaultHandler;
  
  import com.android.powermodel.component.AudioProfile;
  import com.android.powermodel.component.BluetoothProfile;
  import com.android.powermodel.component.CameraProfile;
  import com.android.powermodel.component.CpuProfile;
  import com.android.powermodel.component.FlashlightProfile;
  import com.android.powermodel.component.GpsProfile;
  import com.android.powermodel.component.ModemProfile;
  import com.android.powermodel.component.ScreenProfile;
  import com.android.powermodel.component.VideoProfile;
  import com.android.powermodel.component.WifiProfile;
  import com.android.powermodel.util.Conversion;
  
  public class PowerProfile {
  
      // Remaining fields from the android code for which the actual usage is unclear.
      //   battery.capacity
      //   bluetooth.controller.voltage
      //   modem.controller.voltage
      //   gps.voltage
      //   wifi.controller.voltage
      //   radio.on
      //   radio.scanning
      //   radio.active
      //   memory.bandwidths
      //   wifi.batchedscan
      //   wifi.scan
      //   wifi.on
      //   wifi.active
      //   wifi.controller.tx_levels
  
      private static Pattern RE_CLUSTER_POWER = Pattern.compile("cpu.cluster_power.cluster([0-9]*)");
      private static Pattern RE_CORE_SPEEDS = Pattern.compile("cpu.core_speeds.cluster([0-9]*)");
      private static Pattern RE_CORE_POWER = Pattern.compile("cpu.core_power.cluster([0-9]*)");
  
      private HashMap<Component, ComponentProfile> mComponents = new HashMap();
  
      /**
       * Which element we are currently parsing.
       */
      enum ElementState {
          BEGIN,
          TOP,
          ITEM,
          ARRAY,
          VALUE
      }
  
      /**
       * Implements the reading and power model logic.
       */
      private static class Parser {
          private final InputStream mStream;
          private final PowerProfile mResult;
  
          // Builders for the ComponentProfiles.
          private final AudioProfile mAudio = new AudioProfile();
          private final BluetoothProfile mBluetooth = new BluetoothProfile();
          private final CameraProfile mCamera = new CameraProfile();
          private final CpuProfile.Builder mCpuBuilder = new CpuProfile.Builder();
          private final FlashlightProfile mFlashlight = new FlashlightProfile();
          private final GpsProfile.Builder mGpsBuilder = new GpsProfile.Builder();
          private final ModemProfile.Builder mModemBuilder = new ModemProfile.Builder();
          private final ScreenProfile mScreen = new ScreenProfile();
          private final VideoProfile mVideo = new VideoProfile();
          private final WifiProfile mWifi = new WifiProfile();
  
          /**
           * Constructor to capture the parameters to read.
           */
          Parser(InputStream stream) {
              mStream = stream;
              mResult = new PowerProfile();
          }
  
          /**
           * Read the stream, parse it, and apply the power model.
           * Do not call this more than once.
           */
          PowerProfile parse() throws ParseException {
              final SAXParserFactory factory = SAXParserFactory.newInstance();
              AndroidResourceHandler handler = null;
              try {
                  final SAXParser saxParser = factory.newSAXParser();
  
                  handler = new AndroidResourceHandler() {
                      @Override
                      public void onItem(Locator locator, String name, float value)
                              throws SAXParseException {
                          Parser.this.onItem(locator, name, value);
                      }
  
                      @Override
                      public void onArray(Locator locator, String name, float[] value)
                              throws SAXParseException {
                          Parser.this.onArray(locator, name, value);
                      }
                  };
  
                  saxParser.parse(mStream, handler);
              } catch (ParserConfigurationException ex) {
                  // Coding error, not runtime error.
                  throw new RuntimeException(ex);
              } catch (SAXParseException ex) {
                  throw new ParseException(ex.getLineNumber(), ex.getMessage(), ex);
              } catch (SAXException | IOException ex) {
                  // Make a guess about the line number.
                  throw new ParseException(handler.getLineNumber(), ex.getMessage(), ex);
              }
  
              // TODO: This doesn't cover the multiple algorithms. Some refactoring will
              // be necessary.
              mResult.mComponents.put(Component.AUDIO, mAudio);
              mResult.mComponents.put(Component.BLUETOOTH, mBluetooth);
              mResult.mComponents.put(Component.CAMERA, mCamera);
              mResult.mComponents.put(Component.CPU, mCpuBuilder.build());
              mResult.mComponents.put(Component.FLASHLIGHT, mFlashlight);
              mResult.mComponents.put(Component.GPS, mGpsBuilder.build());
              mResult.mComponents.put(Component.MODEM, mModemBuilder.build());
              mResult.mComponents.put(Component.SCREEN, mScreen);
              mResult.mComponents.put(Component.VIDEO, mVideo);
              mResult.mComponents.put(Component.WIFI, mWifi);
  
              return mResult;
          }
  
          /**
           * Handles an item tag in the power_profile.xml.
           */
          public void onItem(Locator locator, String name, float value) throws SAXParseException {
              Integer index;
              try {
                  if ("ambient.on".equals(name)) {
                      mScreen.ambientMa = value;
                  } else if ("audio".equals(name)) {
                      mAudio.onMa = value;
                  } else if ("bluetooth.controller.idle".equals(name)) {
                      mBluetooth.idleMa = value;
                  } else if ("bluetooth.controller.rx".equals(name)) {
                      mBluetooth.rxMa = value;
                  } else if ("bluetooth.controller.tx".equals(name)) {
                      mBluetooth.txMa = value;
                  } else if ("camera.avg".equals(name)) {
                      mCamera.onMa = value;
                  } else if ("camera.flashlight".equals(name)) {
                      mFlashlight.onMa = value;
                  } else if ("cpu.suspend".equals(name)) {
                      mCpuBuilder.setSuspendMa(value);
                  } else if ("cpu.idle".equals(name)) {
                      mCpuBuilder.setIdleMa(value);
                  } else if ("cpu.active".equals(name)) {
                      mCpuBuilder.setActiveMa(value);
                  } else if ((index = matchIndexedRegex(locator, RE_CLUSTER_POWER, name)) != null) {
                      mCpuBuilder.setClusterPower(index, value);
                  } else if ("gps.on".equals(name)) {
                      mGpsBuilder.setOnMa(value);
                  } else if ("modem.controller.sleep".equals(name)) {
                      mModemBuilder.setSleepMa(value);
                  } else if ("modem.controller.idle".equals(name)) {
                      mModemBuilder.setIdleMa(value);
                  } else if ("modem.controller.rx".equals(name)) {
                      mModemBuilder.setRxMa(value);
                  } else if ("radio.scanning".equals(name)) {
                      mModemBuilder.setScanningMa(value);
                  } else if ("screen.on".equals(name)) {
                      mScreen.onMa = value;
                  } else if ("screen.full".equals(name)) {
                      mScreen.fullMa = value;
                  } else if ("video".equals(name)) {
                      mVideo.onMa = value;
                  } else if ("wifi.controller.idle".equals(name)) {
                      mWifi.idleMa = value;
                  } else if ("wifi.controller.rx".equals(name)) {
                      mWifi.rxMa = value;
                  } else if ("wifi.controller.tx".equals(name)) {
                      mWifi.txMa = value;
                  } else {
                      // TODO: Uncomment this when we have all of the items parsed.
                      // throw new SAXParseException("Unhandled <item name=\"" + name + "\"> element",
                      //        locator, ex);
  
                  }
              } catch (ParseException ex) {
                  throw new SAXParseException(ex.getMessage(), locator, ex);
              }
          }
  
          /**
           * Handles an array tag in the power_profile.xml.
           */
          public void onArray(Locator locator, String name, float[] value) throws SAXParseException {
              Integer index;
              try {
                  if ("cpu.clusters.cores".equals(name)) {
                      mCpuBuilder.setCoreCount(Conversion.toIntArray(value));
                  } else if ((index = matchIndexedRegex(locator, RE_CORE_SPEEDS, name)) != null) {
                      mCpuBuilder.setCoreSpeeds(index, Conversion.toIntArray(value));
                  } else if ((index = matchIndexedRegex(locator, RE_CORE_POWER, name)) != null) {
                      mCpuBuilder.setCorePower(index, value);
                  } else if ("gps.signalqualitybased".equals(name)) {
                      mGpsBuilder.setSignalMa(value);
                  } else if ("modem.controller.tx".equals(name)) {
                      mModemBuilder.setTxMa(value);
                  } else {
                      // TODO: Uncomment this when we have all of the items parsed.
                      // throw new SAXParseException("Unhandled <item name=\"" + name + "\"> element",
                      //        locator, ex);
                  }
              } catch (ParseException ex) {
                  throw new SAXParseException(ex.getMessage(), locator, ex);
              }
          }
      }
  
      /**
       * SAX XML handler that can parse the android resource files.
       * In our case, all elements are floats.
       */
      abstract static class AndroidResourceHandler extends DefaultHandler {
          /**
           * The set of names already processed. Map of name to line number.
           */
          private HashMap<String,Integer> mAlreadySeen = new HashMap<String,Integer>();
  
          /**
           * Where in the document we are parsing.
           */
          private Locator mLocator;
  
          /**
           * Which element we are currently parsing.
           */
          private ElementState mState = ElementState.BEGIN;
  
          /**
           * Saved name from item and array elements.
           */
          private String mName;
  
          /**
           * The text that is currently being captured, or null if {@link #startCapturingText()}
           * has not been called.
           */
          private StringBuilder mText;
  
          /**
           * The array values that have been parsed so for for this array. Null if we are
           * not inside an array tag.
           */
          private ArrayList<Float> mArray;
  
          /**
           * Called when an item tag is encountered.
           */
          public abstract void onItem(Locator locator, String name, float value)
                  throws SAXParseException;
  
          /**
           * Called when an array is encountered.
           */
          public abstract void onArray(Locator locator, String name, float[] value)
                  throws SAXParseException;
  
          /**
           * If we have a Locator set, return the line number, otherwise return 0.
           */
          public int getLineNumber() {
              return mLocator != null ? mLocator.getLineNumber() : 0;
          }
  
          /**
           * Handle setting the parse location object.
           */
          public void setDocumentLocator(Locator locator) {
              mLocator = locator;
          }
  
          /**
           * Handle beginning of an element.
           *
           * @param ns Namespace uri
           * @param ln Local name (inside namespace)
           * @param element Tag name
           */
          @Override
          public void startElement(String ns, String ln, String element,
                  Attributes attr) throws SAXException {
              switch (mState) {
                  case BEGIN:
                      // Outer element, we don't care the tag name.
                      mState = ElementState.TOP;
                      return;
                  case TOP:
                      if ("item".equals(element)) {
                          mState = ElementState.ITEM;
                          saveNameAttribute(attr);
                          startCapturingText();
                          return;
                      } else if ("array".equals(element)) {
                          mState = ElementState.ARRAY;
                          mArray = new ArrayList<Float>();
                          saveNameAttribute(attr);
                          return;
                      }
                      break;
                  case ARRAY:
                      if ("value".equals(element)) {
                          mState = ElementState.VALUE;
                          startCapturingText();
                          return;
                      }
                      break;
              }
              throw new SAXParseException("unexpected element: '" + element + "'", mLocator);
          }
  
          /**
           * Handle end of an element.
           *
           * @param ns Namespace uri
           * @param ln Local name (inside namespace)
           * @param element Tag name
           */
          @Override
          public void endElement(String ns, String ln, String element) throws SAXException {
              switch (mState) {
                  case ITEM: {
                      float value = parseFloat(finishCapturingText());
                      mState = ElementState.TOP;
                      onItem(mLocator, mName, value);
                      break;
                  }
                  case ARRAY: {
                      final int N = mArray.size();
                      float[] values = new float[N];
                      for (int i=0; i<N; i++) {
                          values[i] = mArray.get(i);
                      }
                      mArray = null;
                      mState = ElementState.TOP;
                      onArray(mLocator, mName, values);
                      break;
                  }
                  case VALUE: {
                      mArray.add(parseFloat(finishCapturingText()));
                      mState = ElementState.ARRAY;
                      break;
                  }
              }
          }
  
          /**
           * Interstitial text received.
           *
           * @throws SAXException if there shouldn't be non-whitespace text here
           */
          @Override
          public void characters(char text[], int start, int length) throws SAXException {
              if (mText == null && length > 0 && !isWhitespace(text, start, length)) {
                  throw new SAXParseException("unexpected text: '"
                          + firstLine(text, start, length).trim() + "'", mLocator);
              }
              if (mText != null) {
                  mText.append(text, start, length);
              }
          }
  
          /**
           * Begin collecting text from inside an element.
           */
          private void startCapturingText() {
              if (mText != null) {
                  throw new RuntimeException("ASSERTION FAILED: Shouldn't be already capturing"
                          + " text. mState=" + mState.name()
                          + " line=" + mLocator.getLineNumber()
                          + " column=" + mLocator.getColumnNumber());
              }
              mText = new StringBuilder();
          }
  
          /**
           * Stop capturing text from inside an element.
           *
           * @return the captured text
           */
          private String finishCapturingText() {
              if (mText == null) {
                  throw new RuntimeException("ASSERTION FAILED: Should already be capturing"
                          + " text. mState=" + mState.name()
                          + " line=" + mLocator.getLineNumber()
                          + " column=" + mLocator.getColumnNumber());
              }
              final String result = mText.toString().trim();
              mText = null;
              return result;
          }
  
          /**
           * Get the "name" attribute.
           *
           * @throws SAXParseException if the name attribute is not present or if
           * the name has already been seen in the file.
           */
          private void saveNameAttribute(Attributes attr) throws SAXParseException {
              final String name = attr.getValue("name");
              if (name == null) {
                  throw new SAXParseException("expected 'name' attribute", mLocator);
              }
              Integer prev = mAlreadySeen.put(name, mLocator.getLineNumber());
              if (prev != null) {
                  throw new SAXParseException("name '" + name + "' already seen on line: " + prev,
                          mLocator);
              }
              mName = name;
          }
  
          /**
           * Gets the float value of the string.
           *
           * @throws SAXParseException if 'text' can't be parsed as a float.
           */
          private float parseFloat(String text) throws SAXParseException {
              try {
                  return Float.parseFloat(text);
              } catch (NumberFormatException ex) {
                  throw new SAXParseException("not a valid float value: '" + text + "'",
                          mLocator, ex);
              }
          }
      }
  
      /**
       * Return whether the given substring is all whitespace.
       */
      private static boolean isWhitespace(char[] text, int start, int length) {
          for (int i = start; i < (start + length); i++) {
              if (!Character.isSpace(text[i])) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Return the contents of text up to the first newline.
       */
      private static String firstLine(char[] text, int start, int length) {
          // TODO: The line number will be wrong if we skip preceeding blank lines.
          while (length > 0) {
              if (Character.isSpace(text[start])) {
                  start++;
                  length--;
              }
          }
          int newlen = 0;
          for (; newlen < length; newlen++) {
              final char c = text[newlen];
              if (c == '\n' || c == '\r') {
                  break;
              }
          }
          return new String(text, start, newlen);
      }
  
      /**
       * If the pattern matches, return the first group of that as an Integer.
       * If not return null.
       */
      private static Integer matchIndexedRegex(Locator locator, Pattern pattern, String text)
              throws SAXParseException {
          final Matcher m = pattern.matcher(text);
          if (m.matches()) {
              try {
                  return Integer.parseInt(m.group(1));
              } catch (NumberFormatException ex) {
                  throw new SAXParseException("Invalid field name: '" + text + "'", locator, ex);
              }
          } else {
              return null;
          }
      }
  
      public static PowerProfile parse(InputStream stream) throws ParseException {
          return (new Parser(stream)).parse();
      }
  
      private PowerProfile() {
      }
  
      public ComponentProfile getComponent(Component component) {
          return mComponents.get(component);
      }
  
  }
  