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