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