1package com.android.server.wifi.hotspot2;
2
3import static com.android.server.wifi.hotspot2.anqp.Constants.BYTES_IN_EUI48;
4import static com.android.server.wifi.hotspot2.anqp.Constants.BYTE_MASK;
5
6import android.net.wifi.ScanResult;
7import android.util.Log;
8
9import com.android.server.wifi.hotspot2.anqp.ANQPElement;
10import com.android.server.wifi.hotspot2.anqp.Constants;
11import com.android.server.wifi.hotspot2.anqp.RawByteElement;
12import com.android.server.wifi.util.InformationElementUtil;
13
14import java.nio.BufferUnderflowException;
15import java.nio.ByteBuffer;
16import java.nio.CharBuffer;
17import java.nio.charset.CharacterCodingException;
18import java.nio.charset.CharsetDecoder;
19import java.nio.charset.StandardCharsets;
20import java.util.ArrayList;
21import java.util.List;
22import java.util.Map;
23
24public class NetworkDetail {
25
26    private static final boolean DBG = false;
27
28    private static final String TAG = "NetworkDetail:";
29
30    public enum Ant {
31        Private,
32        PrivateWithGuest,
33        ChargeablePublic,
34        FreePublic,
35        Personal,
36        EmergencyOnly,
37        Resvd6,
38        Resvd7,
39        Resvd8,
40        Resvd9,
41        Resvd10,
42        Resvd11,
43        Resvd12,
44        Resvd13,
45        TestOrExperimental,
46        Wildcard
47    }
48
49    public enum HSRelease {
50        R1,
51        R2,
52        Unknown
53    }
54
55    // General identifiers:
56    private final String mSSID;
57    private final long mHESSID;
58    private final long mBSSID;
59    // True if the SSID is potentially from a hidden network
60    private final boolean mIsHiddenSsid;
61
62    // BSS Load element:
63    private final int mStationCount;
64    private final int mChannelUtilization;
65    private final int mCapacity;
66
67    //channel detailed information
68   /*
69    * 0 -- 20 MHz
70    * 1 -- 40 MHz
71    * 2 -- 80 MHz
72    * 3 -- 160 MHz
73    * 4 -- 80 + 80 MHz
74    */
75    private final int mChannelWidth;
76    private final int mPrimaryFreq;
77    private final int mCenterfreq0;
78    private final int mCenterfreq1;
79
80    /*
81     * 802.11 Standard (calculated from Capabilities and Supported Rates)
82     * 0 -- Unknown
83     * 1 -- 802.11a
84     * 2 -- 802.11b
85     * 3 -- 802.11g
86     * 4 -- 802.11n
87     * 7 -- 802.11ac
88     */
89    private final int mWifiMode;
90    private final int mMaxRate;
91
92    /*
93     * From Interworking element:
94     * mAnt non null indicates the presence of Interworking, i.e. 802.11u
95     */
96    private final Ant mAnt;
97    private final boolean mInternet;
98
99    /*
100     * From HS20 Indication element:
101     * mHSRelease is null only if the HS20 Indication element was not present.
102     * mAnqpDomainID is set to -1 if not present in the element.
103     */
104    private final HSRelease mHSRelease;
105    private final int mAnqpDomainID;
106
107    /*
108     * From beacon:
109     * mAnqpOICount is how many additional OIs are available through ANQP.
110     * mRoamingConsortiums is either null, if the element was not present, or is an array of
111     * 1, 2 or 3 longs in which the roaming consortium values occupy the LSBs.
112     */
113    private final int mAnqpOICount;
114    private final long[] mRoamingConsortiums;
115    private int mDtimInterval = -1;
116
117    private final InformationElementUtil.ExtendedCapabilities mExtendedCapabilities;
118
119    private final Map<Constants.ANQPElementType, ANQPElement> mANQPElements;
120
121    public NetworkDetail(String bssid, ScanResult.InformationElement[] infoElements,
122            List<String> anqpLines, int freq) {
123        if (infoElements == null) {
124            throw new IllegalArgumentException("Null information elements");
125        }
126
127        mBSSID = Utils.parseMac(bssid);
128
129        String ssid = null;
130        boolean isHiddenSsid = false;
131        byte[] ssidOctets = null;
132
133        InformationElementUtil.BssLoad bssLoad = new InformationElementUtil.BssLoad();
134
135        InformationElementUtil.Interworking interworking =
136                new InformationElementUtil.Interworking();
137
138        InformationElementUtil.RoamingConsortium roamingConsortium =
139                new InformationElementUtil.RoamingConsortium();
140
141        InformationElementUtil.Vsa vsa = new InformationElementUtil.Vsa();
142
143        InformationElementUtil.HtOperation htOperation = new InformationElementUtil.HtOperation();
144        InformationElementUtil.VhtOperation vhtOperation =
145                new InformationElementUtil.VhtOperation();
146
147        InformationElementUtil.ExtendedCapabilities extendedCapabilities =
148                new InformationElementUtil.ExtendedCapabilities();
149
150        InformationElementUtil.TrafficIndicationMap trafficIndicationMap =
151                new InformationElementUtil.TrafficIndicationMap();
152
153        InformationElementUtil.SupportedRates supportedRates =
154                new InformationElementUtil.SupportedRates();
155        InformationElementUtil.SupportedRates extendedSupportedRates =
156                new InformationElementUtil.SupportedRates();
157
158        RuntimeException exception = null;
159
160        ArrayList<Integer> iesFound = new ArrayList<Integer>();
161        try {
162            for (ScanResult.InformationElement ie : infoElements) {
163                iesFound.add(ie.id);
164                switch (ie.id) {
165                    case ScanResult.InformationElement.EID_SSID:
166                        ssidOctets = ie.bytes;
167                        break;
168                    case ScanResult.InformationElement.EID_BSS_LOAD:
169                        bssLoad.from(ie);
170                        break;
171                    case ScanResult.InformationElement.EID_HT_OPERATION:
172                        htOperation.from(ie);
173                        break;
174                    case ScanResult.InformationElement.EID_VHT_OPERATION:
175                        vhtOperation.from(ie);
176                        break;
177                    case ScanResult.InformationElement.EID_INTERWORKING:
178                        interworking.from(ie);
179                        break;
180                    case ScanResult.InformationElement.EID_ROAMING_CONSORTIUM:
181                        roamingConsortium.from(ie);
182                        break;
183                    case ScanResult.InformationElement.EID_VSA:
184                        vsa.from(ie);
185                        break;
186                    case ScanResult.InformationElement.EID_EXTENDED_CAPS:
187                        extendedCapabilities.from(ie);
188                        break;
189                    case ScanResult.InformationElement.EID_TIM:
190                        trafficIndicationMap.from(ie);
191                        break;
192                    case ScanResult.InformationElement.EID_SUPPORTED_RATES:
193                        supportedRates.from(ie);
194                        break;
195                    case ScanResult.InformationElement.EID_EXTENDED_SUPPORTED_RATES:
196                        extendedSupportedRates.from(ie);
197                        break;
198                    default:
199                        break;
200                }
201            }
202        }
203        catch (IllegalArgumentException | BufferUnderflowException | ArrayIndexOutOfBoundsException e) {
204            Log.d(Utils.hs2LogTag(getClass()), "Caught " + e);
205            if (ssidOctets == null) {
206                throw new IllegalArgumentException("Malformed IE string (no SSID)", e);
207            }
208            exception = e;
209        }
210        if (ssidOctets != null) {
211            /*
212             * Strict use of the "UTF-8 SSID" bit by APs appears to be spotty at best even if the
213             * encoding truly is in UTF-8. An unconditional attempt to decode the SSID as UTF-8 is
214             * therefore always made with a fall back to 8859-1 under normal circumstances.
215             * If, however, a previous exception was detected and the UTF-8 bit is set, failure to
216             * decode the SSID will be used as an indication that the whole frame is malformed and
217             * an exception will be triggered.
218             */
219            CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
220            try {
221                CharBuffer decoded = decoder.decode(ByteBuffer.wrap(ssidOctets));
222                ssid = decoded.toString();
223            }
224            catch (CharacterCodingException cce) {
225                ssid = null;
226            }
227
228            if (ssid == null) {
229                if (extendedCapabilities.isStrictUtf8() && exception != null) {
230                    throw new IllegalArgumentException("Failed to decode SSID in dubious IE string");
231                }
232                else {
233                    ssid = new String(ssidOctets, StandardCharsets.ISO_8859_1);
234                }
235            }
236            isHiddenSsid = true;
237            for (byte byteVal : ssidOctets) {
238                if (byteVal != 0) {
239                    isHiddenSsid = false;
240                    break;
241                }
242            }
243        }
244
245        mSSID = ssid;
246        mHESSID = interworking.hessid;
247        mIsHiddenSsid = isHiddenSsid;
248        mStationCount = bssLoad.stationCount;
249        mChannelUtilization = bssLoad.channelUtilization;
250        mCapacity = bssLoad.capacity;
251        mAnt = interworking.ant;
252        mInternet = interworking.internet;
253        mHSRelease = vsa.hsRelease;
254        mAnqpDomainID = vsa.anqpDomainID;
255        mAnqpOICount = roamingConsortium.anqpOICount;
256        mRoamingConsortiums = roamingConsortium.roamingConsortiums;
257        mExtendedCapabilities = extendedCapabilities;
258        mANQPElements = null;
259        //set up channel info
260        mPrimaryFreq = freq;
261
262        if (vhtOperation.isValid()) {
263            // 80 or 160 MHz
264            mChannelWidth = vhtOperation.getChannelWidth();
265            mCenterfreq0 = vhtOperation.getCenterFreq0();
266            mCenterfreq1 = vhtOperation.getCenterFreq1();
267        } else {
268            mChannelWidth = htOperation.getChannelWidth();
269            mCenterfreq0 = htOperation.getCenterFreq0(mPrimaryFreq);
270            mCenterfreq1  = 0;
271        }
272
273        // If trafficIndicationMap is not valid, mDtimPeriod will be negative
274        if (trafficIndicationMap.isValid()) {
275            mDtimInterval = trafficIndicationMap.mDtimPeriod;
276        }
277
278        int maxRateA = 0;
279        int maxRateB = 0;
280        // If we got some Extended supported rates, consider them, if not default to 0
281        if (extendedSupportedRates.isValid()) {
282            // rates are sorted from smallest to largest in InformationElement
283            maxRateB = extendedSupportedRates.mRates.get(extendedSupportedRates.mRates.size() - 1);
284        }
285        // Only process the determination logic if we got a 'SupportedRates'
286        if (supportedRates.isValid()) {
287            maxRateA = supportedRates.mRates.get(supportedRates.mRates.size() - 1);
288            mMaxRate = maxRateA > maxRateB ? maxRateA : maxRateB;
289            mWifiMode = InformationElementUtil.WifiMode.determineMode(mPrimaryFreq, mMaxRate,
290                    vhtOperation.isValid(),
291                    iesFound.contains(ScanResult.InformationElement.EID_HT_OPERATION),
292                    iesFound.contains(ScanResult.InformationElement.EID_ERP));
293        } else {
294            mWifiMode = 0;
295            mMaxRate = 0;
296            Log.w("WifiMode", mSSID + ", Invalid SupportedRates!!!");
297        }
298        if (DBG) {
299            Log.d(TAG, mSSID + "ChannelWidth is: " + mChannelWidth + " PrimaryFreq: " + mPrimaryFreq
300                    + " mCenterfreq0: " + mCenterfreq0 + " mCenterfreq1: " + mCenterfreq1
301                    + (extendedCapabilities.is80211McRTTResponder() ? "Support RTT responder"
302                    : "Do not support RTT responder"));
303            Log.v("WifiMode", mSSID
304                    + ", WifiMode: " + InformationElementUtil.WifiMode.toString(mWifiMode)
305                    + ", Freq: " + mPrimaryFreq
306                    + ", mMaxRate: " + mMaxRate
307                    + ", VHT: " + String.valueOf(vhtOperation.isValid())
308                    + ", HT: " + String.valueOf(
309                    iesFound.contains(ScanResult.InformationElement.EID_HT_OPERATION))
310                    + ", ERP: " + String.valueOf(
311                    iesFound.contains(ScanResult.InformationElement.EID_ERP))
312                    + ", SupportedRates: " + supportedRates.toString()
313                    + " ExtendedSupportedRates: " + extendedSupportedRates.toString());
314        }
315    }
316
317    private static ByteBuffer getAndAdvancePayload(ByteBuffer data, int plLength) {
318        ByteBuffer payload = data.duplicate().order(data.order());
319        payload.limit(payload.position() + plLength);
320        data.position(data.position() + plLength);
321        return payload;
322    }
323
324    private NetworkDetail(NetworkDetail base, Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
325        mSSID = base.mSSID;
326        mIsHiddenSsid = base.mIsHiddenSsid;
327        mBSSID = base.mBSSID;
328        mHESSID = base.mHESSID;
329        mStationCount = base.mStationCount;
330        mChannelUtilization = base.mChannelUtilization;
331        mCapacity = base.mCapacity;
332        mAnt = base.mAnt;
333        mInternet = base.mInternet;
334        mHSRelease = base.mHSRelease;
335        mAnqpDomainID = base.mAnqpDomainID;
336        mAnqpOICount = base.mAnqpOICount;
337        mRoamingConsortiums = base.mRoamingConsortiums;
338        mExtendedCapabilities =
339                new InformationElementUtil.ExtendedCapabilities(base.mExtendedCapabilities);
340        mANQPElements = anqpElements;
341        mChannelWidth = base.mChannelWidth;
342        mPrimaryFreq = base.mPrimaryFreq;
343        mCenterfreq0 = base.mCenterfreq0;
344        mCenterfreq1 = base.mCenterfreq1;
345        mDtimInterval = base.mDtimInterval;
346        mWifiMode = base.mWifiMode;
347        mMaxRate = base.mMaxRate;
348    }
349
350    public NetworkDetail complete(Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
351        return new NetworkDetail(this, anqpElements);
352    }
353
354    public boolean queriable(List<Constants.ANQPElementType> queryElements) {
355        return mAnt != null &&
356                (Constants.hasBaseANQPElements(queryElements) ||
357                 Constants.hasR2Elements(queryElements) && mHSRelease == HSRelease.R2);
358    }
359
360    public boolean has80211uInfo() {
361        return mAnt != null || mRoamingConsortiums != null || mHSRelease != null;
362    }
363
364    public boolean hasInterworking() {
365        return mAnt != null;
366    }
367
368    public String getSSID() {
369        return mSSID;
370    }
371
372    public String getTrimmedSSID() {
373        if (mSSID != null) {
374            for (int n = 0; n < mSSID.length(); n++) {
375                if (mSSID.charAt(n) != 0) {
376                    return mSSID;
377                }
378            }
379        }
380        return "";
381    }
382
383    public long getHESSID() {
384        return mHESSID;
385    }
386
387    public long getBSSID() {
388        return mBSSID;
389    }
390
391    public int getStationCount() {
392        return mStationCount;
393    }
394
395    public int getChannelUtilization() {
396        return mChannelUtilization;
397    }
398
399    public int getCapacity() {
400        return mCapacity;
401    }
402
403    public boolean isInterworking() {
404        return mAnt != null;
405    }
406
407    public Ant getAnt() {
408        return mAnt;
409    }
410
411    public boolean isInternet() {
412        return mInternet;
413    }
414
415    public HSRelease getHSRelease() {
416        return mHSRelease;
417    }
418
419    public int getAnqpDomainID() {
420        return mAnqpDomainID;
421    }
422
423    public byte[] getOsuProviders() {
424        if (mANQPElements == null) {
425            return null;
426        }
427        ANQPElement osuProviders = mANQPElements.get(Constants.ANQPElementType.HSOSUProviders);
428        return osuProviders != null ? ((RawByteElement) osuProviders).getPayload() : null;
429    }
430
431    public int getAnqpOICount() {
432        return mAnqpOICount;
433    }
434
435    public long[] getRoamingConsortiums() {
436        return mRoamingConsortiums;
437    }
438
439    public Map<Constants.ANQPElementType, ANQPElement> getANQPElements() {
440        return mANQPElements;
441    }
442
443    public int getChannelWidth() {
444        return mChannelWidth;
445    }
446
447    public int getCenterfreq0() {
448        return mCenterfreq0;
449    }
450
451    public int getCenterfreq1() {
452        return mCenterfreq1;
453    }
454
455    public int getWifiMode() {
456        return mWifiMode;
457    }
458
459    public int getDtimInterval() {
460        return mDtimInterval;
461    }
462
463    public boolean is80211McResponderSupport() {
464        return mExtendedCapabilities.is80211McRTTResponder();
465    }
466
467    public boolean isSSID_UTF8() {
468        return mExtendedCapabilities.isStrictUtf8();
469    }
470
471    @Override
472    public boolean equals(Object thatObject) {
473        if (this == thatObject) {
474            return true;
475        }
476        if (thatObject == null || getClass() != thatObject.getClass()) {
477            return false;
478        }
479
480        NetworkDetail that = (NetworkDetail)thatObject;
481
482        return getSSID().equals(that.getSSID()) && getBSSID() == that.getBSSID();
483    }
484
485    @Override
486    public int hashCode() {
487        return ((mSSID.hashCode() * 31) + (int)(mBSSID >>> 32)) * 31 + (int)mBSSID;
488    }
489
490    @Override
491    public String toString() {
492        return String.format("NetworkInfo{SSID='%s', HESSID=%x, BSSID=%x, StationCount=%d, " +
493                "ChannelUtilization=%d, Capacity=%d, Ant=%s, Internet=%s, " +
494                "HSRelease=%s, AnqpDomainID=%d, " +
495                "AnqpOICount=%d, RoamingConsortiums=%s}",
496                mSSID, mHESSID, mBSSID, mStationCount,
497                mChannelUtilization, mCapacity, mAnt, mInternet,
498                mHSRelease, mAnqpDomainID,
499                mAnqpOICount, Utils.roamingConsortiumsToString(mRoamingConsortiums));
500    }
501
502    public String toKeyString() {
503        return mHESSID != 0 ?
504            String.format("'%s':%012x (%012x)", mSSID, mBSSID, mHESSID) :
505            String.format("'%s':%012x", mSSID, mBSSID);
506    }
507
508    public String getBSSIDString() {
509        return toMACString(mBSSID);
510    }
511
512    /**
513     * Evaluates the ScanResult this NetworkDetail is built from
514     * returns true if built from a Beacon Frame
515     * returns false if built from a Probe Response
516     */
517    public boolean isBeaconFrame() {
518        // Beacon frames have a 'Traffic Indication Map' Information element
519        // Probe Responses do not. This is indicated by a DTIM period > 0
520        return mDtimInterval > 0;
521    }
522
523    /**
524     * Evaluates the ScanResult this NetworkDetail is built from
525     * returns true if built from a hidden Beacon Frame
526     * returns false if not hidden or not a Beacon
527     */
528    public boolean isHiddenBeaconFrame() {
529        // Hidden networks are not 80211 standard, but it is common for a hidden network beacon
530        // frame to either send zero-value bytes as the SSID, or to send no bytes at all.
531        return isBeaconFrame() && mIsHiddenSsid;
532    }
533
534    public static String toMACString(long mac) {
535        StringBuilder sb = new StringBuilder();
536        boolean first = true;
537        for (int n = BYTES_IN_EUI48 - 1; n >= 0; n--) {
538            if (first) {
539                first = false;
540            } else {
541                sb.append(':');
542            }
543            sb.append(String.format("%02x", (mac >>> (n * Byte.SIZE)) & BYTE_MASK));
544        }
545        return sb.toString();
546    }
547}
548