1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wifi.hotspot2.anqp; 18 19 import android.net.Uri; 20 import android.text.TextUtils; 21 22 import com.android.internal.annotations.VisibleForTesting; 23 import com.android.server.wifi.ByteBufferReader; 24 25 import java.net.ProtocolException; 26 import java.nio.BufferUnderflowException; 27 import java.nio.ByteBuffer; 28 import java.nio.ByteOrder; 29 import java.nio.charset.StandardCharsets; 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.HashMap; 33 import java.util.List; 34 import java.util.Locale; 35 import java.util.Map; 36 import java.util.Objects; 37 38 /** 39 * The OSU Provider subfield in the OSU Providers List ANQP Element, 40 * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, 41 * section 4.8.1 42 * 43 * Format: 44 * 45 * | Length | Friendly Name Length | Friendly Name #1 | ... | Friendly Name #n | 46 * 2 2 variable variable 47 * | Server URI length | Server URI | Method List Length | Method List | 48 * 1 variable 1 variable 49 * | Icon Available Length | Icon Available | NAI Length | NAI | Description Length | 50 * 2 variable 1 variable 2 51 * | Description #1 | ... | Description #n | 52 * variable variable 53 * 54 * | Operator Name Duple #N (optional) | 55 * variable 56 */ 57 public class OsuProviderInfo { 58 /** 59 * The raw payload should minimum include the following fields: 60 * - Friendly Name Length (2) 61 * - Server URI Length (1) 62 * - Method List Length (1) 63 * - Icon Available Length (2) 64 * - NAI Length (1) 65 * - Description Length (2) 66 */ 67 @VisibleForTesting 68 public static final int MINIMUM_LENGTH = 9; 69 70 /** 71 * Maximum octets for a I18N string. 72 */ 73 private static final int MAXIMUM_I18N_STRING_LENGTH = 252; 74 75 private final Map<String, String> mFriendlyNames; 76 private final Uri mServerUri; 77 private final List<Integer> mMethodList; 78 private final List<IconInfo> mIconInfoList; 79 private final String mNetworkAccessIdentifier; 80 private final List<I18Name> mServiceDescriptions; 81 82 @VisibleForTesting OsuProviderInfo(List<I18Name> friendlyNames, Uri serverUri, List<Integer> methodList, List<IconInfo> iconInfoList, String nai, List<I18Name> serviceDescriptions)83 public OsuProviderInfo(List<I18Name> friendlyNames, Uri serverUri, List<Integer> methodList, 84 List<IconInfo> iconInfoList, String nai, List<I18Name> serviceDescriptions) { 85 mFriendlyNames = new HashMap<>(); 86 if (friendlyNames != null) { 87 friendlyNames.forEach( 88 e -> mFriendlyNames.put(e.getLocale().getLanguage(), e.getText())); 89 } 90 mServerUri = serverUri; 91 mMethodList = methodList; 92 mIconInfoList = iconInfoList; 93 mNetworkAccessIdentifier = nai; 94 mServiceDescriptions = serviceDescriptions; 95 } 96 97 /** 98 * Parse a OsuProviderInfo from the given buffer. 99 * 100 * @param payload The buffer to read from 101 * @return {@link OsuProviderInfo} 102 * @throws BufferUnderflowException 103 * @throws ProtocolException 104 */ parse(ByteBuffer payload)105 public static OsuProviderInfo parse(ByteBuffer payload) 106 throws ProtocolException { 107 // Parse length field. 108 int length = (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) 109 & 0xFFFF; 110 if (length < MINIMUM_LENGTH) { 111 throw new ProtocolException("Invalid length value: " + length); 112 } 113 114 // Parse friendly names. 115 int friendlyNameLength = 116 (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF; 117 ByteBuffer friendlyNameBuffer = getSubBuffer(payload, friendlyNameLength); 118 List<I18Name> friendlyNameList = parseI18Names(friendlyNameBuffer); 119 120 // Parse server URI. 121 Uri serverUri = Uri.parse( 122 ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8)); 123 124 // Parse method list. 125 int methodListLength = payload.get() & 0xFF; 126 List<Integer> methodList = new ArrayList<>(); 127 while (methodListLength > 0) { 128 methodList.add(payload.get() & 0xFF); 129 methodListLength--; 130 } 131 132 // Parse list of icon info. 133 int availableIconLength = 134 (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF; 135 ByteBuffer iconBuffer = getSubBuffer(payload, availableIconLength); 136 List<IconInfo> iconInfoList = new ArrayList<>(); 137 while (iconBuffer.hasRemaining()) { 138 iconInfoList.add(IconInfo.parse(iconBuffer)); 139 } 140 141 // Parse Network Access Identifier. 142 String nai = ByteBufferReader.readStringWithByteLength(payload, StandardCharsets.UTF_8); 143 144 // Parse service descriptions. 145 int serviceDescriptionLength = 146 (int) ByteBufferReader.readInteger(payload, ByteOrder.LITTLE_ENDIAN, 2) & 0xFFFF; 147 ByteBuffer descriptionsBuffer = getSubBuffer(payload, serviceDescriptionLength); 148 List<I18Name> serviceDescriptionList = parseI18Names(descriptionsBuffer); 149 150 return new OsuProviderInfo(friendlyNameList, serverUri, methodList, iconInfoList, nai, 151 serviceDescriptionList); 152 } 153 154 /** 155 * Returns friendly names for the OSU Provider. 156 * 157 * @return {@link Map} that consists of language code and friendly name expressed in the locale. 158 */ getFriendlyNames()159 public Map<String, String> getFriendlyNames() { 160 return mFriendlyNames; 161 } 162 getServerUri()163 public Uri getServerUri() { 164 return mServerUri; 165 } 166 getMethodList()167 public List<Integer> getMethodList() { 168 return Collections.unmodifiableList(mMethodList); 169 } 170 getIconInfoList()171 public List<IconInfo> getIconInfoList() { 172 return Collections.unmodifiableList(mIconInfoList); 173 } 174 getNetworkAccessIdentifier()175 public String getNetworkAccessIdentifier() { 176 return mNetworkAccessIdentifier; 177 } 178 getServiceDescriptions()179 public List<I18Name> getServiceDescriptions() { 180 return Collections.unmodifiableList(mServiceDescriptions); 181 } 182 183 /** 184 * Return the friendly Name for current language from the list of friendly names of OSU 185 * provider. 186 * 187 * The string matching the default locale will be returned if it is found, otherwise the string 188 * in english or the first string in the list will be returned if english is not found. 189 * A null will be returned if the list is empty. 190 * 191 * @return String matching the default locale, null otherwise 192 */ getFriendlyName()193 public String getFriendlyName() { 194 if (mFriendlyNames == null || mFriendlyNames.isEmpty()) return null; 195 String lang = Locale.getDefault().getLanguage(); 196 String friendlyName = mFriendlyNames.get(lang); 197 if (friendlyName != null) { 198 return friendlyName; 199 } 200 friendlyName = mFriendlyNames.get("en"); 201 if (friendlyName != null) { 202 return friendlyName; 203 } 204 return mFriendlyNames.get(mFriendlyNames.keySet().stream().findFirst().get()); 205 } 206 207 /** 208 * Return the service description string from the service description list. The string 209 * matching the default locale will be returned if it is found, otherwise the first element in 210 * the list will be returned. A null will be returned if the list is empty. 211 * 212 * @return service description string 213 */ getServiceDescription()214 public String getServiceDescription() { 215 return getI18String(mServiceDescriptions); 216 } 217 218 @Override equals(Object thatObject)219 public boolean equals(Object thatObject) { 220 if (this == thatObject) { 221 return true; 222 } 223 if (!(thatObject instanceof OsuProviderInfo)) { 224 return false; 225 } 226 OsuProviderInfo that = (OsuProviderInfo) thatObject; 227 return (mFriendlyNames == null ? that.mFriendlyNames == null 228 : mFriendlyNames.equals(that.mFriendlyNames)) 229 && (mServerUri == null ? that.mServerUri == null 230 : mServerUri.equals(that.mServerUri)) 231 && (mMethodList == null ? that.mMethodList == null 232 : mMethodList.equals(that.mMethodList)) 233 && (mIconInfoList == null ? that.mIconInfoList == null 234 : mIconInfoList.equals(that.mIconInfoList)) 235 && TextUtils.equals(mNetworkAccessIdentifier, that.mNetworkAccessIdentifier) 236 && (mServiceDescriptions == null ? that.mServiceDescriptions == null 237 : mServiceDescriptions.equals(that.mServiceDescriptions)); 238 } 239 240 @Override hashCode()241 public int hashCode() { 242 return Objects.hash(mFriendlyNames, mServerUri, mMethodList, mIconInfoList, 243 mNetworkAccessIdentifier, mServiceDescriptions); 244 } 245 246 @Override toString()247 public String toString() { 248 return "OsuProviderInfo{" 249 + "mFriendlyNames=" + mFriendlyNames 250 + ", mServerUri=" + mServerUri 251 + ", mMethodList=" + mMethodList 252 + ", mIconInfoList=" + mIconInfoList 253 + ", mNetworkAccessIdentifier=" + mNetworkAccessIdentifier 254 + ", mServiceDescriptions=" + mServiceDescriptions 255 + "}"; 256 } 257 258 /** 259 * Parse list of I18N string from the given payload. 260 * 261 * @param payload The payload to parse from 262 * @return List of {@link I18Name} 263 * @throws ProtocolException 264 */ parseI18Names(ByteBuffer payload)265 private static List<I18Name> parseI18Names(ByteBuffer payload) throws ProtocolException { 266 List<I18Name> results = new ArrayList<>(); 267 while (payload.hasRemaining()) { 268 I18Name name = I18Name.parse(payload); 269 // Verify that the number of bytes for the operator name doesn't exceed the max 270 // allowed. 271 int textBytes = name.getText().getBytes(StandardCharsets.UTF_8).length; 272 if (textBytes > MAXIMUM_I18N_STRING_LENGTH) { 273 throw new ProtocolException("I18Name string exceeds the maximum allowed " 274 + textBytes); 275 } 276 results.add(name); 277 } 278 return results; 279 } 280 281 /** 282 * Creates a new byte buffer whose content is a shared subsequence of 283 * the given buffer's content. 284 * 285 * The sub buffer will starts from |payload|'s current position 286 * and ends at |payload|'s current position plus |length|. The |payload|'s current 287 * position will advance pass |length| bytes. 288 * 289 * @param payload The original buffer 290 * @param length The length of the new buffer 291 * @return {@link ByteBuffer} 292 * @throws BufferUnderflowException 293 */ getSubBuffer(ByteBuffer payload, int length)294 private static ByteBuffer getSubBuffer(ByteBuffer payload, int length) { 295 if (payload.remaining() < length) { 296 throw new BufferUnderflowException(); 297 } 298 // Set the subBuffer's starting and ending position. 299 ByteBuffer subBuffer = payload.slice(); 300 subBuffer.limit(length); 301 // Advance the original buffer's current position. 302 payload.position(payload.position() + length); 303 return subBuffer; 304 } 305 306 /** 307 * Return the appropriate I18 string value from the list of I18 string values. 308 * The string matching the default locale will be returned if it is found, otherwise the 309 * first string in the list will be returned. A null will be returned if the list is empty. 310 * 311 * @param i18Strings List of I18 string values 312 * @return String matching the default locale, null otherwise 313 */ getI18String(List<I18Name> i18Strings)314 private static String getI18String(List<I18Name> i18Strings) { 315 for (I18Name name : i18Strings) { 316 if (name.getLanguage().equals(Locale.getDefault().getLanguage())) { 317 return name.getText(); 318 } 319 } 320 if (i18Strings.size() > 0) { 321 return i18Strings.get(0).getText(); 322 } 323 return null; 324 } 325 } 326