summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorNingyuan Wang <nywang@google.com>2017-06-01 22:26:33 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2017-06-01 22:26:33 +0000
commit9da08e01464d39de3f14fe09f535660635d39c42 (patch)
tree35fb83f5ce10e7b63ff0c469a3332f4ed37a3390 /service
parentaf32c06cd4eaca6492614d0c4aad69609022e030 (diff)
parent403df479e25031276c738dbea334f09bb7e4bf37 (diff)
Merge changes from topic 'supplicant_sta_iface_hal_network_change' into oc-dev
* changes: Remove network from supplicant when it is disabled Do not remove and add the same network
Diffstat (limited to 'service')
-rw-r--r--service/java/com/android/server/wifi/SupplicantStaIfaceHal.java128
-rw-r--r--service/java/com/android/server/wifi/WifiConfigManager.java55
-rw-r--r--service/java/com/android/server/wifi/WifiConfigurationUtil.java36
-rw-r--r--service/java/com/android/server/wifi/WifiConnectivityHelper.java10
-rw-r--r--service/java/com/android/server/wifi/WifiConnectivityManager.java27
-rw-r--r--service/java/com/android/server/wifi/WifiNative.java10
6 files changed, 202 insertions, 64 deletions
diff --git a/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java b/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java
index 99067430b..e9c20db32 100644
--- a/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java
+++ b/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java
@@ -26,6 +26,7 @@ import static com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType.HS
import static com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType.HSOSUProviders;
import static com.android.server.wifi.hotspot2.anqp.Constants.ANQPElementType.HSWANMetrics;
+import android.annotation.NonNull;
import android.content.Context;
import android.hardware.wifi.supplicant.V1_0.ISupplicant;
import android.hardware.wifi.supplicant.V1_0.ISupplicantIface;
@@ -49,6 +50,7 @@ import android.os.HwRemoteBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.server.wifi.hotspot2.AnqpEvent;
@@ -125,10 +127,8 @@ public class SupplicantStaIfaceHal {
};
private String mIfaceName;
- // Currently configured network in wpa_supplicant
- private SupplicantStaNetworkHal mCurrentNetwork;
- // Currently configured network's framework network Id.
- private int mFrameworkNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
+ private SupplicantStaNetworkHal mCurrentNetworkRemoteHandle;
+ private WifiConfiguration mCurrentNetworkLocalConfig;
private final Context mContext;
private final WifiMonitor mWifiMonitor;
@@ -254,6 +254,13 @@ public class SupplicantStaIfaceHal {
return true;
}
+ private int getCurrentNetworkId() {
+ if (mCurrentNetworkLocalConfig == null) {
+ return WifiConfiguration.INVALID_NETWORK_ID;
+ }
+ return mCurrentNetworkLocalConfig.networkId;
+ }
+
private boolean initSupplicantStaIface() {
synchronized (mLock) {
/** List all supplicant Ifaces */
@@ -353,9 +360,11 @@ public class SupplicantStaIfaceHal {
* Add a network configuration to wpa_supplicant.
*
* @param config Config corresponding to the network.
- * @return SupplicantStaNetwork of the added network in wpa_supplicant.
+ * @return a Pair object including SupplicantStaNetworkHal and WifiConfiguration objects
+ * for the current network.
*/
- private SupplicantStaNetworkHal addNetworkAndSaveConfig(WifiConfiguration config) {
+ private Pair<SupplicantStaNetworkHal, WifiConfiguration>
+ addNetworkAndSaveConfig(WifiConfiguration config) {
logi("addSupplicantStaNetwork via HIDL");
if (config == null) {
loge("Cannot add NULL network!");
@@ -379,39 +388,43 @@ public class SupplicantStaIfaceHal {
}
return null;
}
- return network;
+ return new Pair(network, new WifiConfiguration(config));
}
/**
* Add the provided network configuration to wpa_supplicant and initiate connection to it.
* This method does the following:
- * 1. Triggers disconnect command to wpa_supplicant (if |shouldDisconnect| is true).
- * 2. Remove any existing network in wpa_supplicant.
- * 3. Add a new network to wpa_supplicant.
- * 4. Save the provided configuration to wpa_supplicant.
- * 5. Select the new network in wpa_supplicant.
+ * 1. If |config| is different to the current supplicant network, removes all supplicant
+ * networks and saves |config|.
+ * 2. Select the new network in wpa_supplicant.
*
* @param config WifiConfiguration parameters for the provided network.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
- public boolean connectToNetwork(WifiConfiguration config) {
- mFrameworkNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
- mCurrentNetwork = null;
+ public boolean connectToNetwork(@NonNull WifiConfiguration config) {
logd("connectToNetwork " + config.configKey());
- if (!removeAllNetworks()) {
- loge("Failed to remove existing networks");
- return false;
- }
- mCurrentNetwork = addNetworkAndSaveConfig(config);
- if (mCurrentNetwork == null) {
- loge("Failed to add/save network configuration: " + config.configKey());
- return false;
+ if (WifiConfigurationUtil.isSameNetwork(config, mCurrentNetworkLocalConfig)) {
+ logd("Network is already saved, will not trigger remove and add operation.");
+ } else {
+ mCurrentNetworkRemoteHandle = null;
+ mCurrentNetworkLocalConfig = null;
+ if (!removeAllNetworks()) {
+ loge("Failed to remove existing networks");
+ return false;
+ }
+ Pair<SupplicantStaNetworkHal, WifiConfiguration> pair = addNetworkAndSaveConfig(config);
+ if (pair == null) {
+ loge("Failed to add/save network configuration: " + config.configKey());
+ return false;
+ }
+ mCurrentNetworkRemoteHandle = pair.first;
+ mCurrentNetworkLocalConfig = pair.second;
}
- if (!mCurrentNetwork.select()) {
+
+ if (!mCurrentNetworkRemoteHandle.select()) {
loge("Failed to select network configuration: " + config.configKey());
return false;
}
- mFrameworkNetworkId = config.networkId;
return true;
}
@@ -428,14 +441,14 @@ public class SupplicantStaIfaceHal {
* @return {@code true} if it succeeds, {@code false} otherwise
*/
public boolean roamToNetwork(WifiConfiguration config) {
- if (mFrameworkNetworkId != config.networkId || mCurrentNetwork == null) {
+ if (getCurrentNetworkId() != config.networkId) {
Log.w(TAG, "Cannot roam to a different network, initiate new connection. "
- + "Current network ID: " + mFrameworkNetworkId);
+ + "Current network ID: " + getCurrentNetworkId());
return connectToNetwork(config);
}
String bssid = config.getNetworkSelectionStatus().getNetworkSelectionBSSID();
logd("roamToNetwork" + config.configKey() + " (bssid " + bssid + ")");
- if (!mCurrentNetwork.setBssid(bssid)) {
+ if (!mCurrentNetworkRemoteHandle.setBssid(bssid)) {
loge("Failed to set new bssid on network: " + config.configKey());
return false;
}
@@ -498,6 +511,21 @@ public class SupplicantStaIfaceHal {
}
/**
+ * Remove the request |networkId| from supplicant if it's the current network,
+ * if the current configured network matches |networkId|.
+ *
+ * @param networkId network id of the network to be removed from supplicant.
+ */
+ public void removeNetworkIfCurrent(int networkId) {
+ synchronized (mLock) {
+ if (getCurrentNetworkId() == networkId) {
+ // Currently we only save 1 network in supplicant.
+ removeAllNetworks();
+ }
+ }
+ }
+
+ /**
* Remove all networks from supplicant
*/
public boolean removeAllNetworks() {
@@ -516,8 +544,8 @@ public class SupplicantStaIfaceHal {
}
// Reset current network info. Probably not needed once we add support to remove/reset
// current network on receiving disconnection event from supplicant (b/32898136).
- mFrameworkNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
- mCurrentNetwork = null;
+ mCurrentNetworkLocalConfig = null;
+ mCurrentNetworkRemoteHandle = null;
return true;
}
@@ -528,8 +556,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean setCurrentNetworkBssid(String bssidStr) {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.setBssid(bssidStr);
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.setBssid(bssidStr);
}
/**
@@ -538,8 +566,8 @@ public class SupplicantStaIfaceHal {
* @return Hex string corresponding to the WPS NFC token.
*/
public String getCurrentNetworkWpsNfcConfigurationToken() {
- if (mCurrentNetwork == null) return null;
- return mCurrentNetwork.getWpsNfcConfigurationToken();
+ if (mCurrentNetworkRemoteHandle == null) return null;
+ return mCurrentNetworkRemoteHandle.getWpsNfcConfigurationToken();
}
/**
@@ -548,8 +576,8 @@ public class SupplicantStaIfaceHal {
* @return anonymous identity string if succeeds, null otherwise.
*/
public String getCurrentNetworkEapAnonymousIdentity() {
- if (mCurrentNetwork == null) return null;
- return mCurrentNetwork.fetchEapAnonymousIdentity();
+ if (mCurrentNetworkRemoteHandle == null) return null;
+ return mCurrentNetworkRemoteHandle.fetchEapAnonymousIdentity();
}
/**
@@ -559,8 +587,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean sendCurrentNetworkEapIdentityResponse(String identityStr) {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.sendNetworkEapIdentityResponse(identityStr);
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.sendNetworkEapIdentityResponse(identityStr);
}
/**
@@ -570,8 +598,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean sendCurrentNetworkEapSimGsmAuthResponse(String paramsStr) {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.sendNetworkEapSimGsmAuthResponse(paramsStr);
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.sendNetworkEapSimGsmAuthResponse(paramsStr);
}
/**
@@ -580,8 +608,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean sendCurrentNetworkEapSimGsmAuthFailure() {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.sendNetworkEapSimGsmAuthFailure();
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.sendNetworkEapSimGsmAuthFailure();
}
/**
@@ -591,8 +619,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean sendCurrentNetworkEapSimUmtsAuthResponse(String paramsStr) {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.sendNetworkEapSimUmtsAuthResponse(paramsStr);
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.sendNetworkEapSimUmtsAuthResponse(paramsStr);
}
/**
@@ -602,8 +630,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean sendCurrentNetworkEapSimUmtsAutsResponse(String paramsStr) {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.sendNetworkEapSimUmtsAutsResponse(paramsStr);
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.sendNetworkEapSimUmtsAutsResponse(paramsStr);
}
/**
@@ -612,8 +640,8 @@ public class SupplicantStaIfaceHal {
* @return true if succeeds, false otherwise.
*/
public boolean sendCurrentNetworkEapSimUmtsAuthFailure() {
- if (mCurrentNetwork == null) return false;
- return mCurrentNetwork.sendNetworkEapSimUmtsAuthFailure();
+ if (mCurrentNetworkRemoteHandle == null) return false;
+ return mCurrentNetworkRemoteHandle.sendNetworkEapSimUmtsAuthFailure();
}
/**
@@ -1858,10 +1886,10 @@ public class SupplicantStaIfaceHal {
mStateIsFourway = (newState == ISupplicantStaIfaceCallback.State.FOURWAY_HANDSHAKE);
if (newSupplicantState == SupplicantState.COMPLETED) {
mWifiMonitor.broadcastNetworkConnectionEvent(
- mIfaceName, mFrameworkNetworkId, bssidStr);
+ mIfaceName, getCurrentNetworkId(), bssidStr);
}
mWifiMonitor.broadcastSupplicantStateChangeEvent(
- mIfaceName, mFrameworkNetworkId, wifiSsid, bssidStr, newSupplicantState);
+ mIfaceName, getCurrentNetworkId(), wifiSsid, bssidStr, newSupplicantState);
}
}
diff --git a/service/java/com/android/server/wifi/WifiConfigManager.java b/service/java/com/android/server/wifi/WifiConfigManager.java
index 3f31c7ff8..4b2bb1c49 100644
--- a/service/java/com/android/server/wifi/WifiConfigManager.java
+++ b/service/java/com/android/server/wifi/WifiConfigManager.java
@@ -152,10 +152,29 @@ public class WifiConfigManager {
*/
public interface OnSavedNetworkUpdateListener {
/**
- * Invoked on saved network being enabled, disabled, blacklisted or
- * un-blacklisted.
+ * Invoked on saved network being added.
*/
- void onSavedNetworkUpdate();
+ void onSavedNetworkAdded(int networkId);
+ /**
+ * Invoked on saved network being enabled.
+ */
+ void onSavedNetworkEnabled(int networkId);
+ /**
+ * Invoked on saved network being permanently disabled.
+ */
+ void onSavedNetworkPermanentlyDisabled(int networkId);
+ /**
+ * Invoked on saved network being removed.
+ */
+ void onSavedNetworkRemoved(int networkId);
+ /**
+ * Invoked on saved network being temporarily disabled.
+ */
+ void onSavedNetworkTemporarilyDisabled(int networkId);
+ /**
+ * Invoked on saved network being updated.
+ */
+ void onSavedNetworkUpdated(int networkId);
}
/**
* Max size of scan details to cache in {@link #mScanDetailCaches}.
@@ -1039,7 +1058,13 @@ public class WifiConfigManager {
// Unless the added network is ephemeral or Passpoint, persist the network update/addition.
if (!config.ephemeral && !config.isPasspoint()) {
saveToStore(true);
- if (mListener != null) mListener.onSavedNetworkUpdate();
+ if (mListener != null) {
+ if (result.isNewNetwork()) {
+ mListener.onSavedNetworkAdded(newConfig.networkId);
+ } else {
+ mListener.onSavedNetworkUpdated(newConfig.networkId);
+ }
+ }
}
return result;
}
@@ -1104,7 +1129,7 @@ public class WifiConfigManager {
// Unless the removed network is ephemeral or Passpoint, persist the network removal.
if (!config.ephemeral && !config.isPasspoint()) {
saveToStore(true);
- if (mListener != null) mListener.onSavedNetworkUpdate();
+ if (mListener != null) mListener.onSavedNetworkRemoved(networkId);
}
return true;
}
@@ -1165,7 +1190,8 @@ public class WifiConfigManager {
/**
* Helper method to mark a network enabled for network selection.
*/
- private void setNetworkSelectionEnabled(NetworkSelectionStatus status) {
+ private void setNetworkSelectionEnabled(WifiConfiguration config) {
+ NetworkSelectionStatus status = config.getNetworkSelectionStatus();
status.setNetworkSelectionStatus(
NetworkSelectionStatus.NETWORK_SELECTION_ENABLED);
status.setDisableTime(
@@ -1174,32 +1200,35 @@ public class WifiConfigManager {
// Clear out all the disable reason counters.
status.clearDisableReasonCounter();
- if (mListener != null) mListener.onSavedNetworkUpdate();
+ if (mListener != null) mListener.onSavedNetworkEnabled(config.networkId);
}
/**
* Helper method to mark a network temporarily disabled for network selection.
*/
private void setNetworkSelectionTemporarilyDisabled(
- NetworkSelectionStatus status, int disableReason) {
+ WifiConfiguration config, int disableReason) {
+ NetworkSelectionStatus status = config.getNetworkSelectionStatus();
status.setNetworkSelectionStatus(
NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED);
// Only need a valid time filled in for temporarily disabled networks.
status.setDisableTime(mClock.getElapsedSinceBootMillis());
status.setNetworkSelectionDisableReason(disableReason);
+ if (mListener != null) mListener.onSavedNetworkTemporarilyDisabled(config.networkId);
}
/**
* Helper method to mark a network permanently disabled for network selection.
*/
private void setNetworkSelectionPermanentlyDisabled(
- NetworkSelectionStatus status, int disableReason) {
+ WifiConfiguration config, int disableReason) {
+ NetworkSelectionStatus status = config.getNetworkSelectionStatus();
status.setNetworkSelectionStatus(
NetworkSelectionStatus.NETWORK_SELECTION_PERMANENTLY_DISABLED);
status.setDisableTime(
NetworkSelectionStatus.INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP);
status.setNetworkSelectionDisableReason(disableReason);
- if (mListener != null) mListener.onSavedNetworkUpdate();
+ if (mListener != null) mListener.onSavedNetworkPermanentlyDisabled(config.networkId);
}
/**
@@ -1230,12 +1259,12 @@ public class WifiConfigManager {
return false;
}
if (reason == NetworkSelectionStatus.NETWORK_SELECTION_ENABLE) {
- setNetworkSelectionEnabled(networkStatus);
+ setNetworkSelectionEnabled(config);
setNetworkStatus(config, WifiConfiguration.Status.ENABLED);
} else if (reason < NetworkSelectionStatus.DISABLED_TLS_VERSION_MISMATCH) {
- setNetworkSelectionTemporarilyDisabled(networkStatus, reason);
+ setNetworkSelectionTemporarilyDisabled(config, reason);
} else {
- setNetworkSelectionPermanentlyDisabled(networkStatus, reason);
+ setNetworkSelectionPermanentlyDisabled(config, reason);
setNetworkStatus(config, WifiConfiguration.Status.DISABLED);
}
localLog("setNetworkSelectionStatus: configKey=" + config.configKey()
diff --git a/service/java/com/android/server/wifi/WifiConfigurationUtil.java b/service/java/com/android/server/wifi/WifiConfigurationUtil.java
index 67f1faded..c726f4966 100644
--- a/service/java/com/android/server/wifi/WifiConfigurationUtil.java
+++ b/service/java/com/android/server/wifi/WifiConfigurationUtil.java
@@ -235,6 +235,41 @@ public class WifiConfigurationUtil {
}
/**
+ * Check if the provided two networks are the same.
+ *
+ * @param config Configuration corresponding to a network.
+ * @param config1 Configuration corresponding to another network.
+ *
+ * @return true if |config| and |config1| are the same network.
+ * false otherwise.
+ */
+ public static boolean isSameNetwork(WifiConfiguration config, WifiConfiguration config1) {
+ if (config == null && config1 == null) {
+ return true;
+ }
+ if (config == null || config1 == null) {
+ return false;
+ }
+ if (config.networkId != config1.networkId) {
+ return false;
+ }
+ if (!Objects.equals(config.SSID, config1.SSID)) {
+ return false;
+ }
+ String networkSelectionBSSID = config.getNetworkSelectionStatus()
+ .getNetworkSelectionBSSID();
+ String networkSelectionBSSID1 = config1.getNetworkSelectionStatus()
+ .getNetworkSelectionBSSID();
+ if (!Objects.equals(networkSelectionBSSID, networkSelectionBSSID1)) {
+ return false;
+ }
+ if (WifiConfigurationUtil.hasCredentialChanged(config, config1)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Create a PnoNetwork object from the provided WifiConfiguration.
*
* @param config Configuration corresponding to the network.
@@ -261,6 +296,7 @@ public class WifiConfigurationUtil {
return pnoNetwork;
}
+
/**
* General WifiConfiguration list sorting algorithm:
* 1, Place the fully enabled networks first.
diff --git a/service/java/com/android/server/wifi/WifiConnectivityHelper.java b/service/java/com/android/server/wifi/WifiConnectivityHelper.java
index 6016b57b5..4aac31168 100644
--- a/service/java/com/android/server/wifi/WifiConnectivityHelper.java
+++ b/service/java/com/android/server/wifi/WifiConnectivityHelper.java
@@ -161,4 +161,14 @@ public class WifiConnectivityHelper {
return mWifiNative.configureRoaming(roamConfig);
}
+
+ /**
+ * Remove the request |networkId| from supplicant if it's the current network,
+ * if the current configured network matches |networkId|.
+ *
+ * @param networkId network id of the network to be removed from supplicant.
+ */
+ public void removeNetworkIfCurrent(int networkId) {
+ mWifiNative.removeNetworkIfCurrent(networkId);
+ }
}
diff --git a/service/java/com/android/server/wifi/WifiConnectivityManager.java b/service/java/com/android/server/wifi/WifiConnectivityManager.java
index 6d82ce869..f45d17b41 100644
--- a/service/java/com/android/server/wifi/WifiConnectivityManager.java
+++ b/service/java/com/android/server/wifi/WifiConnectivityManager.java
@@ -487,7 +487,32 @@ public class WifiConnectivityManager {
private class OnSavedNetworkUpdateListener implements
WifiConfigManager.OnSavedNetworkUpdateListener {
- public void onSavedNetworkUpdate() {
+ @Override
+ public void onSavedNetworkAdded(int networkId) {
+ updatePnoScan();
+ }
+ @Override
+ public void onSavedNetworkEnabled(int networkId) {
+ updatePnoScan();
+ }
+ @Override
+ public void onSavedNetworkRemoved(int networkId) {
+ updatePnoScan();
+ }
+ @Override
+ public void onSavedNetworkUpdated(int networkId) {
+ updatePnoScan();
+ }
+ @Override
+ public void onSavedNetworkTemporarilyDisabled(int networkId) {
+ mConnectivityHelper.removeNetworkIfCurrent(networkId);
+ }
+ @Override
+ public void onSavedNetworkPermanentlyDisabled(int networkId) {
+ mConnectivityHelper.removeNetworkIfCurrent(networkId);
+ updatePnoScan();
+ }
+ private void updatePnoScan() {
// Update the PNO scan network list when screen is off. Here we
// rely on startConnectivityScan() to perform all the checks and clean up.
if (!mScreenOn) {
diff --git a/service/java/com/android/server/wifi/WifiNative.java b/service/java/com/android/server/wifi/WifiNative.java
index abe692346..90f6ac195 100644
--- a/service/java/com/android/server/wifi/WifiNative.java
+++ b/service/java/com/android/server/wifi/WifiNative.java
@@ -787,6 +787,16 @@ public class WifiNative {
public String getCurrentNetworkWpsNfcConfigurationToken() {
return mSupplicantStaIfaceHal.getCurrentNetworkWpsNfcConfigurationToken();
}
+
+ /** Remove the request |networkId| from supplicant if it's the current network,
+ * if the current configured network matches |networkId|.
+ *
+ * @param networkId network id of the network to be removed from supplicant.
+ */
+ public void removeNetworkIfCurrent(int networkId) {
+ mSupplicantStaIfaceHal.removeNetworkIfCurrent(networkId);
+ }
+
/********************************************************
* Vendor HAL operations
********************************************************/