diff options
author | Vinit Deshpande <vinitd@google.com> | 2015-03-09 19:01:33 -0700 |
---|---|---|
committer | Vinit Deshpande <vinitd@google.com> | 2015-03-09 19:01:33 -0700 |
commit | 29267b6dd601a3ed168da0865426fde457ad57aa (patch) | |
tree | 95b2151ec0741fa39aca52909b2ad2c95e8c79b9 /wifi | |
parent | f0bfa91103b9d1b63b3b4115cd26b1387c466c3c (diff) | |
parent | 2eba02a11234c9cfa6c597effb4f32349826cee4 (diff) | |
download | frameworks_base-29267b6dd601a3ed168da0865426fde457ad57aa.zip frameworks_base-29267b6dd601a3ed168da0865426fde457ad57aa.tar.gz frameworks_base-29267b6dd601a3ed168da0865426fde457ad57aa.tar.bz2 |
am "Initial Passpoint code."
merged from partner/m-wireless-wifi-dev
2eba02a Initial Passpoint code.
Diffstat (limited to 'wifi')
41 files changed, 3433 insertions, 0 deletions
diff --git a/wifi/java/android/net/wifi/anqp/ANQPElement.java b/wifi/java/android/net/wifi/anqp/ANQPElement.java new file mode 100644 index 0000000..581f981 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/ANQPElement.java @@ -0,0 +1,16 @@ +package android.net.wifi.anqp; + +/** + * Base class for an IEEE802.11u ANQP element. + */ +public abstract class ANQPElement { + private final Constants.ANQPElementType mID; + + protected ANQPElement(Constants.ANQPElementType id) { + mID = id; + } + + public Constants.ANQPElementType getID() { + return mID; + } +} diff --git a/wifi/java/android/net/wifi/anqp/ANQPFactory.java b/wifi/java/android/net/wifi/anqp/ANQPFactory.java new file mode 100644 index 0000000..7860926 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/ANQPFactory.java @@ -0,0 +1,213 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +import static android.net.wifi.anqp.Constants.*; + +/** + * Factory to build a collection of 802.11u ANQP elements from a byte buffer. + */ +public class ANQPFactory { + + public static ByteBuffer buildQueryRequest(Set<ANQPElementType> elements, ByteBuffer target) { + List<ANQPElementType> list = new ArrayList<ANQPElementType>(elements); + Collections.sort(list); + + ListIterator<ANQPElementType> elementIterator = list.listIterator(); + + target.order(ByteOrder.LITTLE_ENDIAN); + target.putShort((short) Constants.ANQP_QUERY_LIST); + int lenPos = target.position(); + target.putShort((short) 0); + + while (elementIterator.hasNext()) { + Integer id = Constants.getANQPElementID(elementIterator.next()); + if (id != null) { + target.putShort(id.shortValue()); + } else { + elementIterator.previous(); + } + } + target.putShort(lenPos, (short) (target.position() - lenPos - BYTES_IN_SHORT)); + + // Start a new vendor specific element for HS2.0 elements: + if (elementIterator.hasNext()) { + target.putShort((short) ANQP_VENDOR_SPEC); + int vsLenPos = target.position(); + target.putShort((short) 0); + + target.putInt(Constants.HS20_PREFIX); + target.put((byte) Constants.HS_QUERY_LIST); + target.put((byte) 0); + + while (elementIterator.hasNext()) { + ANQPElementType elementType = elementIterator.next(); + Integer id = Constants.getHS20ElementID(elementType); + if (id == null) { + throw new RuntimeException("Unmapped ANQPElementType: " + elementType); + } else { + target.put(id.byteValue()); + } + } + target.putShort(vsLenPos, (short) (target.position() - vsLenPos - BYTES_IN_SHORT)); + } + + target.flip(); + return target; + } + + public static ByteBuffer buildHomeRealmRequest(List<String> realmNames, ByteBuffer target) { + target.order(ByteOrder.LITTLE_ENDIAN); + target.putShort((short) ANQP_VENDOR_SPEC); + int lenPos = target.position(); + target.putShort((short) 0); + + target.putInt(Constants.HS20_PREFIX); + target.put((byte) Constants.HS_NAI_HOME_REALM_QUERY); + target.put((byte) 0); + + target.put((byte) realmNames.size()); + for (String realmName : realmNames) { + target.put((byte) UTF8_INDICATOR); + byte[] octets = realmName.getBytes(StandardCharsets.UTF_8); + target.put((byte) octets.length); + target.put(octets); + } + target.putShort(lenPos, (short) (target.position() - lenPos - BYTES_IN_SHORT)); + + target.flip(); + return target; + } + + public static ByteBuffer buildIconRequest(String fileName, ByteBuffer target) { + target.order(ByteOrder.LITTLE_ENDIAN); + target.putShort((short) ANQP_VENDOR_SPEC); + int lenPos = target.position(); + target.putShort((short) 0); + + target.putInt(Constants.HS20_PREFIX); + target.put((byte) Constants.HS_ICON_REQUEST); + target.put((byte) 0); + + target.put(fileName.getBytes(StandardCharsets.UTF_8)); + target.putShort(lenPos, (short) (target.position() - lenPos - BYTES_IN_SHORT)); + + target.flip(); + return target; + } + + public static List<ANQPElement> parsePayload(ByteBuffer payload) throws ProtocolException { + payload.order(ByteOrder.LITTLE_ENDIAN); + List<ANQPElement> elements = new ArrayList<ANQPElement>(); + while (payload.hasRemaining()) { + elements.add(buildElement(payload)); + } + return elements; + } + + private static ANQPElement buildElement(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 4) + throw new ProtocolException("Runt payload: " + payload.remaining()); + + int infoIDNumber = payload.getShort() & SHORT_MASK; + ANQPElementType infoID = Constants.mapANQPElement(infoIDNumber); + if (infoID == null) { + throw new ProtocolException("Bad info ID: " + infoIDNumber); + } + int length = payload.getShort() & SHORT_MASK; + + if (payload.remaining() < length) { + throw new ProtocolException("Truncated payload"); + } + + ByteBuffer elementPayload = payload.duplicate(); + payload.position(payload.position() + length); + elementPayload.limit(elementPayload.position() + length); + + switch (infoID) { + case ANQPCapabilityList: + return new CapabilityListElement(infoID, elementPayload); + case ANQPVenueName: + return new VenueNameElement(infoID, elementPayload); + case ANQPEmergencyNumber: + return new EmergencyNumberElement(infoID, elementPayload); + case ANQPNwkAuthType: + return new NetworkAuthenticationTypeElement(infoID, elementPayload); + case ANQPRoamingConsortium: + return new RoamingConsortiumElement(infoID, elementPayload); + case ANQPIPAddrAvailability: + return new IPAddressTypeAvailabilityElement(infoID, elementPayload); + case ANQPNAIRealm: + return new NAIRealmElement(infoID, elementPayload); + case ANQP3GPPNetwork: + return new ThreeGPPNetworkElement(infoID, elementPayload); + case ANQPGeoLoc: + return new GEOLocationElement(infoID, elementPayload); + case ANQPCivicLoc: + return new CivicLocationElement(infoID, elementPayload); + case ANQPLocURI: + return new GenericStringElement(infoID, elementPayload); + case ANQPDomName: + return new DomainNameElement(infoID, elementPayload); + case ANQPEmergencyAlert: + return new GenericStringElement(infoID, elementPayload); + case ANQPTDLSCap: + return new GenericBlobElement(infoID, elementPayload); + case ANQPEmergencyNAI: + return new GenericStringElement(infoID, elementPayload); + case ANQPNeighborReport: + return new GenericBlobElement(infoID, elementPayload); + case ANQPVendorSpec: + if (elementPayload.remaining() > 5) { + int oi = elementPayload.getInt(); + if (oi != Constants.HS20_PREFIX) { + return null; + } + int subType = elementPayload.get() & BYTE_MASK; + elementPayload.get(); + return buildHS20Element(subType, elementPayload); + } else { + return new GenericBlobElement(infoID, elementPayload); + } + default: + throw new ProtocolException("Unknown element ID: " + infoID); + } + } + + private static ANQPElement buildHS20Element(int subType, ByteBuffer payload) + throws ProtocolException { + + ANQPElementType infoID = Constants.mapHS20Element(subType); + + if (infoID == null) { + throw new ProtocolException("Bad HS20 info ID: " + subType); + } + + switch (infoID) { + case HSCapabilityList: + return new HSCapabilityListElement(infoID, payload); + case HSFriendlyName: + return new HSFriendlyNameElement(infoID, payload); + case HSWANMetrics: + return new HSWanMetricsElement(infoID, payload); + case HSConnCapability: + return new HSConnectionCapabilityElement(infoID, payload); + case HSOperatingclass: + return new GenericBlobElement(infoID, payload); + case HSOSUProviders: + return new HSOsuProvidersElement(infoID, payload); + case HSIconFile: + return new HSIconFileElement(infoID, payload); + default: + throw new ProtocolException("Unknown HS20 sub type: " + subType); + } + } +} diff --git a/wifi/java/android/net/wifi/anqp/CapabilityListElement.java b/wifi/java/android/net/wifi/anqp/CapabilityListElement.java new file mode 100644 index 0000000..00ee6d1 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/CapabilityListElement.java @@ -0,0 +1,44 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static android.net.wifi.anqp.Constants.ANQPElementType; +import static android.net.wifi.anqp.Constants.BYTES_IN_SHORT; +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * The ANQP Capability List element, 802.11-2012 section 8.4.4.3 + */ +public class CapabilityListElement extends ANQPElement { + private final ANQPElementType[] mCapabilities; + + public CapabilityListElement(ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + if ((payload.remaining() & 1) == 1) + throw new ProtocolException("Odd length"); + mCapabilities = new ANQPElementType[payload.remaining() / BYTES_IN_SHORT]; + + int index = 0; + while (payload.hasRemaining()) { + int capID = payload.getShort() & SHORT_MASK; + ANQPElementType capability = Constants.mapANQPElement(capID); + if (capability == null) + throw new ProtocolException("Unknown capability: " + capID); + mCapabilities[index++] = capability; + } + } + + public ANQPElementType[] getCapabilities() { + return mCapabilities; + } + + @Override + public String toString() { + return "CapabilityListElement{" + + "mCapabilities=" + Arrays.toString(mCapabilities) + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/CivicLocationElement.java b/wifi/java/android/net/wifi/anqp/CivicLocationElement.java new file mode 100644 index 0000000..2cf3c75 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/CivicLocationElement.java @@ -0,0 +1,201 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * The Civic Location ANQP Element, IEEE802.11-2012 section 8.4.4.13 + */ +public class CivicLocationElement extends ANQPElement { + public enum LocationType {DHCPServer, NwkElement, Client} + + private static final int GEOCONF_CIVIC4 = 99; + private static final int RFC4776 = 0; // Table 8-77, 1=vendor specific + + private final LocationType mLocationType; + private final Locale mLocale; + private final Map<CAType, String> mValues; + + public CivicLocationElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + if (payload.remaining() < 6) { + throw new ProtocolException("Runt civic location:" + payload.remaining()); + } + + int locType = payload.get() & BYTE_MASK; + if (locType != RFC4776) { + throw new ProtocolException("Bad Civic location type: " + locType); + } + + int locSubType = payload.get() & BYTE_MASK; + if (locSubType != GEOCONF_CIVIC4) { + throw new ProtocolException("Unexpected Civic location sub-type: " + locSubType + + " (cannot handle sub elements)"); + } + + int length = payload.get() & BYTE_MASK; + if (length > payload.remaining()) { + throw new ProtocolException("Invalid CA type length: " + length); + } + + int what = payload.get() & BYTE_MASK; + mLocationType = what < LocationType.values().length ? LocationType.values()[what] : null; + + mLocale = Locale.forLanguageTag(Constants.getString(payload, 2, StandardCharsets.US_ASCII)); + + mValues = new HashMap<CAType, String>(); + while (payload.hasRemaining()) { + int caTypeNumber = payload.get() & BYTE_MASK; + CAType caType = s_caTypes.get(caTypeNumber); + + int caValLen = payload.get() & BYTE_MASK; + if (caValLen > payload.remaining()) { + throw new ProtocolException("Bad CA value length: " + caValLen); + } + byte[] caValOctets = new byte[caValLen]; + payload.get(caValOctets); + + if (caType != null) { + mValues.put(caType, new String(caValOctets, StandardCharsets.UTF_8)); + } + } + } + + public LocationType getLocationType() { + return mLocationType; + } + + public Locale getLocale() { + return mLocale; + } + + public Map<CAType, String> getValues() { + return Collections.unmodifiableMap(mValues); + } + + @Override + public String toString() { + return "CivicLocationElement{" + + "mLocationType=" + mLocationType + + ", mLocale=" + mLocale + + ", mValues=" + mValues + + '}'; + } + + private static final Map<Integer, CAType> s_caTypes = new HashMap<Integer, CAType>(); + + public static final int LANGUAGE = 0; + public static final int STATE_PROVINCE = 1; + public static final int COUNTY_DISTRICT = 2; + public static final int CITY = 3; + public static final int DIVISION_BOROUGH = 4; + public static final int BLOCK = 5; + public static final int STREET_GROUP = 6; + public static final int STREET_DIRECTION = 16; + public static final int LEADING_STREET_SUFFIX = 17; + public static final int STREET_SUFFIX = 18; + public static final int HOUSE_NUMBER = 19; + public static final int HOUSE_NUMBER_SUFFIX = 20; + public static final int LANDMARK = 21; + public static final int ADDITIONAL_LOCATION = 22; + public static final int NAME = 23; + public static final int POSTAL_ZIP = 24; + public static final int BUILDING = 25; + public static final int UNIT = 26; + public static final int FLOOR = 27; + public static final int ROOM = 28; + public static final int TYPE = 29; + public static final int POSTAL_COMMUNITY = 30; + public static final int PO_BOX = 31; + public static final int ADDITIONAL_CODE = 32; + public static final int SEAT_DESK = 33; + public static final int PRIMARY_ROAD = 34; + public static final int ROAD_SECTION = 35; + public static final int BRANCH_ROAD = 36; + public static final int SUB_BRANCH_ROAD = 37; + public static final int STREET_NAME_PRE_MOD = 38; + public static final int STREET_NAME_POST_MOD = 39; + public static final int SCRIPT = 128; + public static final int RESERVED = 255; + + public enum CAType { + Language, + StateProvince, + CountyDistrict, + City, + DivisionBorough, + Block, + StreetGroup, + StreetDirection, + LeadingStreetSuffix, + StreetSuffix, + HouseNumber, + HouseNumberSuffix, + Landmark, + AdditionalLocation, + Name, + PostalZIP, + Building, + Unit, + Floor, + Room, + Type, + PostalCommunity, + POBox, + AdditionalCode, + SeatDesk, + PrimaryRoad, + RoadSection, + BranchRoad, + SubBranchRoad, + StreetNamePreMod, + StreetNamePostMod, + Script, + Reserved + } + + static { + s_caTypes.put(LANGUAGE, CAType.Language); + s_caTypes.put(STATE_PROVINCE, CAType.StateProvince); + s_caTypes.put(COUNTY_DISTRICT, CAType.CountyDistrict); + s_caTypes.put(CITY, CAType.City); + s_caTypes.put(DIVISION_BOROUGH, CAType.DivisionBorough); + s_caTypes.put(BLOCK, CAType.Block); + s_caTypes.put(STREET_GROUP, CAType.StreetGroup); + s_caTypes.put(STREET_DIRECTION, CAType.StreetDirection); + s_caTypes.put(LEADING_STREET_SUFFIX, CAType.LeadingStreetSuffix); + s_caTypes.put(STREET_SUFFIX, CAType.StreetSuffix); + s_caTypes.put(HOUSE_NUMBER, CAType.HouseNumber); + s_caTypes.put(HOUSE_NUMBER_SUFFIX, CAType.HouseNumberSuffix); + s_caTypes.put(LANDMARK, CAType.Landmark); + s_caTypes.put(ADDITIONAL_LOCATION, CAType.AdditionalLocation); + s_caTypes.put(NAME, CAType.Name); + s_caTypes.put(POSTAL_ZIP, CAType.PostalZIP); + s_caTypes.put(BUILDING, CAType.Building); + s_caTypes.put(UNIT, CAType.Unit); + s_caTypes.put(FLOOR, CAType.Floor); + s_caTypes.put(ROOM, CAType.Room); + s_caTypes.put(TYPE, CAType.Type); + s_caTypes.put(POSTAL_COMMUNITY, CAType.PostalCommunity); + s_caTypes.put(PO_BOX, CAType.POBox); + s_caTypes.put(ADDITIONAL_CODE, CAType.AdditionalCode); + s_caTypes.put(SEAT_DESK, CAType.SeatDesk); + s_caTypes.put(PRIMARY_ROAD, CAType.PrimaryRoad); + s_caTypes.put(ROAD_SECTION, CAType.RoadSection); + s_caTypes.put(BRANCH_ROAD, CAType.BranchRoad); + s_caTypes.put(SUB_BRANCH_ROAD, CAType.SubBranchRoad); + s_caTypes.put(STREET_NAME_PRE_MOD, CAType.StreetNamePreMod); + s_caTypes.put(STREET_NAME_POST_MOD, CAType.StreetNamePostMod); + s_caTypes.put(SCRIPT, CAType.Script); + s_caTypes.put(RESERVED, CAType.Reserved); + } +} diff --git a/wifi/java/android/net/wifi/anqp/Constants.java b/wifi/java/android/net/wifi/anqp/Constants.java new file mode 100644 index 0000000..17af6de --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/Constants.java @@ -0,0 +1,195 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * ANQP related constants (802.11-2012) + */ +public class Constants { + + public static final int BYTE_MASK = 0xff; + public static final int SHORT_MASK = 0xffff; + public static final long INT_MASK = 0xffffffffL; + public static final int BYTES_IN_SHORT = 2; + public static final int BYTES_IN_INT = 4; + + public static final int HS20_PREFIX = 0x119a6f50; // Note that is represented as a LE int + public static final int UTF8_INDICATOR = 1; + + public enum IconStatus {Success, FileNotFound, Unspecified} + + public static final int ANQP_QUERY_LIST = 256; + public static final int ANQP_CAPABILITY_LIST = 257; + public static final int ANQP_VENUE_NAME = 258; + public static final int ANQP_EMERGENCY_NUMBER = 259; + public static final int ANQP_NWK_AUTH_TYPE = 260; + public static final int ANQP_ROAMING_CONSORTIUM = 261; + public static final int ANQP_IP_ADDR_AVAILABILITY = 262; + public static final int ANQP_NAI_REALM = 263; + public static final int ANQP_3GPP_NETWORK = 264; + public static final int ANQP_GEO_LOC = 265; + public static final int ANQP_CIVIC_LOC = 266; + public static final int ANQP_LOC_URI = 267; + public static final int ANQP_DOM_NAME = 268; + public static final int ANQP_EMERGENCY_ALERT = 269; + public static final int ANQP_TDLS_CAP = 270; + public static final int ANQP_EMERGENCY_NAI = 271; + public static final int ANQP_NEIGHBOR_REPORT = 272; + public static final int ANQP_VENDOR_SPEC = 56797; + + public static final int HS_QUERY_LIST = 1; + public static final int HS_CAPABILITY_LIST = 2; + public static final int HS_FRIENDLY_NAME = 3; + public static final int HS_WAN_METRICS = 4; + public static final int HS_CONN_CAPABILITY = 5; + public static final int HS_NAI_HOME_REALM_QUERY = 6; + public static final int HS_OPERATING_CLASS = 7; + public static final int HS_OSU_PROVIDERS = 8; + public static final int HS_ICON_REQUEST = 10; + public static final int HS_ICON_FILE = 11; + + public enum ANQPElementType { + ANQPQueryList, + ANQPCapabilityList, + ANQPVenueName, + ANQPEmergencyNumber, + ANQPNwkAuthType, + ANQPRoamingConsortium, + ANQPIPAddrAvailability, + ANQPNAIRealm, + ANQP3GPPNetwork, + ANQPGeoLoc, + ANQPCivicLoc, + ANQPLocURI, + ANQPDomName, + ANQPEmergencyAlert, + ANQPTDLSCap, + ANQPEmergencyNAI, + ANQPNeighborReport, + ANQPVendorSpec, + HSQueryList, + HSCapabilityList, + HSFriendlyName, + HSWANMetrics, + HSConnCapability, + HSNAIHomeRealmQuery, + HSOperatingclass, + HSOSUProviders, + HSIconRequest, + HSIconFile + } + + private static final Map<Integer, ANQPElementType> sAnqpMap = new HashMap<Integer, ANQPElementType>(); + private static final Map<Integer, ANQPElementType> sHs20Map = new HashMap<Integer, ANQPElementType>(); + private static final Map<ANQPElementType, Integer> sRevAnqpmap = new HashMap<ANQPElementType, Integer>(); + private static final Map<ANQPElementType, Integer> sRevHs20map = new HashMap<ANQPElementType, Integer>(); + + static { + sAnqpMap.put(ANQP_QUERY_LIST, ANQPElementType.ANQPQueryList); + sAnqpMap.put(ANQP_CAPABILITY_LIST, ANQPElementType.ANQPCapabilityList); + sAnqpMap.put(ANQP_VENUE_NAME, ANQPElementType.ANQPVenueName); + sAnqpMap.put(ANQP_EMERGENCY_NUMBER, ANQPElementType.ANQPEmergencyNumber); + sAnqpMap.put(ANQP_NWK_AUTH_TYPE, ANQPElementType.ANQPNwkAuthType); + sAnqpMap.put(ANQP_ROAMING_CONSORTIUM, ANQPElementType.ANQPRoamingConsortium); + sAnqpMap.put(ANQP_IP_ADDR_AVAILABILITY, ANQPElementType.ANQPIPAddrAvailability); + sAnqpMap.put(ANQP_NAI_REALM, ANQPElementType.ANQPNAIRealm); + sAnqpMap.put(ANQP_3GPP_NETWORK, ANQPElementType.ANQP3GPPNetwork); + sAnqpMap.put(ANQP_GEO_LOC, ANQPElementType.ANQPGeoLoc); + sAnqpMap.put(ANQP_CIVIC_LOC, ANQPElementType.ANQPCivicLoc); + sAnqpMap.put(ANQP_LOC_URI, ANQPElementType.ANQPLocURI); + sAnqpMap.put(ANQP_DOM_NAME, ANQPElementType.ANQPDomName); + sAnqpMap.put(ANQP_EMERGENCY_ALERT, ANQPElementType.ANQPEmergencyAlert); + sAnqpMap.put(ANQP_TDLS_CAP, ANQPElementType.ANQPTDLSCap); + sAnqpMap.put(ANQP_EMERGENCY_NAI, ANQPElementType.ANQPEmergencyNAI); + sAnqpMap.put(ANQP_NEIGHBOR_REPORT, ANQPElementType.ANQPNeighborReport); + sAnqpMap.put(ANQP_VENDOR_SPEC, ANQPElementType.ANQPVendorSpec); + + sHs20Map.put(HS_QUERY_LIST, ANQPElementType.HSQueryList); + sHs20Map.put(HS_CAPABILITY_LIST, ANQPElementType.HSCapabilityList); + sHs20Map.put(HS_FRIENDLY_NAME, ANQPElementType.HSFriendlyName); + sHs20Map.put(HS_WAN_METRICS, ANQPElementType.HSWANMetrics); + sHs20Map.put(HS_CONN_CAPABILITY, ANQPElementType.HSConnCapability); + sHs20Map.put(HS_NAI_HOME_REALM_QUERY, ANQPElementType.HSNAIHomeRealmQuery); + sHs20Map.put(HS_OPERATING_CLASS, ANQPElementType.HSOperatingclass); + sHs20Map.put(HS_OSU_PROVIDERS, ANQPElementType.HSOSUProviders); + sHs20Map.put(HS_ICON_REQUEST, ANQPElementType.HSIconRequest); + sHs20Map.put(HS_ICON_FILE, ANQPElementType.HSIconFile); + + for (Map.Entry<Integer, ANQPElementType> entry : sAnqpMap.entrySet()) { + sRevAnqpmap.put(entry.getValue(), entry.getKey()); + } + for (Map.Entry<Integer, ANQPElementType> entry : sHs20Map.entrySet()) { + sRevHs20map.put(entry.getValue(), entry.getKey()); + } + } + + public static ANQPElementType mapANQPElement(int id) { + return sAnqpMap.get(id); + } + + public static ANQPElementType mapHS20Element(int id) { + return sHs20Map.get(id); + } + + public static Integer getANQPElementID(ANQPElementType elementType) { + return sRevAnqpmap.get(elementType); + } + + public static Integer getHS20ElementID(ANQPElementType elementType) { + return sRevHs20map.get(elementType); + } + + public static long getInteger(ByteBuffer payload, int size) { + byte[] octets = new byte[size]; + payload.get(octets); + long value = 0; + for (int n = octets.length - 1; n >= 0; n--) { + value = (value << Byte.SIZE) | (octets[n] & BYTE_MASK); + } + return value; + } + + public static String getPrefixedString(ByteBuffer payload, int lengthLength, Charset charset) + throws ProtocolException { + if (payload.remaining() < lengthLength) { + throw new ProtocolException("Runt string: " + payload.remaining()); + } + return getString(payload, (int) getInteger(payload, lengthLength), charset, false); + } + + public static String getString(ByteBuffer payload, int length, Charset charset) + throws ProtocolException { + return getString(payload, length, charset, false); + } + + public static String getString(ByteBuffer payload, int length, Charset charset, boolean useNull) + throws ProtocolException { + if (length > payload.remaining()) { + throw new ProtocolException("Bad string length: " + length); + } + if (useNull && length == 0) { + return null; + } + byte[] octets = new byte[length]; + return new String(octets, charset); + } + + public static String toHexString(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 3); + + boolean first = true; + for (byte b : data) { + if (first) { + first = false; + } else { + sb.append(' '); + } + sb.append(String.format("%02x", b & BYTE_MASK)); + } + return sb.toString(); + } +} diff --git a/wifi/java/android/net/wifi/anqp/DomainNameElement.java b/wifi/java/android/net/wifi/anqp/DomainNameElement.java new file mode 100644 index 0000000..e683b64 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/DomainNameElement.java @@ -0,0 +1,37 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The Domain Name ANQP Element, IEEE802.11-2012 section 8.4.4.15 + */ +public class DomainNameElement extends ANQPElement { + private final List<String> mDomains; + + public DomainNameElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + mDomains = new ArrayList<String>(); + + while (payload.hasRemaining()) { + // Use latin-1 to decode for now - safe for ASCII and retains encoding + mDomains.add(Constants.getPrefixedString(payload, 1, StandardCharsets.ISO_8859_1)); + } + } + + public List<String> getDomains() { + return Collections.unmodifiableList(mDomains); + } + + @Override + public String toString() { + return "DomainNameElement{" + + "mDomains=" + mDomains + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/EmergencyNumberElement.java b/wifi/java/android/net/wifi/anqp/EmergencyNumberElement.java new file mode 100644 index 0000000..c7d6b56 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/EmergencyNumberElement.java @@ -0,0 +1,36 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * The Emergency Number ANQP Element, IEEE802.11-2012 section 8.4.4.5 + */ +public class EmergencyNumberElement extends ANQPElement { + private final List<String> mNumbers; + + public EmergencyNumberElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + mNumbers = new ArrayList<String>(); + + while (payload.hasRemaining()) { + mNumbers.add(Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8)); + } + } + + public List<String> getNumbers() { + return mNumbers; + } + + @Override + public String toString() { + return "EmergencyNumberElement{" + + "mNumbers=" + mNumbers + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/GEOLocationElement.java b/wifi/java/android/net/wifi/anqp/GEOLocationElement.java new file mode 100644 index 0000000..691fdad --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/GEOLocationElement.java @@ -0,0 +1,314 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * Holds an AP Geospatial Location ANQP Element, as specified in IEEE802.11-2012 section + * 8.4.4.12. + * <p/> + * <p> + * Section 8.4.2.24.10 of the IEEE802.11-2012 specification refers to RFC-3825 for the format of the + * Geospatial location information. RFC-3825 has subsequently been obsoleted by RFC-6225 which + * defines the same basic binary format for the DHCPv4 payload except that a few unused bits of the + * Datum field have been reserved for other uses. + * </p> + * <p/> + * <p> + * RFC-3825 defines a resolution field for each of latitude, longitude and altitude as "the number + * of significant bits" of precision in the respective values and implies through examples and + * otherwise that the non-significant bits should be simply disregarded and the range of values are + * calculated as the numeric interval obtained by varying the range of "insignificant bits" between + * its extremes. As a simple example, consider the value 33 as a simple 8-bit number with three + * significant bits: 33 is 00100001 binary and the leading 001 are the significant bits. With the + * above definition, the range of numbers are [32,63] with 33 asymmetrically located at the low end + * of the interval. In a more realistic setting an instrument, such as a GPS, would most likely + * deliver measurements with a gaussian distribution around the exact value, meaning it is more + * reasonable to assume the value as a "center" value with a symmetric uncertainty interval. + * RFC-6225 redefines the "resolution" from RFC-3825 with an "uncertainty" value with these + * properties, which is also the definition suggested here. + * </p> + * <p/> + * <p> + * The res fields provides the resolution as the exponent to a power of two, + * e.g. 8 means 2^8 = +/- 256, 0 means 2^0 = +/- 1 and -7 means 2^-7 +/- 0.00781250. + * Unknown resolution is indicated by not setting the respective resolution field in the RealValue. + * </p> + */ +public class GEOLocationElement extends ANQPElement { + public enum AltitudeType {Unknown, Meters, Floors} + + public enum Datum {Unknown, WGS84, NAD83Land, NAD83Water} + + private static final int ELEMENT_ID = 123; // ??? + private static final int GEO_LOCATION_LENGTH = 16; + + private static final int LL_FRACTION_SIZE = 25; + private static final int LL_WIDTH = 34; + private static final int ALT_FRACTION_SIZE = 8; + private static final int ALT_WIDTH = 30; + private static final int RES_WIDTH = 6; + private static final int ALT_TYPE_WIDTH = 4; + private static final int DATUM_WIDTH = 8; + + private final RealValue mLatitude; + private final RealValue mLongitude; + private final RealValue mAltitude; + private final AltitudeType mAltitudeType; + private final Datum mDatum; + + public static class RealValue { + private final double mValue; + private final boolean mResolutionSet; + private final int mResolution; + + public RealValue(double value) { + mValue = value; + mResolution = Integer.MIN_VALUE; + mResolutionSet = false; + } + + public RealValue(double value, int resolution) { + mValue = value; + mResolution = resolution; + mResolutionSet = true; + } + + public double getValue() { + return mValue; + } + + public boolean isResolutionSet() { + return mResolutionSet; + } + + public int getResolution() { + return mResolution; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%f", mValue)); + if (mResolutionSet) { + sb.append("+/-2^").append(mResolution); + } + return sb.toString(); + } + } + + public GEOLocationElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + payload.get(); + int locLength = payload.get() & BYTE_MASK; + + if (locLength != GEO_LOCATION_LENGTH) { + throw new ProtocolException("GeoLocation length field value " + locLength + + " incorrect, expected 16"); + } + if (payload.remaining() != GEO_LOCATION_LENGTH) { + throw new ProtocolException("Bad buffer length " + payload.remaining() + + ", expected 16"); + } + + ReverseBitStream reverseBitStream = new ReverseBitStream(payload); + + int rawLatRes = (int) reverseBitStream.sliceOff(RES_WIDTH); + double latitude = + fixToFloat(reverseBitStream.sliceOff(LL_WIDTH), LL_FRACTION_SIZE, LL_WIDTH); + + mLatitude = rawLatRes != 0 ? + new RealValue(latitude, bitsToAbsResolution(rawLatRes, LL_WIDTH, + LL_FRACTION_SIZE)) : + new RealValue(latitude); + + int rawLonRes = (int) reverseBitStream.sliceOff(RES_WIDTH); + double longitude = + fixToFloat(reverseBitStream.sliceOff(LL_WIDTH), LL_FRACTION_SIZE, LL_WIDTH); + + mLongitude = rawLonRes != 0 ? + new RealValue(longitude, bitsToAbsResolution(rawLonRes, LL_WIDTH, + LL_FRACTION_SIZE)) : + new RealValue(longitude); + + int altType = (int) reverseBitStream.sliceOff(ALT_TYPE_WIDTH); + mAltitudeType = altType < AltitudeType.values().length ? + AltitudeType.values()[altType] : + AltitudeType.Unknown; + + int rawAltRes = (int) reverseBitStream.sliceOff(RES_WIDTH); + double altitude = fixToFloat(reverseBitStream.sliceOff(ALT_WIDTH), ALT_FRACTION_SIZE, + ALT_WIDTH); + + mAltitude = rawAltRes != 0 ? + new RealValue(altitude, bitsToAbsResolution(rawAltRes, ALT_WIDTH, + ALT_FRACTION_SIZE)) : + new RealValue(altitude); + + int datumValue = (int) reverseBitStream.sliceOff(DATUM_WIDTH); + mDatum = datumValue < Datum.values().length ? Datum.values()[datumValue] : Datum.Unknown; + } + + public RealValue getLatitude() { + return mLatitude; + } + + public RealValue getLongitude() { + return mLongitude; + } + + public RealValue getAltitude() { + return mAltitude; + } + + public AltitudeType getAltitudeType() { + return mAltitudeType; + } + + public Datum getDatum() { + return mDatum; + } + + @Override + public String toString() { + return "GEOLocationElement{" + + "mLatitude=" + mLatitude + + ", mLongitude=" + mLongitude + + ", mAltitude=" + mAltitude + + ", mAltitudeType=" + mAltitudeType + + ", mDatum=" + mDatum + + '}'; + } + + private static class ReverseBitStream { + + private final byte[] mOctets; + private int mBitoffset; + + private ReverseBitStream(ByteBuffer octets) { + mOctets = new byte[octets.remaining()]; + octets.get(mOctets); + } + + private long sliceOff(int bits) { + final int bn = mBitoffset + bits; + int remaining = bits; + long value = 0; + + while (mBitoffset < bn) { + int sbit = mBitoffset & 0x7; // Bit #0 is MSB, inclusive + int octet = mBitoffset >>> 3; + + // Copy the minimum of what's to the right of sbit + // and how much more goes to the target + int width = Math.min(Byte.SIZE - sbit, remaining); + + value = (value << width) | getBits(mOctets[octet], sbit, width); + + mBitoffset += width; + remaining -= width; + } + + System.out.printf(" - Sliced off %d bits: %x\n", bits, value); + return value; + } + + private static int getBits(byte b, int b0, int width) { + int mask = (1 << width) - 1; + return (b >> (Byte.SIZE - b0 - width)) & mask; + } + } + + private static class BitStream { + + private final byte[] data; + private int bitOffset; // bit 0 is MSB of data[0] + + private BitStream(int octets) { + data = new byte[octets]; + } + + private void append(long value, int width) { + System.out.printf("Appending %x:%d\n", value, width); + for (int sbit = width - 1; sbit >= 0; ) { + int b0 = bitOffset >>> 3; + int dbit = bitOffset & 0x7; + + int shr = sbit - 7 + dbit; + int dmask = 0xff >>> dbit; + + if (shr >= 0) { + data[b0] = (byte) ((data[b0] & ~dmask) | ((value >>> shr) & dmask)); + bitOffset += Byte.SIZE - dbit; + sbit -= Byte.SIZE - dbit; + } else { + data[b0] = (byte) ((data[b0] & ~dmask) | ((value << -shr) & dmask)); + bitOffset += sbit + 1; + sbit = -1; + } + } + } + + private byte[] getOctets() { + return data; + } + } + + static double fixToFloat(long value, int fractionSize, int width) { + long sign = 1L << (width - 1); + if ((value & sign) != 0) { + value = -value; + return -(double) (value & (sign - 1)) / (double) (1L << fractionSize); + } else { + return (double) (value & (sign - 1)) / (double) (1L << fractionSize); + } + } + + private static long floatToFix(double value, int fractionSize, int width) { + return Math.round(value * (1L << fractionSize)) & ((1L << width) - 1); + } + + private static final double LOG2_FACTOR = 1.0 / Math.log(2.0); + + /** + * Convert an absolute variance value into absolute resolution representation, + * where the variance = 2^resolution. + * + * @param variance The absolute variance + * @return the absolute resolution. + */ + private static int getResolution(double variance) { + return (int) Math.ceil(Math.log(variance) * LOG2_FACTOR); + } + + /** + * Convert an absolute resolution, into the "number of significant bits" for the given fixed + * point notation as defined in RFC-3825 and refined in RFC-6225. + * + * @param resolution absolute resolution given as 2^resolution. + * @param fieldWidth Full width of the fixed point number used to represent the value. + * @param fractionBits Number of fraction bits in the fixed point number used to represent the + * value. + * @return The number of "significant bits". + */ + private static int absResolutionToBits(int resolution, int fieldWidth, int fractionBits) { + return fieldWidth - fractionBits - 1 - resolution; + } + + /** + * Convert the protocol definition of "number of significant bits" into an absolute resolution. + * + * @param bits The number of "significant bits" from the binary protocol. + * @param fieldWidth Full width of the fixed point number used to represent the value. + * @param fractionBits Number of fraction bits in the fixed point number used to represent the + * value. + * @return The absolute resolution given as 2^resolution. + */ + private static int bitsToAbsResolution(long bits, int fieldWidth, int fractionBits) { + return fieldWidth - fractionBits - 1 - (int) bits; + } +} diff --git a/wifi/java/android/net/wifi/anqp/GenericBlobElement.java b/wifi/java/android/net/wifi/anqp/GenericBlobElement.java new file mode 100644 index 0000000..ecdf939 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/GenericBlobElement.java @@ -0,0 +1,25 @@ +package android.net.wifi.anqp; + +import java.nio.ByteBuffer; + +/** + * ANQP Element to hold a raw, unparsed, octet blob + */ +public class GenericBlobElement extends ANQPElement { + private final byte[] mData; + + public GenericBlobElement(Constants.ANQPElementType infoID, ByteBuffer payload) { + super(infoID); + mData = new byte[payload.remaining()]; + payload.get(mData); + } + + public byte[] getData() { + return mData; + } + + @Override + public String toString() { + return "Element ID " + getID() + ": " + Constants.toHexString(mData); + } +} diff --git a/wifi/java/android/net/wifi/anqp/GenericStringElement.java b/wifi/java/android/net/wifi/anqp/GenericStringElement.java new file mode 100644 index 0000000..d304444 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/GenericStringElement.java @@ -0,0 +1,26 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * ANQP Element to hold a generic (UTF-8 decoded) character string + */ +public class GenericStringElement extends ANQPElement { + private final String mText; + + public GenericStringElement(Constants.ANQPElementType infoID, ByteBuffer payload) throws ProtocolException { + super(infoID); + mText = Constants.getString(payload, payload.remaining(), StandardCharsets.UTF_8); + } + + public String getM_text() { + return mText; + } + + @Override + public String toString() { + return "Element ID " + getID() + ": '" + mText + "'"; + } +} diff --git a/wifi/java/android/net/wifi/anqp/HSCapabilityListElement.java b/wifi/java/android/net/wifi/anqp/HSCapabilityListElement.java new file mode 100644 index 0000000..367dde5 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/HSCapabilityListElement.java @@ -0,0 +1,43 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * The HS Capability list vendor specific ANQP Element, + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.2 + */ +public class HSCapabilityListElement extends ANQPElement { + private final Constants.ANQPElementType[] mCapabilities; + + public HSCapabilityListElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + mCapabilities = new Constants.ANQPElementType[payload.remaining()]; + + int index = 0; + while (payload.hasRemaining()) { + int capID = payload.get() & BYTE_MASK; + Constants.ANQPElementType capability = Constants.mapANQPElement(capID); + if (capability == null) + throw new ProtocolException("Unknown capability: " + capID); + mCapabilities[index++] = capability; + } + } + + public Constants.ANQPElementType[] getCapabilities() { + return mCapabilities; + } + + @Override + public String toString() { + return "HSCapabilityListElement{" + + "mCapabilities=" + Arrays.toString(mCapabilities) + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/HSConnectionCapabilityElement.java b/wifi/java/android/net/wifi/anqp/HSConnectionCapabilityElement.java new file mode 100644 index 0000000..8937859 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/HSConnectionCapabilityElement.java @@ -0,0 +1,82 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * The Connection Capability vendor specific ANQP Element, + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.5 + */ +public class HSConnectionCapabilityElement extends ANQPElement { + + public enum ProtoStatus {Closed, Open, Unknown} + + private final List<ProtocolTuple> mStatusList; + + public static class ProtocolTuple { + private final int mProtocol; + private final int mPort; + private final ProtoStatus mStatus; + + private ProtocolTuple(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 4) { + throw new ProtocolException("Runt protocol tuple: " + payload.remaining()); + } + mProtocol = payload.get() & BYTE_MASK; + mPort = payload.getShort() & SHORT_MASK; + int statusNumber = payload.get() & BYTE_MASK; + mStatus = statusNumber < ProtoStatus.values().length ? + ProtoStatus.values()[statusNumber] : + null; + } + + public int getProtocol() { + return mProtocol; + } + + public int getPort() { + return mPort; + } + + public ProtoStatus getStatus() { + return mStatus; + } + + @Override + public String toString() { + return "ProtocolTuple{" + + "mProtocol=" + mProtocol + + ", mPort=" + mPort + + ", mStatus=" + mStatus + + '}'; + } + } + + public HSConnectionCapabilityElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + mStatusList = new ArrayList<ProtocolTuple>(); + while (payload.hasRemaining()) { + mStatusList.add(new ProtocolTuple(payload)); + } + } + + public List<ProtocolTuple> getStatusList() { + return Collections.unmodifiableList(mStatusList); + } + + @Override + public String toString() { + return "HSConnectionCapabilityElement{" + + "mStatusList=" + mStatusList + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/HSFriendlyNameElement.java b/wifi/java/android/net/wifi/anqp/HSFriendlyNameElement.java new file mode 100644 index 0000000..0fe3692 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/HSFriendlyNameElement.java @@ -0,0 +1,38 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The Operator Friendly Name vendor specific ANQP Element, + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.3 + */ +public class HSFriendlyNameElement extends ANQPElement { + private final List<I18Name> mNames; + + public HSFriendlyNameElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + mNames = new ArrayList<I18Name>(); + + while (payload.hasRemaining()) { + mNames.add(new I18Name(payload)); + } + } + + public List<I18Name> getNames() { + return Collections.unmodifiableList(mNames); + } + + @Override + public String toString() { + return "HSFriendlyNameElement{" + + "mNames=" + mNames + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/HSIconFileElement.java b/wifi/java/android/net/wifi/anqp/HSIconFileElement.java new file mode 100644 index 0000000..791ae93 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/HSIconFileElement.java @@ -0,0 +1,59 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * The Icon Binary File vendor specific ANQP Element, + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.11 + */ +public class HSIconFileElement extends ANQPElement { + + public enum StatusCode {Success, FileNotFound, Unspecified} + + private final StatusCode mStatusCode; + private final String mType; + private final byte[] mIconData; + + public HSIconFileElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + if (payload.remaining() < 4) { + throw new ProtocolException("Truncated icon file: " + payload.remaining()); + } + + int statusID = payload.get() & BYTE_MASK; + mStatusCode = statusID < StatusCode.values().length ? StatusCode.values()[statusID] : null; + mType = Constants.getString(payload, 1, StandardCharsets.US_ASCII); + + int dataLength = payload.getShort() & SHORT_MASK; + mIconData = new byte[dataLength]; + payload.get(mIconData); + } + + public StatusCode getStatusCode() { + return mStatusCode; + } + + public String getType() { + return mType; + } + + public byte[] getIconData() { + return mIconData; + } + + @Override + public String toString() { + return "HSIconFileElement{" + + "mStatusCode=" + mStatusCode + + ", mType='" + mType + '\'' + + ", mIconData=" + mIconData.length + " bytes }"; + } +} diff --git a/wifi/java/android/net/wifi/anqp/HSOsuProvidersElement.java b/wifi/java/android/net/wifi/anqp/HSOsuProvidersElement.java new file mode 100644 index 0000000..61e28c5 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/HSOsuProvidersElement.java @@ -0,0 +1,51 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * The OSU Providers List vendor specific ANQP Element, + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.8 + */ +public class HSOsuProvidersElement extends ANQPElement { + private final String mSSID; + private final List<OSUProvider> mProviders; + + public HSOsuProvidersElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + mSSID = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8); + int providerCount = payload.get() & BYTE_MASK; + + mProviders = new ArrayList<OSUProvider>(providerCount); + + while (providerCount > 0) { + mProviders.add(new OSUProvider(payload)); + providerCount--; + } + } + + public String getSSID() { + return mSSID; + } + + public List<OSUProvider> getProviders() { + return Collections.unmodifiableList(mProviders); + } + + @Override + public String toString() { + return "HSOsuProvidersElement{" + + "mSSID='" + mSSID + '\'' + + ", mProviders=" + mProviders + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/HSWanMetricsElement.java b/wifi/java/android/net/wifi/anqp/HSWanMetricsElement.java new file mode 100644 index 0000000..1f792a3 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/HSWanMetricsElement.java @@ -0,0 +1,92 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; +import static android.net.wifi.anqp.Constants.INT_MASK; +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * The WAN Metrics vendor specific ANQP Element, + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.4 + */ +public class HSWanMetricsElement extends ANQPElement { + + public enum LinkStatus {Reserved, Up, Down, Test} + + private final LinkStatus mStatus; + private final boolean mSymmetric; + private final boolean mCapped; + private final long mDlSpeed; + private final long mUlSpeed; + private final int mDlLoad; + private final int mUlLoad; + private final int mLMD; + + public HSWanMetricsElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + if (payload.remaining() != 13) { + throw new ProtocolException("Bad WAN metrics length: " + payload.remaining()); + } + + int status = payload.get() & BYTE_MASK; + mStatus = LinkStatus.values()[status & 0x03]; + mSymmetric = (status & 0x04) != 0; + mCapped = (status & 0x08) != 0; + mDlSpeed = payload.getInt() & INT_MASK; + mUlSpeed = payload.getInt() & INT_MASK; + mDlLoad = payload.get() & BYTE_MASK; + mUlLoad = payload.get() & BYTE_MASK; + mLMD = payload.getShort() & SHORT_MASK; + } + + public LinkStatus getStatus() { + return mStatus; + } + + public boolean isSymmetric() { + return mSymmetric; + } + + public boolean isCapped() { + return mCapped; + } + + public long getDlSpeed() { + return mDlSpeed; + } + + public long getUlSpeed() { + return mUlSpeed; + } + + public int getDlLoad() { + return mDlLoad; + } + + public int getUlLoad() { + return mUlLoad; + } + + public int getLMD() { + return mLMD; + } + + @Override + public String toString() { + return "HSWanMetricsElement{" + + "mStatus=" + mStatus + + ", mSymmetric=" + mSymmetric + + ", mCapped=" + mCapped + + ", mDlSpeed=" + mDlSpeed + + ", mUlSpeed=" + mUlSpeed + + ", mDlLoad=" + mDlLoad + + ", mUlLoad=" + mUlLoad + + ", mLMD=" + mLMD + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/I18Name.java b/wifi/java/android/net/wifi/anqp/I18Name.java new file mode 100644 index 0000000..0ceeb18 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/I18Name.java @@ -0,0 +1,45 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * A generic Internationalized name used in ANQP elements as specified in 802.11-2012 and + * "Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00" + */ +public class I18Name { + private static final int LANG_CODE_LENGTH = 3; + + private final Locale mLocale; + private final String mText; + + public I18Name(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 4) { + throw new ProtocolException("Truncated I18Name: " + payload.remaining()); + } + int nameLength = payload.get() & BYTE_MASK; + if (nameLength < 3) { + throw new ProtocolException("Runt I18Name: " + nameLength); + } + String language = Constants.getString(payload, LANG_CODE_LENGTH, StandardCharsets.US_ASCII); + mLocale = Locale.forLanguageTag(language); + mText = Constants.getString(payload, nameLength - LANG_CODE_LENGTH, StandardCharsets.UTF_8); + } + + public Locale getLocale() { + return mLocale; + } + + public String getText() { + return mText; + } + + @Override + public String toString() { + return mText + ':' + mLocale.getLanguage(); + } +} diff --git a/wifi/java/android/net/wifi/anqp/IPAddressTypeAvailabilityElement.java b/wifi/java/android/net/wifi/anqp/IPAddressTypeAvailabilityElement.java new file mode 100644 index 0000000..8c8d388 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/IPAddressTypeAvailabilityElement.java @@ -0,0 +1,52 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +/** + * The IP Address Type availability ANQP Element, IEEE802.11-2012 section 8.4.4.9 + */ +public class IPAddressTypeAvailabilityElement extends ANQPElement { + public enum IPv4Availability { + NotAvailable, Public, PortRestricted, SingleNATA, DoubleNAT, + PortRestrictedAndSingleNAT, PortRestrictedAndDoubleNAT, Unknown + } + + public enum IPv6Availability {NotAvailable, Available, Unknown, Reserved} + + private final IPv4Availability mV4Availability; + private final IPv6Availability mV6Availability; + + public IPAddressTypeAvailabilityElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + if (payload.remaining() != 1) + throw new ProtocolException("Bad IP Address Type Availability length: " + + payload.remaining()); + + int ipField = payload.get(); + mV6Availability = IPv6Availability.values()[ipField & 0x3]; + + ipField = (ipField >> 2) & 0x3f; + mV4Availability = ipField <= IPv4Availability.values().length ? + IPv4Availability.values()[ipField] : + IPv4Availability.Unknown; + } + + public IPv4Availability getV4Availability() { + return mV4Availability; + } + + public IPv6Availability getV6Availability() { + return mV6Availability; + } + + @Override + public String toString() { + return "IPAddressTypeAvailabilityElement{" + + "mV4Availability=" + mV4Availability + + ", mV6Availability=" + mV6Availability + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/IconInfo.java b/wifi/java/android/net/wifi/anqp/IconInfo.java new file mode 100644 index 0000000..33d4c77 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/IconInfo.java @@ -0,0 +1,64 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * The Icons available OSU Providers sub field, as specified in + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.8.1.4 + */ +public class IconInfo { + private final int mWidth; + private final int mHeight; + private final Locale mLocale; + private final String mIconType; + private final String mFileName; + + public IconInfo(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 9) { + throw new ProtocolException("Truncated icon meta data"); + } + + mWidth = payload.getShort() & SHORT_MASK; + mHeight = payload.getShort() & SHORT_MASK; + mLocale = Locale.forLanguageTag(Constants.getString(payload, 3, StandardCharsets.US_ASCII)); + mIconType = Constants.getPrefixedString(payload, 1, StandardCharsets.US_ASCII); + mFileName = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public Locale getLocale() { + return mLocale; + } + + public String getIconType() { + return mIconType; + } + + public String getFileName() { + return mFileName; + } + + @Override + public String toString() { + return "IconInfo{" + + "mWidth=" + mWidth + + ", mHeight=" + mHeight + + ", mLocale=" + mLocale + + ", mIconType='" + mIconType + '\'' + + ", mFileName='" + mFileName + '\'' + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/NAIRealmData.java b/wifi/java/android/net/wifi/anqp/NAIRealmData.java new file mode 100644 index 0000000..14d5aaf --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/NAIRealmData.java @@ -0,0 +1,68 @@ +package android.net.wifi.anqp; + +import android.net.wifi.anqp.eap.EAPMethod; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; +import static android.net.wifi.anqp.Constants.SHORT_MASK; +import static android.net.wifi.anqp.Constants.UTF8_INDICATOR; + +/** + * The NAI Realm Data ANQP sub-element, IEEE802.11-2012 section 8.4.4.10 figure 8-418 + */ +public class NAIRealmData { + private final List<String> mRealms; + private final List<EAPMethod> mEAPMethods; + + public NAIRealmData(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 5) { + throw new ProtocolException("Runt payload: " + payload.remaining()); + } + + int length = payload.getShort() & SHORT_MASK; + if (length > payload.remaining()) { + throw new ProtocolException("Invalid data length: " + length); + } + boolean utf8 = (payload.get() & 1) == UTF8_INDICATOR; + + String realm = Constants.getPrefixedString(payload, 1, utf8 ? + StandardCharsets.UTF_8 : + StandardCharsets.US_ASCII); + String[] realms = realm.split(";"); + mRealms = new ArrayList<String>(); + for (String realmElement : realms) { + if (realmElement.length() > 0) { + mRealms.add(realmElement); + } + } + + int methodCount = payload.get() & BYTE_MASK; + mEAPMethods = new ArrayList<EAPMethod>(methodCount); + while (methodCount > 0) { + mEAPMethods.add(new EAPMethod(payload)); + methodCount--; + } + } + + public List<String> getRealms() { + return Collections.unmodifiableList(mRealms); + } + + public List<EAPMethod> getEAPMethods() { + return Collections.unmodifiableList(mEAPMethods); + } + + @Override + public String toString() { + return "NAIRealmData{" + + "mRealms='" + mRealms + '\'' + + ", mEAPMethods=" + mEAPMethods + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/NAIRealmElement.java b/wifi/java/android/net/wifi/anqp/NAIRealmElement.java new file mode 100644 index 0000000..9083518 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/NAIRealmElement.java @@ -0,0 +1,49 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTES_IN_SHORT; +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * The NAI Realm ANQP Element, IEEE802.11-2012 section 8.4.4.10 + */ +public class NAIRealmElement extends ANQPElement { + private final List<NAIRealmData> mRealmData; + + public NAIRealmElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + if (!payload.hasRemaining()) { + mRealmData = Collections.emptyList(); + return; + } + + if (payload.remaining() < BYTES_IN_SHORT) { + throw new ProtocolException("Runt NAI Realm: " + payload.remaining()); + } + + int count = payload.getShort() & SHORT_MASK; + mRealmData = new ArrayList<NAIRealmData>(count); + while (count > 0) { + mRealmData.add(new NAIRealmData(payload)); + count--; + } + } + + public List<NAIRealmData> getRealmData() { + return Collections.unmodifiableList(mRealmData); + } + + @Override + public String toString() { + return "NAIRealmElement{" + + "mRealmData=" + mRealmData + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/NetworkAuthenticationTypeElement.java b/wifi/java/android/net/wifi/anqp/NetworkAuthenticationTypeElement.java new file mode 100644 index 0000000..d26c19d --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/NetworkAuthenticationTypeElement.java @@ -0,0 +1,75 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * The Network Authentication Type ANQP Element, IEEE802.11-2012 section 8.4.4.6 + */ +public class NetworkAuthenticationTypeElement extends ANQPElement { + + private final List<NetworkAuthentication> m_authenticationTypes; + + public enum NwkAuthTypeEnum { + TermsAndConditions, + OnLineEnrollment, + HTTPRedirection, + DNSRedirection, + Reserved + } + + public static class NetworkAuthentication { + private final NwkAuthTypeEnum m_type; + private final String m_url; + + private NetworkAuthentication(NwkAuthTypeEnum type, String url) { + m_type = type; + m_url = url; + } + + public NwkAuthTypeEnum getType() { + return m_type; + } + + public String getURL() { + return m_url; + } + + @Override + public String toString() { + return "NetworkAuthentication{" + + "m_type=" + m_type + + ", m_url='" + m_url + '\'' + + '}'; + } + } + + public NetworkAuthenticationTypeElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + + super(infoID); + + m_authenticationTypes = new ArrayList<NetworkAuthentication>(); + + while (payload.hasRemaining()) { + int typeNumber = payload.get() & BYTE_MASK; + NwkAuthTypeEnum type; + type = typeNumber >= NwkAuthTypeEnum.values().length ? + NwkAuthTypeEnum.Reserved : + NwkAuthTypeEnum.values()[typeNumber]; + + m_authenticationTypes.add(new NetworkAuthentication(type, + Constants.getPrefixedString(payload, 2, StandardCharsets.UTF_8))); + } + } + + public List<NetworkAuthentication> getAuthenticationTypes() { + return Collections.unmodifiableList(m_authenticationTypes); + } +} diff --git a/wifi/java/android/net/wifi/anqp/OSUProvider.java b/wifi/java/android/net/wifi/anqp/OSUProvider.java new file mode 100644 index 0000000..aead563 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/OSUProvider.java @@ -0,0 +1,117 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; +import static android.net.wifi.anqp.Constants.SHORT_MASK; + +/** + * An OSU Provider, as specified in + * Wi-Fi Alliance Hotspot 2.0 (Release 2) Technical Specification - Version 5.00, + * section 4.8.1 + */ +public class OSUProvider { + + public enum OSUMethod {OmaDm, SoapXml} + + private final List<I18Name> mNames; + private final String mOSUServer; + private final List<OSUMethod> mOSUMethods; + private final List<IconInfo> mIcons; + private final String mOsuNai; + private final List<I18Name> mServiceDescriptions; + + public OSUProvider(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 11) { + throw new ProtocolException("Truncated OSU provider: " + payload.remaining()); + } + + int length = payload.getShort() & SHORT_MASK; + int namesLength = payload.getShort() & SHORT_MASK; + + ByteBuffer namesBuffer = payload.duplicate(); + namesBuffer.limit(namesBuffer.position() + namesLength); + payload.position(payload.position() + namesLength); + + mNames = new ArrayList<I18Name>(); + + while (namesBuffer.hasRemaining()) { + mNames.add(new I18Name(namesBuffer)); + } + + mOSUServer = Constants.getPrefixedString(payload, 1, StandardCharsets.UTF_8); + int methodLength = payload.get() & BYTE_MASK; + mOSUMethods = new ArrayList<OSUMethod>(methodLength); + while (methodLength > 0) { + int methodID = payload.get() & BYTE_MASK; + mOSUMethods.add(methodID < OSUMethod.values().length ? + OSUMethod.values()[methodID] : + null); + methodLength--; + } + + int iconsLength = payload.getShort() & SHORT_MASK; + ByteBuffer iconsBuffer = payload.duplicate(); + iconsBuffer.limit(iconsBuffer.position() + iconsLength); + payload.position(payload.position() + iconsLength); + + mIcons = new ArrayList<IconInfo>(); + + while (iconsBuffer.hasRemaining()) { + mIcons.add(new IconInfo(iconsBuffer)); + } + + mOsuNai = Constants.getString(payload, 1, StandardCharsets.UTF_8, true); + + int descriptionsLength = payload.getShort() & SHORT_MASK; + ByteBuffer descriptionsBuffer = payload.duplicate(); + descriptionsBuffer.limit(descriptionsBuffer.position() + descriptionsLength); + payload.position(payload.position() + descriptionsLength); + + mServiceDescriptions = new ArrayList<I18Name>(); + + while (descriptionsBuffer.hasRemaining()) { + mServiceDescriptions.add(new I18Name(descriptionsBuffer)); + } + } + + public List<I18Name> getNames() { + return mNames; + } + + public String getOSUServer() { + return mOSUServer; + } + + public List<OSUMethod> getOSUMethods() { + return mOSUMethods; + } + + public List<IconInfo> getIcons() { + return mIcons; + } + + public String getOsuNai() { + return mOsuNai; + } + + public List<I18Name> getServiceDescriptions() { + return mServiceDescriptions; + } + + @Override + public String toString() { + return "OSUProvider{" + + "mNames=" + mNames + + ", mOSUServer='" + mOSUServer + '\'' + + ", mOSUMethods=" + mOSUMethods + + ", mIcons=" + mIcons + + ", mOsuNai='" + mOsuNai + '\'' + + ", mServiceDescriptions=" + mServiceDescriptions + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/RoamingConsortiumElement.java b/wifi/java/android/net/wifi/anqp/RoamingConsortiumElement.java new file mode 100644 index 0000000..ff8727e --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/RoamingConsortiumElement.java @@ -0,0 +1,44 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; +import static android.net.wifi.anqp.Constants.getInteger; + +/** + * The Roaming Consortium ANQP Element, IEEE802.11-2012 section 8.4.4.7 + */ +public class RoamingConsortiumElement extends ANQPElement { + + private final List<Long> mOis; + + public RoamingConsortiumElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + mOis = new ArrayList<Long>(); + + while (payload.hasRemaining()) { + int length = payload.get() & BYTE_MASK; + if (length > payload.remaining()) { + throw new ProtocolException("Bad OI length: " + length); + } + mOis.add(getInteger(payload, length)); + } + } + + public List<Long> getOIs() { + return Collections.unmodifiableList(mOis); + } + + @Override + public String toString() { + return "RoamingConsortiumElement{" + + "mOis=" + mOis + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/ThreeGPPNetworkElement.java b/wifi/java/android/net/wifi/anqp/ThreeGPPNetworkElement.java new file mode 100644 index 0000000..57dbe77 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/ThreeGPPNetworkElement.java @@ -0,0 +1,28 @@ +package android.net.wifi.anqp; + +import java.nio.ByteBuffer; + +/** + * The 3GPP Cellular Network ANQP Element, IEEE802.11-2012 section 8.4.4.11 + */ +public class ThreeGPPNetworkElement extends ANQPElement { + + private final byte[] mData; + + public ThreeGPPNetworkElement(Constants.ANQPElementType infoID, ByteBuffer payload) { + super(infoID); + mData = new byte[payload.remaining()]; + payload.get(mData); + } + + public byte[] getData() { + return mData; + } + + @Override + public String toString() { + return "ThreeGPPNetworkElement{" + + "mData=" + Constants.toHexString(mData) + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/VenueNameElement.java b/wifi/java/android/net/wifi/anqp/VenueNameElement.java new file mode 100644 index 0000000..bb71524 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/VenueNameElement.java @@ -0,0 +1,194 @@ +package android.net.wifi.anqp; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * The Venue Name ANQP Element, IEEE802.11-2012 section 8.4.4.4 + */ +public class VenueNameElement extends ANQPElement { + private final VenueGroup mGroup; + private final VenueType mType; + private final List<I18Name> mNames; + + private static final Map<VenueGroup, Integer> s_groupBases = + new EnumMap<VenueGroup, Integer>(VenueGroup.class); + + public VenueNameElement(Constants.ANQPElementType infoID, ByteBuffer payload) + throws ProtocolException { + super(infoID); + + if (payload.remaining() < 2) + throw new ProtocolException("Runt Venue Name"); + + int group = payload.get() & BYTE_MASK; + int type = payload.get() & BYTE_MASK; + + if (group >= VenueGroup.values().length) { + mGroup = VenueGroup.Reserved; + mType = VenueType.Reserved; + } else { + mGroup = VenueGroup.values()[group]; + type += s_groupBases.get(mGroup); + if (type >= VenueType.values().length) { + mType = VenueType.Reserved; + } else { + mType = VenueType.values()[type]; + } + } + + mNames = new ArrayList<I18Name>(); + while (payload.hasRemaining()) { + mNames.add(new I18Name(payload)); + } + } + + public VenueGroup getGroup() { + return mGroup; + } + + public VenueType getType() { + return mType; + } + + public List<I18Name> getNames() { + return Collections.unmodifiableList(mNames); + } + + @Override + public String toString() { + return "VenueNameElement{" + + "m_group=" + mGroup + + ", m_type=" + mType + + ", m_names=" + mNames + + '}'; + } + + public enum VenueGroup { + Unspecified, + Assembly, + Business, + Educational, + FactoryIndustrial, + Institutional, + Mercantile, + Residential, + Storage, + UtilityMiscellaneous, + Vehicular, + Outdoor, + Reserved + } + + public enum VenueType { + Unspecified, + + UnspecifiedAssembly, + Arena, + Stadium, + PassengerTerminal, + Amphitheater, + AmusementPark, + PlaceOfWorship, + ConventionCenter, + Library, + Museum, + Restaurant, + Theater, + Bar, + CoffeeShop, + ZooOrAquarium, + EmergencyCoordinationCenter, + + UnspecifiedBusiness, + DoctorDentistoffice, + Bank, + FireStation, + PoliceStation, + PostOffice, + ProfessionalOffice, + ResearchDevelopmentFacility, + AttorneyOffice, + + UnspecifiedEducational, + SchoolPrimary, + SchoolSecondary, + UniversityCollege, + + UnspecifiedFactoryIndustrial, + Factory, + + UnspecifiedInstitutional, + Hospital, + LongTermCareFacility, + AlcoholAndDrugRehabilitationCenter, + GroupHome, + PrisonJail, + + UnspecifiedMercantile, + RetailStore, + GroceryMarket, + AutomotiveServiceStation, + ShoppingMall, + GasStation, + + UnspecifiedResidential, + PrivateResidence, + HotelMotel, + Dormitory, + BoardingHouse, + + UnspecifiedStorage, + + UnspecifiedUtilityMiscellaneous, + + AutomobileOrTruck, + Airplane, + Bus, + Ferry, + ShipOrBoat, + Train, + MotorBike, + + UnspecifiedOutdoor, + MuniMeshNetwork, + CityPark, + RestArea, + TrafficControl, + BusStop, + Kiosk, + + Reserved + } + + private static final VenueType[] PerGroup = + { + VenueType.Unspecified, + VenueType.UnspecifiedAssembly, + VenueType.UnspecifiedBusiness, + VenueType.UnspecifiedEducational, + VenueType.UnspecifiedFactoryIndustrial, + VenueType.UnspecifiedInstitutional, + VenueType.UnspecifiedMercantile, + VenueType.UnspecifiedResidential, + VenueType.UnspecifiedStorage, + VenueType.UnspecifiedUtilityMiscellaneous, + VenueType.AutomobileOrTruck, + VenueType.UnspecifiedOutdoor, + null + }; + + static { + int index = 0; + for (VenueType venue : PerGroup) { + s_groupBases.put(VenueGroup.values()[index++], venue.ordinal()); + } + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/AuthParam.java b/wifi/java/android/net/wifi/anqp/eap/AuthParam.java new file mode 100644 index 0000000..9c40300 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/AuthParam.java @@ -0,0 +1,9 @@ +package android.net.wifi.anqp.eap; + +/** + * An Authentication parameter, part of the NAI Realm ANQP element, specified in + * IEEE802.11-2012 section 8.4.4.10, table 8-188 + */ +public interface AuthParam { + public EAP.AuthInfoID getAuthInfoID(); +} diff --git a/wifi/java/android/net/wifi/anqp/eap/Credential.java b/wifi/java/android/net/wifi/anqp/eap/Credential.java new file mode 100644 index 0000000..df77b89 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/Credential.java @@ -0,0 +1,74 @@ +package android.net.wifi.anqp.eap; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * An EAP authentication parameter, IEEE802.11-2012, table 8-188 + */ +public class Credential implements AuthParam { + + public enum CredType { + Reserved, + SIM, + USIM, + NFC, + HWToken, + Softoken, + Certificate, + Username, + None, + Anonymous, + VendorSpecific} + + private final EAP.AuthInfoID mAuthInfoID; + private final CredType mCredType; + + public Credential(EAP.AuthInfoID infoID, ByteBuffer payload) throws ProtocolException { + mAuthInfoID = infoID; + + if (payload.remaining() != 1) { + throw new ProtocolException("Bad length: " + payload.remaining()); + } + + int typeID = payload.get() & BYTE_MASK; + mCredType = typeID < CredType.values().length ? + CredType.values()[typeID] : + CredType.Reserved; + } + + @Override + public EAP.AuthInfoID getAuthInfoID() { + return mAuthInfoID; + } + + @Override + public int hashCode() { + return mAuthInfoID.hashCode() * 31 + mCredType.hashCode(); + } + + @Override + public boolean equals(Object thatObject) { + if (thatObject == this) { + return true; + } else if (thatObject == null || thatObject.getClass() != AuthParam.class) { + return false; + } else { + return ((Credential) thatObject).getCredType() == getCredType(); + } + } + + public CredType getCredType() { + return mCredType; + } + + @Override + public String toString() { + return "Credential{" + + "mAuthInfoID=" + mAuthInfoID + + ", mCredType=" + mCredType + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/EAP.java b/wifi/java/android/net/wifi/anqp/eap/EAP.java new file mode 100644 index 0000000..35d28d9 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/EAP.java @@ -0,0 +1,132 @@ +package android.net.wifi.anqp.eap; + +import java.util.HashMap; +import java.util.Map; + +/** + * EAP Related constants for the ANQP NAIRealm element, IEEE802.11-2012 section 8.4.4.10 + */ +public abstract class EAP { + + private static final Map<Integer, EAPMethodID> s_eapIds = new HashMap<Integer, EAPMethodID>(); + + public static final int EAP_MD5 = 4; + public static final int EAP_OTP = 5; + public static final int EAP_RSA = 9; + public static final int EAP_KEA = 11; + public static final int EAP_KEA_VALIDATE = 12; + public static final int EAP_TLS = 13; + public static final int EAP_LEAP = 17; + public static final int EAP_SIM = 18; + public static final int EAP_TTLS = 21; + public static final int EAP_AKA = 23; + public static final int EAP_3Com = 24; + public static final int EAP_MSCHAPv2 = 26; + public static final int EAP_PEAP = 29; + public static final int EAP_POTP = 32; + public static final int EAP_ActiontecWireless = 35; + public static final int EAP_HTTPDigest = 38; + public static final int EAP_SPEKE = 41; + public static final int EAP_MOBAC = 42; + public static final int EAP_FAST = 43; + public static final int EAP_ZLXEAP = 44; + public static final int EAP_Link = 45; + public static final int EAP_PAX = 46; + public static final int EAP_PSK = 47; + public static final int EAP_SAKE = 48; + public static final int EAP_IKEv2 = 49; + public static final int EAP_AKAPrim = 50; + public static final int EAP_GPSK = 51; + public static final int EAP_PWD = 52; + public static final int EAP_EKE = 53; + public static final int EAP_TEAP = 55; + + public enum EAPMethodID { + EAP_MD5, + EAP_OTP, + EAP_RSA, + EAP_KEA, + EAP_KEA_VALIDATE, + EAP_TLS, + EAP_LEAP, + EAP_SIM, + EAP_TTLS, + EAP_AKA, + EAP_3Com, + EAP_MSCHAPv2, + EAP_PEAP, + EAP_POTP, + EAP_ActiontecWireless, + EAP_HTTPDigest, + EAP_SPEKE, + EAP_MOBAC, + EAP_FAST, + EAP_ZLXEAP, + EAP_Link, + EAP_PAX, + EAP_PSK, + EAP_SAKE, + EAP_IKEv2, + EAP_AKAPrim, + EAP_GPSK, + EAP_PWD, + EAP_EKE, + EAP_TEAP + } + + public static final int ExpandedEAPMethod = 1; + public static final int NonEAPInnerAuthType = 2; + public static final int InnerAuthEAPMethodType = 3; + public static final int ExpandedInnerEAPMethod = 4; + public static final int CredentialType = 5; + public static final int TunneledEAPMethodCredType = 6; + public static final int VendorSpecific = 221; + + public enum AuthInfoID { + Undefined, + ExpandedEAPMethod, + NonEAPInnerAuthType, + InnerAuthEAPMethodType, + ExpandedInnerEAPMethod, + CredentialType, + TunneledEAPMethodCredType, + VendorSpecific + } + + static { + s_eapIds.put(EAP_MD5, EAPMethodID.EAP_MD5); + s_eapIds.put(EAP_OTP, EAPMethodID.EAP_OTP); + s_eapIds.put(EAP_RSA, EAPMethodID.EAP_RSA); + s_eapIds.put(EAP_KEA, EAPMethodID.EAP_KEA); + s_eapIds.put(EAP_KEA_VALIDATE, EAPMethodID.EAP_KEA_VALIDATE); + s_eapIds.put(EAP_TLS, EAPMethodID.EAP_TLS); + s_eapIds.put(EAP_LEAP, EAPMethodID.EAP_LEAP); + s_eapIds.put(EAP_SIM, EAPMethodID.EAP_SIM); + s_eapIds.put(EAP_TTLS, EAPMethodID.EAP_TTLS); + s_eapIds.put(EAP_AKA, EAPMethodID.EAP_AKA); + s_eapIds.put(EAP_3Com, EAPMethodID.EAP_3Com); + s_eapIds.put(EAP_MSCHAPv2, EAPMethodID.EAP_MSCHAPv2); + s_eapIds.put(EAP_PEAP, EAPMethodID.EAP_PEAP); + s_eapIds.put(EAP_POTP, EAPMethodID.EAP_POTP); + s_eapIds.put(EAP_ActiontecWireless, EAPMethodID.EAP_ActiontecWireless); + s_eapIds.put(EAP_HTTPDigest, EAPMethodID.EAP_HTTPDigest); + s_eapIds.put(EAP_SPEKE, EAPMethodID.EAP_SPEKE); + s_eapIds.put(EAP_MOBAC, EAPMethodID.EAP_MOBAC); + s_eapIds.put(EAP_FAST, EAPMethodID.EAP_FAST); + s_eapIds.put(EAP_ZLXEAP, EAPMethodID.EAP_ZLXEAP); + s_eapIds.put(EAP_Link, EAPMethodID.EAP_Link); + s_eapIds.put(EAP_PAX, EAPMethodID.EAP_PAX); + s_eapIds.put(EAP_PSK, EAPMethodID.EAP_PSK); + s_eapIds.put(EAP_SAKE, EAPMethodID.EAP_SAKE); + s_eapIds.put(EAP_IKEv2, EAPMethodID.EAP_IKEv2); + s_eapIds.put(EAP_AKAPrim, EAPMethodID.EAP_AKAPrim); + s_eapIds.put(EAP_GPSK, EAPMethodID.EAP_GPSK); + s_eapIds.put(EAP_PWD, EAPMethodID.EAP_PWD); + s_eapIds.put(EAP_EKE, EAPMethodID.EAP_EKE); + s_eapIds.put(EAP_TEAP, EAPMethodID.EAP_TEAP); + } + + public static EAPMethodID mapEAPMethod(int methodID) { + return s_eapIds.get(methodID); + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/EAPMethod.java b/wifi/java/android/net/wifi/anqp/eap/EAPMethod.java new file mode 100644 index 0000000..968da36 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/EAPMethod.java @@ -0,0 +1,134 @@ +package android.net.wifi.anqp.eap; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * An EAP Method, part of the NAI Realm ANQP element, specified in + * IEEE802.11-2012 section 8.4.4.10, figure 8-420 + */ +public class EAPMethod { + private final EAP.EAPMethodID mEAPMethodID; + private final Map<EAP.AuthInfoID, Set<AuthParam>> mAuthParams; + + public EAPMethod(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 3) { + throw new ProtocolException("Runt EAP Method: " + payload.remaining()); + } + + int length = payload.get() & BYTE_MASK; + int methodID = payload.get() & BYTE_MASK; + int count = payload.get() & BYTE_MASK; + + mEAPMethodID = EAP.mapEAPMethod(methodID); + mAuthParams = new EnumMap<EAP.AuthInfoID, Set<AuthParam>>(EAP.AuthInfoID.class); + + int realCount = 0; + + ByteBuffer paramPayload = payload.duplicate(); + paramPayload.limit(paramPayload.position() + length); + payload.position(payload.position() + length); + while (paramPayload.hasRemaining()) { + int id = paramPayload.get() & BYTE_MASK; + + EAP.AuthInfoID authInfoID; + + if (id == EAP.VendorSpecific) { + authInfoID = EAP.AuthInfoID.VendorSpecific; + } else if (id <= EAP.AuthInfoID.TunneledEAPMethodCredType.ordinal() && id > 0) { + authInfoID = EAP.AuthInfoID.values()[id]; + } else { + throw new ProtocolException("Unknown auth parameter ID: " + id); + } + + switch (authInfoID) { + case ExpandedEAPMethod: + addAuthParam(new ExpandedEAPMethod(authInfoID, paramPayload)); + break; + case NonEAPInnerAuthType: + addAuthParam(new NonEAPInnerAuth(paramPayload)); + break; + case InnerAuthEAPMethodType: + addAuthParam(new InnerAuthEAP(paramPayload)); + break; + case ExpandedInnerEAPMethod: + addAuthParam(new ExpandedEAPMethod(authInfoID, paramPayload)); + break; + case CredentialType: + addAuthParam(new Credential(authInfoID, paramPayload)); + break; + case TunneledEAPMethodCredType: + addAuthParam(new Credential(authInfoID, paramPayload)); + break; + case VendorSpecific: + addAuthParam(new VendorSpecificAuth(paramPayload)); + break; + } + + realCount++; + } + if (realCount != count) + throw new ProtocolException("Invalid parameter count: " + realCount + + ", expected " + count); + } + + private void addAuthParam(AuthParam param) { + Set<AuthParam> authParams = mAuthParams.get(param.getAuthInfoID()); + if (authParams == null) { + authParams = new HashSet<AuthParam>(); + mAuthParams.put(param.getAuthInfoID(), authParams); + } + authParams.add(param); + } + + public Map<EAP.AuthInfoID, Set<AuthParam>> getAuthParams() { + return Collections.unmodifiableMap(mAuthParams); + } + + public EAP.EAPMethodID getEAPMethodID() { + return mEAPMethodID; + } + + public boolean matchesAuthParams(EAPMethod other) { + for (Map.Entry<EAP.AuthInfoID, Set<AuthParam>> entry : other.getAuthParams().entrySet()) { + + Set<AuthParam> myParams = mAuthParams.get(entry.getKey()); + if (myParams == null) + continue; + + Set<AuthParam> otherParams = entry.getValue(); + + Set<AuthParam> iterationSet; + Set<AuthParam> seekSet; + if (myParams.size() >= otherParams.size()) { + seekSet = myParams; + iterationSet = otherParams; + } else { + seekSet = otherParams; + iterationSet = myParams; + } + + for (AuthParam param : iterationSet) { + if (seekSet.contains(param)) { + return true; + } + } + } + return false; + } + + @Override + public String toString() { + return "EAPMethod{" + + "mEAPMethodID=" + mEAPMethodID + + ", mAuthParams=" + mAuthParams + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/ExpandedEAPMethod.java b/wifi/java/android/net/wifi/anqp/eap/ExpandedEAPMethod.java new file mode 100644 index 0000000..635d3b1 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/ExpandedEAPMethod.java @@ -0,0 +1,66 @@ +package android.net.wifi.anqp.eap; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static android.net.wifi.anqp.Constants.INT_MASK; +import static android.net.wifi.anqp.Constants.getInteger; + +/** + * An EAP authentication parameter, IEEE802.11-2012, table 8-188 + */ +public class ExpandedEAPMethod implements AuthParam { + + private final EAP.AuthInfoID m_authInfoID; + private final int m_vendorID; + private final long m_vendorType; + + public ExpandedEAPMethod(EAP.AuthInfoID authInfoID, ByteBuffer payload) throws ProtocolException { + m_authInfoID = authInfoID; + if (payload.remaining() != 7) { + throw new ProtocolException("Bad length: " + payload.remaining()); + } + + m_vendorID = (int) getInteger(payload, 3); + m_vendorType = payload.getInt() & INT_MASK; + } + + @Override + public EAP.AuthInfoID getAuthInfoID() { + return m_authInfoID; + } + + @Override + public int hashCode() { + return (m_authInfoID.hashCode() * 31 + m_vendorID) * 31 + (int) m_vendorType; + } + + @Override + public boolean equals(Object thatObject) { + if (thatObject == this) { + return true; + } else if (thatObject == null || thatObject.getClass() != ExpandedEAPMethod.class) { + return false; + } else { + ExpandedEAPMethod that = (ExpandedEAPMethod) thatObject; + return that.getVendorID() == getVendorID() && that.getVendorType() == getVendorType(); + } + } + + public int getVendorID() { + return m_vendorID; + } + + public long getVendorType() { + return m_vendorType; + } + + @Override + public String toString() { + return "ExpandedEAPMethod{" + + "m_authInfoID=" + m_authInfoID + + ", m_vendorID=" + m_vendorID + + ", m_vendorType=" + m_vendorType + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/InnerAuthEAP.java b/wifi/java/android/net/wifi/anqp/eap/InnerAuthEAP.java new file mode 100644 index 0000000..06ce0f3 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/InnerAuthEAP.java @@ -0,0 +1,55 @@ +package android.net.wifi.anqp.eap; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * An EAP authentication parameter, IEEE802.11-2012, table 8-188 + */ +public class InnerAuthEAP implements AuthParam { + + private final EAP.EAPMethodID mEapMethodID; + + public InnerAuthEAP(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() != 1) { + throw new ProtocolException("Bad length: " + payload.remaining()); + } + + int typeID = payload.get() & BYTE_MASK; + mEapMethodID = EAP.mapEAPMethod(typeID); + } + + @Override + public EAP.AuthInfoID getAuthInfoID() { + return EAP.AuthInfoID.InnerAuthEAPMethodType; + } + + public EAP.EAPMethodID getEAPMethodID() { + return mEapMethodID; + } + + @Override + public int hashCode() { + return mEapMethodID.hashCode(); + } + + @Override + public boolean equals(Object thatObject) { + if (thatObject == this) { + return true; + } else if (thatObject == null || thatObject.getClass() != InnerAuthEAP.class) { + return false; + } else { + return ((InnerAuthEAP) thatObject).getEAPMethodID() == getEAPMethodID(); + } + } + + @Override + public String toString() { + return "InnerAuthEAP{" + + "EapMethodID=" + mEapMethodID + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/NonEAPInnerAuth.java b/wifi/java/android/net/wifi/anqp/eap/NonEAPInnerAuth.java new file mode 100644 index 0000000..188472c --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/NonEAPInnerAuth.java @@ -0,0 +1,59 @@ +package android.net.wifi.anqp.eap; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * An EAP authentication parameter, IEEE802.11-2012, table 8-188 + */ +public class NonEAPInnerAuth implements AuthParam { + + public enum NonEAPType {Reserved, PAP, CHAP, MSCHAP, MSCHAPv2} + + private final NonEAPType mType; + + public NonEAPInnerAuth(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() != 1) { + throw new ProtocolException("Bad length: " + payload.remaining()); + } + + int typeID = payload.get() & BYTE_MASK; + mType = typeID < NonEAPType.values().length ? + NonEAPType.values()[typeID] : + NonEAPType.Reserved; + } + + @Override + public EAP.AuthInfoID getAuthInfoID() { + return EAP.AuthInfoID.NonEAPInnerAuthType; + } + + public NonEAPType getType() { + return mType; + } + + @Override + public int hashCode() { + return mType.hashCode(); + } + + @Override + public boolean equals(Object thatObject) { + if (thatObject == this) { + return true; + } else if (thatObject == null || thatObject.getClass() != NonEAPInnerAuth.class) { + return false; + } else { + return ((NonEAPInnerAuth) thatObject).getType() == getType(); + } + } + + @Override + public String toString() { + return "NonEAPInnerAuth{" + + "mType=" + mType + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/anqp/eap/VendorSpecificAuth.java b/wifi/java/android/net/wifi/anqp/eap/VendorSpecificAuth.java new file mode 100644 index 0000000..eaefd91 --- /dev/null +++ b/wifi/java/android/net/wifi/anqp/eap/VendorSpecificAuth.java @@ -0,0 +1,61 @@ +package android.net.wifi.anqp.eap; + +import android.net.wifi.anqp.Constants; + +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static android.net.wifi.anqp.Constants.BYTE_MASK; + +/** + * An EAP authentication parameter, IEEE802.11-2012, table 8-188 + */ +public class VendorSpecificAuth implements AuthParam { + + private final byte[] mData; + + public VendorSpecificAuth(ByteBuffer payload) throws ProtocolException { + if (payload.remaining() < 1 || payload.remaining() > 256) { + throw new ProtocolException("Bad length: " + payload.remaining()); + } + + int length = payload.get() & BYTE_MASK; + if (length > payload.remaining()) { + throw new ProtocolException("Excessive length: " + length); + } + mData = new byte[length]; + payload.get(mData); + } + + @Override + public EAP.AuthInfoID getAuthInfoID() { + return EAP.AuthInfoID.VendorSpecific; + } + + public int hashCode() { + return Arrays.hashCode(mData); + } + + @Override + public boolean equals(Object thatObject) { + if (thatObject == this) { + return true; + } else if (thatObject == null || thatObject.getClass() != VendorSpecificAuth.class) { + return false; + } else { + return Arrays.equals(((VendorSpecificAuth) thatObject).getData(), getData()); + } + } + + public byte[] getData() { + return mData; + } + + @Override + public String toString() { + return "VendorSpecificAuth{" + + "mData=" + Constants.toHexString(mData) + + '}'; + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/ANQPData.java b/wifi/java/android/net/wifi/hotspot2/ANQPData.java new file mode 100644 index 0000000..626fdb1 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/ANQPData.java @@ -0,0 +1,26 @@ +package android.net.wifi.hotspot2; + +import android.net.wifi.anqp.ANQPElement; + +import java.util.Collections; +import java.util.List; + +/** + * Created by jannq on 1/21/15. + */ +public class ANQPData { + private final List<ANQPElement> mANQPElements; + private final long mCtime; + private volatile long mAtime; + + public ANQPData( List<ANQPElement> ANQPElements ) { + mANQPElements = Collections.unmodifiableList( ANQPElements ); + mCtime = System.currentTimeMillis(); + mAtime = mCtime; + } + + public List<ANQPElement> getANQPElements() { + mAtime = System.currentTimeMillis(); + return mANQPElements; + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/NetworkInfo.java b/wifi/java/android/net/wifi/hotspot2/NetworkInfo.java new file mode 100644 index 0000000..add5fde --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/NetworkInfo.java @@ -0,0 +1,141 @@ +package android.net.wifi.hotspot2; + +import android.net.wifi.anqp.VenueNameElement; + +/** + * Created by jannq on 1/20/15. + */ +public class NetworkInfo { + + public enum Ant { + Private, + PrivateWithGuest, + ChargeablePublic, + FreePublic, + Personal, + EmergencyOnly, + TestOrExperimental, + Wildcard + } + + public enum HSRelease { + R1, + R2, + Unknown + } + + // General identifiers: + private final String mSSID; + private final String mHESSID; + private final long mBSSID; + + // BSS Load element: + private final int mStationCount; + private final int mChannelUtilization; + private final int mCapacity; + + /* + * 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: + * 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 long[] mRoamingConsortiums; + + public NetworkInfo(String SSID, + String HESSID, + long BSSID, + int stationCount, + int channelUtilization, + int capacity, + Ant ant, + boolean internet, + VenueNameElement.VenueGroup venueGroup, + VenueNameElement.VenueType venueType, + HSRelease HSRelease, + int anqpDomainID, + long[] roamingConsortiums) { + mSSID = SSID; + mHESSID = HESSID; + mBSSID = BSSID; + mStationCount = stationCount; + mChannelUtilization = channelUtilization; + mCapacity = capacity; + mAnt = ant; + mInternet = internet; + mVenueGroup = venueGroup; + mVenueType = venueType; + mHSRelease = HSRelease; + mAnqpDomainID = anqpDomainID; + mRoamingConsortiums = roamingConsortiums; + } + + public String getSSID() { + return mSSID; + } + + public String getHESSID() { + return mHESSID; + } + + public long getBSSID() { + return mBSSID; + } + + public int getStationCount() { + return mStationCount; + } + + public int getChannelUtilization() { + return mChannelUtilization; + } + + public int getCapacity() { + return mCapacity; + } + + 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 long[] getRoamingConsortiums() { + return mRoamingConsortiums; + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/NetworkKey.java b/wifi/java/android/net/wifi/hotspot2/NetworkKey.java new file mode 100644 index 0000000..0631a35 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/NetworkKey.java @@ -0,0 +1,50 @@ +package android.net.wifi.hotspot2; + +/** + * Created by jannq on 1/20/15. + */ +public class NetworkKey { + private final String mSSID; + private final long mBSSID; + private final int mANQPDomainID; + + public NetworkKey(String SSID, long BSSID, int ANQPDomainID) { + mSSID = SSID; + mBSSID = BSSID; + mANQPDomainID = ANQPDomainID; + } + + public String getSSID() { + return mSSID; + } + + public long getBSSID() { + return mBSSID; + } + + public int getANQPDomainID() { + return mANQPDomainID; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NetworkKey that = (NetworkKey) o; + + if (mANQPDomainID != that.mANQPDomainID) return false; + if (mBSSID != that.mBSSID) return false; + if (!mSSID.equals(that.mSSID)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = mSSID.hashCode(); + result = 31 * result + (int) (mBSSID ^ (mBSSID >>> 32)); + result = 31 * result + mANQPDomainID; + return result; + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/PasspointMatch.java b/wifi/java/android/net/wifi/hotspot2/PasspointMatch.java new file mode 100644 index 0000000..0f430e5 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/PasspointMatch.java @@ -0,0 +1,12 @@ +package android.net.wifi.hotspot2; + +/** + * Created by jannq on 1/21/15. + */ +public enum PasspointMatch { + HomeProvider, + RoamingProvider, + Incomplete, + None, + Declined +} diff --git a/wifi/java/android/net/wifi/hotspot2/SelectionManager.java b/wifi/java/android/net/wifi/hotspot2/SelectionManager.java new file mode 100644 index 0000000..8f2de36 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/SelectionManager.java @@ -0,0 +1,122 @@ +package android.net.wifi.hotspot2; + +import android.net.wifi.anqp.ANQPElement; +import android.net.wifi.hotspot2.pps.HomeSP; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Created by jannq on 1/20/15. + */ +public class SelectionManager { + private final List<HomeSP> mHomeSPs; + private final Map<NetworkKey,ANQPData> mANQPCache; + private final Map<NetworkKey,NetworkInfo> mPendingANQP; + private final List<ScoredNetwork> mScoredNetworks; + + private static class ScoredNetwork implements Comparable<ScoredNetwork> { + private final PasspointMatch mMatch; + private final NetworkInfo mNetworkInfo; + + private ScoredNetwork(PasspointMatch match, NetworkInfo networkInfo) { + mMatch = match; + mNetworkInfo = networkInfo; + // !!! Further score on BSS Load, ANT, "Internet" and HSRelease + } + + public PasspointMatch getMatch() { + return mMatch; + } + + public NetworkInfo getNetworkInfo() { + return mNetworkInfo; + } + + @Override + public int compareTo( ScoredNetwork other ) { + if ( getMatch() == other.getMatch() ) { + return 0; + } + else { + return getMatch().ordinal() > other.getMatch().ordinal() ? 1 : -1; + } + } + } + + public SelectionManager( List<HomeSP> homeSPs ) { + mHomeSPs = homeSPs; + mANQPCache = new HashMap<NetworkKey,ANQPData>(); + mPendingANQP = new HashMap<NetworkKey, NetworkInfo>(); + mScoredNetworks = new ArrayList<ScoredNetwork>(); + } + + public NetworkInfo findNetwork( NetworkInfo networkInfo ) { + + NetworkKey networkKey = new NetworkKey( networkInfo.getSSID(), networkInfo.getBSSID(), networkInfo.getAnqpDomainID() ); + ANQPData anqpData = mANQPCache.get( networkKey ); + List<ANQPElement> anqpElements = anqpData != null ? anqpData.getANQPElements() : null; + for ( HomeSP homeSP : mHomeSPs ) { + PasspointMatch match = homeSP.match( networkInfo, anqpElements ); + if ( match == PasspointMatch.HomeProvider || match == PasspointMatch.RoamingProvider ) { + mScoredNetworks.add( new ScoredNetwork( match, networkInfo ) ); + } + else if ( match == PasspointMatch.Incomplete && networkInfo.getAnt() != null ) { + mPendingANQP.put(networkKey, networkInfo); + } + } + + // !!! Should really return a score-sorted list. + Collections.sort( mScoredNetworks ); + if ( ! mScoredNetworks.isEmpty() && + mScoredNetworks.get( 0 ).getMatch() == PasspointMatch.HomeProvider ) { + return mScoredNetworks.get( 0 ).getNetworkInfo(); + } + else { + return null; + } + } + + public void notifyANQPResponse( NetworkInfo networkInfo, List<ANQPElement> anqpElements ) { + NetworkKey networkKey = new NetworkKey( networkInfo.getSSID(), networkInfo.getBSSID(), networkInfo.getAnqpDomainID() ); + mPendingANQP.remove( networkKey ); + mANQPCache.put( networkKey, new ANQPData( anqpElements ) ); + + for ( HomeSP homeSP : mHomeSPs ) { + PasspointMatch match = homeSP.match( networkInfo, anqpElements ); + if ( match == PasspointMatch.HomeProvider || match == PasspointMatch.RoamingProvider ) { + mScoredNetworks.add( new ScoredNetwork( match, networkInfo ) ); + } + else if ( match == PasspointMatch.Declined ) { + Iterator<ScoredNetwork> scoredNetworkIterator = mScoredNetworks.iterator(); + while ( scoredNetworkIterator.hasNext() ) { + ScoredNetwork scoredNetwork = scoredNetworkIterator.next(); + if ( scoredNetwork.getNetworkInfo().getBSSID() == networkInfo.getBSSID() && + scoredNetwork.getNetworkInfo().getSSID().equals( networkInfo.getSSID() ) ) { + scoredNetworkIterator.remove(); + break; + } + } + } + } + Collections.sort( mScoredNetworks ); + if ( ! mScoredNetworks.isEmpty() && + mScoredNetworks.get( 0 ).getMatch() == PasspointMatch.HomeProvider ) { + // Kill mPendingANQP? + // Connect to mScoredNetworks.get( 0 ).getNetworkInfo()? + } + } + + private void sendANQPQuery( NetworkInfo network ) { + if ( network.getHSRelease() != null ) { + // Query for 802.11u + potential HS2.0 elements + } + else if ( network.getAnt() != null ) { + // !!! Query for 802.11u + } + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/pps/DomainMatcher.java b/wifi/java/android/net/wifi/hotspot2/pps/DomainMatcher.java new file mode 100644 index 0000000..7e906d7 --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/pps/DomainMatcher.java @@ -0,0 +1,70 @@ +package android.net.wifi.hotspot2.pps; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Created by jannq on 1/21/15. + */ +public class DomainMatcher { + + public enum Match { None, Primary, Secondary } + + private final Label mRoot; + + private static class Label { + private final Map<String,Label> mSubDomains; + private final Match mMatch; + + private Label( Match match ) { + mMatch = match; + mSubDomains = match == Match.None ? null : new HashMap<String,Label>(); + } + + private void addDomain( Iterator<String> labels, Match match ) { + String labelName = labels.next(); + if ( labels.hasNext() ) { + Label subLabel = new Label( Match.None ); + mSubDomains.put( labelName, subLabel ); + subLabel.addDomain( labels, match ); + } + else { + mSubDomains.put( labelName, new Label( match ) ); + } + } + + private Label getSubLabel( String labelString ) { + return mSubDomains.get( labelString ); + } + + public Match getMatch() { + return mMatch; + } + } + + public DomainMatcher( List<String> primary, List<List<String>> secondary ) { + mRoot = new Label( Match.None ); + for ( List<String> secondaryLabel : secondary ) { + mRoot.addDomain( secondaryLabel.iterator(), Match.Secondary ); + } + // Primary overwrites secondary. + mRoot.addDomain( primary.iterator(), Match.Primary ); + } + + public Match isSubDomain( List<String> domain ) { + + Label label = mRoot; + for ( String labelString : domain ) { + label = label.getSubLabel( labelString ); + if ( label == null ) { + return Match.None; + } + else if ( label.getMatch() != Match.None ) { + return label.getMatch(); + } + } + return Match.None; // Domain is a super domain + } +} diff --git a/wifi/java/android/net/wifi/hotspot2/pps/HomeSP.java b/wifi/java/android/net/wifi/hotspot2/pps/HomeSP.java new file mode 100644 index 0000000..de9423c --- /dev/null +++ b/wifi/java/android/net/wifi/hotspot2/pps/HomeSP.java @@ -0,0 +1,214 @@ +package android.net.wifi.hotspot2.pps; + +import android.net.wifi.anqp.ANQPElement; +import android.net.wifi.anqp.DomainNameElement; +import android.net.wifi.anqp.HSConnectionCapabilityElement; +import android.net.wifi.anqp.HSWanMetricsElement; +import android.net.wifi.anqp.IPAddressTypeAvailabilityElement; +import android.net.wifi.anqp.NAIRealmData; +import android.net.wifi.anqp.NAIRealmElement; +import android.net.wifi.anqp.RoamingConsortiumElement; +import android.net.wifi.anqp.ThreeGPPNetworkElement; +import android.net.wifi.anqp.eap.EAP; +import android.net.wifi.anqp.eap.EAPMethod; +import android.net.wifi.hotspot2.NetworkInfo; +import android.net.wifi.hotspot2.PasspointMatch; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static android.net.wifi.anqp.Constants.ANQPElementType; + +/** + * Created by jannq on 1/20/15. + */ +public class HomeSP { + private final Map<String, String> mSSIDs; // SSID, HESSID, [0,N] + private final DomainMatcher mDomainMatcher; + private final Set<Long> mRoamingConsortiums; // [0,N] + private final Set<Long> mMatchAnyOIs; // [0,N] + private final List<Long> mMatchAllOIs; // [0,N] + + private final Map<EAP.EAPMethodID, EAPMethod> mCredentials; + + // Informational: + private final String mFriendlyName; // [1] + private final String mIconURL; // [0,1] + + public HomeSP(Map<String, String> ssidMap, + /*@NotNull*/ String fqdn, + /*@NotNull*/ Set<Long> roamingConsortiums, + /*@NotNull*/ Set<String> otherHomePartners, + /*@NotNull*/ Set<Long> matchAnyOIs, + /*@NotNull*/ List<Long> matchAllOIs, + String friendlyName, + String iconURL, + Map<EAP.EAPMethodID, EAPMethod> credentials) { + + mSSIDs = ssidMap; + List<List<String>> otherPartners = new ArrayList<List<String>>(otherHomePartners.size()); + for (String otherPartner : otherHomePartners) { + otherPartners.add(splitDomain(otherPartner)); + } + mDomainMatcher = new DomainMatcher(splitDomain(fqdn), otherPartners); + mRoamingConsortiums = roamingConsortiums; + mMatchAnyOIs = matchAnyOIs; + mMatchAllOIs = matchAllOIs; + mFriendlyName = friendlyName; + mIconURL = iconURL; + mCredentials = credentials; + } + + public PasspointMatch match(NetworkInfo networkInfo, List<ANQPElement> anqpElements) { + + if (mSSIDs.containsKey(networkInfo.getSSID())) { + String hessid = mSSIDs.get(networkInfo.getSSID()); + if (hessid == null || networkInfo.getHESSID().equals(hessid)) { + return PasspointMatch.HomeProvider; + } + } + + List<Long> allOIs = null; + + if (networkInfo.getRoamingConsortiums() != null) { + allOIs = new ArrayList<Long>(); + for (long oi : networkInfo.getRoamingConsortiums()) { + allOIs.add(oi); + } + } + + Map<ANQPElementType, ANQPElement> anqpElementMap = null; + + if (anqpElements != null) { + anqpElementMap = new EnumMap<ANQPElementType, ANQPElement>(ANQPElementType.class); + for (ANQPElement element : anqpElements) { + anqpElementMap.put(element.getID(), element); + if (element.getID() == ANQPElementType.ANQPRoamingConsortium) { + RoamingConsortiumElement rcElement = (RoamingConsortiumElement) element; + if (!rcElement.getOIs().isEmpty()) { + if (allOIs == null) { + allOIs = new ArrayList<Long>(rcElement.getOIs()); + } else { + allOIs.addAll(rcElement.getOIs()); + } + } + } + } + } + + if (allOIs != null) { + if (!mRoamingConsortiums.isEmpty()) { + for (long oi : allOIs) { + if (mRoamingConsortiums.contains(oi)) { + return PasspointMatch.HomeProvider; + } + } + } + if (!mMatchAnyOIs.isEmpty() || !mMatchAllOIs.isEmpty()) { + for (long anOI : allOIs) { + + boolean oneMatchesAll = true; + + for (long spOI : mMatchAllOIs) { + if (spOI != anOI) { + oneMatchesAll = false; + break; + } + } + + if (oneMatchesAll) { + return PasspointMatch.HomeProvider; + } + + if (mMatchAnyOIs.contains(anOI)) { + return PasspointMatch.HomeProvider; + } + } + } + } + + if (anqpElementMap == null) { + return PasspointMatch.Incomplete; + } + + DomainNameElement domainNameElement = + (DomainNameElement) anqpElementMap.get(ANQPElementType.ANQPDomName); + NAIRealmElement naiRealmElement = + (NAIRealmElement) anqpElementMap.get(ANQPElementType.ANQPNAIRealm); + ThreeGPPNetworkElement threeGPPNetworkElement = + (ThreeGPPNetworkElement) anqpElementMap.get(ANQPElementType.ANQP3GPPNetwork); + + // For future policy decisions: + IPAddressTypeAvailabilityElement ipAddressAvailabilityElement = + (IPAddressTypeAvailabilityElement) anqpElementMap.get( + ANQPElementType.ANQPIPAddrAvailability); + HSConnectionCapabilityElement hsConnCapElement = + (HSConnectionCapabilityElement) anqpElementMap.get( + ANQPElementType.HSConnCapability); + HSWanMetricsElement hsWanMetricsElement = + (HSWanMetricsElement) anqpElementMap.get(ANQPElementType.HSWANMetrics); + + if (domainNameElement != null) { + for (String domain : domainNameElement.getDomains()) { + DomainMatcher.Match match = mDomainMatcher.isSubDomain(splitDomain(domain)); + if (match != DomainMatcher.Match.None) { + return PasspointMatch.HomeProvider; + } + } + } + + /* + if ( threeGPPNetworkElement != null ) { + !!! Insert matching based on 3GPP credentials here + } + */ + + if (naiRealmElement != null) { + + for (NAIRealmData naiRealmData : naiRealmElement.getRealmData()) { + + DomainMatcher.Match match = DomainMatcher.Match.None; + for (String anRealm : naiRealmData.getRealms()) { + match = mDomainMatcher.isSubDomain(splitDomain(anRealm)); + if (match != DomainMatcher.Match.None) { + break; + } + } + if (match != DomainMatcher.Match.None) { + if (mCredentials == null) { + return PasspointMatch.RoamingProvider; + } else { + for (EAPMethod anMethod : naiRealmData.getEAPMethods()) { + EAPMethod spMethod = mCredentials.get(anMethod.getEAPMethodID()); + if (spMethod.matchesAuthParams(anMethod)) { + return PasspointMatch.RoamingProvider; + } + } + } + } + } + } + return PasspointMatch.None; + } + + private static List<String> splitDomain(String domain) { + + if (domain.endsWith(".")) + domain = domain.substring(0, domain.length() - 1); + int at = domain.indexOf('@'); + if (at >= 0) + domain = domain.substring(at + 1); + + String[] labels = domain.split("\\."); + LinkedList<String> labelList = new LinkedList<String>(); + for (String label : labels) { + labelList.addFirst(label); + } + + return labelList; + } +} |