content: package org.altbeacon.beacon;
  
  import android.annotation.TargetApi;
  import android.bluetooth.BluetoothDevice;
  import android.os.Build;
  import android.util.Log;
  
  import org.altbeacon.beacon.logging.LogManager;
  import org.altbeacon.bluetooth.BleAdvertisement;
  import org.altbeacon.bluetooth.Pdu;
  
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * Created by dyoung on 7/21/14.
   *
   * <p>A <code>BeaconParser</code> may be used to tell the library how to decode a beacon's fields
   * from a Bluetooth LE advertisement by specifying what byte offsets match what fields, and what
   * byte sequence signifies the beacon.  Defining a parser for a specific beacon type may be handled
   * via subclassing ({@link AltBeaconParser see AltBeaconParser}) or by simply constructing an instance and calling the
   * <code>setLayout</code> method.  Either way, you will then need to tell the BeaconManager about
   * it like so:</p>
   *
   * <pre><code>
   * BeaconManager.getBeaconParsers().add(new BeaconParser()
   *   .setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"));
   * </pre></code>
   *
   * <p>
   * For more information on how to set up parsing of a beacon,
   * {@link #setBeaconLayout(String) see setBeaconLayout(String)}
   * </p>
   *
   */
  public class BeaconParser implements Serializable {
      private static final String TAG = "BeaconParser";
      public static final String ALTBEACON_LAYOUT = "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25";
      public static final String EDDYSTONE_TLM_LAYOUT = "x,s:0-1=feaa,m:2-2=20,d:3-3,d:4-5,d:6-7,d:8-11,d:12-15";
      public static final String EDDYSTONE_UID_LAYOUT = "s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19";
      public static final String EDDYSTONE_URL_LAYOUT = "s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-21v";
      public static final String URI_BEACON_LAYOUT = "s:0-1=fed8,m:2-2=00,p:3-3:-41,i:4-21v";
      private static final Pattern I_PATTERN = Pattern.compile("i\\:(\\d+)\\-(\\d+)([blv]*)?");
      private static final Pattern M_PATTERN = Pattern.compile("m\\:(\\d+)-(\\d+)\\=([0-9A-Fa-f]+)");
      private static final Pattern S_PATTERN = Pattern.compile("s\\:(\\d+)-(\\d+)\\=([0-9A-Fa-f\\-]+)");
      private static final Pattern D_PATTERN = Pattern.compile("d\\:(\\d+)\\-(\\d+)([bl]*)?");
      private static final Pattern P_PATTERN = Pattern.compile("p\\:(\\d+)?\\-(\\d+)?\\:?([\\-\\d]+)?");
      private static final Pattern X_PATTERN = Pattern.compile("x");
      private static final char[] HEX_ARRAY = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
      private static final String LITTLE_ENDIAN_SUFFIX = "l";
      private static final String VARIABLE_LENGTH_SUFFIX = "v";
  
      protected String mBeaconLayout;
      private Long mMatchingBeaconTypeCode;
      protected final List<Integer> mIdentifierStartOffsets = new ArrayList<Integer>();
      protected final List<Integer> mIdentifierEndOffsets = new ArrayList<Integer>();
      protected final List<Boolean> mIdentifierLittleEndianFlags = new ArrayList<Boolean>();
      protected final List<Integer> mDataStartOffsets = new ArrayList<Integer>();
      protected final List<Integer> mDataEndOffsets = new ArrayList<Integer>();
      protected final List<Boolean> mDataLittleEndianFlags = new ArrayList<Boolean>();
      protected final List<Boolean> mIdentifierVariableLengthFlags = new ArrayList<Boolean>();
      protected Integer mMatchingBeaconTypeCodeStartOffset;
      protected Integer mMatchingBeaconTypeCodeEndOffset;
      protected Integer mServiceUuidStartOffset;
      protected Integer mServiceUuidEndOffset;
      protected Long mServiceUuid;
      protected byte[] mServiceUuid128Bit = {};
      protected Boolean mExtraFrame;
  
      protected Integer mPowerStartOffset;
      protected Integer mPowerEndOffset;
      protected Integer mDBmCorrection;
      protected Integer mLayoutSize;
      protected Boolean mAllowPduOverflow = true;
      protected String mIdentifier;
      protected int[] mHardwareAssistManufacturers = new int[] { 0x004c };
  
      protected List<BeaconParser> extraParsers = new ArrayList<BeaconParser>();
  
  
      /**
       * Makes a new BeaconParser.  Should normally be immediately followed by a call to #setLayout
       */
      public BeaconParser() {
      }
  
      /**
       * Makes a new BeaconParser with an identifier that can be used to identify beacons decoded with
       * this parser
       */
      public BeaconParser(String identifier) {
          mIdentifier = identifier;
      }
  
      /**
       * <p>Defines a beacon field parsing algorithm based on a string designating the zero-indexed
       * offsets to bytes within a BLE advertisement.</p>
       *
       * <p>If you want to see examples of how other folks have set up BeaconParsers for different
       * kinds of beacons, try doing a Google search for "getBeaconParsers" (include the quotes in
       * the search.)</p>
       *
       * <p>Four prefixes are allowed in the string:</p>
       *
       * <pre>
       *   m - matching byte sequence for this beacon type to parse (exactly one required)
       *   s - ServiceUuid for this beacon type to parse (optional, only for Gatt-based beacons)
       *   i - identifier (at least one required, multiple allowed)
       *   p - power calibration field (exactly one required)
       *   d - data field (optional, multiple allowed)
       *   x - extra layout.  Signifies that the layout is secondary to a primary layout with the same
       *       matching byte sequence (or ServiceUuid).  Extra layouts do not require power or
       *       identifier fields and create Beacon objects without identifiers.
       * </pre>
       *
       * <p>Each prefix is followed by a colon, then an inclusive decimal byte offset for the field from
       * the beginning of the advertisement.  In the case of the m prefix, an = sign follows the byte
       * offset, followed by a big endian hex representation of the bytes that must be matched for
       * this beacon type.  When multiple i or d entries exist in the string, they will be added in
       * order of definition to the identifier or data array for the beacon when parsing the beacon
       * advertisement.  Terms are separated by commas.</p>
       *
       * <p>All offsets from the start of the advertisement are relative to the first byte of the
       * two byte manufacturer code.  The manufacturer code is therefore always at position 0-1</p>
       *
       * <p>All data field and identifier expressions may be optionally suffixed with the letter l, which
       * indicates the field should be parsed as little endian.  If not present, the field will be presumed
       * to be big endian.  Note: serviceUuid fields are always little endian.
       *
       * <p>Identifier fields may be optionally suffixed with the letter v, which
       * indicates the field is variable length, and may be shorter than the declared length if the
       * parsed PDU for the advertisement is shorter than needed to parse the full identifier.
       *
       * <p>If the expression cannot be parsed, a <code>BeaconLayoutException</code> is thrown.</p>
       *
       * <p>Example of a parser string for AltBeacon:</p>
       *
       * </pre>
       *   "m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25"
       * </pre>
       *
       * <p>This signifies that the beacon type will be decoded when an advertisement is found with
       * 0xbeac in bytes 2-3, and a three-part identifier will be pulled out of bytes 4-19, bytes
       * 20-21 and bytes 22-23, respectively.  A signed power calibration value will be pulled out of
       * byte 24, and a data field will be pulled out of byte 25.</p>
       *
       * Note: bytes 0-1 of the BLE manufacturer advertisements are the two byte manufacturer code.
       * Generally you should not match on these two bytes when using a BeaconParser, because it will
       * limit your parser to matching only a transmitter made by a specific manufacturer.  Software
       * and operating systems that scan for beacons typically ignore these two bytes, allowing beacon
       * manufacturers to use their own company code assigned by Bluetooth SIG.  The default parser
       * implementation will already pull out this company code and store it in the
       * beacon.mManufacturer field.  Matcher expressions should therefore start with "m2-3:" followed
       * by the multi-byte hex value that signifies the beacon type.
       *
       * <p>
       * Extra layouts can also be added by using:
       * @see #addExtraDataParser(BeaconParser)
       * This is the preferred method and matching BeaconLayouts by serviceUUID will be deprecated in
       * the future.
       * </p>
       *
       * @param beaconLayout
       * @return the BeaconParser instance
       */
      public BeaconParser setBeaconLayout(String beaconLayout) {
          LogManager.d(TAG, "API setBeaconLayout "+beaconLayout);
  
          mBeaconLayout = beaconLayout;
          Log.d(TAG, "Parsing beacon layout: "+beaconLayout);
  
          String[] terms =  beaconLayout.split(",");
          mExtraFrame = false; // this is not an extra frame by default
  
          for (String term : terms) {
              boolean found = false;
  
              Matcher matcher = I_PATTERN.matcher(term);
              while (matcher.find()) {
                  found = true;
                  try {
                      int startOffset = Integer.parseInt(matcher.group(1));
                      int endOffset = Integer.parseInt(matcher.group(2));
                      Boolean littleEndian = matcher.group(3).contains(LITTLE_ENDIAN_SUFFIX);
                      mIdentifierLittleEndianFlags.add(littleEndian);
                      Boolean variableLength = matcher.group(3).contains(VARIABLE_LENGTH_SUFFIX);
                      mIdentifierVariableLengthFlags.add(variableLength);
                      mIdentifierStartOffsets.add(startOffset);
                      mIdentifierEndOffsets.add(endOffset);
                  } catch (NumberFormatException e) {
                      throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
                  }
              }
              matcher = D_PATTERN.matcher(term);
              while (matcher.find()) {
                  found = true;
                  try {
                      int startOffset = Integer.parseInt(matcher.group(1));
                      int endOffset = Integer.parseInt(matcher.group(2));
                      Boolean littleEndian = matcher.group(3).contains("l");
                      mDataLittleEndianFlags.add(littleEndian);
                      mDataStartOffsets.add(startOffset);
                      mDataEndOffsets.add(endOffset);
                  } catch (NumberFormatException e) {
                      throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
                  }
              }
              matcher = P_PATTERN.matcher(term);
  
              while (matcher.find()) {
                  found = true;
                  String correctionString = "none";
                  try {
                      if (matcher.group(1) != null && matcher.group(2) != null) {
                          int startOffset = Integer.parseInt(matcher.group(1));
                          int endOffset = Integer.parseInt(matcher.group(2));
                          mPowerStartOffset=startOffset;
                          mPowerEndOffset=endOffset;
                      }
  
                      int dBmCorrection = 0;
                      if (matcher.group(3) != null) {
                          correctionString = matcher.group(3);
                          dBmCorrection = Integer.parseInt(correctionString);
                      }
                      mDBmCorrection=dBmCorrection;
                  } catch (NumberFormatException e) {
                      throw new BeaconLayoutException("Cannot parse integer power byte offset ("+correctionString+") in term: " + term);
                  }
              }
              matcher = M_PATTERN.matcher(term);
              while (matcher.find()) {
                  found = true;
                  try {
                      int startOffset = Integer.parseInt(matcher.group(1));
                      int endOffset = Integer.parseInt(matcher.group(2));
                      mMatchingBeaconTypeCodeStartOffset = startOffset;
                      mMatchingBeaconTypeCodeEndOffset = endOffset;
                  } catch (NumberFormatException e) {
                      throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
                  }
                  String hexString = matcher.group(3);
                  try {
                      mMatchingBeaconTypeCode = Long.decode("0x"+hexString);
                  }
                  catch (NumberFormatException e) {
                      throw new BeaconLayoutException("Cannot parse beacon type code: "+hexString+" in term: " + term);
                  }
              }
              matcher = S_PATTERN.matcher(term);
              while (matcher.find()) {
                  found = true;
                  try {
                      int startOffset = Integer.parseInt(matcher.group(1));
                      int endOffset = Integer.parseInt(matcher.group(2));
                      mServiceUuidStartOffset = startOffset;
                      mServiceUuidEndOffset = endOffset;
                  } catch (NumberFormatException e) {
                      throw new BeaconLayoutException("Cannot parse integer byte offset in term: " + term);
                  }
                  String hexString = matcher.group(3);
                  int serviceUuidByteLength = mServiceUuidEndOffset - mServiceUuidStartOffset + 1;
                  if (serviceUuidByteLength == 2) {
                      try {
                          mServiceUuid = Long.decode("0x" + hexString);
                      } catch (NumberFormatException e) {
                          throw new BeaconLayoutException("Cannot parse serviceUuid: " + hexString + " in term: " + term);
                      }
                  } else if (serviceUuidByteLength == 16 || serviceUuidByteLength == 4) {
                      String bytesString = hexString.replace("-", "");
                      if ((bytesString.length()/2) != serviceUuidByteLength) {
                          throw new BeaconLayoutException("ServiceUuid specified: "+hexString+" must be "+serviceUuidByteLength+" bytes long but is "+(bytesString.length()/2)+" bytes long in term: " + term);
                      }
                      mServiceUuid128Bit = new byte[serviceUuidByteLength];
                      for (int i = 0; i < serviceUuidByteLength; i++) {
                          String byteString = bytesString.substring(i*2,i*2+2);
                          try {
                              mServiceUuid128Bit[serviceUuidByteLength-i-1] = (byte) Integer.parseInt(byteString, 16);
                          }
                          catch (NumberFormatException e) {
                              throw new BeaconLayoutException("Cannot parse serviceUuid byte "+byteString+" in term: " + term);
                          }
                      }
                  }
                  else {
                      throw new BeaconLayoutException("Cannot parse serviceUuid -- it must be 2, 4 or 16 bytes long: " + hexString + " in term: " + term);
                  }
              }
              matcher = X_PATTERN.matcher(term);
              while (matcher.find()) {
                  found = true;
                  mExtraFrame = true;
              }
  
              if (!found) {
                  LogManager.d(TAG, "cannot parse term %s", term);
                  throw new BeaconLayoutException("Cannot parse beacon layout term: " + term);
              }
          }
          mLayoutSize = calculateLayoutSize();
          return this;
      }
  
      /**
       * Adds a <code>BeaconParser</code> used for parsing extra BLE beacon advertisement packets for
       * beacons that send multiple different advertisement packets (for example, Eddystone-TLM)
       *
       * @param extraDataParser a parser that must be configured with an "extra layout" prefix
       * @return true when the extra parser is added successfully
       */
      public boolean addExtraDataParser(BeaconParser extraDataParser) {
          //add an extra data parser only if it is not null and it is an extra frame parser
          return extraDataParser != null && extraDataParser.mExtraFrame && extraParsers.add(extraDataParser);
      }
  
      /**
       * Gets a list of extra parsers configured for this <code>BeaconParser</code>.
       *
       * @see #addExtraDataParser
       * @see #setBeaconLayout
       * @return
       */
      public List<BeaconParser> getExtraDataParsers() {
          return new ArrayList<>(extraParsers);
      }
  
      /**
       * Gets an optional identifier field that may be used to identify this parser.  If set, it will
       * be passed along to any beacons decoded with this parser.
       * @return
       */
      public String getIdentifier() {
          return mIdentifier;
      }
  
      /**
       * Returns a list of Bluetooth manufacturer codes which will be used for hardware-assisted
       * accelerated looking for this beacon type
       *
       * The possible codes are defined on this list:
       * https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
       *
       * @return manufacturers
       */
      public int[] getHardwareAssistManufacturers() {
          return mHardwareAssistManufacturers;
      }
  
      /**
       * Sets a list of Bluetooth manufacturer codes which will be used for hardware-assisted
       * accelerated looking for this beacon type
       *
       * The possible codes are defined on this list:
       * https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
       *
       */
      public void setHardwareAssistManufacturerCodes(int[] manufacturers) {
          mHardwareAssistManufacturers = manufacturers;
      }
  
      /**
       * Setting to true indicates that packets should be rejected if the PDU length is too short for
       * the fields.  Some beacons transmit malformed PDU packets that understate their length, so
       * this defaults to false.
       * @param enabled
       */
      public void setAllowPduOverflow(Boolean enabled) {
          mAllowPduOverflow = enabled;
      }
  
      /**
       * @see #mMatchingBeaconTypeCode
       * @return
       */
      public Long getMatchingBeaconTypeCode() {
          if (mMatchingBeaconTypeCode == null) {
              return -1l;
          }
          return mMatchingBeaconTypeCode;
      }
  
      /**
       * see #mMatchingBeaconTypeCodeStartOffset
       * @return
       */
      public int getMatchingBeaconTypeCodeStartOffset() {
          if (mMatchingBeaconTypeCodeStartOffset == null) {
              return -1;
          }
          return mMatchingBeaconTypeCodeStartOffset;
      }
  
      /**
       * see #mMatchingBeaconTypeCodeEndOffset
       * @return
       */
      public int getMatchingBeaconTypeCodeEndOffset() {
          if (mMatchingBeaconTypeCodeEndOffset == null) {
              return -1;
          }
          return mMatchingBeaconTypeCodeEndOffset;
      }
  
  
      /**
       * @see #mServiceUuid
       * @return
       */
      public Long getServiceUuid() {
          return mServiceUuid;
      }
  
      /**
       * @see #mServiceUuid128Bit
       * @return
       */
      public byte[] getServiceUuid128Bit() {
          return mServiceUuid128Bit;
      }
  
  
      /**
       * see #mServiceUuidStartOffset
       * @return
       */
      public int getMServiceUuidStartOffset() {
          return mServiceUuidStartOffset;
      }
  
      /**
       * see #mServiceUuidEndOffset
       * @return
       */
      public int getServiceUuidEndOffset() {
          return mServiceUuidEndOffset;
      }
  
  
      /**
       * Construct a Beacon from a Bluetooth LE packet collected by Android's Bluetooth APIs,
       * including the raw Bluetooth device info.
       *
       * timestampMs excluded for backward compatibility with older api consumers.
       *
       * @Deprecated New implementations should not use this method. It is not intended as a public
       * API and is subject to change or removal in the future.
       *
       * @param scanData The actual packet bytes
       * @param rssi The measured signal strength of the packet
       * @param device The Bluetooth device that was detected
       * @return An instance of a <code>Beacon</code>
       */
      @Deprecated
      public Beacon fromScanData(byte[] scanData, int rssi, BluetoothDevice device) {
          return fromScanData(scanData, rssi, device, System.currentTimeMillis(), new Beacon());
      }
  
      /**
       * Construct a Beacon from a Bluetooth LE packet collected by Android's Bluetooth APIs,
       * including the raw Bluetooth device info
       *
       * @param scanData The actual packet bytes
       * @param rssi The measured signal strength of the packet
       * @param device The Bluetooth device that was detected
       * @param timestampMs The timestamp in milliseconds of the scan execution
       * @return An instance of a <code>Beacon</code>
       */
      public Beacon fromScanData(byte[] scanData, int rssi, BluetoothDevice device, long timestampMs) {
          return fromScanData(scanData, rssi, device, timestampMs, new Beacon());
      }
  
      protected Beacon fromScanData(byte[] bytesToProcess, int rssi, BluetoothDevice device, long timestampMs, Beacon beacon) {
          BleAdvertisement advert = new BleAdvertisement(bytesToProcess);
          boolean parseSucceeded = false;
          ArrayList<Pdu> pdusToParse = new ArrayList<Pdu>();
          int startByte = 0;
          ArrayList<Identifier> identifiers = new ArrayList<Identifier>();
          ArrayList<Long> dataFields = new ArrayList<Long>();
          int txPower = 0;
  
  
          for (Pdu pdu: advert.getPdus()) {
              if ((pdu.getType() == Pdu.GATT_SERVICE_DATA_UUID_16_BIT_AD_TYPE && mServiceUuid != null) ||
                  (pdu.getType() == Pdu.GATT_SERVICE_DATA_UUID_128_BIT_AD_TYPE && mServiceUuid128Bit.length == 16)  ||
                  (pdu.getType() == Pdu.GATT_SERVICE_DATA_UUID_32_BIT_AD_TYPE && mServiceUuid128Bit.length == 4)  ||
                  (pdu.getType() == Pdu.GATT_SERVICE_COMPLETE_UUID_128_BIT_AD_TYPE && mServiceUuid128Bit.length == 16)  ||
                  pdu.getType() == Pdu.MANUFACTURER_DATA_AD_TYPE) {
  
                  pdusToParse.add(pdu);
                  if (LogManager.isVerboseLoggingEnabled()) {
                      LogManager.d(TAG, "Processing pdu type %02X: %s with startIndex: %d, endIndex: %d", pdu.getType(), bytesToHex(bytesToProcess), pdu.getStartIndex(), pdu.getEndIndex());
                  }
              }
              else {
                  if (LogManager.isVerboseLoggingEnabled()) {
                      LogManager.d(TAG, "Ignoring pdu type %02X", pdu.getType());
                  }
              }
          }
          if (pdusToParse.size() == 0) {
              if (LogManager.isVerboseLoggingEnabled()) {
                  LogManager.d(TAG, "No PDUs to process in this packet.");
              }
          }
          else {
              boolean parseFailed = false;
              for (Pdu pduToParse : pdusToParse) {
                  byte[] serviceUuidBytes = null;
                  byte[] typeCodeBytes = {};
                  if (mMatchingBeaconTypeCodeEndOffset != null && mMatchingBeaconTypeCodeStartOffset >= 0) {
                      typeCodeBytes = longToByteArray(getMatchingBeaconTypeCode(), mMatchingBeaconTypeCodeEndOffset - mMatchingBeaconTypeCodeStartOffset + 1);
                  }
                  serviceUuidBytes = getServiceUuid128Bit();
                  if (getServiceUuid() != null) {
                      serviceUuidBytes = longToByteArray(getServiceUuid(), mServiceUuidEndOffset - mServiceUuidStartOffset + 1, false);
                  }
                  startByte = pduToParse.getStartIndex();
                  boolean patternFound = false;
  
                  if (serviceUuidBytes.length == 0) {
                      if (mMatchingBeaconTypeCodeEndOffset != null) {
                          if (byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) {
                              patternFound = true;
                          }
                      }
                  }
                  else {
                      boolean lengthIsExpected = false;
                      if (pduToParse.getType() == Pdu.GATT_SERVICE_DATA_UUID_128_BIT_AD_TYPE) {
                          if (serviceUuidBytes.length == 16) {
                              lengthIsExpected = true;
                          }
                      }
                      if (pduToParse.getType() == Pdu.GATT_SERVICE_COMPLETE_UUID_128_BIT_AD_TYPE) {
                          if (serviceUuidBytes.length == 16) {
                              lengthIsExpected = true;
                          }
                      }
                      if (pduToParse.getType() == Pdu.GATT_SERVICE_DATA_UUID_16_BIT_AD_TYPE) {
                          if (serviceUuidBytes.length == 2) {
                              lengthIsExpected = true;
                          }
                      }
                      if (pduToParse.getType() == Pdu.GATT_SERVICE_DATA_UUID_32_BIT_AD_TYPE) {
                          if (serviceUuidBytes.length == 4) {
                              lengthIsExpected = true;
                          }
                      }
                      if (lengthIsExpected) {
                          if (byteArraysMatch(bytesToProcess, startByte + mServiceUuidStartOffset, serviceUuidBytes)) {
                              if (mMatchingBeaconTypeCodeEndOffset != null) {
                                  if (byteArraysMatch(bytesToProcess, startByte + mMatchingBeaconTypeCodeStartOffset, typeCodeBytes)) {
                                      patternFound = true;
                                  }
                              }
                              else {
                                  if (pduToParse.getType() == Pdu.GATT_SERVICE_DATA_UUID_16_BIT_AD_TYPE || pduToParse.getType() == Pdu.GATT_SERVICE_DATA_UUID_128_BIT_AD_TYPE || pduToParse.getType() == Pdu.GATT_SERVICE_DATA_UUID_32_BIT_AD_TYPE || pduToParse.getType() == Pdu.GATT_SERVICE_COMPLETE_UUID_128_BIT_AD_TYPE) {
                                      patternFound = true;
                                  }
                              }
  
                          }
                      }
                  }
  
                  if (patternFound == false) {
                      // This is not a beacon
                      if (getServiceUuid() != null || getServiceUuid128Bit().length != 0) {
                          if (LogManager.isVerboseLoggingEnabled()) {
                              int offset = 0;
                              if (mMatchingBeaconTypeCodeStartOffset != null) {
                                  offset = mMatchingBeaconTypeCodeStartOffset;
                              }
                              LogManager.d(TAG, "This is not a matching Beacon advertisement. Was expecting %s at offset %d and %s at offset %d.  "
                                              + "The bytes I see are: %s",
                                      byteArrayToString(serviceUuidBytes),
                                      startByte + mServiceUuidStartOffset,
                                      byteArrayToString(typeCodeBytes),
                                      startByte + offset,
                                      bytesToHex(bytesToProcess));
                          }
                      }
                      else {
                          if (LogManager.isVerboseLoggingEnabled()) {
                              LogManager.d(TAG, "This is not a matching Beacon advertisement. (Was expecting %s.  "
                                              + "The bytes I see are: %s", byteArrayToString(typeCodeBytes),
                                      bytesToHex(bytesToProcess));
  
                          }
                      }
                  } else {
                      if (LogManager.isVerboseLoggingEnabled()) {
                          LogManager.d(TAG, "This is a recognized beacon advertisement -- %s seen",
                                  byteArrayToString(typeCodeBytes));
                          LogManager.d(TAG, "Bytes are: %s", bytesToHex(bytesToProcess));
                      }
                  }
  
                  if (patternFound) {
                      if (bytesToProcess.length <= startByte+mLayoutSize && mAllowPduOverflow) {
                          // If the layout size is bigger than this PDU, and we allow overflow.  Make sure
                          // the byte buffer is big enough by zero padding the end so we don't try to read
                          // outside the byte array of the advertisement
                          if (LogManager.isVerboseLoggingEnabled()) {
                              LogManager.d(TAG, "Expanding buffer because it is too short to parse: "+bytesToProcess.length+", needed: "+(startByte+mLayoutSize));
                          }
                          bytesToProcess = ensureMaxSize(bytesToProcess, startByte+mLayoutSize);
                      }
                      for (int i = 0; i < mIdentifierEndOffsets.size(); i++) {
                          int endIndex = mIdentifierEndOffsets.get(i) + startByte;
  
                          if (endIndex > pduToParse.getEndIndex() && mIdentifierVariableLengthFlags.get(i)) {
                              if (LogManager.isVerboseLoggingEnabled()) {
                                  LogManager.d(TAG, "Need to truncate identifier by "+(endIndex-pduToParse.getEndIndex()));
                              }
                              // If this is a variable length identifier, we truncate it to the size that
                              // is available in the packet
                              int start = mIdentifierStartOffsets.get(i) + startByte;
                              int end = pduToParse.getEndIndex()+1;
                              if (end <= start) {
                                  LogManager.d(TAG, "PDU is too short for identifer.  Packet is malformed");
                                  return null;
                              }
                              Identifier identifier = Identifier.fromBytes(bytesToProcess, start, end, mIdentifierLittleEndianFlags.get(i));
                              identifiers.add(identifier);
                          }
                          else if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
                              parseFailed = true;
                              if (LogManager.isVerboseLoggingEnabled()) {
                                  LogManager.d(TAG, "Cannot parse identifier "+i+" because PDU is too short.  endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex());
                              }
                          }
                          else {
                              Identifier identifier = Identifier.fromBytes(bytesToProcess, mIdentifierStartOffsets.get(i) + startByte, endIndex+1, mIdentifierLittleEndianFlags.get(i));
                              identifiers.add(identifier);
                          }
                      }
                      for (int i = 0; i < mDataEndOffsets.size(); i++) {
                          int endIndex = mDataEndOffsets.get(i) + startByte;
                          if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
                              if (LogManager.isVerboseLoggingEnabled()) {
                                  LogManager.d(TAG, "Cannot parse data field "+i+" because PDU is too short.  endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex()+".  Setting value to 0");
                              }
                              dataFields.add(new Long(0l));
                          }
                          else {
                              String dataString = byteArrayToFormattedString(bytesToProcess, mDataStartOffsets.get(i) + startByte, endIndex, mDataLittleEndianFlags.get(i));
                              dataFields.add(Long.decode(dataString));
                          }
                      }
  
                      if (mPowerStartOffset != null) {
                          int endIndex = mPowerEndOffset + startByte;
                          try {
                              if (endIndex > pduToParse.getEndIndex() && !mAllowPduOverflow) {
                                  parseFailed = true;
                                  if (LogManager.isVerboseLoggingEnabled()) {
                                      LogManager.d(TAG, "Cannot parse power field because PDU is too short.  endIndex: " + endIndex + " PDU endIndex: " + pduToParse.getEndIndex());
                                  }
                              }
                              else {
                                  String powerString = byteArrayToFormattedString(bytesToProcess, mPowerStartOffset + startByte, mPowerEndOffset + startByte, false);
                                  txPower = Integer.parseInt(powerString)+mDBmCorrection;
                                  // make sure it is a signed integer
                                  if (txPower > 127) {
                                      txPower -= 256;
                                  }
                              }
                          }
                          catch (NumberFormatException e1) {
                              // keep default value
                          }
                          catch (NullPointerException e2) {
                              // keep default value
                          }
                      }
                      else {
                          if (mDBmCorrection != null) {
                              txPower = mDBmCorrection;
                          }
                      }
                      if (!parseFailed) {
                          parseSucceeded = true;
                          // exit processing PDUs on the first beacon we find.  Only one beacon per advertisement!
                          break;
                      }
                  }
              }
          }
  
          if (parseSucceeded) {
              int beaconTypeCode = -1;
              if (mMatchingBeaconTypeCodeEndOffset != null) {
                  String beaconTypeString = byteArrayToFormattedString(bytesToProcess, mMatchingBeaconTypeCodeStartOffset+startByte, mMatchingBeaconTypeCodeEndOffset+startByte, false);
                  beaconTypeCode = Integer.parseInt(beaconTypeString);
              }
  
              int manufacturer = 0;
              String manufacturerString = byteArrayToFormattedString(bytesToProcess, startByte, startByte+1, true);
              manufacturer = Integer.parseInt(manufacturerString);
  
              String macAddress = null;
              String name = null;
              if (device != null) {
                  macAddress = device.getAddress();
                  try {
                    name = device.getName();
                  }
                  catch (SecurityException e) {
                    LogManager.d(TAG,"Cannot read device name without Manifest.permission.BLUETOOTH_CONNECT");
                  }
              }
  
              beacon.mIdentifiers = identifiers;
              beacon.mDataFields = dataFields;
              beacon.mRssi = rssi;
              beacon.mBeaconTypeCode = beaconTypeCode;
              if (mServiceUuid != null) {
                  beacon.mServiceUuid = (int) mServiceUuid.longValue();
              }
              else {
                  beacon.mServiceUuid = -1;
              }
  
              beacon.mBluetoothAddress = macAddress;
              beacon.mBluetoothName= name;
              beacon.mManufacturer = manufacturer;
              beacon.mParserIdentifier = mIdentifier;
              beacon.mMultiFrameBeacon = extraParsers.size() > 0 || mExtraFrame;
              beacon.mFirstCycleDetectionTimestamp = timestampMs;
              beacon.mLastCycleDetectionTimestamp = timestampMs;
              beacon.mLastPacketRawBytes = bytesToProcess;
              beacon.mTxPower = txPower;
              return beacon;
          }
          else {
              return null;
          }
      }
  
      /**
       * Get BLE advertisement bytes for a Beacon
       * @param beacon the beacon containing the data to be transmitted
       * @return the byte array of the advertisement
       */
      @TargetApi(Build.VERSION_CODES.GINGERBREAD)
      public byte[] getBeaconAdvertisementData(Beacon beacon) {
          byte[] advertisingBytes;
  
          if (beacon.getIdentifiers().size() != getIdentifierCount()) {
              throw new IllegalArgumentException("Beacon has "+beacon.getIdentifiers().size()+" identifiers but format requires "+getIdentifierCount());
          }
  
          int lastIndex = -1;
          if (mMatchingBeaconTypeCodeEndOffset != null && mMatchingBeaconTypeCodeEndOffset > lastIndex) {
              lastIndex = mMatchingBeaconTypeCodeEndOffset;
          }
          if (mPowerEndOffset != null && mPowerEndOffset > lastIndex) {
              lastIndex = mPowerEndOffset;
          }
          for (int identifierNum = 0; identifierNum < this.mIdentifierEndOffsets.size(); identifierNum++) {
              if (this.mIdentifierEndOffsets.get(identifierNum) != null && this.mIdentifierEndOffsets.get(identifierNum) > lastIndex) {
                  lastIndex = this.mIdentifierEndOffsets.get(identifierNum);
              }
          }
          for (int identifierNum = 0; identifierNum < this.mDataEndOffsets.size(); identifierNum++) {
              if (this.mDataEndOffsets.get(identifierNum) != null && this.mDataEndOffsets.get(identifierNum) > lastIndex) {
                  lastIndex = this.mDataEndOffsets.get(identifierNum);
              }
          }
  
          // we must adjust the lastIndex to account for variable length identifiers, if there are any.
          int adjustedIdentifiersLength = 0;
          for (int identifierNum = 0; identifierNum < this.mIdentifierStartOffsets.size(); identifierNum++) {
              if (mIdentifierVariableLengthFlags.get(identifierNum)) {
                  int declaredIdentifierLength = (this.mIdentifierEndOffsets.get(identifierNum) - this.mIdentifierStartOffsets.get(identifierNum)+1);
                  int actualIdentifierLength = beacon.getIdentifier(identifierNum).getByteCount();
                  adjustedIdentifiersLength += actualIdentifierLength;
                  adjustedIdentifiersLength -= declaredIdentifierLength;
              }
          }
          lastIndex += adjustedIdentifiersLength;
  
          int serviceUuidByteLength = 0;
  
          int prefixLength = 2; // 2 bytes for manufacturer code and 16 bit service uuid
          if (mServiceUuid128Bit != null && mServiceUuid128Bit.length > 0) {
              serviceUuidByteLength = mServiceUuidEndOffset - mServiceUuidStartOffset + 1;
              prefixLength = serviceUuidByteLength; // 4/16 bytes for 32/128 bit service uuid serviceUuid
          }
  
          advertisingBytes = new byte[lastIndex+1-prefixLength];
          if (mMatchingBeaconTypeCodeEndOffset != null) {
              long beaconTypeCode = this.getMatchingBeaconTypeCode();
              // set type code
              for (int index = this.mMatchingBeaconTypeCodeStartOffset; index <= this.mMatchingBeaconTypeCodeEndOffset; index++) {
                  byte value = (byte) (this.getMatchingBeaconTypeCode() >> (8*(this.mMatchingBeaconTypeCodeEndOffset-index)) & 0xff);
                  advertisingBytes[index-prefixLength] = value;
              }
          }
  
          // set identifiers
          for (int identifierNum = 0; identifierNum < this.mIdentifierStartOffsets.size(); identifierNum++) {
              byte[] identifierBytes = beacon.getIdentifier(identifierNum).toByteArrayOfSpecifiedEndianness(!this.mIdentifierLittleEndianFlags.get(identifierNum));
  
              // If the identifier we are trying to stuff into the space is different than the space available
              // adjust it
              if (identifierBytes.length < getIdentifierByteCount(identifierNum)) {
                  if (!mIdentifierVariableLengthFlags.get(identifierNum)) {
                      // Pad it, but only if this is not a variable length identifier
                      if (mIdentifierLittleEndianFlags.get(identifierNum)) {
                          // this is little endian.  Pad at the end of the array
                          identifierBytes = Arrays.copyOf(identifierBytes,getIdentifierByteCount(identifierNum));
                      }
                      else {
                          // this is big endian.  Pad at the beginning of the array
                          byte[] newIdentifierBytes = new byte[getIdentifierByteCount(identifierNum)];
                          System.arraycopy(identifierBytes, 0, newIdentifierBytes, getIdentifierByteCount(identifierNum)-identifierBytes.length, identifierBytes.length);
                          identifierBytes = newIdentifierBytes;
                      }
                  }
                  LogManager.d(TAG, "Expanded identifier because it is too short.  It is now: "+byteArrayToString(identifierBytes));
              }
              else if (identifierBytes.length > getIdentifierByteCount(identifierNum)) {
                  if (mIdentifierLittleEndianFlags.get(identifierNum)) {
                      // Truncate it at the beginning for big endian
                      identifierBytes = Arrays.copyOfRange(identifierBytes, getIdentifierByteCount(identifierNum)-identifierBytes.length, getIdentifierByteCount(identifierNum));
                  }
                  else {
                      // Truncate it at the end for little endian
                      identifierBytes = Arrays.copyOf(identifierBytes,getIdentifierByteCount(identifierNum));
                  }
                  LogManager.d(TAG, "Truncated identifier because it is too long.  It is now: "+byteArrayToString(identifierBytes));
              }
              else {
                  LogManager.d(TAG, "Identifier size is just right: "+byteArrayToString(identifierBytes));
              }
              for (int index = this.mIdentifierStartOffsets.get(identifierNum); index <= this.mIdentifierStartOffsets.get(identifierNum)+identifierBytes.length-1; index ++) {
                  advertisingBytes[index-prefixLength] = (byte) identifierBytes[index-this.mIdentifierStartOffsets.get(identifierNum)];
              }
          }
  
          // set power
          if (this.mPowerStartOffset != null && this.mPowerEndOffset != null  && this.mPowerStartOffset >= 2) {
              for (int index = this.mPowerStartOffset; index <= this.mPowerEndOffset; index ++) {
                  advertisingBytes[index-prefixLength] = (byte) (beacon.getTxPower() >> (8*(index - this.mPowerStartOffset)) & 0xff);
              }
          }
  
          // set data fields
          for (int dataFieldNum = 0; dataFieldNum < this.mDataStartOffsets.size(); dataFieldNum++) {
              long dataField = beacon.getDataFields().get(dataFieldNum);
              int dataFieldLength = this.mDataEndOffsets.get(dataFieldNum) - this.mDataStartOffsets.get(dataFieldNum);
              for (int index = 0; index <= dataFieldLength; index ++) {
                  int endianCorrectedIndex = index;
                  if (!this.mDataLittleEndianFlags.get(dataFieldNum)) {
                      endianCorrectedIndex = dataFieldLength-index;
                  }
                  advertisingBytes[this.mDataStartOffsets.get(dataFieldNum)-prefixLength+endianCorrectedIndex] = (byte) (dataField >> (8*index) & 0xff);
              }
          }
          return advertisingBytes;
      }
  
      public BeaconParser setMatchingBeaconTypeCode(Long typeCode) {
          mMatchingBeaconTypeCode = typeCode;
          return this;
      }
  
      /**
       * Caclculates the byte size of the specified identifier in this format
       * @param identifierNum
       * @return bytes
       */
      public int getIdentifierByteCount(int identifierNum) {
          return mIdentifierEndOffsets.get(identifierNum) - mIdentifierStartOffsets.get(identifierNum) + 1;
      }
  
      /**
       * @return the number of identifiers in this beacon format
       */
      public int getIdentifierCount() {
          return mIdentifierStartOffsets.size();
      }
  
      /**
       * @return the number of data fields in this beacon format
       */
      public int getDataFieldCount() {
          return mDataStartOffsets.size();
      }
  
      /**
       * @return the layout string for the parser
       */
      public String getLayout() {
          return mBeaconLayout;
      }
  
      /**
       * @return the correction value in dBm to apply to the calibrated txPower to get a 1m calibrated value.
       * Some formats like Eddystone use a 0m calibrated value, which requires this correction
       */
      public int getPowerCorrection() { return mDBmCorrection; }
  
      protected static String bytesToHex(byte[] bytes) {
          char[] hexChars = new char[bytes.length * 2];
          int v;
          for ( int j = 0; j < bytes.length; j++ ) {
              v = bytes[j] & 0xFF;
              hexChars[j * 2] = HEX_ARRAY[v >>> 4];
              hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
          }
          return new String(hexChars);
      }
  
      public static class BeaconLayoutException extends RuntimeException {
          public BeaconLayoutException(String s) {
          super(s);
          }
      }
  
      public static byte[] longToByteArray(long longValue, int length) {
          return longToByteArray(longValue, length, true);
      }
  
      public static byte[] longToByteArray(long longValue, int length, boolean bigEndian) {
          byte[] array = new byte[length];
          for (int i = 0; i < length; i++){
              int adjustedI = bigEndian ? i : length - i -1;
              long mask = 0xffl << (length-adjustedI-1)*8;
              long shift = (length-adjustedI-1)*8;
              long value = ((longValue & mask)  >> shift);
              array[i] = (byte) value;
          }
          return array;
      }
      private int calculateLayoutSize() {
          int lastEndOffset = 0;
          if (mIdentifierEndOffsets != null) {
              for (int endOffset : mIdentifierEndOffsets) {
                  if (endOffset > lastEndOffset) {
                      lastEndOffset = endOffset;
                  }
              }
          }
          if (mDataEndOffsets != null) {
              for (int endOffset : mDataEndOffsets) {
                  if (endOffset > lastEndOffset) {
                      lastEndOffset = endOffset;
                  }
              }
          }
          if (mPowerEndOffset != null && mPowerEndOffset > lastEndOffset ) {
              lastEndOffset = mPowerEndOffset;
          }
          if (mServiceUuidEndOffset != null && mServiceUuidEndOffset > lastEndOffset) {
              lastEndOffset = mServiceUuidEndOffset;
          }
          return lastEndOffset+1;
      }
  
      private boolean byteArraysMatch(byte[] source, int offset, byte[] expected) {
          int length = expected.length;
          if (source.length - offset < length) {
              return false;
          }
          for (int i = 0; i <  length; i++) {
              if (source[offset + i] != expected[i]) {
                  return false;
              }
          }
          return true;
      }
  
      private String byteArrayToString(byte[] bytes) {
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < bytes.length; i++) {
              sb.append(String.format("%02x", bytes[i]));
              sb.append(" ");
          }
          return sb.toString().trim();
      }
  
      private String byteArrayToFormattedString(byte[] byteBuffer, int startIndex, int endIndex, boolean littleEndian) {
          byte[] bytes = new byte[endIndex-startIndex+1];
          if (littleEndian) {
              for (int i = 0; i <= endIndex-startIndex; i++) {
                  bytes[i] = byteBuffer[startIndex+bytes.length-1-i];
              }
          }
          else {
              for (int i = 0; i <= endIndex-startIndex; i++) {
                  bytes[i] = byteBuffer[startIndex+i];
              }
          }
  
  
          int length = endIndex-startIndex +1;
          // We treat a 1-4 byte number as decimal string
          if (length < 5) {
              long number = 0l;
              for (int i = 0; i < bytes.length; i++)  {
                  long byteValue = (long) (bytes[bytes.length - i-1] & 0xff);
                  long positionValue = (long) Math.pow(256.0,i*1.0);
                  long calculatedValue =  (byteValue * positionValue);
                  number += calculatedValue;
              }
              return Long.toString(number);
          }
  
          // We treat a 7+ byte number as a hex string
          String hexString = bytesToHex(bytes);
  
          // And if it is a 12 byte number we add dashes to it to make it look like a standard UUID
          if (bytes.length == 16) {
              StringBuilder sb = new StringBuilder();
              sb.append(hexString.substring(0,8));
              sb.append("-");
              sb.append(hexString.substring(8,12));
              sb.append("-");
              sb.append(hexString.substring(12,16));
              sb.append("-");
              sb.append(hexString.substring(16,20));
              sb.append("-");
              sb.append(hexString.substring(20,32));
              return sb.toString();
          }
          return "0x"+hexString;
      }
  
      @TargetApi(Build.VERSION_CODES.GINGERBREAD)
      private byte[] ensureMaxSize(byte[] array, int requiredLength) {
          if (array.length >= requiredLength) {
              return array;
          }
          return Arrays.copyOf(array, requiredLength);
      }
  
      @Override
      public int hashCode() {
          return Arrays.hashCode(new Object[] {
                  mMatchingBeaconTypeCode,
                  mIdentifierStartOffsets,
                  mIdentifierEndOffsets,
                  mIdentifierLittleEndianFlags,
                  mDataStartOffsets,
                  mDataEndOffsets,
                  mDataLittleEndianFlags,
                  mIdentifierVariableLengthFlags,
                  mMatchingBeaconTypeCodeStartOffset,
                  mMatchingBeaconTypeCodeEndOffset,
                  mServiceUuidStartOffset,
                  mServiceUuidEndOffset,
                  mServiceUuid,
                  mServiceUuid128Bit,
                  mExtraFrame,
                  mPowerStartOffset,
                  mPowerEndOffset,
                  mDBmCorrection,
                  mLayoutSize,
                  mAllowPduOverflow,
                  mIdentifier,
                  mHardwareAssistManufacturers,
                  extraParsers
              }
          );
      }
  
      @Override
      public boolean equals(Object o) {
          BeaconParser that = null;
          try {
              that = (BeaconParser) o;
              if (that.mBeaconLayout != null && that.mBeaconLayout.equals(this.mBeaconLayout)) {
                  if (that.mIdentifier != null && that.mIdentifier.equals(this.mIdentifier)) {
                      return true;
                  }
              }
          }
          catch (ClassCastException e ) { }
          return false;
      }
  
      @Override
      public String toString() {
          if (mIdentifier == null) {
              return mBeaconLayout;
          }
          return mIdentifier + "~" + mBeaconLayout;
      }
  
      /**
       * Creates a BeaconParser instance from a string in the format <identifier>~<layout>
       * @param string the string to parse
       * @return BeaconParser
       */
      public static BeaconParser fromString(String string) {
          String[] parts = string.split("~");
          if (parts.length != 2) {
              return new BeaconParser().setBeaconLayout(string);
          }
          String identifier = parts[0];
          String layout = parts[1];
          return new BeaconParser(identifier).setBeaconLayout(layout);
      }
  
  }
  