/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net.wifi.rtt; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.location.Address; import android.location.Location; import android.net.MacAddress; import android.net.Uri; import android.net.wifi.rtt.CivicLocationKeys.CivicLocationKeysType; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.SparseArray; import android.webkit.MimeTypeMap; import java.lang.annotation.Retention; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; /** * ResponderLocation is both a Location Configuration Information (LCI) decoder and a Location Civic * Report (LCR) decoder for information received from a Wi-Fi Access Point (AP) during Wi-Fi RTT * ranging process. * *
This is based on the IEEE P802.11-REVmc/D8.0 spec section 9.4.2.22, under Measurement Report * Element. Subelement location data-fields parsed from separate input LCI and LCR Information * Elements are unified in this class.
* *Note: The information provided by this class is broadcast by a responder (usually an Access * Point), and passed on as-is. There is no guarantee this information is accurate or correct, and * as a result developers should carefully consider how this information should be used and provide * corresponding advice to users.
*/ public final class ResponderLocation implements Parcelable { private static final int BYTE_MASK = 0xFF; private static final int LSB_IN_BYTE = 0x01; private static final int MSB_IN_BYTE = 0x80; private static final int MIN_BUFFER_SIZE = 3; // length of LEAD_LCI_ELEMENT_BYTES private static final int MAX_BUFFER_SIZE = 256; // Information Element (IE) fields private static final byte MEASUREMENT_TOKEN_AUTONOMOUS = 0x01; private static final byte MEASUREMENT_REPORT_MODE = 0x00; private static final byte MEASUREMENT_TYPE_LCI = 0x08; private static final byte MEASUREMENT_TYPE_LCR = 0x0b; // LCI Subelement IDs private static final byte SUBELEMENT_LCI = 0x00; private static final byte SUBELEMENT_Z = 0x04; private static final byte SUBELEMENT_USAGE = 0x06; private static final byte SUBELEMENT_BSSID_LIST = 0x07; // LCI Subelement Lengths private static final int SUBELEMENT_LCI_LENGTH = 16; private static final int SUBELEMENT_Z_LENGTH = 6; private static final int SUBELEMENT_USAGE_LENGTH1 = 1; private static final int SUBELEMENT_USAGE_LENGTH3 = 3; private static final int SUBELEMENT_BSSID_LIST_MIN_BUFFER_LENGTH = 1; private static final byte[] LEAD_LCI_ELEMENT_BYTES = { MEASUREMENT_TOKEN_AUTONOMOUS, MEASUREMENT_REPORT_MODE, MEASUREMENT_TYPE_LCI }; // Subelement LCI constants /* The LCI subelement bit-field lengths are described in Figure 9-214 of the REVmc spec and represented here as a an array of integers */ private static final int[] SUBELEMENT_LCI_BIT_FIELD_LENGTHS = { 6, 34, 6, 34, 4, 6, 30, 3, 1, 1, 1, 2 }; private static final int LATLNG_FRACTION_BITS = 25; private static final int LATLNG_UNCERTAINTY_BASE = 8; private static final int ALTITUDE_FRACTION_BITS = 8; private static final int ALTITUDE_UNCERTAINTY_BASE = 21; private static final double LAT_ABS_LIMIT = 90.0; private static final double LNG_ABS_LIMIT = 180.0; private static final int UNCERTAINTY_UNDEFINED = 0; // Subelement LCI fields indices private static final int SUBELEMENT_LCI_LAT_UNCERTAINTY_INDEX = 0; private static final int SUBELEMENT_LCI_LAT_INDEX = 1; private static final int SUBELEMENT_LCI_LNG_UNCERTAINTY_INDEX = 2; private static final int SUBELEMENT_LCI_LNG_INDEX = 3; private static final int SUBELEMENT_LCI_ALT_TYPE_INDEX = 4; private static final int SUBELEMENT_LCI_ALT_UNCERTAINTY_INDEX = 5; private static final int SUBELEMENT_LCI_ALT_INDEX = 6; private static final int SUBELEMENT_LCI_DATUM_INDEX = 7; private static final int SUBELEMENT_LCI_REGLOC_AGREEMENT_INDEX = 8; private static final int SUBELEMENT_LCI_REGLOC_DSE_INDEX = 9; private static final int SUBELEMENT_LCI_DEPENDENT_STA_INDEX = 10; private static final int SUBELEMENT_LCI_VERSION_INDEX = 11; /** * The Altitude value is interpreted based on the Altitude Type, and the selected mDatum. * * @hide */ @Retention(SOURCE) @IntDef({ALTITUDE_UNDEFINED, ALTITUDE_METERS, ALTITUDE_FLOORS}) public @interface AltitudeType { } /** * Altitude is not defined for the Responder. * The altitude and altitude uncertainty should not be used: see section 2.4 of IETF RFC 6225. */ public static final int ALTITUDE_UNDEFINED = 0; /** Responder Altitude is measured in meters. */ public static final int ALTITUDE_METERS = 1; /** Responder Altitude is measured in floors. */ public static final int ALTITUDE_FLOORS = 2; /** * The Datum value determines how coordinates are organized in relation to the real world. * * @hide */ @Retention(SOURCE) @IntDef({DATUM_UNDEFINED, DATUM_WGS84, DATUM_NAD83_NAV88, DATUM_NAD83_MLLW}) public @interface DatumType { } /** Datum is not defined. */ public static final int DATUM_UNDEFINED = 0; /** Datum used is WGS84. */ public static final int DATUM_WGS84 = 1; /** Datum used is NAD83 NAV88. */ public static final int DATUM_NAD83_NAV88 = 2; /** Datum used is NAD83 MLLW. */ public static final int DATUM_NAD83_MLLW = 3; /** Version of the LCI protocol is 1.0, the only defined protocol at this time. */ public static final int LCI_VERSION_1 = 1; /** Provider/Source of the location */ private static final String LOCATION_PROVIDER = "WiFi Access Point"; // LCI Subelement Z constants private static final int[] SUBELEMENT_Z_BIT_FIELD_LENGTHS = {2, 14, 24, 8}; private static final int Z_FLOOR_NUMBER_FRACTION_BITS = 4; private static final int Z_FLOOR_HEIGHT_FRACTION_BITS = 12; private static final int Z_MAX_HEIGHT_UNCERTAINTY_FACTOR = 25; // LCI Subelement Z fields indices private static final int SUBELEMENT_Z_LAT_EXPECTED_TO_MOVE_INDEX = 0; private static final int SUBELEMENT_Z_FLOOR_NUMBER_INDEX = 1; private static final int SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_INDEX = 2; private static final int SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_UNCERTAINTY_INDEX = 3; // LCI Subelement Usage Rules constants private static final int SUBELEMENT_USAGE_MASK_RETRANSMIT = 0x01; private static final int SUBELEMENT_USAGE_MASK_RETENTION_EXPIRES = 0x02; private static final int SUBELEMENT_USAGE_MASK_STA_LOCATION_POLICY = 0x04; // LCI Subelement Usage Rules field indices private static final int SUBELEMENT_USAGE_PARAMS_INDEX = 0; // 8 bits // LCI Subelement BSSID List private static final int SUBELEMENT_BSSID_MAX_INDICATOR_INDEX = 0; private static final int SUBELEMENT_BSSID_LIST_INDEX = 1; private static final int BYTES_IN_A_BSSID = 6; /** * The Expected-To-Move value determines how mobile we expect the STA to be. * * @hide */ @Retention(SOURCE) @IntDef({LOCATION_FIXED, LOCATION_VARIABLE, LOCATION_MOVEMENT_UNKNOWN, LOCATION_RESERVED}) public @interface ExpectedToMoveType { } /** Location of responder is fixed (does not move) */ public static final int LOCATION_FIXED = 0; /** Location of the responder is variable, and may move */ public static final int LOCATION_VARIABLE = 1; /** Location of the responder is not known to be either fixed or variable. */ public static final int LOCATION_MOVEMENT_UNKNOWN = 2; /** Location of the responder status is a reserved value */ public static final int LOCATION_RESERVED = 3; // LCR Subelement IDs private static final byte SUBELEMENT_LOCATION_CIVIC = 0x00; private static final byte SUBELEMENT_MAP_IMAGE = 0x05; // LCR Subelement Lengths private static final int SUBELEMENT_LOCATION_CIVIC_MIN_LENGTH = 2; private static final int SUBELEMENT_LOCATION_CIVIC_MAX_LENGTH = 256; private static final int SUBELEMENT_MAP_IMAGE_URL_MAX_LENGTH = 256; private static final byte[] LEAD_LCR_ELEMENT_BYTES = { MEASUREMENT_TOKEN_AUTONOMOUS, MEASUREMENT_REPORT_MODE, MEASUREMENT_TYPE_LCR }; // LCR Location Civic Subelement private static final int CIVIC_COUNTRY_CODE_INDEX = 0; private static final int CIVIC_TLV_LIST_INDEX = 2; // LCR Map Image Subelement field indexes. private static final int SUBELEMENT_IMAGE_MAP_TYPE_INDEX = 0; private static final int MAP_TYPE_URL_DEFINED = 0; private static final String[] SUPPORTED_IMAGE_FILE_EXTENSIONS = { "", "png", "gif", "jpg", "svg", "dxf", "dwg", "dwf", "cad", "tif", "gml", "kml", "bmp", "pgm", "ppm", "xbm", "xpm", "ico" }; // General LCI and LCR state private final boolean mIsValid; /* These members are not final because we are not sure if the corresponding subelement will be present until after the parsing process. However, the default value should be set as listed. */ private boolean mIsLciValid = false; private boolean mIsZValid = false; private boolean mIsUsageValid = true; // By default this is assumed true private boolean mIsBssidListValid = false; private boolean mIsLocationCivicValid = false; private boolean mIsMapImageValid = false; // LCI Subelement LCI state private double mLatitudeUncertainty; private double mLatitude; private double mLongitudeUncertainty; private double mLongitude; private int mAltitudeType; private double mAltitudeUncertainty; private double mAltitude; private int mDatum; private boolean mLciRegisteredLocationAgreement; private boolean mLciRegisteredLocationDse; private boolean mLciDependentStation; private int mLciVersion; // LCI Subelement Z state private int mExpectedToMove; private double mFloorNumber; private double mHeightAboveFloorMeters; private double mHeightAboveFloorUncertaintyMeters; // LCI Subelement Usage Rights state private boolean mUsageRetransmit; private boolean mUsageRetentionExpires; private boolean mUsageExtraInfoOnAssociation; // LCI Subelement BSSID List state private ArrayListThis method tells us if the responder has provided its Location Configuration * Information (LCI) directly, and is useful when an external database of responder locations * is not available
* *If isLciSubelementValid() returns true, all the LCI values provided by the corresponding * getter methods will have been set as described by the responder, or else if false, they * should not be used and will throw an IllegalStateException.
*/ public boolean isLciSubelementValid() { return mIsLciValid; } /** * @return the latitude uncertainty in degrees. ** Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. *
*An unknown uncertainty is indicated by 0.
*/ public double getLatitudeUncertainty() { if (!mIsLciValid) { throw new IllegalStateException( "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false."); } return mLatitudeUncertainty; } /** * @return the latitude in degrees ** Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ public double getLatitude() { if (!mIsLciValid) { throw new IllegalStateException( "getLatitude(): invoked on an invalid result: mIsLciValid = false."); } return mLatitude; } /** * @return the Longitude uncertainty in degrees. *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. *
*An unknown uncertainty is indicated by 0.
*/ public double getLongitudeUncertainty() { if (!mIsLciValid) { throw new IllegalStateException( "getLongitudeUncertainty(): invoked on an invalid result: mIsLciValid = false."); } return mLongitudeUncertainty; } /** * @return the Longitude in degrees.. ** Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ public double getLongitude() { if (!mIsLciValid) { throw new IllegalStateException( "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false."); } return mLongitude; } /** * @return the Altitude type. *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ @AltitudeType public int getAltitudeType() { if (!mIsLciValid) { throw new IllegalStateException( "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false."); } return mAltitudeType; } /** * @return the Altitude uncertainty in meters. *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. *
*An unknown uncertainty is indicated by 0.
*/ public double getAltitudeUncertainty() { if (!mIsLciValid) { throw new IllegalStateException( "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false."); } return mAltitudeUncertainty; } /** * @return the Altitude in units defined by the altitude type. ** Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ public double getAltitude() { if (!mIsLciValid) { throw new IllegalStateException( "getAltitude(): invoked on an invalid result: mIsLciValid = false."); } return mAltitude; } /** * @return the Datum used for the LCI positioning information. *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ @DatumType public int getDatum() { if (!mIsLciValid) { throw new IllegalStateException( "getDatum(): invoked on an invalid result: mIsLciValid = false."); } return mDatum; } /** * @return true if the station is operating within a national policy area or an international * agreement area near a national border, otherwise false * (see 802.11REVmc Section 11.12.3 - Registered STA Operation). *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ public boolean getRegisteredLocationAgreementIndication() { if (!mIsLciValid) { throw new IllegalStateException( "getRegisteredLocationAgreementIndication(): " + "invoked on an invalid result: mIsLciValid = false."); } return mLciRegisteredLocationAgreement; } /** * @return true indicating this is an enabling station, enabling the operation of nearby STAs * with Dynamic Station Enablement (DSE), otherwise false. * (see 802.11REVmc Section 11.12.3 - Registered STA Operation). *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. * * @hide */ public boolean getRegisteredLocationDseIndication() { if (!mIsLciValid) { throw new IllegalStateException( "getRegisteredLocationDseIndication(): " + "invoked on an invalid result: mIsLciValid = false."); } return mLciRegisteredLocationDse; } /** * @return true indicating this is a dependent station that is operating with the enablement of * an enabling station whose LCI is being reported, otherwise false. * (see 802.11REVmc Section 11.12.3 - Registered STA Operation). *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. * * @hide */ public boolean getDependentStationIndication() { if (!mIsLciValid) { throw new IllegalStateException( "getDependentStationIndication(): " + "invoked on an invalid result: mIsLciValid = false."); } return mLciDependentStation; } /** * @return a value greater or equal to 1, indicating the current version number * of the LCI protocol. *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ public int getLciVersion() { if (!mIsLciValid) { throw new IllegalStateException( "getLciVersion(): " + "invoked on an invalid result: mIsLciValid = false."); } return mLciVersion; } /** * @return the LCI location represented as a {@link Location} object (best effort). *
* Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception. */ @NonNull public Location toLocation() { if (!mIsLciValid) { throw new IllegalStateException( "toLocation(): " + "invoked on an invalid result: mIsLciValid = false."); } Location location = new Location(LOCATION_PROVIDER); location.setLatitude(mLatitude); location.setLongitude(mLongitude); location.setAccuracy((float) (mLatitudeUncertainty + mLongitudeUncertainty) / 2); location.setAltitude(mAltitude); location.setVerticalAccuracyMeters((float) mAltitudeUncertainty); location.setTime(System.currentTimeMillis()); return location; } /** * @return if the Z subelement (containing mobility, Floor, Height above floor) is valid. */ public boolean isZaxisSubelementValid() { return mIsZValid; } /** * @return an integer representing the mobility of the responder. *
* Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception. */ @ExpectedToMoveType public int getExpectedToMove() { if (!mIsZValid) { throw new IllegalStateException( "getExpectedToMove(): invoked on an invalid result: mIsZValid = false."); } return mExpectedToMove; } /** * @return the Z sub element Floor Number. *
* Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception. *
*Note: this number can be positive or negative, with value increments of +/- 1/16 of a * floor.
. */ public double getFloorNumber() { if (!mIsZValid) { throw new IllegalStateException( "getFloorNumber(): invoked on an invalid result: mIsZValid = false)"); } return mFloorNumber; } /** * @return the Z subelement Height above the floor in meters. ** Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception. *
*This value can be positive or negative.
*/ public double getHeightAboveFloorMeters() { if (!mIsZValid) { throw new IllegalStateException( "getHeightAboveFloorMeters(): invoked on an invalid result: mIsZValid = false)"); } return mHeightAboveFloorMeters; } /** * @return the Z subelement Height above the floor uncertainty in meters. ** Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception. *
*An unknown uncertainty is indicated by 0.
*/ public double getHeightAboveFloorUncertaintyMeters() { if (!mIsZValid) { throw new IllegalStateException( "getHeightAboveFloorUncertaintyMeters():" + "invoked on an invalid result: mIsZValid = false)"); } return mHeightAboveFloorUncertaintyMeters; } /** * @return true if the location information received from the responder can be * retransmitted to another device, physically separate from the one that received it. * * @hide */ public boolean getRetransmitPolicyIndication() { return mUsageRetransmit; } /** * @return true if location-data received should expire (and be deleted) * by the time provided in the getRelativeExpirationTimeHours() method. * * @hide */ public boolean getRetentionExpiresIndication() { return mUsageRetentionExpires; } /** * @return true if there is extra location info available on association. * * @hide */ @SystemApi public boolean getExtraInfoOnAssociationIndication() { return mUsageExtraInfoOnAssociation; } /** * @return the Immutable list of colocated BSSIDs at the responder. * * Will return an empty list when there are no bssids listed.
*/
public List Will return a {@code null} when there is no Civic Location defined.
*/
@Nullable
public Address toCivicLocationAddress() {
if (mCivicLocation != null && mCivicLocation.isValid()) {
return mCivicLocation.toAddress();
} else {
return null;
}
}
/**
* @return the civic location represented as a {@link SparseArray}
*
* Valid keys to access the SparseArray can be found in {@code CivicLocationKeys}.
* Will return a {@code null} when there is no Civic Location defined.
*
*/
@Nullable
@SuppressLint("ChangedType")
public SparseArray Will return a {@code null} when there is no country code defined.
*
* @hide
*/
@Nullable
public String getCivicLocationCountryCode() {
return mCivicLocationCountryCode;
}
/**
* @return the value of the Civic Location String associated with a key.
*
* Will return a {@code null} when there is no value associated with the key provided.
*
* @param key used to find a corresponding value in the Civic Location Tuple list
*
* @hide
*/
@Nullable
public String getCivicLocationElementValue(@CivicLocationKeysType int key) {
return mCivicLocation.getCivicElementValue(key);
}
/**
* @return the Map Image file Mime type, referred to by getMapImageUrl().
*/
@Nullable
public String getMapImageMimeType() {
if (mMapImageUri == null) {
return null;
} else {
return imageTypeToMime(mMapImageType, mMapImageUri.toString());
}
}
/**
* @return a URI referencing a map-file showing the local floor plan.
*
* Will return a {@code null} when there is no URI defined.
*/
@Nullable
public Uri getMapImageUri() {
return mMapImageUri;
}
}