content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.wifi.hotspot2.anqp;
  
  import android.net.Uri;
  import android.text.TextUtils;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.wifi.ByteBufferReader;
  
  import java.net.ProtocolException;
  import java.nio.BufferUnderflowException;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  
  /**
   * The OSU Provider subfield in the OSU Providers List ANQP Element,
   * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00,
   * section 4.8.1
   *
   * Format:
   *
   * | Length | Friendly Name Length | Friendly Name #1 | ... | Friendly Name #n |
   *     2               2                variable                  variable
   * | Server URI length | Server URI | Method List Length | Method List |
   *          1             variable             1             variable
   * | Icon Available Length | Icon Available | NAI Length | NAI | Description Length |
   *            2                variable            1     variable      2
   * | Description #1 | ... | Description #n |
   *      variable               variable
   *
   * | Operator Name Duple #N (optional) |
   *             variable
   */
  public class OsuProviderInfo {
      /**
       * The raw payload should minimum include the following fields:
       * - Friendly Name Length (2)
       * - Server URI Length (1)
       * - Method List Length (1)
       * - Icon Available Length (2)
       * - NAI Length (1)
       * - Description Length (2)
       */
      @VisibleForTesting
      public static final int MINIMUM_LENGTH = 9;
  
      /**
       * Maximum octets for a I18N string.
       */
      private static final int MAXIMUM_I18N_STRING_LENGTH = 252;
  
      private final Map<String, String> mFriendlyNames;
      private final Uri mServerUri;
      private final List<Integer> mMethodList;
      private final List<IconInfo> mIconInfoList;
      private final String mNetworkAccessIdentifier;
      private final List<I18Name> mServiceDescriptions;
  
      @VisibleForTesting
      public OsuProviderInfo(List<I18Name> friendlyNames, Uri serverUri, List<Integer> methodList,
              List<IconInfo> iconInfoList, String nai, List<I18Name> serviceDescriptions) {
          mFriendlyNames = new HashMap<>();
          if (friendlyNames != null) {
              friendlyNames.forEach(
                      e -> mFriendlyNames.put(e.getLocale().getLanguage(), e.getText()));
          }
          mServerUri = serverUri;
          mMethodList = methodList;
          mIconInfoList = iconInfoList;
          mNetworkAccessIdentifier = nai;
          mServiceDescriptions = serviceDescriptions;
      }
  
      /**
       * Parse a OsuProviderInfo from the given buffer.
       *
       * @param payload The buffer to read from
       * @return {@link OsuProviderInfo}
       * @throws BufferUnderflowException
       * @throws ProtocolException
       */
      public static OsuProviderInfo parse(ByteBuffer payload)
              throws ProtocolException {
          // Parse length field.
          int length = (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2)
                  & 0xFFFF;
          if (length < MINIMUM_LENGTH) {
              throw new ProtocolException("Invalid length value: " + length);
          }
  
          // Parse friendly names.
          int friendlyNameLength =
                  (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF;
          ByteBuffer friendlyNameBuffer = getSubBuffer(payload, friendlyNameLength);
          List<I18Name> friendlyNameList = parseI18Names(friendlyNameBuffer);
  
          // Parse server URI.
          Uri serverUri = Uri.parse(
                  ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8));
  
          // Parse method list.
          int methodListLength = payload.get() & 0xFF;
          List<Integer> methodList = new ArrayList<>();
          while (methodListLength > 0) {
              methodList.add(payload.get() & 0xFF);
              methodListLength--;
          }
  
          // Parse list of icon info.
          int availableIconLength =
                  (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF;
          ByteBuffer iconBuffer = getSubBuffer(payload, availableIconLength);
          List<IconInfo> iconInfoList = new ArrayList<>();
          while (iconBuffer.hasRemaining()) {
              iconInfoList.add(IconInfo.parse(iconBuffer));
          }
  
          // Parse Network Access Identifier.
          String nai = ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8);
  
          // Parse service descriptions.
          int serviceDescriptionLength =
                  (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF;
          ByteBuffer descriptionsBuffer = getSubBuffer(payload, serviceDescriptionLength);
          List<I18Name> serviceDescriptionList = parseI18Names(descriptionsBuffer);
  
          return new OsuProviderInfo(friendlyNameList, serverUri, methodList, iconInfoList, nai,
                  serviceDescriptionList);
      }
  
      /**
       * Returns friendly names for the OSU Provider.
       *
       * @return {@link Map} that consists of language code and friendly name expressed in the locale.
       */
      public Map<String, String> getFriendlyNames() {
          return mFriendlyNames;
      }
  
      public Uri getServerUri() {
          return mServerUri;
      }
  
      public List<Integer> getMethodList() {
          return Collections.unmodifiableList(mMethodList);
      }
  
      public List<IconInfo> getIconInfoList() {
          return Collections.unmodifiableList(mIconInfoList);
      }
  
      public String getNetworkAccessIdentifier() {
          return mNetworkAccessIdentifier;
      }
  
      public List<I18Name> getServiceDescriptions() {
          return Collections.unmodifiableList(mServiceDescriptions);
      }
  
      /**
       * Return the friendly Name for current language from the list of friendly names of OSU
       * provider.
       *
       * The string matching the default locale will be returned if it is found, otherwise the string
       * in english or the first string in the list will be returned if english is not found.
       * A null will be returned if the list is empty.
       *
       * @return String matching the default locale, null otherwise
       */
      public String getFriendlyName() {
          if (mFriendlyNames == null || mFriendlyNames.isEmpty()) return null;
          String lang = Locale.getDefault().getLanguage();
          String friendlyName = mFriendlyNames.get(lang);
          if (friendlyName != null) {
              return friendlyName;
          }
          friendlyName = mFriendlyNames.get("en");
          if (friendlyName != null) {
              return friendlyName;
          }
          return mFriendlyNames.get(mFriendlyNames.keySet().stream().findFirst().get());
      }
  
      /**
       * Return the service description string from the service description list.  The string
       * matching the default locale will be returned if it is found, otherwise the first element in
       * the list will be returned.  A null will be returned if the list is empty.
       *
       * @return service description string
       */
      public String getServiceDescription() {
          return getI18String(mServiceDescriptions);
      }
  
      @Override
      public boolean equals(Object thatObject) {
          if (this == thatObject) {
              return true;
          }
          if (!(thatObject instanceof OsuProviderInfo)) {
              return false;
          }
          OsuProviderInfo that = (OsuProviderInfo) thatObject;
          return (mFriendlyNames == null ? that.mFriendlyNames == null
                          : mFriendlyNames.equals(that.mFriendlyNames))
                  && (mServerUri == null ? that.mServerUri == null
                          : mServerUri.equals(that.mServerUri))
                  && (mMethodList == null ? that.mMethodList == null
                          : mMethodList.equals(that.mMethodList))
                  && (mIconInfoList == null ? that.mIconInfoList == null
                          : mIconInfoList.equals(that.mIconInfoList))
                  && TextUtils.equals(mNetworkAccessIdentifier, that.mNetworkAccessIdentifier)
                  && (mServiceDescriptions == null ? that.mServiceDescriptions == null
                          : mServiceDescriptions.equals(that.mServiceDescriptions));
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(mFriendlyNames, mServerUri, mMethodList, mIconInfoList,
                  mNetworkAccessIdentifier, mServiceDescriptions);
      }
  
      @Override
      public String toString() {
          return "OsuProviderInfo{"
                  + "mFriendlyNames=" + mFriendlyNames
                  + ", mServerUri=" + mServerUri
                  + ", mMethodList=" + mMethodList
                  + ", mIconInfoList=" + mIconInfoList
                  + ", mNetworkAccessIdentifier=" + mNetworkAccessIdentifier
                  + ", mServiceDescriptions=" + mServiceDescriptions
                  + "}";
      }
  
      /**
       * Parse list of I18N string from the given payload.
       *
       * @param payload The payload to parse from
       * @return List of {@link I18Name}
       * @throws ProtocolException
       */
      private static List<I18Name> parseI18Names(ByteBuffer payload) throws ProtocolException {
          List<I18Name> results = new ArrayList<>();
          while (payload.hasRemaining()) {
              I18Name name = I18Name.parse(payload);
              // Verify that the number of bytes for the operator name doesn't exceed the max
              // allowed.
              int textBytes = name.getText().getBytes(StandardCharsets.UTF_8).length;
              if (textBytes > MAXIMUM_I18N_STRING_LENGTH) {
                  throw new ProtocolException("I18Name string exceeds the maximum allowed "
                          + textBytes);
              }
              results.add(name);
          }
          return results;
      }
  
      /**
       * Creates a new byte buffer whose content is a shared subsequence of
       * the given buffer's content.
       *
       * The sub buffer will starts from |payload|'s current position
       * and ends at |payload|'s current position plus |length|.  The |payload|'s current
       * position will advance pass |length| bytes.
       *
       * @param payload The original buffer
       * @param length The length of the new buffer
       * @return {@link ByteBuffer}
       * @throws BufferUnderflowException
       */
      private static ByteBuffer getSubBuffer(ByteBuffer payload, int length) {
          if (payload.remaining() < length) {
              throw new BufferUnderflowException();
          }
          // Set the subBuffer's starting and ending position.
          ByteBuffer subBuffer = payload.slice();
          subBuffer.limit(length);
          // Advance the original buffer's current position.
          payload.position(payload.position() + length);
          return subBuffer;
      }
  
      /**
       * Return the appropriate I18 string value from the list of I18 string values.
       * The string matching the default locale will be returned if it is found, otherwise the
       * first string in the list will be returned.  A null will be returned if the list is empty.
       *
       * @param i18Strings List of I18 string values
       * @return String matching the default locale, null otherwise
       */
      private static String getI18String(List<I18Name> i18Strings) {
          for (I18Name name : i18Strings) {
              if (name.getLanguage().equals(Locale.getDefault().getLanguage())) {
                  return name.getText();
              }
          }
          if (i18Strings.size() > 0) {
              return i18Strings.get(0).getText();
          }
          return null;
      }
  }
  