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 android.net.wifi.rtt;
  
  import static java.lang.annotation.RetentionPolicy.SOURCE;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.SystemApi;
  import android.location.Address;
  import android.location.Location;
  import android.net.MacAddress;
  import android.net.Uri;
  import android.net.wifi.rtt.CivicLocationKeys.CivicLocationKeysType;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.text.TextUtils;
  import android.util.SparseArray;
  import android.webkit.MimeTypeMap;
  
  import java.lang.annotation.Retention;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  import java.util.Objects;
  
  /**
   * ResponderLocation is both a Location Configuration Information (LCI) decoder and a Location Civic
   * Report (LCR) decoder for information received from a Wi-Fi Access Point (AP) during Wi-Fi RTT
   * ranging process.
   *
   * <p>This is based on the IEEE P802.11-REVmc/D8.0 spec section 9.4.2.22, under Measurement Report
   * Element. Subelement location data-fields parsed from separate input LCI and LCR Information
   * Elements are unified in this class.</p>
   *
   * <p>Note: The information provided by this class is broadcast by a responder (usually an Access
   * Point), and passed on as-is. There is no guarantee this information is accurate or correct, and
   * as a result developers should carefully consider how this information should be used and provide
   * corresponding advice to users.</p>
   */
  public final class ResponderLocation implements Parcelable {
      private static final int BYTE_MASK = 0xFF;
      private static final int LSB_IN_BYTE = 0x01;
      private static final int MSB_IN_BYTE = 0x80;
      private static final int MIN_BUFFER_SIZE = 3; // length of LEAD_LCI_ELEMENT_BYTES
      private static final int MAX_BUFFER_SIZE = 256;
  
      // Information Element (IE) fields
      private static final byte MEASUREMENT_TOKEN_AUTONOMOUS = 0x01;
      private static final byte MEASUREMENT_REPORT_MODE = 0x00;
      private static final byte MEASUREMENT_TYPE_LCI = 0x08;
      private static final byte MEASUREMENT_TYPE_LCR = 0x0b;
  
      // LCI Subelement IDs
      private static final byte SUBELEMENT_LCI = 0x00;
      private static final byte SUBELEMENT_Z = 0x04;
      private static final byte SUBELEMENT_USAGE = 0x06;
      private static final byte SUBELEMENT_BSSID_LIST = 0x07;
  
      // LCI Subelement Lengths
      private static final int SUBELEMENT_LCI_LENGTH = 16;
      private static final int SUBELEMENT_Z_LENGTH = 6;
      private static final int SUBELEMENT_USAGE_LENGTH1 = 1;
      private static final int SUBELEMENT_USAGE_LENGTH3 = 3;
      private static final int SUBELEMENT_BSSID_LIST_MIN_BUFFER_LENGTH = 1;
  
      private static final byte[] LEAD_LCI_ELEMENT_BYTES = {
              MEASUREMENT_TOKEN_AUTONOMOUS, MEASUREMENT_REPORT_MODE, MEASUREMENT_TYPE_LCI
      };
  
      // Subelement LCI constants
  
      /* The LCI subelement bit-field lengths are described in Figure 9-214 of the REVmc spec and
      represented here as a an array of integers */
      private static final int[] SUBELEMENT_LCI_BIT_FIELD_LENGTHS = {
              6, 34, 6, 34, 4, 6, 30, 3, 1, 1, 1, 2
      };
      private static final int LATLNG_FRACTION_BITS = 25;
      private static final int LATLNG_UNCERTAINTY_BASE = 8;
      private static final int ALTITUDE_FRACTION_BITS = 8;
      private static final int ALTITUDE_UNCERTAINTY_BASE = 21;
      private static final double LAT_ABS_LIMIT = 90.0;
      private static final double LNG_ABS_LIMIT = 180.0;
      private static final int UNCERTAINTY_UNDEFINED = 0;
  
      // Subelement LCI fields indices
      private static final int SUBELEMENT_LCI_LAT_UNCERTAINTY_INDEX = 0;
      private static final int SUBELEMENT_LCI_LAT_INDEX = 1;
      private static final int SUBELEMENT_LCI_LNG_UNCERTAINTY_INDEX = 2;
      private static final int SUBELEMENT_LCI_LNG_INDEX = 3;
      private static final int SUBELEMENT_LCI_ALT_TYPE_INDEX = 4;
      private static final int SUBELEMENT_LCI_ALT_UNCERTAINTY_INDEX = 5;
      private static final int SUBELEMENT_LCI_ALT_INDEX = 6;
      private static final int SUBELEMENT_LCI_DATUM_INDEX = 7;
      private static final int SUBELEMENT_LCI_REGLOC_AGREEMENT_INDEX = 8;
      private static final int SUBELEMENT_LCI_REGLOC_DSE_INDEX = 9;
      private static final int SUBELEMENT_LCI_DEPENDENT_STA_INDEX = 10;
      private static final int SUBELEMENT_LCI_VERSION_INDEX = 11;
  
      /**
       * The Altitude value is interpreted based on the Altitude Type, and the selected mDatum.
       *
       * @hide
       */
      @Retention(SOURCE)
      @IntDef({ALTITUDE_UNDEFINED, ALTITUDE_METERS, ALTITUDE_FLOORS})
      public @interface AltitudeType {
      }
  
      /**
       * Altitude is not defined for the Responder.
       * The altitude and altitude uncertainty should not be used: see section 2.4 of IETF RFC 6225.
       */
      public static final int ALTITUDE_UNDEFINED = 0;
      /** Responder Altitude is measured in meters.  */
      public static final int ALTITUDE_METERS = 1;
      /** Responder Altitude is measured in floors. */
      public static final int ALTITUDE_FLOORS = 2;
  
      /**
       * The Datum value determines how coordinates are organized in relation to the real world.
       *
       * @hide
       */
      @Retention(SOURCE)
      @IntDef({DATUM_UNDEFINED, DATUM_WGS84, DATUM_NAD83_NAV88, DATUM_NAD83_MLLW})
      public @interface DatumType {
      }
  
      /** Datum is not defined. */
      public static final int DATUM_UNDEFINED = 0;
      /** Datum used is WGS84. */
      public static final int DATUM_WGS84 = 1;
      /** Datum used is NAD83 NAV88. */
      public static final int DATUM_NAD83_NAV88 = 2;
      /** Datum used is NAD83 MLLW. */
      public static final int DATUM_NAD83_MLLW = 3;
  
  
      /** Version of the LCI protocol is 1.0, the only defined protocol at this time. */
      public static final int LCI_VERSION_1 = 1;
  
      /** Provider/Source of the location */
      private static final String LOCATION_PROVIDER = "WiFi Access Point";
  
      // LCI Subelement Z constants
      private static final int[] SUBELEMENT_Z_BIT_FIELD_LENGTHS = {2, 14, 24, 8};
      private static final int Z_FLOOR_NUMBER_FRACTION_BITS = 4;
      private static final int Z_FLOOR_HEIGHT_FRACTION_BITS = 12;
      private static final int Z_MAX_HEIGHT_UNCERTAINTY_FACTOR = 25;
  
      // LCI Subelement Z fields indices
      private static final int SUBELEMENT_Z_LAT_EXPECTED_TO_MOVE_INDEX = 0;
      private static final int SUBELEMENT_Z_FLOOR_NUMBER_INDEX = 1;
      private static final int SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_INDEX = 2;
      private static final int SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_UNCERTAINTY_INDEX = 3;
  
      // LCI Subelement Usage Rules constants
      private static final int SUBELEMENT_USAGE_MASK_RETRANSMIT = 0x01;
      private static final int SUBELEMENT_USAGE_MASK_RETENTION_EXPIRES = 0x02;
      private static final int SUBELEMENT_USAGE_MASK_STA_LOCATION_POLICY = 0x04;
  
      // LCI Subelement Usage Rules field indices
      private static final int SUBELEMENT_USAGE_PARAMS_INDEX = 0; // 8 bits
  
      // LCI Subelement BSSID List
      private static final int SUBELEMENT_BSSID_MAX_INDICATOR_INDEX = 0;
      private static final int SUBELEMENT_BSSID_LIST_INDEX = 1;
      private static final int BYTES_IN_A_BSSID = 6;
  
      /**
       * The Expected-To-Move value determines how mobile we expect the STA to be.
       *
       * @hide
       */
      @Retention(SOURCE)
      @IntDef({LOCATION_FIXED, LOCATION_VARIABLE, LOCATION_MOVEMENT_UNKNOWN, LOCATION_RESERVED})
      public @interface ExpectedToMoveType {
      }
  
      /** Location of responder is fixed (does not move) */
      public static final int LOCATION_FIXED = 0;
      /** Location of the responder is variable, and may move */
      public static final int LOCATION_VARIABLE = 1;
      /** Location of the responder is not known to be either fixed or variable. */
      public static final int LOCATION_MOVEMENT_UNKNOWN = 2;
      /** Location of the responder status is a reserved value */
      public static final int LOCATION_RESERVED = 3;
  
      // LCR Subelement IDs
      private static final byte SUBELEMENT_LOCATION_CIVIC = 0x00;
      private static final byte SUBELEMENT_MAP_IMAGE = 0x05;
  
      // LCR Subelement Lengths
      private static final int SUBELEMENT_LOCATION_CIVIC_MIN_LENGTH = 2;
      private static final int SUBELEMENT_LOCATION_CIVIC_MAX_LENGTH = 256;
      private static final int SUBELEMENT_MAP_IMAGE_URL_MAX_LENGTH = 256;
  
      private static final byte[] LEAD_LCR_ELEMENT_BYTES = {
              MEASUREMENT_TOKEN_AUTONOMOUS, MEASUREMENT_REPORT_MODE, MEASUREMENT_TYPE_LCR
      };
  
      // LCR Location Civic Subelement
      private static final int CIVIC_COUNTRY_CODE_INDEX = 0;
      private static final int CIVIC_TLV_LIST_INDEX = 2;
  
      // LCR Map Image Subelement field indexes.
      private static final int SUBELEMENT_IMAGE_MAP_TYPE_INDEX = 0;
      private static final int MAP_TYPE_URL_DEFINED = 0;
      private static final String[] SUPPORTED_IMAGE_FILE_EXTENSIONS = {
              "",
              "png",
              "gif",
              "jpg",
              "svg",
              "dxf",
              "dwg",
              "dwf",
              "cad",
              "tif",
              "gml",
              "kml",
              "bmp",
              "pgm",
              "ppm",
              "xbm",
              "xpm",
              "ico"
      };
  
      // General LCI and LCR state
      private final boolean mIsValid;
  
      /*
        These members are not final because we are not sure if the corresponding subelement will be
        present until after the parsing process. However, the default value should be set as listed.
      */
      private boolean mIsLciValid = false;
      private boolean mIsZValid = false;
      private boolean mIsUsageValid = true; // By default this is assumed true
      private boolean mIsBssidListValid = false;
      private boolean mIsLocationCivicValid = false;
      private boolean mIsMapImageValid = false;
  
      // LCI Subelement LCI state
      private double mLatitudeUncertainty;
      private double mLatitude;
      private double mLongitudeUncertainty;
      private double mLongitude;
      private int mAltitudeType;
      private double mAltitudeUncertainty;
      private double mAltitude;
      private int mDatum;
      private boolean mLciRegisteredLocationAgreement;
      private boolean mLciRegisteredLocationDse;
      private boolean mLciDependentStation;
      private int mLciVersion;
  
      // LCI Subelement Z state
      private int mExpectedToMove;
      private double mFloorNumber;
      private double mHeightAboveFloorMeters;
      private double mHeightAboveFloorUncertaintyMeters;
  
      // LCI Subelement Usage Rights state
      private boolean mUsageRetransmit;
      private boolean mUsageRetentionExpires;
      private boolean mUsageExtraInfoOnAssociation;
  
      // LCI Subelement BSSID List state
      private ArrayList<MacAddress> mBssidList;
  
      // LCR Subelement Location Civic state
      private String mCivicLocationCountryCode;
      private String mCivicLocationString;
      private CivicLocation mCivicLocation;
  
      // LCR Subelement Map Image state
      private int mMapImageType;
      private Uri mMapImageUri;
  
      /**
       * Constructor
       *
       * @param lciBuffer the bytes received in the LCI Measurement Report Information Element
       * @param lcrBuffer the bytes received in the LCR Measurement Report Information Element
       *
       * @hide
       */
      public ResponderLocation(byte[] lciBuffer, byte[] lcrBuffer) {
          boolean isLciIeValid = false;
          boolean isLcrIeValid = false;
          setLciSubelementDefaults();
          setZaxisSubelementDefaults();
          setUsageSubelementDefaults();
          setBssidListSubelementDefaults();
          setCivicLocationSubelementDefaults();
          setMapImageSubelementDefaults();
          if (lciBuffer != null && lciBuffer.length > LEAD_LCI_ELEMENT_BYTES.length) {
              isLciIeValid = parseInformationElementBuffer(
                  MEASUREMENT_TYPE_LCI, lciBuffer, LEAD_LCI_ELEMENT_BYTES);
          }
          if (lcrBuffer != null && lcrBuffer.length > LEAD_LCR_ELEMENT_BYTES.length) {
              isLcrIeValid = parseInformationElementBuffer(
                  MEASUREMENT_TYPE_LCR, lcrBuffer, LEAD_LCR_ELEMENT_BYTES);
          }
  
          boolean isLciValid = isLciIeValid && mIsUsageValid
                  && (mIsLciValid || mIsZValid || mIsBssidListValid);
  
          boolean isLcrValid = isLcrIeValid && mIsUsageValid
                  && (mIsLocationCivicValid || mIsMapImageValid);
  
          mIsValid = isLciValid || isLcrValid;
  
          if (!mIsValid) {
              setLciSubelementDefaults();
              setZaxisSubelementDefaults();
              setCivicLocationSubelementDefaults();
              setMapImageSubelementDefaults();
          }
      }
  
      private ResponderLocation(Parcel in) {
          // Object Validation
          mIsValid = in.readByte() != 0;
          mIsLciValid = in.readByte() != 0;
          mIsZValid = in.readByte() != 0;
          mIsUsageValid = in.readByte() != 0;
          mIsBssidListValid = in.readByte() != 0;
          mIsLocationCivicValid = in.readByte() != 0;
          mIsMapImageValid = in.readByte() != 0;
  
          // LCI Subelement LCI state
          mLatitudeUncertainty = in.readDouble();
          mLatitude = in.readDouble();
          mLongitudeUncertainty = in.readDouble();
          mLongitude = in.readDouble();
          mAltitudeType = in.readInt();
          mAltitudeUncertainty = in.readDouble();
          mAltitude = in.readDouble();
          mDatum = in.readInt();
          mLciRegisteredLocationAgreement = in.readByte() != 0;
          mLciRegisteredLocationDse = in.readByte() != 0;
          mLciDependentStation = in.readByte() != 0;
          mLciVersion = in.readInt();
  
          // LCI Subelement Z state
          mExpectedToMove = in.readInt();
          mFloorNumber = in.readDouble();
          mHeightAboveFloorMeters = in.readDouble();
          mHeightAboveFloorUncertaintyMeters = in.readDouble();
  
          // LCI Usage Rights
          mUsageRetransmit = in.readByte() != 0;
          mUsageRetentionExpires = in.readByte() != 0;
          mUsageExtraInfoOnAssociation = in.readByte() != 0;
  
          // LCI Subelement BSSID List
          mBssidList = in.readArrayList(MacAddress.class.getClassLoader());
  
          // LCR Subelement Location Civic
          mCivicLocationCountryCode = in.readString();
          mCivicLocationString = in.readString();
          mCivicLocation = in.readParcelable(this.getClass().getClassLoader());
  
          // LCR Subelement Map Image
          mMapImageType = in.readInt();
          String urlString = in.readString();
          if (TextUtils.isEmpty(urlString)) {
              mMapImageUri = null;
          } else {
              mMapImageUri = Uri.parse(urlString);
          }
      }
  
      public static final @android.annotation.NonNull Creator<ResponderLocation> CREATOR = new Creator<ResponderLocation>() {
          @Override
          public ResponderLocation createFromParcel(Parcel in) {
              return new ResponderLocation(in);
          }
  
          @Override
          public ResponderLocation[] newArray(int size) {
              return new ResponderLocation[size];
          }
      };
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel parcel, int flags) {
          // Object
          parcel.writeByte((byte) (mIsValid ? 1 : 0));
          parcel.writeByte((byte) (mIsLciValid ? 1 : 0));
          parcel.writeByte((byte) (mIsZValid ? 1 : 0));
          parcel.writeByte((byte) (mIsUsageValid ? 1 : 0));
          parcel.writeByte((byte) (mIsBssidListValid ? 1 : 0));
          parcel.writeByte((byte) (mIsLocationCivicValid ? 1 : 0));
          parcel.writeByte((byte) (mIsMapImageValid ? 1 : 0));
  
          // LCI Subelement LCI state
          parcel.writeDouble(mLatitudeUncertainty);
          parcel.writeDouble(mLatitude);
          parcel.writeDouble(mLongitudeUncertainty);
          parcel.writeDouble(mLongitude);
          parcel.writeInt(mAltitudeType);
          parcel.writeDouble(mAltitudeUncertainty);
          parcel.writeDouble(mAltitude);
          parcel.writeInt(mDatum);
          parcel.writeByte((byte) (mLciRegisteredLocationAgreement ? 1 : 0));
          parcel.writeByte((byte) (mLciRegisteredLocationDse ? 1 : 0));
          parcel.writeByte((byte) (mLciDependentStation ? 1 : 0));
          parcel.writeInt(mLciVersion);
  
          // LCI Subelement Z state
          parcel.writeInt(mExpectedToMove);
          parcel.writeDouble(mFloorNumber);
          parcel.writeDouble(mHeightAboveFloorMeters);
          parcel.writeDouble(mHeightAboveFloorUncertaintyMeters);
  
          // LCI Usage Rights
          parcel.writeByte((byte) (mUsageRetransmit ? 1 : 0));
          parcel.writeByte((byte) (mUsageRetentionExpires ? 1 : 0));
          parcel.writeByte((byte) (mUsageExtraInfoOnAssociation ? 1 : 0));
  
          // LCI Subelement BSSID List
          parcel.writeList(mBssidList);
  
          // LCR Subelement Location Civic
          parcel.writeString(mCivicLocationCountryCode);
          parcel.writeString(mCivicLocationString);
          parcel.writeParcelable(mCivicLocation, flags);
  
          // LCR Subelement Map Image
          parcel.writeInt(mMapImageType);
          if (mMapImageUri != null) {
              parcel.writeString(mMapImageUri.toString());
          } else {
              parcel.writeString("");
          }
      }
  
      /**
       * Test if the Information Element (IE) is in the correct format, and then parse its Subelements
       * based on their type, and setting state in this object when present.
       *
       * @return a boolean indicating the success of the parsing function
       */
      private boolean parseInformationElementBuffer(
              int ieType, byte[] buffer, byte[] expectedLeadBytes) {
          int bufferPtr = 0;
          int bufferLength = buffer.length;
  
          // Ensure the buffer size is within expected limits
          if (bufferLength < MIN_BUFFER_SIZE || bufferLength > MAX_BUFFER_SIZE) {
              return false;
          }
  
          // Ensure the IE contains the correct leading bytes
          byte[] leadBufferBytes = Arrays.copyOfRange(buffer, bufferPtr, expectedLeadBytes.length);
          if (!Arrays.equals(leadBufferBytes, expectedLeadBytes)) {
              return false;
          }
  
          // Iterate through the sub-elements contained in the Information Element (IE)
          bufferPtr += expectedLeadBytes.length;
          // Loop over the buffer ensuring there are 2-bytes available for each new subelement tested.
          while (bufferPtr + 1 < bufferLength) {
              byte subelement = buffer[bufferPtr++];
              int subelementLength = buffer[bufferPtr++];
              // Check there is enough data for the next subelement
              if ((bufferPtr + subelementLength) > bufferLength || subelementLength <= 0) {
                  return false;
              }
  
              byte[] subelementData =
                      Arrays.copyOfRange(buffer, bufferPtr, bufferPtr + subelementLength);
  
              if (ieType == MEASUREMENT_TYPE_LCI) {
                  switch (subelement) {
                      case SUBELEMENT_LCI:
                          mIsLciValid = parseSubelementLci(subelementData);
                          if (!mIsLciValid || mLciVersion != LCI_VERSION_1) {
                              setLciSubelementDefaults();
                          }
                          break;
                      case SUBELEMENT_Z:
                          mIsZValid = parseSubelementZ(subelementData);
                          if (!mIsZValid) {
                              setZaxisSubelementDefaults();
                          }
                          break;
                      case SUBELEMENT_USAGE:
                          mIsUsageValid = parseSubelementUsage(subelementData);
                          // Note: if the Usage Subelement is not valid, don't reset the state, as
                          // it is now indicating the whole ResponderLocation is invalid.
                          break;
                      case SUBELEMENT_BSSID_LIST:
                          mIsBssidListValid = parseSubelementBssidList(subelementData);
                          if (!mIsBssidListValid) {
                              setBssidListSubelementDefaults();
                          }
                          break;
                      default:
                          break; // skip over unused or vendor specific subelements
                  }
              } else if (ieType == MEASUREMENT_TYPE_LCR) {
                  switch (subelement) {
                      case SUBELEMENT_LOCATION_CIVIC:
                          mIsLocationCivicValid = parseSubelementLocationCivic(subelementData);
                          if (!mIsLocationCivicValid) {
                              setCivicLocationSubelementDefaults();
                          }
                          break;
                      case SUBELEMENT_MAP_IMAGE:
                          mIsMapImageValid = parseSubelementMapImage(subelementData);
                          if (!mIsMapImageValid) {
                              setMapImageSubelementDefaults();
                          }
                          break;
                      default:
                          break; // skip over unused or other vendor specific subelements
                  }
              }
  
              bufferPtr += subelementLength;
          }
          return true;
      }
  
      /**
       * Parse the LCI Sub-Element in the LCI Information Element (IE).
       *
       * @param buffer a buffer containing the subelement
       * @return boolean true indicates success
       */
      private boolean parseSubelementLci(byte[] buffer) {
          if (buffer.length > SUBELEMENT_LCI_LENGTH) {
              return false;
          }
          swapEndianByteByByte(buffer);
          long[] subelementLciFields = getFieldData(buffer, SUBELEMENT_LCI_BIT_FIELD_LENGTHS);
          if (subelementLciFields == null) {
              return false;
          }
          // Set member state based on parsed buffer data
          mLatitudeUncertainty = decodeLciLatLngUncertainty(
                  subelementLciFields[SUBELEMENT_LCI_LAT_UNCERTAINTY_INDEX]);
          mLatitude = decodeLciLatLng(subelementLciFields, SUBELEMENT_LCI_BIT_FIELD_LENGTHS,
                  SUBELEMENT_LCI_LAT_INDEX, LAT_ABS_LIMIT);
          mLongitudeUncertainty = decodeLciLatLngUncertainty(
                  subelementLciFields[SUBELEMENT_LCI_LNG_UNCERTAINTY_INDEX]);
          mLongitude =
                  decodeLciLatLng(subelementLciFields, SUBELEMENT_LCI_BIT_FIELD_LENGTHS,
                          SUBELEMENT_LCI_LNG_INDEX, LNG_ABS_LIMIT);
          mAltitudeType = (int) subelementLciFields[SUBELEMENT_LCI_ALT_TYPE_INDEX] & BYTE_MASK;
          mAltitudeUncertainty =
                  decodeLciAltUncertainty(subelementLciFields[SUBELEMENT_LCI_ALT_UNCERTAINTY_INDEX]);
          mAltitude =
                  Math.scalb(subelementLciFields[SUBELEMENT_LCI_ALT_INDEX], -ALTITUDE_FRACTION_BITS);
          mDatum = (int) subelementLciFields[SUBELEMENT_LCI_DATUM_INDEX] & BYTE_MASK;
          mLciRegisteredLocationAgreement =
                  (subelementLciFields[SUBELEMENT_LCI_REGLOC_AGREEMENT_INDEX] == 1);
          mLciRegisteredLocationDse =
                  (subelementLciFields[SUBELEMENT_LCI_REGLOC_DSE_INDEX] == 1);
          mLciDependentStation =
                  (subelementLciFields[SUBELEMENT_LCI_DEPENDENT_STA_INDEX] == 1);
          mLciVersion = (int) subelementLciFields[SUBELEMENT_LCI_VERSION_INDEX];
          return true;
      }
  
      /**
       * Decode the floating point value of an encoded lat or lng in the LCI subelement field.
       *
       * @param fields        the array of field data represented as longs
       * @param bitFieldSizes the lengths of each field
       * @param offset        the offset of the field being decoded
       * @param limit the maximum absolute value (note: different for lat vs lng)
       * @return the floating point value of the lat or lng
       */
      private double decodeLciLatLng(long[] fields, int[] bitFieldSizes, int offset, double limit) {
          double angle;
          if ((fields[offset] & (long) Math.pow(2, bitFieldSizes[offset] - 1)) != 0) {
              // Negative 2's complement value
              // Note: The Math.pow(...) method cannot return a NaN value because the bitFieldSize
              // for Lat or Lng is limited to exactly 34 bits.
              angle = Math.scalb((double) fields[offset] - Math.pow(2, bitFieldSizes[offset]),
                      -LATLNG_FRACTION_BITS);
          } else {
              // Positive 2's complement value
              angle = Math.scalb((double) fields[offset], -LATLNG_FRACTION_BITS);
          }
          if (angle > limit) {
              angle = limit;
          } else if (angle < -limit) {
              angle = -limit;
          }
          return angle;
      }
  
      /**
       * Coverts an encoded Lat/Lng uncertainty into a number of degrees.
       *
       * @param encodedValue the encoded uncertainty
       * @return the value in degrees
       */
      private double decodeLciLatLngUncertainty(long encodedValue) {
          return Math.pow(2, LATLNG_UNCERTAINTY_BASE - encodedValue);
      }
  
      /**
       * Converts an encoded Alt uncertainty into a number of degrees.
       *
       * @param encodedValue the encoded uncertainty
       * @return the value in degrees
       */
      private double decodeLciAltUncertainty(long encodedValue) {
          return Math.pow(2, ALTITUDE_UNCERTAINTY_BASE - encodedValue);
      }
  
      /**
       * Parse the Z subelement of the LCI IE.
       *
       * @param buffer a buffer containing the subelement
       * @return boolean true indicates success
       */
      private boolean parseSubelementZ(byte[] buffer) {
          if (buffer.length != SUBELEMENT_Z_LENGTH) {
              return false;
          }
          swapEndianByteByByte(buffer);
          long[] subelementZFields = getFieldData(buffer, SUBELEMENT_Z_BIT_FIELD_LENGTHS);
          if (subelementZFields == null) {
              return false;
          }
  
          mExpectedToMove =
                  (int) subelementZFields[SUBELEMENT_Z_LAT_EXPECTED_TO_MOVE_INDEX] & BYTE_MASK;
          mFloorNumber = decodeZUnsignedToSignedValue(subelementZFields,
                  SUBELEMENT_Z_BIT_FIELD_LENGTHS, SUBELEMENT_Z_FLOOR_NUMBER_INDEX,
                  Z_FLOOR_NUMBER_FRACTION_BITS);
  
          mHeightAboveFloorMeters = decodeZUnsignedToSignedValue(subelementZFields,
                  SUBELEMENT_Z_BIT_FIELD_LENGTHS, SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_INDEX,
                  Z_FLOOR_HEIGHT_FRACTION_BITS);
  
          long zHeightUncertainty =
                  subelementZFields[SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_UNCERTAINTY_INDEX];
          if (zHeightUncertainty > 0 && zHeightUncertainty < Z_MAX_HEIGHT_UNCERTAINTY_FACTOR) {
              mHeightAboveFloorUncertaintyMeters =
                      Math.pow(2, Z_FLOOR_HEIGHT_FRACTION_BITS - zHeightUncertainty - 1);
          } else {
              return false;
          }
          return true;
      }
  
      /**
       * Decode a two's complement encoded value, to a signed double based on the field length.
       *
       * @param fieldValues the array of field values reprented as longs
       * @param fieldLengths the array of field lengths
       * @param index the index of the field being decoded
       * @param fraction the number of fractional bits in the value
       * @return the signed value represented as a double
       */
      private double decodeZUnsignedToSignedValue(long[] fieldValues, int[] fieldLengths, int index,
              int fraction) {
          int value = (int) fieldValues[index];
          int maxPositiveValue = (int) Math.pow(2, fieldLengths[index] - 1) - 1;
          if (value > maxPositiveValue) {
              value -= Math.pow(2, fieldLengths[index]);
          }
          return Math.scalb(value, -fraction);
      }
  
      /**
       * Parse Subelement Usage Rights
       */
      private boolean parseSubelementUsage(byte[] buffer) {
          if (buffer.length != SUBELEMENT_USAGE_LENGTH1
                  && buffer.length != SUBELEMENT_USAGE_LENGTH3) {
              return false;
          }
          mUsageRetransmit =
                  (buffer[SUBELEMENT_USAGE_PARAMS_INDEX] & SUBELEMENT_USAGE_MASK_RETRANSMIT) != 0;
          mUsageRetentionExpires =
                  (buffer[SUBELEMENT_USAGE_PARAMS_INDEX] & SUBELEMENT_USAGE_MASK_RETENTION_EXPIRES)
                          != 0;
          mUsageExtraInfoOnAssociation =
                  (buffer[SUBELEMENT_USAGE_PARAMS_INDEX] & SUBELEMENT_USAGE_MASK_STA_LOCATION_POLICY)
                          != 0;
          // Note: the Retransmit flag must be true, and RetentionExpires, false for the
          // ResponderLocation object to be usable by public applications.
          return (mUsageRetransmit && !mUsageRetentionExpires);
      }
  
      /**
       * Parse the BSSID List Subelement of the LCI IE.
       *
       * @param buffer a buffer containing the subelement
       * @return boolean true indicates success
       */
      private boolean parseSubelementBssidList(byte[] buffer) {
          if (buffer.length < SUBELEMENT_BSSID_LIST_MIN_BUFFER_LENGTH) {
              return false;
          }
          if ((buffer.length - 1) % BYTES_IN_A_BSSID != 0) {
              return false;
          }
  
          int maxBssidIndicator = (int) buffer[SUBELEMENT_BSSID_MAX_INDICATOR_INDEX] & BYTE_MASK;
          int bssidListLength = (buffer.length - 1) / BYTES_IN_A_BSSID;
          // The maxBSSIDIndicator is ignored. Its use is still being clarified in 802.11REVmd,
          // which is not published at this time. This field will be used in a future
          // release of Android after 802.11REVmd is public. Here, we interpret the following
          // params as an explicit list of BSSIDs.
  
  
          int bssidOffset = SUBELEMENT_BSSID_LIST_INDEX;
          for (int i = 0; i < bssidListLength; i++) {
              byte[] bssid = Arrays.copyOfRange(buffer, bssidOffset, bssidOffset + BYTES_IN_A_BSSID);
              MacAddress macAddress = MacAddress.fromBytes(bssid);
              mBssidList.add(macAddress);
              bssidOffset += BYTES_IN_A_BSSID;
          }
          return true;
      }
  
      /**
       * Parse the Location Civic subelement in the LCR IE.
       *
       * @param buffer a buffer containing the subelement
       * @return boolean true indicates success
       */
      private boolean parseSubelementLocationCivic(byte[] buffer) {
          if (buffer.length <  SUBELEMENT_LOCATION_CIVIC_MIN_LENGTH
                  || buffer.length > SUBELEMENT_LOCATION_CIVIC_MAX_LENGTH) {
              return false;
          }
          mCivicLocationCountryCode =
                  new String(Arrays.copyOfRange(buffer, CIVIC_COUNTRY_CODE_INDEX,
                          CIVIC_TLV_LIST_INDEX)).toUpperCase();
          CivicLocation civicLocation =
                  new CivicLocation(
                          Arrays.copyOfRange(buffer, CIVIC_TLV_LIST_INDEX, buffer.length),
                          mCivicLocationCountryCode);
          if (!civicLocation.isValid()) {
              return false;
          }
          this.mCivicLocation = civicLocation;
          mCivicLocationString = civicLocation.toString();
          return true;
      }
  
      /**
       * Parse the Map Image subelement in the LCR IE.
       *
       * @param buffer a buffer containing the subelement
       * @return boolean true indicates success
       */
      private boolean parseSubelementMapImage(byte[] buffer) {
          if (buffer.length > SUBELEMENT_MAP_IMAGE_URL_MAX_LENGTH) {
              return false;
          }
          int mapImageType = buffer[SUBELEMENT_IMAGE_MAP_TYPE_INDEX];
          int supportedTypesMax = SUPPORTED_IMAGE_FILE_EXTENSIONS.length - 1;
          if (mapImageType < MAP_TYPE_URL_DEFINED || mapImageType > supportedTypesMax) {
              return false;
          }
          this.mMapImageType = mapImageType;
          byte[] urlBytes = Arrays.copyOfRange(buffer, 1, buffer.length);
          mMapImageUri = Uri.parse(new String(urlBytes, StandardCharsets.UTF_8));
          return true;
      }
  
      /**
       * Convert an image type code to a Mime type
       *
       * @param imageTypeCode encoded as an integer
       * @return the mime type of the image file
       */
      private String imageTypeToMime(int imageTypeCode, String imageUrl) {
          int supportedExtensionsMax = SUPPORTED_IMAGE_FILE_EXTENSIONS.length - 1;
          if ((imageTypeCode == 0 && imageUrl == null) || imageTypeCode > supportedExtensionsMax) {
              return null;
          }
          MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
          if (imageTypeCode == 0) {
              return mimeTypeMap.getMimeTypeFromExtension(
                      MimeTypeMap.getFileExtensionFromUrl(imageUrl));
          } else {
              return mimeTypeMap.getMimeTypeFromExtension(
                      SUPPORTED_IMAGE_FILE_EXTENSIONS[imageTypeCode]);
          }
      }
  
      /**
       * Converts a byte array containing fields of variable size, into an array of longs where the
       * field boundaries are defined in a constant int array passed as an argument.
       *
       * @param buffer        the byte array containing all the fields
       * @param bitFieldSizes the int array defining the size of each field
       */
      private long[] getFieldData(byte[] buffer, int[] bitFieldSizes) {
          int bufferLengthBits = buffer.length * Byte.SIZE;
          int sumBitFieldSizes = 0;
          for (int i : bitFieldSizes) {
              if (i > Long.SIZE) {
                  return null;
              }
              sumBitFieldSizes += i;
          }
          if (bufferLengthBits != sumBitFieldSizes) {
              return null;
          }
          long[] fieldData = new long[bitFieldSizes.length];
          int bufferBitPos = 0;
          for (int fieldIndex = 0; fieldIndex < bitFieldSizes.length; fieldIndex++) {
              int bitFieldSize = bitFieldSizes[fieldIndex];
              long field = 0;
              for (int n = 0; n < bitFieldSize; n++) {
                  field |= ((long) getBitAtBitOffsetInByteArray(buffer, bufferBitPos + n) << n);
              }
              fieldData[fieldIndex] = field;
              bufferBitPos += bitFieldSize;
          }
          return fieldData;
      }
  
      /**
       * Retrieves state of a bit at the bit-offset in a byte array, where the offset represents the
       * bytes in contiguous data with each value in big endian order.
       *
       * @param buffer          the data buffer of bytes containing all the fields
       * @param bufferBitOffset the bit offset into the entire buffer
       * @return a zero or one value, representing the state of that bit.
       */
      private int getBitAtBitOffsetInByteArray(byte[] buffer, int bufferBitOffset) {
          int bufferIndex = bufferBitOffset / Byte.SIZE; // The byte index that contains the bit
          int bitOffsetInByte = bufferBitOffset % Byte.SIZE; // The bit offset within that byte
          int result = (buffer[bufferIndex] & (MSB_IN_BYTE >> bitOffsetInByte)) == 0 ? 0 : 1;
          return result;
      }
  
      /**
       * Reverses the order of the bits in each byte of a byte array.
       *
       * @param buffer the array containing each byte that will be reversed
       */
      private void swapEndianByteByByte(byte[] buffer) {
          for (int n = 0; n < buffer.length; n++) {
              byte currentByte = buffer[n];
              byte reversedByte = 0; // Cleared value
              byte bitSelectorMask = LSB_IN_BYTE;
              for (int i = 0; i < Byte.SIZE; i++) {
                  reversedByte = (byte) (reversedByte << 1);
                  if ((currentByte & bitSelectorMask) != 0) {
                      reversedByte = (byte) (reversedByte | LSB_IN_BYTE);
                  }
                  bitSelectorMask = (byte) (bitSelectorMask << 1);
              }
              buffer[n] = reversedByte;
          }
      }
  
      /**
       * Sets the LCI subelement fields to the default undefined values.
       */
      private void setLciSubelementDefaults() {
          mIsLciValid = false;
          mLatitudeUncertainty = UNCERTAINTY_UNDEFINED;
          mLatitude = 0;
          mLongitudeUncertainty = UNCERTAINTY_UNDEFINED;
          mLongitude = 0;
          mAltitudeType = ALTITUDE_UNDEFINED;
          mAltitudeUncertainty = UNCERTAINTY_UNDEFINED;
          mAltitude = 0;
          mDatum = DATUM_UNDEFINED;
          mLciRegisteredLocationAgreement = false;
          mLciRegisteredLocationDse = false;
          mLciDependentStation = false;
          mLciVersion = 0;
      }
  
      /**
       * Sets the Z subelement fields to the default values when undefined.
       */
      private void setZaxisSubelementDefaults() {
          mIsZValid = false;
          mExpectedToMove = 0;
          mFloorNumber = 0;
          mHeightAboveFloorMeters = 0;
          mHeightAboveFloorUncertaintyMeters = 0;
      }
  
      /**
       * Sets the Usage Policy subelement fields to the default undefined values.
       */
      private void setUsageSubelementDefaults() {
          mUsageRetransmit = true;
          mUsageRetentionExpires = false;
          mUsageExtraInfoOnAssociation = false;
      }
  
      /**
       * Sets the BSSID List subelement fields to the default values when undefined.
       */
      private void setBssidListSubelementDefaults() {
          mIsBssidListValid = false;
          mBssidList = new ArrayList<>();
      }
  
      /**
       * Sets the LCR Civic Location subelement field to the default undefined value.
       *
       * @hide
       */
      public void setCivicLocationSubelementDefaults() {
          mIsLocationCivicValid = false;
          mCivicLocationCountryCode = "";
          mCivicLocationString = "";
          mCivicLocation = null;
      }
  
      /**
       * Sets the LCR Map Image subelement field to the default values when undefined.
       */
      private void setMapImageSubelementDefaults() {
          mIsMapImageValid = false;
          mMapImageType = MAP_TYPE_URL_DEFINED;
          mMapImageUri = null;
      }
  
      @Override
      public boolean equals(Object obj) {
          if (this == obj) {
              return true;
          }
          if (obj == null || getClass() != obj.getClass()) {
              return false;
          }
          ResponderLocation other = (ResponderLocation) obj;
          return mIsValid == other.mIsValid
                  && mIsLciValid == other.mIsLciValid
                  && mIsZValid == other.mIsZValid
                  && mIsUsageValid == other.mIsUsageValid
                  && mIsBssidListValid == other.mIsBssidListValid
                  && mIsLocationCivicValid == other.mIsLocationCivicValid
                  && mIsMapImageValid == other.mIsMapImageValid
                  && mLatitudeUncertainty == other.mLatitudeUncertainty
                  && mLatitude == other.mLatitude
                  && mLongitudeUncertainty == other.mLongitudeUncertainty
                  && mLongitude == other.mLongitude
                  && mAltitudeType == other.mAltitudeType
                  && mAltitudeUncertainty == other.mAltitudeUncertainty
                  && mAltitude == other.mAltitude
                  && mDatum == other.mDatum
                  && mLciRegisteredLocationAgreement == other.mLciRegisteredLocationAgreement
                  && mLciRegisteredLocationDse == other.mLciRegisteredLocationDse
                  && mLciDependentStation == other.mLciDependentStation
                  && mLciVersion == other.mLciVersion
                  && mExpectedToMove == other.mExpectedToMove
                  && mFloorNumber == other.mFloorNumber
                  && mHeightAboveFloorMeters == other.mHeightAboveFloorMeters
                  && mHeightAboveFloorUncertaintyMeters
                          == other.mHeightAboveFloorUncertaintyMeters
                  && mUsageRetransmit == other.mUsageRetransmit
                  && mUsageRetentionExpires == other.mUsageRetentionExpires
                  && mUsageExtraInfoOnAssociation == other.mUsageExtraInfoOnAssociation
                  && mBssidList.equals(other.mBssidList)
                  && mCivicLocationCountryCode.equals(other.mCivicLocationCountryCode)
                  && mCivicLocationString.equals(other.mCivicLocationString)
                  && Objects.equals(mCivicLocation, other.mCivicLocation)
                  && mMapImageType == other.mMapImageType
                  && Objects.equals(mMapImageUri, other.mMapImageUri);
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(mIsValid, mIsLciValid, mIsZValid, mIsUsageValid, mIsBssidListValid,
                  mIsLocationCivicValid, mIsMapImageValid, mLatitudeUncertainty, mLatitude,
                  mLongitudeUncertainty, mLongitude, mAltitudeType, mAltitudeUncertainty, mAltitude,
                  mDatum, mLciRegisteredLocationAgreement,
                  mLciRegisteredLocationDse, mLciDependentStation, mLciVersion,
                  mExpectedToMove, mFloorNumber, mHeightAboveFloorMeters,
                  mHeightAboveFloorUncertaintyMeters, mUsageRetransmit, mUsageRetentionExpires,
                  mUsageExtraInfoOnAssociation, mBssidList, mCivicLocationCountryCode,
                  mCivicLocationString, mCivicLocation, mMapImageType, mMapImageUri);
      }
  
      /**
       * @return true if the ResponderLocation object is valid and contains useful information
       * relevant to the location of the Responder. If this is ever false, this object will not be
       * available to developers, and have a null value.
       *
       * @hide
       */
      public boolean isValid() {
          return mIsValid;
      }
  
      /**
       * @return true if the LCI subelement (containing Latitude, Longitude and Altitude) is valid.
       *
       * <p> This method tells us if the responder has provided its Location Configuration
       * Information (LCI) directly, and is useful when an external database of responder locations
       * is not available</p>
       *
       * <p>If isLciSubelementValid() returns true, all the LCI values provided by the corresponding
       * getter methods will have been set as described by the responder, or else if false, they
       * should not be used and will throw an IllegalStateException.</p>
       */
      public boolean isLciSubelementValid() {
          return mIsLciValid;
      }
  
      /**
       * @return the latitude uncertainty in degrees.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       * </p>
       * <p> An unknown uncertainty is indicated by 0.</p>
       */
      public double getLatitudeUncertainty() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mLatitudeUncertainty;
      }
  
      /**
       * @return the latitude in degrees
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      public double getLatitude() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLatitude(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mLatitude;
      }
  
      /**
       * @return the Longitude uncertainty in degrees.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       * </p>
       * <p> An unknown uncertainty is indicated by 0.</p>
       */
      public double getLongitudeUncertainty() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLongitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mLongitudeUncertainty;
      }
  
      /**
       * @return the Longitude in degrees..
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      public double getLongitude() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mLongitude;
      }
  
      /**
       * @return the Altitude type.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      @AltitudeType
      public int getAltitudeType() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mAltitudeType;
      }
  
      /**
       * @return the Altitude uncertainty in meters.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       * </p>
       * <p>An unknown uncertainty is indicated by 0.</p>
       */
      public double getAltitudeUncertainty() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mAltitudeUncertainty;
      }
  
      /**
       * @return the Altitude in units defined by the altitude type.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      public double getAltitude() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getAltitude(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mAltitude;
      }
  
      /**
       * @return the Datum used for the LCI positioning information.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      @DatumType
      public int getDatum() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getDatum(): invoked on an invalid result: mIsLciValid = false.");
          }
          return mDatum;
      }
  
      /**
       * @return true if the station is operating within a national policy area or an international
       * agreement area near a national border, otherwise false
       * (see 802.11REVmc Section 11.12.3 - Registered STA Operation).
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      public boolean getRegisteredLocationAgreementIndication() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getRegisteredLocationAgreementIndication(): "
                          + "invoked on an invalid result: mIsLciValid = false.");
          }
          return mLciRegisteredLocationAgreement;
      }
  
      /**
       * @return true indicating this is an enabling station, enabling the operation of nearby STAs
       * with Dynamic Station Enablement (DSE), otherwise false.
       * (see 802.11REVmc Section 11.12.3 - Registered STA Operation).
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       *
       * @hide
       */
      public boolean getRegisteredLocationDseIndication() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getRegisteredLocationDseIndication(): "
                      + "invoked on an invalid result: mIsLciValid = false.");
          }
          return mLciRegisteredLocationDse;
      }
  
      /**
       * @return true indicating this is a dependent station that is operating with the enablement of
       * an enabling station whose LCI is being reported, otherwise false.
       * (see 802.11REVmc Section 11.12.3 - Registered STA Operation).
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       *
       * @hide
       */
      public boolean getDependentStationIndication() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getDependentStationIndication(): "
                      + "invoked on an invalid result: mIsLciValid = false.");
          }
          return mLciDependentStation;
      }
  
      /**
       * @return a value greater or equal to 1, indicating the current version number
       * of the LCI protocol.
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      public int getLciVersion() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "getLciVersion(): "
                      + "invoked on an invalid result: mIsLciValid = false.");
          }
          return mLciVersion;
      }
  
      /**
       * @return the LCI location represented as a {@link Location} object (best effort).
       * <p>
       * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
       */
      @NonNull
      public Location toLocation() {
          if (!mIsLciValid) {
              throw new IllegalStateException(
                  "toLocation(): "
                      + "invoked on an invalid result: mIsLciValid = false.");
          }
          Location location = new Location(LOCATION_PROVIDER);
          location.setLatitude(mLatitude);
          location.setLongitude(mLongitude);
          location.setAccuracy((float) (mLatitudeUncertainty + mLongitudeUncertainty) / 2);
          location.setAltitude(mAltitude);
          location.setVerticalAccuracyMeters((float) mAltitudeUncertainty);
          location.setTime(System.currentTimeMillis());
          return location;
      }
  
      /**
       * @return if the Z subelement (containing mobility, Floor, Height above floor) is valid.
       */
      public boolean isZaxisSubelementValid() {
          return mIsZValid;
      }
  
      /**
       * @return an integer representing the mobility of the responder.
       * <p>
       * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
       */
      @ExpectedToMoveType
      public int getExpectedToMove() {
          if (!mIsZValid) {
              throw new IllegalStateException(
                  "getExpectedToMove(): invoked on an invalid result: mIsZValid = false.");
          }
          return mExpectedToMove;
      }
  
      /**
       * @return the Z sub element Floor Number.
       * <p>
       * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
       * </p>
       * <p>Note: this number can be positive or negative, with value increments of +/- 1/16 of a
       * floor.</p>.
       */
      public double getFloorNumber() {
          if (!mIsZValid) {
              throw new IllegalStateException(
                  "getFloorNumber(): invoked on an invalid result: mIsZValid = false)");
          }
          return mFloorNumber;
      }
  
      /**
       * @return the Z subelement Height above the floor in meters.
       * <p>
       * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
       * </p>
       * <p>This value can be positive or negative. </p>
       */
      public double getHeightAboveFloorMeters() {
          if (!mIsZValid) {
              throw new IllegalStateException(
                  "getHeightAboveFloorMeters(): invoked on an invalid result: mIsZValid = false)");
          }
          return mHeightAboveFloorMeters;
      }
  
      /**
       * @return the Z subelement Height above the floor uncertainty in meters.
       * <p>
       * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
       * </p>
       * <p>An unknown uncertainty is indicated by 0.</p>
       */
      public double getHeightAboveFloorUncertaintyMeters() {
          if (!mIsZValid) {
              throw new IllegalStateException(
                  "getHeightAboveFloorUncertaintyMeters():"
                      + "invoked on an invalid result: mIsZValid = false)");
          }
          return mHeightAboveFloorUncertaintyMeters;
      }
  
      /**
       * @return true if the location information received from the responder can be
       * retransmitted to another device, physically separate from the one that received it.
       *
       * @hide
       */
      public boolean getRetransmitPolicyIndication() {
          return mUsageRetransmit;
      }
  
      /**
       * @return true if location-data received should expire (and be deleted)
       * by the time provided in the getRelativeExpirationTimeHours() method.
       *
       * @hide
       */
      public boolean getRetentionExpiresIndication() {
          return mUsageRetentionExpires;
      }
  
      /**
       * @return true if there is extra location info available on association.
       *
       * @hide
       */
      @SystemApi
      public boolean getExtraInfoOnAssociationIndication() {
          return mUsageExtraInfoOnAssociation;
      }
  
      /**
       * @return the Immutable list of colocated BSSIDs at the responder.
       *
       * <p> Will return an empty list when there are no bssids listed.
       */
      public List<MacAddress> getColocatedBssids() {
          return Collections.unmodifiableList(mBssidList);
      }
  
      /**
       * @return the civic location represented as an {@link Address} object (best effort).
       *
       * <p> Will return a {@code null} when there is no Civic Location defined.
       */
      @Nullable
      public Address toCivicLocationAddress() {
          if (mCivicLocation != null && mCivicLocation.isValid()) {
              return mCivicLocation.toAddress();
          } else {
              return null;
          }
      }
  
      /**
       * @return the civic location represented as a {@link SparseArray}
       * <p>
       * Valid keys to access the SparseArray can be found in {@code CivicLocationKeys}.
       * </p>
       * <p> Will return a {@code null} when there is no Civic Location defined.
       *
       */
      @Nullable
      public SparseArray toCivicLocationSparseArray() {
          if (mCivicLocation != null && mCivicLocation.isValid()) {
              return mCivicLocation.toSparseArray();
          } else {
              return null;
          }
      }
  
      /**
       * @return the civic location two upper-case ASCII character country code defined in ISO 3166.
       *
       * <p> Will return a {@code null} when there is no country code defined.
       *
       * @hide
       */
      @Nullable
      public String getCivicLocationCountryCode() {
          return mCivicLocationCountryCode;
      }
  
      /**
       * @return the value of the Civic Location String associated with a key.
       *
       * <p> Will return a {@code null} when there is no value associated with the key provided.
       *
       * @param key used to find a corresponding value in the Civic Location Tuple list
       *
       * @hide
       */
      @Nullable
      public String getCivicLocationElementValue(@CivicLocationKeysType int key) {
          return mCivicLocation.getCivicElementValue(key);
      }
  
      /**
       * @return the Map Image file Mime type, referred to by getMapImageUrl().
       */
      @Nullable
      public String getMapImageMimeType() {
          if (mMapImageUri == null) {
              return null;
          } else {
              return imageTypeToMime(mMapImageType, mMapImageUri.toString());
          }
      }
  
      /**
       * @return a URI referencing a map-file showing the local floor plan.
       *
       * <p> Will return a {@code null} when there is no URI defined.
       */
      @Nullable
      public Uri getMapImageUri() {
          return mMapImageUri;
      }
  }
  