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 android.annotation.Nullable;
  import android.location.Address;
  import android.net.wifi.rtt.CivicLocationKeys.CivicLocationKeysType;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.Parcelable.Creator;
  import android.util.SparseArray;
  
  import java.nio.charset.StandardCharsets;
  import java.util.Locale;
  import java.util.Objects;
  
  /**
   * Decodes the Type Length Value (TLV) elements found in a Location Civic Record as defined by IEEE
   * P802.11-REVmc/D8.0 section 9.4.2.22.13 using the format described in IETF RFC 4776.
   *
   * <p>The TLVs each define a key, value pair for a civic address type such as apt, street, city,
   * county, and country. The class provides a general getter method to extract a value for an element
   * key, returning null if not set.
   *
   * @hide
   */
  public final class CivicLocation implements Parcelable {
      // Address (class) line indexes
      private static final int ADDRESS_LINE_0_ROOM_DESK_FLOOR = 0;
      private static final int ADDRESS_LINE_1_NUMBER_ROAD_SUFFIX_APT = 1;
      private static final int ADDRESS_LINE_2_CITY = 2;
      private static final int ADDRESS_LINE_3_STATE_POSTAL_CODE = 3;
      private static final int ADDRESS_LINE_4_COUNTRY = 4;
  
      // Buffer management
      private static final int MIN_CIVIC_BUFFER_SIZE = 3;
      private static final int MAX_CIVIC_BUFFER_SIZE = 256;
      private static final int COUNTRY_CODE_LENGTH = 2;
      private static final int BYTE_MASK = 0xFF;
      private static final int TLV_TYPE_INDEX = 0;
      private static final int TLV_LENGTH_INDEX = 1;
      private static final int TLV_VALUE_INDEX = 2;
  
      private final boolean mIsValid;
      private final String mCountryCode; // Two character country code (ISO 3166 standard).
      private SparseArray<String> mCivicAddressElements =
              new SparseArray<>(MIN_CIVIC_BUFFER_SIZE);
  
  
      /**
       * Constructor
       *
       * @param civicTLVs    a byte buffer containing parameters in the form type, length, value
       * @param countryCode the two letter code defined by the ISO 3166 standard
       *
       * @hide
       */
      public CivicLocation(@Nullable byte[] civicTLVs, @Nullable String countryCode) {
          this.mCountryCode = countryCode;
          if (countryCode == null || countryCode.length() != COUNTRY_CODE_LENGTH) {
              this.mIsValid = false;
              return;
          }
          boolean isValid = false;
          if (civicTLVs != null
                  && civicTLVs.length >= MIN_CIVIC_BUFFER_SIZE
                  && civicTLVs.length < MAX_CIVIC_BUFFER_SIZE) {
              isValid = parseCivicTLVs(civicTLVs);
          }
  
          mIsValid = isValid;
      }
  
      private CivicLocation(Parcel in) {
          mIsValid = in.readByte() != 0;
          mCountryCode = in.readString();
          mCivicAddressElements = in.readSparseArray(this.getClass().getClassLoader());
      }
  
      public static final @android.annotation.NonNull Creator<CivicLocation> CREATOR = new Creator<CivicLocation>() {
          @Override
          public CivicLocation createFromParcel(Parcel in) {
              return new CivicLocation(in);
          }
  
          @Override
          public CivicLocation[] newArray(int size) {
              return new CivicLocation[size];
          }
      };
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel parcel, int flags) {
          parcel.writeByte((byte) (mIsValid ? 1 : 0));
          parcel.writeString(mCountryCode);
          parcel.writeSparseArray((android.util.SparseArray) mCivicAddressElements);
      }
  
      /**
       * Check TLV format and store TLV key/value pairs in this object so they can be queried by key.
       *
       * @param civicTLVs the buffer of TLV elements
       * @return a boolean indicating success of the parsing process
       */
      private boolean parseCivicTLVs(byte[] civicTLVs) {
          int bufferPtr = 0;
          int bufferLength = civicTLVs.length;
  
          // Iterate through the sub-elements contained in the LCI IE checking the accumulated
          // element lengths do not overflow the total buffer length
          while (bufferPtr < bufferLength) {
              int civicAddressType = civicTLVs[bufferPtr + TLV_TYPE_INDEX] & BYTE_MASK;
              int civicAddressTypeLength = civicTLVs[bufferPtr + TLV_LENGTH_INDEX];
              if (civicAddressTypeLength != 0) {
                  if (bufferPtr + TLV_VALUE_INDEX + civicAddressTypeLength > bufferLength) {
                      return false;
                  }
                  mCivicAddressElements.put(civicAddressType,
                          new String(civicTLVs, bufferPtr + TLV_VALUE_INDEX,
                                  civicAddressTypeLength, StandardCharsets.UTF_8));
              }
              bufferPtr += civicAddressTypeLength + TLV_VALUE_INDEX;
          }
          return true;
      }
  
      /**
       * Getter for the value of a civic Address element type.
       *
       * @param key an integer code for the element type key
       * @return the string value associated with that element type
       */
      @Nullable
      public String getCivicElementValue(@CivicLocationKeysType int key) {
          return mCivicAddressElements.get(key);
      }
  
      /**
       * Converts a CivicLocation object to a SparseArray.
       *
       * @return the SparseArray<string> representation of the CivicLocation
       */
      @Nullable
      public SparseArray<String> toSparseArray() {
          return mCivicAddressElements;
      }
  
      /**
       * Generates a comma separated string of all the defined elements.
       *
       * @return a compiled string representing all elements
       */
      @Override
      public String toString() {
          return mCivicAddressElements.toString();
      }
  
      /**
       * Converts Civic Location to the best effort Address Object.
       *
       * @return the {@link Address} object based on the Civic Location data
       */
      @Nullable
      public Address toAddress() {
          if (!mIsValid) {
              return null;
          }
          Address address = new Address(Locale.US);
          String room = formatAddressElement("Room: ", getCivicElementValue(CivicLocationKeys.ROOM));
          String desk =
                  formatAddressElement(" Desk: ", getCivicElementValue(CivicLocationKeys.DESK));
          String floor =
                  formatAddressElement(", Flr: ", getCivicElementValue(CivicLocationKeys.FLOOR));
          String houseNumber = formatAddressElement("", getCivicElementValue(CivicLocationKeys.HNO));
          String houseNumberSuffix =
                  formatAddressElement("", getCivicElementValue(CivicLocationKeys.HNS));
          String road =
                  formatAddressElement(" ", getCivicElementValue(
                          CivicLocationKeys.PRIMARY_ROAD_NAME));
          String roadSuffix = formatAddressElement(" ", getCivicElementValue(CivicLocationKeys.STS));
          String apt = formatAddressElement(", Apt: ", getCivicElementValue(CivicLocationKeys.APT));
          String city = formatAddressElement("", getCivicElementValue(CivicLocationKeys.CITY));
          String state = formatAddressElement("", getCivicElementValue(CivicLocationKeys.STATE));
          String postalCode =
                  formatAddressElement(" ", getCivicElementValue(CivicLocationKeys.POSTAL_CODE));
  
          // Aggregation into common address format
          String addressLine0 =
                  new StringBuilder().append(room).append(desk).append(floor).toString();
          String addressLine1 =
                  new StringBuilder().append(houseNumber).append(houseNumberSuffix).append(road)
                          .append(roadSuffix).append(apt).toString();
          String addressLine2 = city;
          String addressLine3 = new StringBuilder().append(state).append(postalCode).toString();
          String addressLine4 = mCountryCode;
  
          // Setting Address object line fields by common convention.
          address.setAddressLine(ADDRESS_LINE_0_ROOM_DESK_FLOOR, addressLine0);
          address.setAddressLine(ADDRESS_LINE_1_NUMBER_ROAD_SUFFIX_APT, addressLine1);
          address.setAddressLine(ADDRESS_LINE_2_CITY, addressLine2);
          address.setAddressLine(ADDRESS_LINE_3_STATE_POSTAL_CODE, addressLine3);
          address.setAddressLine(ADDRESS_LINE_4_COUNTRY, addressLine4);
  
          // Other compatible fields between the CIVIC_ADDRESS and the Address Class.
          address.setFeatureName(getCivicElementValue(CivicLocationKeys.NAM)); // Structure name
          address.setSubThoroughfare(getCivicElementValue(CivicLocationKeys.HNO));
          address.setThoroughfare(getCivicElementValue(CivicLocationKeys.PRIMARY_ROAD_NAME));
          address.setSubLocality(getCivicElementValue(CivicLocationKeys.NEIGHBORHOOD));
          address.setSubAdminArea(getCivicElementValue(CivicLocationKeys.COUNTY));
          address.setAdminArea(getCivicElementValue(CivicLocationKeys.STATE));
          address.setPostalCode(getCivicElementValue(CivicLocationKeys.POSTAL_CODE));
          address.setCountryCode(mCountryCode); // Country
          return address;
      }
  
      /**
       * Prepares an address element so that it can be integrated into an address line convention.
       *
       * <p>If an address element is null, the return string will be empty e.g. "".
       *
       * @param label a string defining the type of address element
       * @param value a string defining the elements value
       * @return the formatted version of the value, with null values converted to empty strings
       */
      private String formatAddressElement(String label, String value) {
          if (value != null) {
              return label + value;
          } else {
              return "";
          }
      }
  
      @Override
      public boolean equals(Object obj) {
          if (this == obj) {
              return true;
          }
          if (!(obj instanceof CivicLocation)) {
              return false;
          }
          CivicLocation other = (CivicLocation) obj;
          return mIsValid == other.mIsValid
                  && Objects.equals(mCountryCode, other.mCountryCode)
                  && isSparseArrayStringEqual(mCivicAddressElements, other.mCivicAddressElements);
      }
  
      @Override
      public int hashCode() {
          int[] civicAddressKeys = getSparseArrayKeys(mCivicAddressElements);
          String[] civicAddressValues = getSparseArrayValues(mCivicAddressElements);
          return Objects.hash(mIsValid, mCountryCode, civicAddressKeys, civicAddressValues);
      }
  
      /**
       * Tests if the Civic Location object is valid
       *
       * @return a boolean defining mIsValid
       */
      public boolean isValid() {
          return mIsValid;
      }
  
      /**
       * Tests if two sparse arrays are equal on a key for key basis
       *
       * @param sa1 the first sparse array
       * @param sa2 the second sparse array
       * @return the boolean result after comparing values key by key
       */
      private boolean isSparseArrayStringEqual(SparseArray<String> sa1, SparseArray<String> sa2) {
          int size = sa1.size();
          if (size != sa2.size()) {
              return false;
          }
          for (int i = 0; i < size; i++) {
              String sa1Value = sa1.valueAt(i);
              String sa2Value = sa2.valueAt(i);
              if (!sa1Value.equals(sa2Value)) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Extract an array of all the keys in a SparseArray<String>
       *
       * @param sa the sparse array of Strings
       * @return an integer array of all keys in the SparseArray<String>
       */
      private int[] getSparseArrayKeys(SparseArray<String> sa) {
          int size = sa.size();
          int[] keys = new int[size];
          for (int i = 0; i < size; i++) {
              keys[i] = sa.keyAt(i);
          }
          return keys;
      }
  
      /**
       * Extract an array of all the String values in a SparseArray<String>
       *
       * @param sa the sparse array of Strings
       * @return a String array of all values in the SparseArray<String>
       */
      private String[] getSparseArrayValues(SparseArray<String> sa) {
          int size = sa.size();
          String[] values = new String[size];
          for (int i = 0; i < size; i++) {
              values[i] = sa.valueAt(i);
          }
          return values;
      }
  }
  