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