summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorRoshan Pius <rpius@google.com>2018-11-06 17:43:02 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2018-11-06 17:43:02 +0000
commitd2a20c1a3975668bc5aeab53a8f0daae6662366a (patch)
tree37b5ec26895032b06815f6b78c2abde4c8a5eea6 /service
parent894960003ef7ccb181dee54da59fe787f471e406 (diff)
parent3ff9e5cb3fe2fab47365bdf3ffb75139c099b3da (diff)
Merge changes from topics "network_request_match_callback", "wifi_network_specifier"
* changes: WifiNetworkFactory: Implement network matching using network specifier ScanResultMatchInfo: Refactor network type retrieval WifiServiceImpl: Network request match callback registration WifiNetworkFactory: Always allow requests from signature app WifiNetworkFactory: Trigger periodic scans WifiConfigurationUtil: Validation for network specifier WifiNetworkFactory: Implement |acceptRequest|
Diffstat (limited to 'service')
-rw-r--r--service/java/com/android/server/wifi/ClientModeImpl.java26
-rw-r--r--service/java/com/android/server/wifi/ScanResultMatchInfo.java69
-rw-r--r--service/java/com/android/server/wifi/WifiConfigurationUtil.java195
-rw-r--r--service/java/com/android/server/wifi/WifiInjector.java6
-rw-r--r--service/java/com/android/server/wifi/WifiNetworkFactory.java489
-rw-r--r--service/java/com/android/server/wifi/WifiServiceImpl.java61
6 files changed, 794 insertions, 52 deletions
diff --git a/service/java/com/android/server/wifi/ClientModeImpl.java b/service/java/com/android/server/wifi/ClientModeImpl.java
index dd852945b..b401515d5 100644
--- a/service/java/com/android/server/wifi/ClientModeImpl.java
+++ b/service/java/com/android/server/wifi/ClientModeImpl.java
@@ -38,6 +38,7 @@ import android.net.IpConfiguration;
import android.net.KeepalivePacketData;
import android.net.LinkProperties;
import android.net.MacAddress;
+import android.net.MatchAllNetworkSpecifier;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkCapabilities;
@@ -50,6 +51,7 @@ import android.net.StaticIpConfiguration;
import android.net.TrafficStats;
import android.net.dhcp.DhcpClient;
import android.net.ip.IpClient;
+import android.net.wifi.INetworkRequestMatchCallback;
import android.net.wifi.RssiPacketCountInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.SupplicantState;
@@ -793,15 +795,18 @@ public class ClientModeImpl extends StateMachine {
// TODO - needs to be a bit more dynamic
mDfltNetworkCapabilities = new NetworkCapabilities(mNetworkCapabilitiesFilter);
+ NetworkCapabilities factoryNetworkCapabilities =
+ new NetworkCapabilities(mNetworkCapabilitiesFilter);
+ factoryNetworkCapabilities.setNetworkSpecifier(new MatchAllNetworkSpecifier());
// Make the network factories.
mNetworkFactory = mWifiInjector.makeWifiNetworkFactory(
- mNetworkCapabilitiesFilter, mWifiConnectivityManager);
+ factoryNetworkCapabilities, mWifiConnectivityManager);
// We can't filter untrusted network in the capabilities filter because a trusted
// network would still satisfy a request that accepts untrusted ones.
// We need a second network factory for untrusted network requests because we need a
// different score filter for these requests.
mUntrustedNetworkFactory = mWifiInjector.makeUntrustedWifiNetworkFactory(
- mNetworkCapabilitiesFilter, mWifiConnectivityManager);
+ factoryNetworkCapabilities, mWifiConnectivityManager);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
@@ -1063,6 +1068,7 @@ public class ClientModeImpl extends StateMachine {
mWifiConfigManager.enableVerboseLogging(verbose);
mSupplicantStateTracker.enableVerboseLogging(verbose);
mPasspointManager.enableVerboseLogging(verbose);
+ mNetworkFactory.enableVerboseLogging(verbose);
}
private static final String SYSTEM_PROPERTY_LOG_CONTROL_WIFIHAL = "log.tag.WifiHAL";
@@ -5763,4 +5769,20 @@ public class ClientModeImpl extends StateMachine {
resultMsg.recycle();
return result;
}
+
+ /**
+ * Add a network request match callback to {@link WifiNetworkFactory}.
+ */
+ public void addNetworkRequestMatchCallback(IBinder binder,
+ INetworkRequestMatchCallback callback,
+ int callbackIdentifier) {
+ mNetworkFactory.addCallback(binder, callback, callbackIdentifier);
+ }
+
+ /**
+ * Remove a network request match callback from {@link WifiNetworkFactory}.
+ */
+ public void removeNetworkRequestMatchCallback(int callbackIdentifier) {
+ mNetworkFactory.removeCallback(callbackIdentifier);
+ }
}
diff --git a/service/java/com/android/server/wifi/ScanResultMatchInfo.java b/service/java/com/android/server/wifi/ScanResultMatchInfo.java
index ad29c2312..72fab6bc0 100644
--- a/service/java/com/android/server/wifi/ScanResultMatchInfo.java
+++ b/service/java/com/android/server/wifi/ScanResultMatchInfo.java
@@ -15,11 +15,15 @@
*/
package com.android.server.wifi;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import com.android.server.wifi.util.ScanResultUtil;
+import java.lang.annotation.Retention;
import java.util.Objects;
/**
@@ -31,6 +35,15 @@ public class ScanResultMatchInfo {
public static final int NETWORK_TYPE_PSK = 2;
public static final int NETWORK_TYPE_EAP = 3;
+ @Retention(SOURCE)
+ @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
+ NETWORK_TYPE_OPEN,
+ NETWORK_TYPE_WEP,
+ NETWORK_TYPE_PSK,
+ NETWORK_TYPE_EAP
+ })
+ public @interface NetworkType {}
+
/**
* SSID of the network.
*/
@@ -38,29 +51,51 @@ public class ScanResultMatchInfo {
/**
* Security Type of the network.
*/
- public int networkType;
+ public @NetworkType int networkType;
/**
- * Get the ScanResultMatchInfo for the given WifiConfiguration
+ * Fetch network type from network configuration.
*/
- public static ScanResultMatchInfo fromWifiConfiguration(WifiConfiguration config) {
- ScanResultMatchInfo info = new ScanResultMatchInfo();
- info.networkSsid = config.SSID;
+ public static @NetworkType int getNetworkType(WifiConfiguration config) {
if (WifiConfigurationUtil.isConfigForPskNetwork(config)) {
- info.networkType = NETWORK_TYPE_PSK;
+ return NETWORK_TYPE_PSK;
} else if (WifiConfigurationUtil.isConfigForEapNetwork(config)) {
- info.networkType = NETWORK_TYPE_EAP;
+ return NETWORK_TYPE_EAP;
} else if (WifiConfigurationUtil.isConfigForWepNetwork(config)) {
- info.networkType = NETWORK_TYPE_WEP;
+ return NETWORK_TYPE_WEP;
} else if (WifiConfigurationUtil.isConfigForOpenNetwork(config)) {
- info.networkType = NETWORK_TYPE_OPEN;
- } else {
- throw new IllegalArgumentException("Invalid WifiConfiguration: " + config);
+ return NETWORK_TYPE_OPEN;
}
+ throw new IllegalArgumentException("Invalid WifiConfiguration: " + config);
+ }
+
+ /**
+ * Get the ScanResultMatchInfo for the given WifiConfiguration
+ */
+ public static ScanResultMatchInfo fromWifiConfiguration(WifiConfiguration config) {
+ ScanResultMatchInfo info = new ScanResultMatchInfo();
+ info.networkSsid = config.SSID;
+ info.networkType = getNetworkType(config);
return info;
}
/**
+ * Fetch network type from scan result.
+ */
+ public static @NetworkType int getNetworkType(ScanResult scanResult) {
+ if (ScanResultUtil.isScanResultForPskNetwork(scanResult)) {
+ return NETWORK_TYPE_PSK;
+ } else if (ScanResultUtil.isScanResultForEapNetwork(scanResult)) {
+ return NETWORK_TYPE_EAP;
+ } else if (ScanResultUtil.isScanResultForWepNetwork(scanResult)) {
+ return NETWORK_TYPE_WEP;
+ } else if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
+ return NETWORK_TYPE_OPEN;
+ }
+ throw new IllegalArgumentException("Invalid ScanResult: " + scanResult);
+ }
+
+ /**
* Get the ScanResultMatchInfo for the given ScanResult
*/
public static ScanResultMatchInfo fromScanResult(ScanResult scanResult) {
@@ -70,17 +105,7 @@ public class ScanResultMatchInfo {
// However, according to our public documentation ths {@link WifiConfiguration#SSID} can
// either have a hex string or quoted ASCII string SSID.
info.networkSsid = ScanResultUtil.createQuotedSSID(scanResult.SSID);
- if (ScanResultUtil.isScanResultForPskNetwork(scanResult)) {
- info.networkType = NETWORK_TYPE_PSK;
- } else if (ScanResultUtil.isScanResultForEapNetwork(scanResult)) {
- info.networkType = NETWORK_TYPE_EAP;
- } else if (ScanResultUtil.isScanResultForWepNetwork(scanResult)) {
- info.networkType = NETWORK_TYPE_WEP;
- } else if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
- info.networkType = NETWORK_TYPE_OPEN;
- } else {
- throw new IllegalArgumentException("Invalid ScanResult: " + scanResult);
- }
+ info.networkType = getNetworkType(scanResult);
return info;
}
diff --git a/service/java/com/android/server/wifi/WifiConfigurationUtil.java b/service/java/com/android/server/wifi/WifiConfigurationUtil.java
index 0258834ea..9bf6e327d 100644
--- a/service/java/com/android/server/wifi/WifiConfigurationUtil.java
+++ b/service/java/com/android/server/wifi/WifiConfigurationUtil.java
@@ -16,15 +16,21 @@
package com.android.server.wifi;
+import static com.android.server.wifi.util.NativeUtil.addEnclosingQuotes;
+
import android.content.pm.UserInfo;
import android.net.IpConfiguration;
+import android.net.MacAddress;
import android.net.StaticIpConfiguration;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiNetworkSpecifier;
import android.net.wifi.WifiScanner;
+import android.os.PatternMatcher;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.util.NativeUtil;
@@ -50,16 +56,21 @@ public class WifiConfigurationUtil {
/**
* Constants used for validating external config objects.
*/
- private static final int ENCLOSING_QUTOES_LEN = 2;
- private static final int SSID_UTF_8_MIN_LEN = 1 + ENCLOSING_QUTOES_LEN;
- private static final int SSID_UTF_8_MAX_LEN = 32 + ENCLOSING_QUTOES_LEN;
+ private static final int ENCLOSING_QUOTES_LEN = 2;
+ private static final int SSID_UTF_8_MIN_LEN = 1 + ENCLOSING_QUOTES_LEN;
+ private static final int SSID_UTF_8_MAX_LEN = 32 + ENCLOSING_QUOTES_LEN;
private static final int SSID_HEX_MIN_LEN = 2;
private static final int SSID_HEX_MAX_LEN = 64;
- private static final int PSK_ASCII_MIN_LEN = 8 + ENCLOSING_QUTOES_LEN;
- private static final int PSK_ASCII_MAX_LEN = 63 + ENCLOSING_QUTOES_LEN;
+ private static final int PSK_ASCII_MIN_LEN = 8 + ENCLOSING_QUOTES_LEN;
+ private static final int PSK_ASCII_MAX_LEN = 63 + ENCLOSING_QUOTES_LEN;
private static final int PSK_HEX_LEN = 64;
@VisibleForTesting
public static final String PASSWORD_MASK = "*";
+ private static final String MATCH_EMPTY_SSID_PATTERN_PATH = "";
+ private static final Pair<MacAddress, MacAddress> MATCH_NONE_BSSID_PATTERN =
+ new Pair(MacAddress.BROADCAST_ADDRESS, MacAddress.BROADCAST_ADDRESS);
+ private static final Pair<MacAddress, MacAddress> MATCH_ALL_BSSID_PATTERN =
+ new Pair(MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
/**
* Check whether a network configuration is visible to a user or any of its managed profiles.
@@ -319,6 +330,34 @@ public class WifiConfigurationUtil {
return true;
}
+ private static boolean validateBssid(MacAddress bssid) {
+ if (bssid == null) return true;
+ if (bssid.getAddressType() != MacAddress.TYPE_UNICAST) {
+ Log.e(TAG, "validateBssid failed: invalid bssid");
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean validateBssid(String bssid) {
+ if (bssid == null) return true;
+ if (bssid.isEmpty()) {
+ Log.e(TAG, "validateBssid failed: empty string");
+ return false;
+ }
+ MacAddress bssidMacAddress;
+ try {
+ bssidMacAddress = MacAddress.fromString(bssid);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "validateBssid failed: malformed string: " + bssid);
+ return false;
+ }
+ if (!validateBssid(bssidMacAddress)) {
+ return false;
+ }
+ return true;
+ }
+
private static boolean validatePsk(String psk, boolean isAdd) {
if (isAdd) {
if (psk == null) {
@@ -461,13 +500,14 @@ public class WifiConfigurationUtil {
*
* This method checks for the following parameters:
* 1. {@link WifiConfiguration#SSID}
- * 2. {@link WifiConfiguration#preSharedKey}
- * 3. {@link WifiConfiguration#allowedKeyManagement}
- * 4. {@link WifiConfiguration#allowedProtocols}
- * 5. {@link WifiConfiguration#allowedAuthAlgorithms}
- * 6. {@link WifiConfiguration#allowedGroupCiphers}
- * 7. {@link WifiConfiguration#allowedPairwiseCiphers}
- * 8. {@link WifiConfiguration#getIpConfiguration()}
+ * 2. {@link WifiConfiguration#BSSID}
+ * 3. {@link WifiConfiguration#preSharedKey}
+ * 4. {@link WifiConfiguration#allowedKeyManagement}
+ * 5. {@link WifiConfiguration#allowedProtocols}
+ * 6. {@link WifiConfiguration#allowedAuthAlgorithms}
+ * 7. {@link WifiConfiguration#allowedGroupCiphers}
+ * 8. {@link WifiConfiguration#allowedPairwiseCiphers}
+ * 9. {@link WifiConfiguration#getIpConfiguration()}
*
* @param config {@link WifiConfiguration} received from an external application.
* @param isAdd {@link #VALIDATE_FOR_ADD} to indicate a network config received for an add,
@@ -480,6 +520,9 @@ public class WifiConfigurationUtil {
if (!validateSsid(config.SSID, isAdd)) {
return false;
}
+ if (!validateBssid(config.BSSID)) {
+ return false;
+ }
if (!validateBitSets(config)) {
return false;
}
@@ -497,6 +540,134 @@ public class WifiConfigurationUtil {
return true;
}
+ private static boolean validateBssidPattern(
+ Pair<MacAddress, MacAddress> bssidPatternMatcher) {
+ if (bssidPatternMatcher == null) return true;
+ MacAddress baseAddress = bssidPatternMatcher.first;
+ MacAddress mask = bssidPatternMatcher.second;
+ if (baseAddress.getAddressType() != MacAddress.TYPE_UNICAST) {
+ Log.e(TAG, "validateBssidPatternMatcher failed : invalid base address: " + baseAddress);
+ return false;
+ }
+ if (mask.equals(MacAddress.ALL_ZEROS_ADDRESS)
+ && !baseAddress.equals(MacAddress.ALL_ZEROS_ADDRESS)) {
+ Log.e(TAG, "validateBssidPatternMatcher failed : invalid mask/base: " + mask + "/"
+ + baseAddress);
+ return false;
+ }
+ // TBD: Can we do any more sanity checks?
+ return true;
+ }
+
+ // TODO(b/113878056): Some of this is duplicated in {@link WifiNetworkConfigBuilder}.
+ // Merge them somehow?.
+ private static boolean isValidNetworkSpecifier(WifiNetworkSpecifier specifier) {
+ PatternMatcher ssidPatternMatcher = specifier.ssidPatternMatcher;
+ Pair<MacAddress, MacAddress> bssidPatternMatcher = specifier.bssidPatternMatcher;
+ if (ssidPatternMatcher == null || bssidPatternMatcher == null) {
+ return false;
+ }
+ if (ssidPatternMatcher.getPath() == null || bssidPatternMatcher.first == null
+ || bssidPatternMatcher.second == null) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isMatchNoneNetworkSpecifier(WifiNetworkSpecifier specifier) {
+ PatternMatcher ssidPatternMatcher = specifier.ssidPatternMatcher;
+ Pair<MacAddress, MacAddress> bssidPatternMatcher = specifier.bssidPatternMatcher;
+ if (ssidPatternMatcher.getType() != PatternMatcher.PATTERN_PREFIX
+ && ssidPatternMatcher.getPath().equals(MATCH_EMPTY_SSID_PATTERN_PATH)) {
+ return true;
+ }
+ if (bssidPatternMatcher.equals(MATCH_NONE_BSSID_PATTERN)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isMatchAllNetworkSpecifier(WifiNetworkSpecifier specifier) {
+ PatternMatcher ssidPatternMatcher = specifier.ssidPatternMatcher;
+ Pair<MacAddress, MacAddress> bssidPatternMatcher = specifier.bssidPatternMatcher;
+ if (ssidPatternMatcher.match(MATCH_EMPTY_SSID_PATTERN_PATH)
+ && bssidPatternMatcher.equals(MATCH_ALL_BSSID_PATTERN)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Validate the configuration received from an external application inside
+ * {@link WifiNetworkSpecifier}.
+ *
+ * This method checks for the following parameters:
+ * 1. {@link WifiNetworkSpecifier#ssidPatternMatcher}
+ * 2. {@link WifiNetworkSpecifier#bssidPatternMatcher}
+ * 3. {@link WifiConfiguration#SSID}
+ * 4. {@link WifiConfiguration#BSSID}
+ * 5. {@link WifiConfiguration#preSharedKey}
+ * 6. {@link WifiConfiguration#allowedKeyManagement}
+ * 7. {@link WifiConfiguration#allowedProtocols}
+ * 8. {@link WifiConfiguration#allowedAuthAlgorithms}
+ * 9. {@link WifiConfiguration#allowedGroupCiphers}
+ * 10. {@link WifiConfiguration#allowedPairwiseCiphers}
+ * 11. {@link WifiConfiguration#getIpConfiguration()}
+ *
+ * @param specifier Instance of {@link WifiNetworkSpecifier}.
+ * @return true if the parameters are valid, false otherwise.
+ */
+ public static boolean validateNetworkSpecifier(WifiNetworkSpecifier specifier) {
+ if (!isValidNetworkSpecifier(specifier)) {
+ Log.e(TAG, "validateNetworkSpecifier failed : invalid network specifier");
+ return false;
+ }
+ if (isMatchNoneNetworkSpecifier(specifier)) {
+ Log.e(TAG, "validateNetworkSpecifier failed : match-none specifier");
+ return false;
+ }
+ if (isMatchAllNetworkSpecifier(specifier)) {
+ Log.e(TAG, "validateNetworkSpecifier failed : match-all specifier");
+ return false;
+ }
+ WifiConfiguration config = specifier.wifiConfiguration;
+ if (specifier.ssidPatternMatcher.getType() == PatternMatcher.PATTERN_LITERAL) {
+ // For literal SSID matches, the value should satisfy SSID requirements.
+ // WifiConfiguration.SSID needs quotes around ASCII SSID.
+ if (!validateSsid(addEnclosingQuotes(specifier.ssidPatternMatcher.getPath()), true)) {
+ return false;
+ }
+ } else {
+ if (config.hiddenSSID) {
+ Log.e(TAG, "validateNetworkSpecifier failed : ssid pattern not supported "
+ + "for hidden networks");
+ return false;
+ }
+ }
+ if (Objects.equals(specifier.bssidPatternMatcher.second, MacAddress.BROADCAST_ADDRESS)) {
+ // For literal BSSID matches, the value should satisfy MAC address requirements.
+ if (!validateBssid(specifier.bssidPatternMatcher.first)) {
+ return false;
+ }
+ } else {
+ if (!validateBssidPattern(specifier.bssidPatternMatcher)) {
+ return false;
+ }
+ }
+ if (!validateBitSets(config)) {
+ return false;
+ }
+ if (!validateKeyMgmt(config.allowedKeyManagement)) {
+ return false;
+ }
+ if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)
+ && !validatePsk(config.preSharedKey, true)) {
+ return false;
+ }
+ // TBD: Validate some enterprise params as well in the future here.
+ return true;
+ }
+
/**
* Check if the provided two networks are the same.
* Note: This does not check if network selection BSSID's are the same.
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java
index 53b669e4c..e708e2bde 100644
--- a/service/java/com/android/server/wifi/WifiInjector.java
+++ b/service/java/com/android/server/wifi/WifiInjector.java
@@ -18,6 +18,7 @@ package com.android.server.wifi;
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.app.AlarmManager;
import android.app.AppOpsManager;
import android.content.Context;
import android.hardware.SystemSensorManager;
@@ -548,7 +549,10 @@ public class WifiInjector {
public WifiNetworkFactory makeWifiNetworkFactory(
NetworkCapabilities nc, WifiConnectivityManager wifiConnectivityManager) {
return new WifiNetworkFactory(
- mWifiCoreHandlerThread.getLooper(), mContext, nc, wifiConnectivityManager);
+ mWifiCoreHandlerThread.getLooper(), mContext, nc,
+ (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE),
+ (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE),
+ mClock, this, wifiConnectivityManager, mWifiPermissionsUtil);
}
/**
diff --git a/service/java/com/android/server/wifi/WifiNetworkFactory.java b/service/java/com/android/server/wifi/WifiNetworkFactory.java
index 489601d0d..9da64d231 100644
--- a/service/java/com/android/server/wifi/WifiNetworkFactory.java
+++ b/service/java/com/android/server/wifi/WifiNetworkFactory.java
@@ -16,63 +16,524 @@
package com.android.server.wifi;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.server.wifi.util.NativeUtil.addEnclosingQuotes;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
import android.content.Context;
+import android.net.MacAddress;
import android.net.NetworkCapabilities;
import android.net.NetworkFactory;
import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.wifi.INetworkRequestMatchCallback;
+import android.net.wifi.INetworkRequestUserSelectionCallback;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiNetworkSpecifier;
+import android.net.wifi.WifiScanner;
+import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
+import android.os.RemoteException;
+import android.os.WorkSource;
import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wifi.util.ExternalCallbackTracker;
+import com.android.server.wifi.util.WifiPermissionsUtil;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
/**
* Network factory to handle trusted wifi network requests.
*/
public class WifiNetworkFactory extends NetworkFactory {
private static final String TAG = "WifiNetworkFactory";
+ @VisibleForTesting
private static final int SCORE_FILTER = 60;
+ @VisibleForTesting
+ public static final int PERIODIC_SCAN_INTERVAL_MS = 10 * 1000; // 10 seconds
+ private final Context mContext;
+ private final ActivityManager mActivityManager;
+ private final AlarmManager mAlarmManager;
+ private final Clock mClock;
+ private final Handler mHandler;
+ private final WifiInjector mWifiInjector;
private final WifiConnectivityManager mWifiConnectivityManager;
- private int mConnectionReqCount = 0;
+ private final WifiPermissionsUtil mWifiPermissionsUtil;
+ private final WifiScanner.ScanSettings mScanSettings;
+ private final NetworkFactoryScanListener mScanListener;
+ private final NetworkFactoryAlarmListener mPeriodicScanTimerListener;
+ private final ExternalCallbackTracker<INetworkRequestMatchCallback> mRegisteredCallbacks;
+ private WifiScanner mWifiScanner;
+
+ private int mGenericConnectionReqCount = 0;
+ private NetworkRequest mActiveSpecificNetworkRequest;
+ private WifiNetworkSpecifier mActiveSpecificNetworkRequestSpecifier;
+ private List<WifiConfiguration> mActiveMatchedNetworks;
+ // Verbose logging flag.
+ private boolean mVerboseLoggingEnabled = false;
+ private boolean mPeriodicScanTimerSet = false;
+
+ // Scan listener for scan requests.
+ private class NetworkFactoryScanListener implements WifiScanner.ScanListener {
+ @Override
+ public void onSuccess() {
+ // Scan request succeeded, wait for results to report to external clients.
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Scan request succeeded");
+ }
+ }
+
+ @Override
+ public void onFailure(int reason, String description) {
+ Log.e(TAG, "Scan failure received. reason: " + reason
+ + ", description: " + description);
+ // TODO(b/113878056): Retry scan to workaround any transient scan failures.
+ scheduleNextPeriodicScan();
+ }
+
+ @Override
+ public void onResults(WifiScanner.ScanData[] scanDatas) {
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Scan results received");
+ }
+ // For single scans, the array size should always be 1.
+ if (scanDatas.length != 1) {
+ Log.wtf(TAG, "Found more than 1 batch of scan results, Ignoring...");
+ return;
+ }
+ WifiScanner.ScanData scanData = scanDatas[0];
+ ScanResult[] scanResults = scanData.getResults();
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Received " + scanResults.length + " scan results");
+ }
+ List<WifiConfiguration> matchedNetworks =
+ getNetworksMatchingActiveNetworkRequest(scanResults);
+ sendNetworkRequestMatchCallbacksForActiveRequest(matchedNetworks);
+ mActiveMatchedNetworks = matchedNetworks;
+
+ // Didn't find a match, schedule the next scan.
+ scheduleNextPeriodicScan();
+ }
+
+ @Override
+ public void onFullResult(ScanResult fullScanResult) {
+ // Ignore for single scans.
+ }
- public WifiNetworkFactory(Looper l, Context c, NetworkCapabilities f,
- WifiConnectivityManager connectivityManager) {
- super(l, c, TAG, f);
+ @Override
+ public void onPeriodChanged(int periodInMs) {
+ // Ignore for single scans.
+ }
+ };
+
+ private class NetworkFactoryAlarmListener implements AlarmManager.OnAlarmListener {
+ @Override
+ public void onAlarm() {
+ // Trigger the next scan.
+ startScan();
+ }
+ }
+
+ // Callback result from settings UI.
+ private class NetworkFactoryUserSelectionCallback extends
+ INetworkRequestUserSelectionCallback.Stub {
+ @Override
+ public void select(WifiConfiguration wifiConfiguration) {
+ mHandler.post(() -> {
+ if (mActiveSpecificNetworkRequest == null) {
+ Log.e(TAG, "Stale callback select received");
+ return;
+ }
+ // TODO(b/113878056): Trigger network connection to |wificonfiguration|.
+ });
+ }
+
+ @Override
+ public void reject() {
+ mHandler.post(() -> {
+ if (mActiveSpecificNetworkRequest == null) {
+ Log.e(TAG, "Stale callback reject received");
+ return;
+ }
+ // TODO(b/113878056): Clear the active network request.
+ });
+ }
+ }
+
+ public WifiNetworkFactory(Looper looper, Context context, NetworkCapabilities nc,
+ ActivityManager activityManager, AlarmManager alarmManager,
+ Clock clock, WifiInjector wifiInjector,
+ WifiConnectivityManager connectivityManager,
+ WifiPermissionsUtil wifiPermissionsUtil) {
+ super(looper, context, TAG, nc);
+ mContext = context;
+ mActivityManager = activityManager;
+ mAlarmManager = alarmManager;
+ mClock = clock;
+ mHandler = new Handler(looper);
+ mWifiInjector = wifiInjector;
mWifiConnectivityManager = connectivityManager;
+ mWifiPermissionsUtil = wifiPermissionsUtil;
+ // Create the scan settings.
+ mScanSettings = new WifiScanner.ScanSettings();
+ mScanSettings.type = WifiScanner.TYPE_HIGH_ACCURACY;
+ mScanSettings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
+ mScanSettings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
+ mScanListener = new NetworkFactoryScanListener();
+ mPeriodicScanTimerListener = new NetworkFactoryAlarmListener();
+ mRegisteredCallbacks = new ExternalCallbackTracker<INetworkRequestMatchCallback>(mHandler);
setScoreFilter(SCORE_FILTER);
}
+ /**
+ * Enable verbose logging.
+ */
+ public void enableVerboseLogging(int verbose) {
+ mVerboseLoggingEnabled = (verbose > 0);
+ }
+
+ /**
+ * Add a new callback for network request match handling.
+ */
+ public void addCallback(IBinder binder, INetworkRequestMatchCallback callback,
+ int callbackIdentifier) {
+ if (!mRegisteredCallbacks.add(binder, callback, callbackIdentifier)) {
+ Log.e(TAG, "Failed to add callback");
+ return;
+ }
+ // Register our user selection callback immediately.
+ try {
+ callback.onUserSelectionCallbackRegistration(new NetworkFactoryUserSelectionCallback());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to invoke user selection registration callback " + callback, e);
+ }
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Adding callback. Num callbacks: " + mRegisteredCallbacks.getNumCallbacks());
+ }
+ }
+
+ /**
+ * Remove an existing callback for network request match handling.
+ */
+ public void removeCallback(int callbackIdentifier) {
+ mRegisteredCallbacks.remove(callbackIdentifier);
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Removing callback. Num callbacks: "
+ + mRegisteredCallbacks.getNumCallbacks());
+ }
+ }
+
+ /**
+ * Check whether to accept the new network connection request.
+ *
+ * All the validation of the incoming request is done in this method.
+ */
+ @Override
+ public boolean acceptRequest(NetworkRequest networkRequest, int score) {
+ NetworkSpecifier ns = networkRequest.networkCapabilities.getNetworkSpecifier();
+ // Generic wifi request. Always accept.
+ if (ns == null) {
+ // Generic wifi request. Always accept.
+ } else {
+ // Invalid network specifier.
+ if (!(ns instanceof WifiNetworkSpecifier)) {
+ Log.e(TAG, "Invalid network specifier mentioned. Rejecting");
+ return false;
+ }
+
+ WifiNetworkSpecifier wns = (WifiNetworkSpecifier) ns;
+ if (!WifiConfigurationUtil.validateNetworkSpecifier(wns)) {
+ Log.e(TAG, "Invalid network specifier."
+ + " Rejecting request from " + wns.requestorUid);
+ return false;
+ }
+ // Only allow specific wifi network request from foreground app/service.
+ if (!mWifiPermissionsUtil.checkNetworkSettingsPermission(wns.requestorUid)
+ && !isRequestFromForegroundAppOrService(wns.requestorUid)) {
+ Log.e(TAG, "Request not from foreground app or service."
+ + " Rejecting request from " + wns.requestorUid);
+ return false;
+ }
+ // If there is a pending request, only proceed if the new request is from a foreground
+ // app.
+ if (mActiveSpecificNetworkRequest != null
+ && !mWifiPermissionsUtil.checkNetworkSettingsPermission(wns.requestorUid)
+ && !isRequestFromForegroundApp(wns.requestorUid)) {
+ WifiNetworkSpecifier aWns =
+ (WifiNetworkSpecifier) mActiveSpecificNetworkRequest
+ .networkCapabilities
+ .getNetworkSpecifier();
+ if (isRequestFromForegroundApp(aWns.requestorUid)) {
+ Log.e(TAG, "Already processing active request from a foreground app "
+ + aWns.requestorUid + ". Rejecting request from " + wns.requestorUid);
+ return false;
+ }
+ }
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Accepted network request with specifier from fg "
+ + (isRequestFromForegroundApp(wns.requestorUid) ? "app" : "service"));
+ }
+ }
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Accepted network request " + networkRequest);
+ }
+ return true;
+ }
+
+ /**
+ * Handle new network connection requests.
+ *
+ * The assumption here is that {@link #acceptRequest(NetworkRequest, int)} has already sanitized
+ * the incoming request.
+ */
@Override
protected void needNetworkFor(NetworkRequest networkRequest, int score) {
- if (++mConnectionReqCount == 1) {
- mWifiConnectivityManager.setTrustedConnectionAllowed(true);
+ NetworkSpecifier ns = networkRequest.networkCapabilities.getNetworkSpecifier();
+ if (ns == null) {
+ // Generic wifi request. Turn on auto-join if necessary.
+ if (++mGenericConnectionReqCount == 1) {
+ mWifiConnectivityManager.setTrustedConnectionAllowed(true);
+ }
+ } else {
+ // Invalid network specifier.
+ if (!(ns instanceof WifiNetworkSpecifier)) {
+ Log.e(TAG, "Invalid network specifier mentioned. Rejecting");
+ return;
+ }
+ retrieveWifiScanner();
+
+ // Store the active network request.
+ mActiveSpecificNetworkRequest = new NetworkRequest(networkRequest);
+ WifiNetworkSpecifier wns = (WifiNetworkSpecifier) ns;
+ mActiveSpecificNetworkRequestSpecifier = new WifiNetworkSpecifier(
+ wns.ssidPatternMatcher, wns.bssidPatternMatcher, wns.wifiConfiguration,
+ wns.requestorUid);
+
+ // Trigger periodic scans for finding a network in the request.
+ startPeriodicScans();
+ // Disable Auto-join so that NetworkFactory can take control of the network selection.
+ // TODO(b/117979585): Defer turning off auto-join.
+ mWifiConnectivityManager.enable(false);
}
}
@Override
protected void releaseNetworkFor(NetworkRequest networkRequest) {
- if (mConnectionReqCount == 0) {
- Log.e(TAG, "No valid network request to release");
- return;
- }
- if (--mConnectionReqCount == 0) {
- mWifiConnectivityManager.setTrustedConnectionAllowed(false);
+ NetworkSpecifier ns = networkRequest.networkCapabilities.getNetworkSpecifier();
+ if (ns == null) {
+ // Generic wifi request. Turn off auto-join if necessary.
+ if (mGenericConnectionReqCount == 0) {
+ Log.e(TAG, "No valid network request to release");
+ return;
+ }
+ if (--mGenericConnectionReqCount == 0) {
+ mWifiConnectivityManager.setTrustedConnectionAllowed(false);
+ }
+ } else {
+ // Invalid network specifier.
+ if (!(ns instanceof WifiNetworkSpecifier)) {
+ Log.e(TAG, "Invalid network specifier mentioned. Ingoring");
+ return;
+ }
+ if (!mActiveSpecificNetworkRequest.equals(networkRequest)) {
+ Log.e(TAG, "Network specifier does not match the active request. Ignoring");
+ return;
+ }
+ // Release the active network request.
+ mActiveSpecificNetworkRequest = null;
+ mActiveSpecificNetworkRequestSpecifier = null;
+ // Cancel the periodic scans.
+ cancelPeriodicScans();
+ // Re-enable Auto-join (if there is a generic request pending).
+ if (mGenericConnectionReqCount > 0) {
+ mWifiConnectivityManager.enable(true);
+ }
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
super.dump(fd, pw, args);
- pw.println(TAG + ": mConnectionReqCount " + mConnectionReqCount);
+ pw.println(TAG + ": mGenericConnectionReqCount " + mGenericConnectionReqCount);
+ pw.println(TAG + ": mActiveSpecificNetworkRequest " + mActiveSpecificNetworkRequest);
}
/**
* Check if there is at-least one connection request.
*/
public boolean hasConnectionRequests() {
- return mConnectionReqCount > 0;
+ return mGenericConnectionReqCount > 0 || mActiveSpecificNetworkRequest != null;
+ }
+
+ /**
+ * Check if the request comes from foreground app/service.
+ */
+ private boolean isRequestFromForegroundAppOrService(int requestorUid) {
+ try {
+ String requestorPackageName = mContext.getPackageManager().getNameForUid(requestorUid);
+ return mActivityManager.getPackageImportance(requestorPackageName)
+ <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+ } catch (SecurityException e) {
+ Log.e(TAG, "Failed to check the app state", e);
+ return false;
+ }
+ }
+
+ /**
+ * Check if the request comes from foreground app.
+ */
+ private boolean isRequestFromForegroundApp(int requestorUid) {
+ try {
+ String requestorPackageName = mContext.getPackageManager().getNameForUid(requestorUid);
+ return mActivityManager.getPackageImportance(requestorPackageName)
+ <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+ } catch (SecurityException e) {
+ Log.e(TAG, "Failed to check the app state", e);
+ return false;
+ }
+ }
+
+ /**
+ * Helper method to populate WifiScanner handle. This is done lazily because
+ * WifiScanningService is started after WifiService.
+ */
+ private void retrieveWifiScanner() {
+ if (mWifiScanner != null) return;
+ mWifiScanner = mWifiInjector.getWifiScanner();
+ checkNotNull(mWifiScanner);
+ }
+
+ private void startPeriodicScans() {
+ if (mActiveSpecificNetworkRequestSpecifier == null) {
+ Log.e(TAG, "Periodic scan triggered when there is no active network request. "
+ + "Ignoring...");
+ return;
+ }
+ WifiNetworkSpecifier wns = mActiveSpecificNetworkRequestSpecifier;
+ WifiConfiguration wifiConfiguration = wns.wifiConfiguration;
+ if (wifiConfiguration.hiddenSSID) {
+ mScanSettings.hiddenNetworks = new WifiScanner.ScanSettings.HiddenNetwork[1];
+ // Can't search for SSID pattern in hidden networks.
+ mScanSettings.hiddenNetworks[0] =
+ new WifiScanner.ScanSettings.HiddenNetwork(
+ addEnclosingQuotes(wns.ssidPatternMatcher.getPath()));
+ }
+ startScan();
+ }
+
+ private void cancelPeriodicScans() {
+ if (mPeriodicScanTimerSet) {
+ mAlarmManager.cancel(mPeriodicScanTimerListener);
+ mPeriodicScanTimerSet = false;
+ }
+ // Clear the hidden networks field after each request.
+ mScanSettings.hiddenNetworks = null;
+ }
+
+ private void scheduleNextPeriodicScan() {
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ mClock.getElapsedSinceBootMillis() + PERIODIC_SCAN_INTERVAL_MS,
+ TAG, mPeriodicScanTimerListener, mHandler);
+ mPeriodicScanTimerSet = true;
+ }
+
+ private void startScan() {
+ if (mActiveSpecificNetworkRequestSpecifier == null) {
+ Log.e(TAG, "Scan triggered when there is no active network request. Ignoring...");
+ return;
+ }
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Starting the next scan for " + mActiveSpecificNetworkRequestSpecifier);
+ }
+ // Create a worksource using the caller's UID.
+ WorkSource workSource = new WorkSource(mActiveSpecificNetworkRequestSpecifier.requestorUid);
+ mWifiScanner.startScan(mScanSettings, mScanListener, workSource);
+ }
+
+ private boolean doesScanResultMatchWifiNetworkSpecifier(
+ WifiNetworkSpecifier wns, ScanResult scanResult) {
+ if (!wns.ssidPatternMatcher.match(scanResult.SSID)) {
+ return false;
+ }
+ MacAddress bssid = MacAddress.fromString(scanResult.BSSID);
+ MacAddress matchBaseAddress = wns.bssidPatternMatcher.first;
+ MacAddress matchMask = wns.bssidPatternMatcher.second;
+ if (!bssid.matches(matchBaseAddress, matchMask)) {
+ return false;
+ }
+ if (ScanResultMatchInfo.getNetworkType(wns.wifiConfiguration)
+ != ScanResultMatchInfo.getNetworkType(scanResult)) {
+ return false;
+ }
+ return true;
+ }
+
+ // Loops through the scan results and finds scan results matching the active network
+ // request. Returns a list of WifiConfiguration representing all the networks that
+ // match the active network request's specifier.
+ private List<WifiConfiguration> getNetworksMatchingActiveNetworkRequest(
+ ScanResult[] scanResults) {
+ // There could be multiple bssid's (i.e ScanResult) within the same ssid matching the
+ // specifier, we need to collapse all of them into a single network represented by a
+ // WifiConfiguration object.
+ // Use a map keyed in by pair of SSID & network type to collect the list of unique
+ // networks (i.e wificonfiguration objects) matching the specifier.
+ if (mActiveSpecificNetworkRequest == null) {
+ Log.e(TAG, "Scan results received with no active network request. Ignoring...");
+ return new ArrayList<>();
+ }
+ Map<Pair<String, Integer>, WifiConfiguration> matchedNetworks = new HashMap<>();
+ WifiNetworkSpecifier wns = (WifiNetworkSpecifier)
+ mActiveSpecificNetworkRequest.networkCapabilities.getNetworkSpecifier();
+ checkNotNull(wns);
+
+ for (ScanResult scanResult : scanResults) {
+ if (doesScanResultMatchWifiNetworkSpecifier(wns, scanResult)) {
+ // Use the WifiConfiguration provided in the request & fill in the SSID
+ // & BSSID fields from ScanResult.
+ WifiConfiguration matchedNetwork = new WifiConfiguration(wns.wifiConfiguration);
+ matchedNetwork.SSID = addEnclosingQuotes(scanResult.SSID);
+ matchedNetwork.BSSID = scanResult.BSSID;
+ matchedNetworks.put(
+ Pair.create(matchedNetwork.SSID,
+ ScanResultMatchInfo.getNetworkType(matchedNetwork)),
+ matchedNetwork);
+ }
+ }
+ if (mVerboseLoggingEnabled) {
+ Log.e(TAG, "List of networks matching the active request "
+ + matchedNetworks.values());
+ }
+ return new ArrayList<>(matchedNetworks.values());
+ }
+
+ private void sendNetworkRequestMatchCallbacksForActiveRequest(
+ List<WifiConfiguration> matchedNetworks) {
+ if (mRegisteredCallbacks.getNumCallbacks() == 0) {
+ Log.e(TAG, "No callback registered for sending network request matches. "
+ + "Ignoring...");
+ return;
+ }
+ for (INetworkRequestMatchCallback callback : mRegisteredCallbacks.getCallbacks()) {
+ try {
+ callback.onMatch(matchedNetworks);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to invoke network request match callback " + callback, e);
+ }
+ }
}
}
diff --git a/service/java/com/android/server/wifi/WifiServiceImpl.java b/service/java/com/android/server/wifi/WifiServiceImpl.java
index 6206d9b15..4b1766d79 100644
--- a/service/java/com/android/server/wifi/WifiServiceImpl.java
+++ b/service/java/com/android/server/wifi/WifiServiceImpl.java
@@ -58,6 +58,7 @@ import android.net.Network;
import android.net.NetworkUtils;
import android.net.Uri;
import android.net.ip.IpClient;
+import android.net.wifi.INetworkRequestMatchCallback;
import android.net.wifi.ISoftApCallback;
import android.net.wifi.ITrafficStateCallback;
import android.net.wifi.IWifiManager;
@@ -75,6 +76,7 @@ import android.os.AsyncTask;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Bundle;
+import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
@@ -2822,7 +2824,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
/**
* see {@link android.net.wifi.WifiManager#registerTrafficStateCallback(
- * TrafficStateCallback, Handler)}
+ * WifiManager.TrafficStateCallback, Handler)}
*
* @param binder IBinder instance to allow cleanup if the app dies
* @param callback Traffic State callback to register
@@ -2890,4 +2892,61 @@ public class WifiServiceImpl extends IWifiManager.Stub {
private static boolean hasAutomotiveFeature(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
+
+ /**
+ * see {@link android.net.wifi.WifiManager#registerNetworkRequestMatchCallback(
+ * WifiManager.NetworkRequestMatchCallback, Handler)} (
+ *
+ * @param binder IBinder instance to allow cleanup if the app dies
+ * @param callback Network Request Match callback to register
+ * @param callbackIdentifier Unique ID of the registering callback. This ID will be used to
+ * unregister the callback.
+ * See {@link #unregisterNetworkRequestMatchCallback(int)} (int)}
+ *
+ * @throws SecurityException if the caller does not have permission to register a callback
+ * @throws RemoteException if remote exception happens
+ * @throws IllegalArgumentException if the arguments are null or invalid
+ */
+ @Override
+ public void registerNetworkRequestMatchCallback(IBinder binder,
+ INetworkRequestMatchCallback callback,
+ int callbackIdentifier) {
+ // verify arguments
+ if (binder == null) {
+ throw new IllegalArgumentException("Binder must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("Callback must not be null");
+ }
+ enforceNetworkSettingsPermission();
+ if (mVerboseLoggingEnabled) {
+ mLog.info("registerNetworkRequestMatchCallback uid=%")
+ .c(Binder.getCallingUid()).flush();
+ }
+ // Post operation to handler thread
+ mClientHandler.post(() -> {
+ mClientModeImpl.addNetworkRequestMatchCallback(binder, callback, callbackIdentifier);
+ });
+ }
+
+ /**
+ * see {@link android.net.wifi.WifiManager#unregisterNetworkRequestMatchCallback(
+ * WifiManager.NetworkRequestMatchCallback)}
+ *
+ * @param callbackIdentifier Unique ID of the callback to be unregistered.
+ *
+ * @throws SecurityException if the caller does not have permission to register a callback
+ */
+ @Override
+ public void unregisterNetworkRequestMatchCallback(int callbackIdentifier) {
+ enforceNetworkSettingsPermission();
+ if (mVerboseLoggingEnabled) {
+ mLog.info("unregisterNetworkRequestMatchCallback uid=%")
+ .c(Binder.getCallingUid()).flush();
+ }
+ // Post operation to handler thread
+ mClientHandler.post(() -> {
+ mClientModeImpl.removeNetworkRequestMatchCallback(callbackIdentifier);
+ });
+ }
}