| /* |
| * 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; |
| } |
| } |