package com.android.server.wifi.hotspot2; import android.net.wifi.ScanResult; import android.util.Log; import com.android.server.wifi.anqp.ANQPElement; import com.android.server.wifi.anqp.Constants; import com.android.server.wifi.anqp.VenueNameElement; import java.net.ProtocolException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import static com.android.server.wifi.anqp.Constants.BYTES_IN_EUI48; import static com.android.server.wifi.anqp.Constants.BYTE_MASK; import static com.android.server.wifi.anqp.Constants.getInteger; public class NetworkDetail { private static final int EID_SSID = 0; private static final int EID_BSSLoad = 11; private static final int EID_HT_OPERATION = 61; private static final int EID_VHT_OPERATION = 192; private static final int EID_Interworking = 107; private static final int EID_RoamingConsortium = 111; private static final int EID_ExtendedCaps = 127; private static final int EID_VSA = 221; private static final int ANQP_DOMID_BIT = 0x04; private static final int RTT_RESP_ENABLE_BIT = 70; private static final long SSID_UTF8_BIT = 0x0001000000000000L; //turn off when SHIP private static final boolean DBG = true; private static final boolean VDBG = false; private static final String TAG = "NetworkDetail:"; public enum Ant { Private, PrivateWithGuest, ChargeablePublic, FreePublic, Personal, EmergencyOnly, Resvd6, Resvd7, Resvd8, Resvd9, Resvd10, Resvd11, Resvd12, Resvd13, TestOrExperimental, Wildcard } public enum HSRelease { R1, R2, Unknown } // General identifiers: private final String mSSID; private final long mHESSID; private final long mBSSID; // BSS Load element: private final int mStationCount; private final int mChannelUtilization; private final int mCapacity; //channel detailed information /* * 0 -- 20 MHz * 1 -- 40 MHz * 2 -- 80 MHz * 3 -- 160 MHz * 4 -- 80 + 80 MHz */ private final int mChannelWidth; private final int mPrimaryFreq; private final int mCenterfreq0; private final int mCenterfreq1; private final boolean m80211McRTTResponder; /* * From Interworking element: * mAnt non null indicates the presence of Interworking, i.e. 802.11u * mVenueGroup and mVenueType may be null if not present in the Interworking element. */ private final Ant mAnt; private final boolean mInternet; private final VenueNameElement.VenueGroup mVenueGroup; private final VenueNameElement.VenueType mVenueType; /* * From HS20 Indication element: * mHSRelease is null only if the HS20 Indication element was not present. * mAnqpDomainID is set to -1 if not present in the element. */ private final HSRelease mHSRelease; private final int mAnqpDomainID; /* * From beacon: * mAnqpOICount is how many additional OIs are available through ANQP. * mRoamingConsortiums is either null, if the element was not present, or is an array of * 1, 2 or 3 longs in which the roaming consortium values occupy the LSBs. */ private final int mAnqpOICount; private final long[] mRoamingConsortiums; private final Long mExtendedCapabilities; private final Map<Constants.ANQPElementType, ANQPElement> mANQPElements; public NetworkDetail(String bssid, String infoElements, List<String> anqpLines, int freq) { if (infoElements == null) { throw new IllegalArgumentException("Null information element string"); } int separator = infoElements.indexOf('='); if (separator<0) { throw new IllegalArgumentException("No element separator"); } mBSSID = Utils.parseMac(bssid); ByteBuffer data = ByteBuffer.wrap(Utils.hexToBytes(infoElements.substring(separator + 1))) .order(ByteOrder.LITTLE_ENDIAN); String ssid = null; byte[] ssidOctets = null; int stationCount = 0; int channelUtilization = 0; int capacity = 0; Ant ant = null; boolean internet = false; VenueNameElement.VenueGroup venueGroup = null; VenueNameElement.VenueType venueType = null; long hessid = 0L; int anqpOICount = 0; long[] roamingConsortiums = null; HSRelease hsRelease = null; int anqpDomainID = 0; // No domain ID treated the same as a 0; unique info per AP. Long extendedCapabilities = null; int secondChanelOffset = 0; int channelMode = 0; int centerFreqIndex1 = 0; int centerFreqIndex2 = 0; boolean RTTResponder = false; RuntimeException exception = null; try { while (data.remaining() > 1) { int eid = data.get() & Constants.BYTE_MASK; int elementLength = data.get() & Constants.BYTE_MASK; if (elementLength > data.remaining()) { throw new IllegalArgumentException("Element length " + elementLength + " exceeds payload length " + data.remaining() + " @ " + data.position()); } if (eid == 0 && elementLength == 0 && ssidOctets != null) { // Don't overwrite SSID (eid 0) with trailing zero garbage continue; } ByteBuffer element; switch (eid) { case EID_SSID: ssidOctets = new byte[elementLength]; data.get(ssidOctets); break; case EID_BSSLoad: if (elementLength != 5) { throw new IllegalArgumentException("BSS Load element length is not 5: " + elementLength); } stationCount = data.getShort() & Constants.SHORT_MASK; channelUtilization = data.get() & Constants.BYTE_MASK; capacity = data.getShort() & Constants.SHORT_MASK; break; case EID_HT_OPERATION: element = getAndAdvancePayload(data, elementLength); int primary_channel = element.get(); secondChanelOffset = element.get() & 0x3; break; case EID_VHT_OPERATION: element = getAndAdvancePayload(data, elementLength); channelMode = element.get() & Constants.BYTE_MASK; centerFreqIndex1 = element.get() & Constants.BYTE_MASK; centerFreqIndex2 = element.get() & Constants.BYTE_MASK; break; case EID_Interworking: int anOptions = data.get() & Constants.BYTE_MASK; ant = Ant.values()[anOptions & 0x0f]; internet = (anOptions & 0x10) != 0; // Len 1 none, 3 venue-info, 7 HESSID, 9 venue-info & HESSID if (elementLength == 3 || elementLength == 9) { try { ByteBuffer vinfo = data.duplicate(); vinfo.limit(vinfo.position() + 2); VenueNameElement vne = new VenueNameElement(Constants.ANQPElementType.ANQPVenueName, vinfo); venueGroup = vne.getGroup(); venueType = vne.getType(); data.getShort(); } catch (ProtocolException pe) { /*Cannot happen*/ } } else if (elementLength != 1 && elementLength != 7) { throw new IllegalArgumentException("Bad Interworking element length: " + elementLength); } if (elementLength == 7 || elementLength == 9) { hessid = getInteger(data, ByteOrder.BIG_ENDIAN, 6); } break; case EID_RoamingConsortium: anqpOICount = data.get() & Constants.BYTE_MASK; int oi12Length = data.get() & Constants.BYTE_MASK; int oi1Length = oi12Length & Constants.NIBBLE_MASK; int oi2Length = (oi12Length >>> 4) & Constants.NIBBLE_MASK; int oi3Length = elementLength - 2 - oi1Length - oi2Length; int oiCount = 0; if (oi1Length > 0) { oiCount++; if (oi2Length > 0) { oiCount++; if (oi3Length > 0) { oiCount++; } } } roamingConsortiums = new long[oiCount]; if (oi1Length > 0 && roamingConsortiums.length > 0) { roamingConsortiums[0] = getInteger(data, ByteOrder.BIG_ENDIAN, oi1Length); } if (oi2Length > 0 && roamingConsortiums.length > 1) { roamingConsortiums[1] = getInteger(data, ByteOrder.BIG_ENDIAN, oi2Length); } if (oi3Length > 0 && roamingConsortiums.length > 2) { roamingConsortiums[2] = getInteger(data, ByteOrder.BIG_ENDIAN, oi3Length); } break; case EID_VSA: element = getAndAdvancePayload(data, elementLength); if (elementLength >= 5 && element.getInt() == Constants.HS20_FRAME_PREFIX) { int hsConf = element.get() & Constants.BYTE_MASK; switch ((hsConf >> 4) & Constants.NIBBLE_MASK) { case 0: hsRelease = HSRelease.R1; break; case 1: hsRelease = HSRelease.R2; break; default: hsRelease = HSRelease.Unknown; break; } if ((hsConf & ANQP_DOMID_BIT) != 0) { if (elementLength < 7) { throw new IllegalArgumentException( "HS20 indication element too short: " + elementLength); } anqpDomainID = element.getShort() & Constants.SHORT_MASK; } } break; case EID_ExtendedCaps: element = data.duplicate(); extendedCapabilities = Constants.getInteger(data, ByteOrder.LITTLE_ENDIAN, elementLength); int index = RTT_RESP_ENABLE_BIT / 8; byte offset = RTT_RESP_ENABLE_BIT % 8; if (elementLength < index + 1) { RTTResponder = false; element.position(element.position() + elementLength); break; } element.position(element.position() + index); RTTResponder = (element.get() & (0x1 << offset)) != 0; break; default: data.position(data.position() + elementLength); break; } } } catch (IllegalArgumentException | BufferUnderflowException | ArrayIndexOutOfBoundsException e) { Log.d(Utils.hs2LogTag(getClass()), "Caught " + e); if (ssidOctets == null) { throw new IllegalArgumentException("Malformed IE string (no SSID)", e); } exception = e; } if (ssidOctets != null) { boolean strictUTF8 = extendedCapabilities != null && ( extendedCapabilities & SSID_UTF8_BIT ) != 0; /* * Strict use of the "UTF-8 SSID" bit by APs appears to be spotty at best even if the * encoding truly is in UTF-8. An unconditional attempt to decode the SSID as UTF-8 is * therefore always made with a fall back to 8859-1 under normal circumstances. * If, however, a previous exception was detected and the UTF-8 bit is set, failure to * decode the SSID will be used as an indication that the whole frame is malformed and * an exception will be triggered. */ CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); try { CharBuffer decoded = decoder.decode(ByteBuffer.wrap(ssidOctets)); ssid = decoded.toString(); } catch (CharacterCodingException cce) { ssid = null; } if (ssid == null) { if (strictUTF8 && exception != null) { throw new IllegalArgumentException("Failed to decode SSID in dubious IE string"); } else { ssid = new String(ssidOctets, StandardCharsets.ISO_8859_1); } } } mSSID = ssid; mHESSID = hessid; mStationCount = stationCount; mChannelUtilization = channelUtilization; mCapacity = capacity; mAnt = ant; mInternet = internet; mVenueGroup = venueGroup; mVenueType = venueType; mHSRelease = hsRelease; mAnqpDomainID = anqpDomainID; mAnqpOICount = anqpOICount; mRoamingConsortiums = roamingConsortiums; mExtendedCapabilities = extendedCapabilities; mANQPElements = SupplicantBridge.parseANQPLines(anqpLines); //set up channel info mPrimaryFreq = freq; if (channelMode != 0) { // 80 or 160 MHz mChannelWidth = channelMode + 1; mCenterfreq0 = (centerFreqIndex1 - 36) * 5 + 5180; if(channelMode > 1) { //160MHz mCenterfreq1 = (centerFreqIndex2 - 36) * 5 + 5180; } else { mCenterfreq1 = 0; } } else { //20 or 40 MHz if (secondChanelOffset != 0) {//40MHz mChannelWidth = 1; if (secondChanelOffset == 1) { mCenterfreq0 = mPrimaryFreq + 20; } else if (secondChanelOffset == 3) { mCenterfreq0 = mPrimaryFreq - 20; } else { mCenterfreq0 = 0; Log.e(TAG,"Error on secondChanelOffset"); } } else { mCenterfreq0 = 0; mChannelWidth = 0; } mCenterfreq1 = 0; } m80211McRTTResponder = RTTResponder; if (VDBG) { Log.d(TAG, mSSID + "ChannelWidth is: " + mChannelWidth + " PrimaryFreq: " + mPrimaryFreq + " mCenterfreq0: " + mCenterfreq0 + " mCenterfreq1: " + mCenterfreq1 + (m80211McRTTResponder ? "Support RTT reponder" : "Do not support RTT responder")); } } private static ByteBuffer getAndAdvancePayload(ByteBuffer data, int plLength) { ByteBuffer payload = data.duplicate().order(data.order()); payload.limit(payload.position() + plLength); data.position(data.position() + plLength); return payload; } private NetworkDetail(NetworkDetail base, Map<Constants.ANQPElementType, ANQPElement> anqpElements) { mSSID = base.mSSID; mBSSID = base.mBSSID; mHESSID = base.mHESSID; mStationCount = base.mStationCount; mChannelUtilization = base.mChannelUtilization; mCapacity = base.mCapacity; mAnt = base.mAnt; mInternet = base.mInternet; mVenueGroup = base.mVenueGroup; mVenueType = base.mVenueType; mHSRelease = base.mHSRelease; mAnqpDomainID = base.mAnqpDomainID; mAnqpOICount = base.mAnqpOICount; mRoamingConsortiums = base.mRoamingConsortiums; mExtendedCapabilities = base.mExtendedCapabilities; mANQPElements = anqpElements; mChannelWidth = base.mChannelWidth; mPrimaryFreq = base.mPrimaryFreq; mCenterfreq0 = base.mCenterfreq0; mCenterfreq1 = base.mCenterfreq1; m80211McRTTResponder = base.m80211McRTTResponder; } public NetworkDetail complete(Map<Constants.ANQPElementType, ANQPElement> anqpElements) { return new NetworkDetail(this, anqpElements); } private static long parseMac(String s) { long mac = 0; int count = 0; for (int n = 0; n < s.length(); n++) { int nibble = Utils.fromHex(s.charAt(n), true); if (nibble >= 0) { mac = (mac << 4) | nibble; count++; } } if (count < 12 || (count&1) == 1) { throw new IllegalArgumentException("Bad MAC address: '" + s + "'"); } return mac; } public boolean has80211uInfo() { return mAnt != null || mRoamingConsortiums != null || mHSRelease != null; } public boolean hasInterworking() { return mAnt != null; } public String getSSID() { return mSSID; } public String getTrimmedSSID() { for (int n = 0; n < mSSID.length(); n++) { if (mSSID.charAt(n) != 0) { return mSSID; } } return ""; } public long getHESSID() { return mHESSID; } public long getBSSID() { return mBSSID; } public int getStationCount() { return mStationCount; } public int getChannelUtilization() { return mChannelUtilization; } public int getCapacity() { return mCapacity; } public boolean isInterworking() { return mAnt != null; } public Ant getAnt() { return mAnt; } public boolean isInternet() { return mInternet; } public VenueNameElement.VenueGroup getVenueGroup() { return mVenueGroup; } public VenueNameElement.VenueType getVenueType() { return mVenueType; } public HSRelease getHSRelease() { return mHSRelease; } public int getAnqpDomainID() { return mAnqpDomainID; } public int getAnqpOICount() { return mAnqpOICount; } public long[] getRoamingConsortiums() { return mRoamingConsortiums; } public Long getExtendedCapabilities() { return mExtendedCapabilities; } public Map<Constants.ANQPElementType, ANQPElement> getANQPElements() { return mANQPElements; } public int getChannelWidth() { return mChannelWidth; } public int getCenterfreq0() { return mCenterfreq0; } public int getCenterfreq1() { return mCenterfreq1; } public boolean is80211McResponderSupport() { return m80211McRTTResponder; } public boolean isSSID_UTF8() { return mExtendedCapabilities != null && (mExtendedCapabilities & SSID_UTF8_BIT) != 0; } @Override public boolean equals(Object thatObject) { if (this == thatObject) { return true; } if (thatObject == null || getClass() != thatObject.getClass()) { return false; } NetworkDetail that = (NetworkDetail)thatObject; return getSSID().equals(that.getSSID()) && getBSSID() == that.getBSSID(); } @Override public int hashCode() { return ((mSSID.hashCode() * 31) + (int)(mBSSID >>> 32)) * 31 + (int)mBSSID; } @Override public String toString() { return String.format("NetworkInfo{mSSID='%s', mHESSID=%x, mBSSID=%x, mStationCount=%d, " + "mChannelUtilization=%d, mCapacity=%d, mAnt=%s, mInternet=%s, " + "mVenueGroup=%s, mVenueType=%s, mHSRelease=%s, mAnqpDomainID=%d, " + "mAnqpOICount=%d, mRoamingConsortiums=%s}", mSSID, mHESSID, mBSSID, mStationCount, mChannelUtilization, mCapacity, mAnt, mInternet, mVenueGroup, mVenueType, mHSRelease, mAnqpDomainID, mAnqpOICount, Utils.roamingConsortiumsToString(mRoamingConsortiums)); } public String toKeyString() { return mHESSID != 0 ? String.format("'%s':%012x (%012x)", mSSID, mBSSID, mHESSID) : String.format("'%s':%012x", mSSID, mBSSID); } public String getBSSIDString() { return toMACString(mBSSID); } public static String toMACString(long mac) { StringBuilder sb = new StringBuilder(); boolean first = true; for (int n = BYTES_IN_EUI48 - 1; n >= 0; n--) { if (first) { first = false; } else { sb.append(':'); } sb.append(String.format("%02x", (mac >>> (n * Byte.SIZE)) & BYTE_MASK)); } return sb.toString(); } private static final String IE = "ie=" + "000477696e67" + // SSID wing "0b052a00cf611e" + // BSS Load 42:207:7777 "6b091e0a01610408621205" + // internet:Experimental:Vehicular:Auto:hessid "6f0a0e530111112222222229" + // 14:111111:2222222229 "dd07506f9a10143a01"; // r2:314 private static final String IE2 = "ie=000f4578616d706c65204e6574776f726b010882848b960c1218240301012a010432043048606c30140100000fac040100000fac040100000fac0100007f04000000806b091e07010203040506076c027f006f1001531122331020304050010203040506dd05506f9a1000"; public static void main(String[] args) { ScanResult scanResult = new ScanResult(); scanResult.SSID = "wing"; scanResult.BSSID = "610408"; NetworkDetail nwkDetail = new NetworkDetail(scanResult.BSSID, IE2, null, 0); System.out.println(nwkDetail); } }