Add internal comments to the |flags| and |capabilities| fields of ScanResult to clarify their relationship to the fields in the per-BSS scan results from WPA supplicant. BUG: None Change-Id: I8685283f8cc1006ffd22f2cb2ee8f8605cc0fd2b TEST: N/A
647 lines
21 KiB
Java
647 lines
21 KiB
Java
/*
|
|
* Copyright (C) 2008 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;
|
|
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Describes information about a detected access point. In addition
|
|
* to the attributes described here, the supplicant keeps track of
|
|
* {@code quality}, {@code noise}, and {@code maxbitrate} attributes,
|
|
* but does not currently report them to external clients.
|
|
*/
|
|
public class ScanResult implements Parcelable {
|
|
/**
|
|
* The network name.
|
|
*/
|
|
public String SSID;
|
|
|
|
/**
|
|
* Ascii encoded SSID. This will replace SSID when we deprecate it. @hide
|
|
*/
|
|
public WifiSsid wifiSsid;
|
|
|
|
/**
|
|
* The address of the access point.
|
|
*/
|
|
public String BSSID;
|
|
|
|
/**
|
|
* The HESSID from the beacon.
|
|
* @hide
|
|
*/
|
|
public long hessid;
|
|
|
|
/**
|
|
* The ANQP Domain ID from the Hotspot 2.0 Indication element, if present.
|
|
* @hide
|
|
*/
|
|
public int anqpDomainId;
|
|
|
|
/*
|
|
* This field is equivalent to the |flags|, rather than the |capabilities| field
|
|
* of the per-BSS scan results returned by WPA supplicant. See the definition of
|
|
* |struct wpa_bss| in wpa_supplicant/bss.h for more details.
|
|
*/
|
|
/**
|
|
* Describes the authentication, key management, and encryption schemes
|
|
* supported by the access point.
|
|
*/
|
|
public String capabilities;
|
|
/**
|
|
* The detected signal level in dBm, also known as the RSSI.
|
|
*
|
|
* <p>Use {@link android.net.wifi.WifiManager#calculateSignalLevel} to convert this number into
|
|
* an absolute signal level which can be displayed to a user.
|
|
*/
|
|
public int level;
|
|
/**
|
|
* The primary 20 MHz frequency (in MHz) of the channel over which the client is communicating
|
|
* with the access point.
|
|
*/
|
|
public int frequency;
|
|
|
|
/**
|
|
* AP Channel bandwidth is 20 MHZ
|
|
*/
|
|
public static final int CHANNEL_WIDTH_20MHZ = 0;
|
|
/**
|
|
* AP Channel bandwidth is 40 MHZ
|
|
*/
|
|
public static final int CHANNEL_WIDTH_40MHZ = 1;
|
|
/**
|
|
* AP Channel bandwidth is 80 MHZ
|
|
*/
|
|
public static final int CHANNEL_WIDTH_80MHZ = 2;
|
|
/**
|
|
* AP Channel bandwidth is 160 MHZ
|
|
*/
|
|
public static final int CHANNEL_WIDTH_160MHZ = 3;
|
|
/**
|
|
* AP Channel bandwidth is 160 MHZ, but 80MHZ + 80MHZ
|
|
*/
|
|
public static final int CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4;
|
|
|
|
/**
|
|
* AP Channel bandwidth; one of {@link #CHANNEL_WIDTH_20MHZ}, {@link #CHANNEL_WIDTH_40MHZ},
|
|
* {@link #CHANNEL_WIDTH_80MHZ}, {@link #CHANNEL_WIDTH_160MHZ}
|
|
* or {@link #CHANNEL_WIDTH_80MHZ_PLUS_MHZ}.
|
|
*/
|
|
public int channelWidth;
|
|
|
|
/**
|
|
* Not used if the AP bandwidth is 20 MHz
|
|
* If the AP use 40, 80 or 160 MHz, this is the center frequency (in MHz)
|
|
* if the AP use 80 + 80 MHz, this is the center frequency of the first segment (in MHz)
|
|
*/
|
|
public int centerFreq0;
|
|
|
|
/**
|
|
* Only used if the AP bandwidth is 80 + 80 MHz
|
|
* if the AP use 80 + 80 MHz, this is the center frequency of the second segment (in MHz)
|
|
*/
|
|
public int centerFreq1;
|
|
|
|
/**
|
|
* @deprecated use is80211mcResponder() instead
|
|
* @hide
|
|
*/
|
|
public boolean is80211McRTTResponder;
|
|
|
|
/**
|
|
* timestamp in microseconds (since boot) when
|
|
* this result was last seen.
|
|
*/
|
|
public long timestamp;
|
|
|
|
/**
|
|
* Timestamp representing date when this result was last seen, in milliseconds from 1970
|
|
* {@hide}
|
|
*/
|
|
public long seen;
|
|
|
|
/**
|
|
* If the scan result is a valid autojoin candidate
|
|
* {@hide}
|
|
*/
|
|
public int isAutoJoinCandidate;
|
|
|
|
/**
|
|
* @hide
|
|
* Update RSSI of the scan result
|
|
* @param previousRssi
|
|
* @param previousSeen
|
|
* @param maxAge
|
|
*/
|
|
public void averageRssi(int previousRssi, long previousSeen, int maxAge) {
|
|
|
|
if (seen == 0) {
|
|
seen = System.currentTimeMillis();
|
|
}
|
|
long age = seen - previousSeen;
|
|
|
|
if (previousSeen > 0 && age > 0 && age < maxAge/2) {
|
|
// Average the RSSI with previously seen instances of this scan result
|
|
double alpha = 0.5 - (double) age / (double) maxAge;
|
|
level = (int) ((double) level * (1 - alpha) + (double) previousRssi * alpha);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* num IP configuration failures
|
|
* @hide
|
|
*/
|
|
public int numIpConfigFailures;
|
|
|
|
/**
|
|
* @hide
|
|
* Last time we blacklisted the ScanResult
|
|
*/
|
|
public long blackListTimestamp;
|
|
|
|
/**
|
|
* Status: indicating the scan result is not a result
|
|
* that is part of user's saved configurations
|
|
* @hide
|
|
*/
|
|
public boolean untrusted;
|
|
|
|
/**
|
|
* Number of time we connected to it
|
|
* @hide
|
|
*/
|
|
public int numConnection;
|
|
|
|
/**
|
|
* Number of time autojoin used it
|
|
* @hide
|
|
*/
|
|
public int numUsage;
|
|
|
|
/**
|
|
* The approximate distance to the AP in centimeter, if available. Else
|
|
* {@link UNSPECIFIED}.
|
|
* {@hide}
|
|
*/
|
|
public int distanceCm;
|
|
|
|
/**
|
|
* The standard deviation of the distance to the access point, if available.
|
|
* Else {@link UNSPECIFIED}.
|
|
* {@hide}
|
|
*/
|
|
public int distanceSdCm;
|
|
|
|
/** {@hide} */
|
|
public static final long FLAG_PASSPOINT_NETWORK = 0x0000000000000001;
|
|
|
|
/** {@hide} */
|
|
public static final long FLAG_80211mc_RESPONDER = 0x0000000000000002;
|
|
|
|
/*
|
|
* These flags are specific to the ScanResult class, and are not related to the |flags|
|
|
* field of the per-BSS scan results from WPA supplicant.
|
|
*/
|
|
/**
|
|
* Defines flags; such as {@link #FLAG_PASSPOINT_NETWORK}.
|
|
* {@hide}
|
|
*/
|
|
public long flags;
|
|
|
|
/**
|
|
* sets a flag in {@link #flags} field
|
|
* @param flag flag to set
|
|
* @hide
|
|
*/
|
|
public void setFlag(long flag) {
|
|
flags |= flag;
|
|
}
|
|
|
|
/**
|
|
* clears a flag in {@link #flags} field
|
|
* @param flag flag to set
|
|
* @hide
|
|
*/
|
|
public void clearFlag(long flag) {
|
|
flags &= ~flag;
|
|
}
|
|
|
|
public boolean is80211mcResponder() {
|
|
return (flags & FLAG_80211mc_RESPONDER) != 0;
|
|
}
|
|
|
|
public boolean isPasspointNetwork() {
|
|
return (flags & FLAG_PASSPOINT_NETWORK) != 0;
|
|
}
|
|
|
|
/**
|
|
* Indicates venue name (such as 'San Francisco Airport') published by access point; only
|
|
* available on passpoint network and if published by access point.
|
|
*/
|
|
public CharSequence venueName;
|
|
|
|
/**
|
|
* Indicates passpoint operator name published by access point.
|
|
*/
|
|
public CharSequence operatorFriendlyName;
|
|
|
|
/**
|
|
* {@hide}
|
|
*/
|
|
public final static int UNSPECIFIED = -1;
|
|
/**
|
|
* @hide
|
|
*/
|
|
public boolean is24GHz() {
|
|
return ScanResult.is24GHz(frequency);
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* TODO: makes real freq boundaries
|
|
*/
|
|
public static boolean is24GHz(int freq) {
|
|
return freq > 2400 && freq < 2500;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
public boolean is5GHz() {
|
|
return ScanResult.is5GHz(frequency);
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* TODO: makes real freq boundaries
|
|
*/
|
|
public static boolean is5GHz(int freq) {
|
|
return freq > 4900 && freq < 5900;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* anqp lines from supplicant BSS response
|
|
*/
|
|
public List<String> anqpLines;
|
|
|
|
/**
|
|
* @hide
|
|
* storing the raw bytes of full result IEs
|
|
**/
|
|
public byte[] bytes;
|
|
|
|
/** information elements from beacon
|
|
* @hide
|
|
*/
|
|
public static class InformationElement {
|
|
public static final int EID_SSID = 0;
|
|
public static final int EID_SUPPORTED_RATES = 1;
|
|
public static final int EID_TIM = 5;
|
|
public static final int EID_BSS_LOAD = 11;
|
|
public static final int EID_ERP = 42;
|
|
public static final int EID_RSN = 48;
|
|
public static final int EID_EXTENDED_SUPPORTED_RATES = 50;
|
|
public static final int EID_HT_OPERATION = 61;
|
|
public static final int EID_INTERWORKING = 107;
|
|
public static final int EID_ROAMING_CONSORTIUM = 111;
|
|
public static final int EID_EXTENDED_CAPS = 127;
|
|
public static final int EID_VHT_OPERATION = 192;
|
|
public static final int EID_VSA = 221;
|
|
|
|
public int id;
|
|
public byte[] bytes;
|
|
|
|
public InformationElement() {
|
|
}
|
|
|
|
public InformationElement(InformationElement rhs) {
|
|
this.id = rhs.id;
|
|
this.bytes = rhs.bytes.clone();
|
|
}
|
|
}
|
|
|
|
/** information elements found in the beacon
|
|
* @hide
|
|
*/
|
|
public InformationElement[] informationElements;
|
|
|
|
/** ANQP response elements.
|
|
* @hide
|
|
*/
|
|
public AnqpInformationElement[] anqpElements;
|
|
|
|
/** {@hide} */
|
|
public ScanResult(WifiSsid wifiSsid, String BSSID, long hessid, int anqpDomainId,
|
|
byte[] osuProviders, String caps, int level, int frequency, long tsf) {
|
|
this.wifiSsid = wifiSsid;
|
|
this.SSID = (wifiSsid != null) ? wifiSsid.toString() : WifiSsid.NONE;
|
|
this.BSSID = BSSID;
|
|
this.hessid = hessid;
|
|
this.anqpDomainId = anqpDomainId;
|
|
if (osuProviders != null) {
|
|
this.anqpElements = new AnqpInformationElement[1];
|
|
this.anqpElements[0] =
|
|
new AnqpInformationElement(AnqpInformationElement.HOTSPOT20_VENDOR_ID,
|
|
AnqpInformationElement.HS_OSU_PROVIDERS, osuProviders);
|
|
}
|
|
this.capabilities = caps;
|
|
this.level = level;
|
|
this.frequency = frequency;
|
|
this.timestamp = tsf;
|
|
this.distanceCm = UNSPECIFIED;
|
|
this.distanceSdCm = UNSPECIFIED;
|
|
this.channelWidth = UNSPECIFIED;
|
|
this.centerFreq0 = UNSPECIFIED;
|
|
this.centerFreq1 = UNSPECIFIED;
|
|
this.flags = 0;
|
|
}
|
|
|
|
/** {@hide} */
|
|
public ScanResult(WifiSsid wifiSsid, String BSSID, String caps, int level, int frequency,
|
|
long tsf, int distCm, int distSdCm) {
|
|
this.wifiSsid = wifiSsid;
|
|
this.SSID = (wifiSsid != null) ? wifiSsid.toString() : WifiSsid.NONE;
|
|
this.BSSID = BSSID;
|
|
this.capabilities = caps;
|
|
this.level = level;
|
|
this.frequency = frequency;
|
|
this.timestamp = tsf;
|
|
this.distanceCm = distCm;
|
|
this.distanceSdCm = distSdCm;
|
|
this.channelWidth = UNSPECIFIED;
|
|
this.centerFreq0 = UNSPECIFIED;
|
|
this.centerFreq1 = UNSPECIFIED;
|
|
this.flags = 0;
|
|
}
|
|
|
|
/** {@hide} */
|
|
public ScanResult(String Ssid, String BSSID, long hessid, int anqpDomainId, String caps,
|
|
int level, int frequency,
|
|
long tsf, int distCm, int distSdCm, int channelWidth, int centerFreq0, int centerFreq1,
|
|
boolean is80211McRTTResponder) {
|
|
this.SSID = Ssid;
|
|
this.BSSID = BSSID;
|
|
this.hessid = hessid;
|
|
this.anqpDomainId = anqpDomainId;
|
|
this.capabilities = caps;
|
|
this.level = level;
|
|
this.frequency = frequency;
|
|
this.timestamp = tsf;
|
|
this.distanceCm = distCm;
|
|
this.distanceSdCm = distSdCm;
|
|
this.channelWidth = channelWidth;
|
|
this.centerFreq0 = centerFreq0;
|
|
this.centerFreq1 = centerFreq1;
|
|
if (is80211McRTTResponder) {
|
|
this.flags = FLAG_80211mc_RESPONDER;
|
|
} else {
|
|
this.flags = 0;
|
|
}
|
|
}
|
|
|
|
/** {@hide} */
|
|
public ScanResult(WifiSsid wifiSsid, String Ssid, String BSSID, long hessid, int anqpDomainId,
|
|
String caps, int level,
|
|
int frequency, long tsf, int distCm, int distSdCm, int channelWidth,
|
|
int centerFreq0, int centerFreq1, boolean is80211McRTTResponder) {
|
|
this(Ssid, BSSID, hessid, anqpDomainId, caps, level, frequency, tsf, distCm,
|
|
distSdCm, channelWidth, centerFreq0, centerFreq1, is80211McRTTResponder);
|
|
this.wifiSsid = wifiSsid;
|
|
}
|
|
|
|
/** copy constructor {@hide} */
|
|
public ScanResult(ScanResult source) {
|
|
if (source != null) {
|
|
wifiSsid = source.wifiSsid;
|
|
SSID = source.SSID;
|
|
BSSID = source.BSSID;
|
|
hessid = source.hessid;
|
|
anqpDomainId = source.anqpDomainId;
|
|
informationElements = source.informationElements;
|
|
anqpElements = source.anqpElements;
|
|
capabilities = source.capabilities;
|
|
level = source.level;
|
|
frequency = source.frequency;
|
|
channelWidth = source.channelWidth;
|
|
centerFreq0 = source.centerFreq0;
|
|
centerFreq1 = source.centerFreq1;
|
|
timestamp = source.timestamp;
|
|
distanceCm = source.distanceCm;
|
|
distanceSdCm = source.distanceSdCm;
|
|
seen = source.seen;
|
|
untrusted = source.untrusted;
|
|
numConnection = source.numConnection;
|
|
numUsage = source.numUsage;
|
|
numIpConfigFailures = source.numIpConfigFailures;
|
|
isAutoJoinCandidate = source.isAutoJoinCandidate;
|
|
venueName = source.venueName;
|
|
operatorFriendlyName = source.operatorFriendlyName;
|
|
flags = source.flags;
|
|
}
|
|
}
|
|
|
|
/** empty scan result
|
|
*
|
|
* {@hide}
|
|
* */
|
|
public ScanResult() {
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
StringBuffer sb = new StringBuffer();
|
|
String none = "<none>";
|
|
|
|
sb.append("SSID: ").
|
|
append(wifiSsid == null ? WifiSsid.NONE : wifiSsid).
|
|
append(", BSSID: ").
|
|
append(BSSID == null ? none : BSSID).
|
|
append(", capabilities: ").
|
|
append(capabilities == null ? none : capabilities).
|
|
append(", level: ").
|
|
append(level).
|
|
append(", frequency: ").
|
|
append(frequency).
|
|
append(", timestamp: ").
|
|
append(timestamp);
|
|
|
|
sb.append(", distance: ").append((distanceCm != UNSPECIFIED ? distanceCm : "?")).
|
|
append("(cm)");
|
|
sb.append(", distanceSd: ").append((distanceSdCm != UNSPECIFIED ? distanceSdCm : "?")).
|
|
append("(cm)");
|
|
|
|
sb.append(", passpoint: ");
|
|
sb.append(((flags & FLAG_PASSPOINT_NETWORK) != 0) ? "yes" : "no");
|
|
sb.append(", ChannelBandwidth: ").append(channelWidth);
|
|
sb.append(", centerFreq0: ").append(centerFreq0);
|
|
sb.append(", centerFreq1: ").append(centerFreq1);
|
|
sb.append(", 80211mcResponder: ");
|
|
sb.append(((flags & FLAG_80211mc_RESPONDER) != 0) ? "is supported" : "is not supported");
|
|
return sb.toString();
|
|
}
|
|
|
|
/** Implement the Parcelable interface {@hide} */
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
/** Implement the Parcelable interface {@hide} */
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
if (wifiSsid != null) {
|
|
dest.writeInt(1);
|
|
wifiSsid.writeToParcel(dest, flags);
|
|
} else {
|
|
dest.writeInt(0);
|
|
}
|
|
dest.writeString(SSID);
|
|
dest.writeString(BSSID);
|
|
dest.writeLong(hessid);
|
|
dest.writeInt(anqpDomainId);
|
|
dest.writeString(capabilities);
|
|
dest.writeInt(level);
|
|
dest.writeInt(frequency);
|
|
dest.writeLong(timestamp);
|
|
dest.writeInt(distanceCm);
|
|
dest.writeInt(distanceSdCm);
|
|
dest.writeInt(channelWidth);
|
|
dest.writeInt(centerFreq0);
|
|
dest.writeInt(centerFreq1);
|
|
dest.writeLong(seen);
|
|
dest.writeInt(untrusted ? 1 : 0);
|
|
dest.writeInt(numConnection);
|
|
dest.writeInt(numUsage);
|
|
dest.writeInt(numIpConfigFailures);
|
|
dest.writeInt(isAutoJoinCandidate);
|
|
dest.writeString((venueName != null) ? venueName.toString() : "");
|
|
dest.writeString((operatorFriendlyName != null) ? operatorFriendlyName.toString() : "");
|
|
dest.writeLong(this.flags);
|
|
|
|
if (informationElements != null) {
|
|
dest.writeInt(informationElements.length);
|
|
for (int i = 0; i < informationElements.length; i++) {
|
|
dest.writeInt(informationElements[i].id);
|
|
dest.writeInt(informationElements[i].bytes.length);
|
|
dest.writeByteArray(informationElements[i].bytes);
|
|
}
|
|
} else {
|
|
dest.writeInt(0);
|
|
}
|
|
|
|
if (anqpLines != null) {
|
|
dest.writeInt(anqpLines.size());
|
|
for (int i = 0; i < anqpLines.size(); i++) {
|
|
dest.writeString(anqpLines.get(i));
|
|
}
|
|
}
|
|
else {
|
|
dest.writeInt(0);
|
|
}
|
|
if (anqpElements != null) {
|
|
dest.writeInt(anqpElements.length);
|
|
for (AnqpInformationElement element : anqpElements) {
|
|
dest.writeInt(element.getVendorId());
|
|
dest.writeInt(element.getElementId());
|
|
dest.writeInt(element.getPayload().length);
|
|
dest.writeByteArray(element.getPayload());
|
|
}
|
|
} else {
|
|
dest.writeInt(0);
|
|
}
|
|
}
|
|
|
|
/** Implement the Parcelable interface {@hide} */
|
|
public static final Creator<ScanResult> CREATOR =
|
|
new Creator<ScanResult>() {
|
|
public ScanResult createFromParcel(Parcel in) {
|
|
WifiSsid wifiSsid = null;
|
|
if (in.readInt() == 1) {
|
|
wifiSsid = WifiSsid.CREATOR.createFromParcel(in);
|
|
}
|
|
ScanResult sr = new ScanResult(
|
|
wifiSsid,
|
|
in.readString(), /* SSID */
|
|
in.readString(), /* BSSID */
|
|
in.readLong(), /* HESSID */
|
|
in.readInt(), /* ANQP Domain ID */
|
|
in.readString(), /* capabilities */
|
|
in.readInt(), /* level */
|
|
in.readInt(), /* frequency */
|
|
in.readLong(), /* timestamp */
|
|
in.readInt(), /* distanceCm */
|
|
in.readInt(), /* distanceSdCm */
|
|
in.readInt(), /* channelWidth */
|
|
in.readInt(), /* centerFreq0 */
|
|
in.readInt(), /* centerFreq1 */
|
|
false /* rtt responder,
|
|
fixed with flags below */
|
|
);
|
|
|
|
sr.seen = in.readLong();
|
|
sr.untrusted = in.readInt() != 0;
|
|
sr.numConnection = in.readInt();
|
|
sr.numUsage = in.readInt();
|
|
sr.numIpConfigFailures = in.readInt();
|
|
sr.isAutoJoinCandidate = in.readInt();
|
|
sr.venueName = in.readString();
|
|
sr.operatorFriendlyName = in.readString();
|
|
sr.flags = in.readLong();
|
|
int n = in.readInt();
|
|
if (n != 0) {
|
|
sr.informationElements = new InformationElement[n];
|
|
for (int i = 0; i < n; i++) {
|
|
sr.informationElements[i] = new InformationElement();
|
|
sr.informationElements[i].id = in.readInt();
|
|
int len = in.readInt();
|
|
sr.informationElements[i].bytes = new byte[len];
|
|
in.readByteArray(sr.informationElements[i].bytes);
|
|
}
|
|
}
|
|
|
|
n = in.readInt();
|
|
if (n != 0) {
|
|
sr.anqpLines = new ArrayList<String>();
|
|
for (int i = 0; i < n; i++) {
|
|
sr.anqpLines.add(in.readString());
|
|
}
|
|
}
|
|
n = in.readInt();
|
|
if (n != 0) {
|
|
sr.anqpElements = new AnqpInformationElement[n];
|
|
for (int i = 0; i < n; i++) {
|
|
int vendorId = in.readInt();
|
|
int elementId = in.readInt();
|
|
int len = in.readInt();
|
|
byte[] payload = new byte[len];
|
|
in.readByteArray(payload);
|
|
sr.anqpElements[i] =
|
|
new AnqpInformationElement(vendorId, elementId, payload);
|
|
}
|
|
}
|
|
return sr;
|
|
}
|
|
|
|
public ScanResult[] newArray(int size) {
|
|
return new ScanResult[size];
|
|
}
|
|
};
|
|
}
|