diff options
author | lesl <lesl@google.com> | 2020-03-23 14:34:11 +0800 |
---|---|---|
committer | lesl <lesl@google.com> | 2020-03-24 15:35:29 +0800 |
commit | 1a0feaa49bb5c7c6145e0ee52a9a5a6ed734e3ba (patch) | |
tree | 316b59cb8f71442f67cdffd09db61a06aaa0e4cb | |
parent | 44f8e09fa0da59aa5d9519715635b9ec9af69e48 (diff) |
wifi: Add Softap configuration/capability metric
Bug: 152168936
Test: atest frameworks/opt/net/wifi/tests/wifitests/
Change-Id: I080cc86676181ba3e9a94e4f24ba3faca1925ea2
5 files changed, 191 insertions, 14 deletions
diff --git a/service/java/com/android/server/wifi/SoftApManager.java b/service/java/com/android/server/wifi/SoftApManager.java index 745214539..20d462bf7 100644 --- a/service/java/com/android/server/wifi/SoftApManager.java +++ b/service/java/com/android/server/wifi/SoftApManager.java @@ -114,6 +114,8 @@ public class SoftApManager implements ActiveModeManager { private String mStartTimestamp; + private long mDefaultShutDownTimeoutMills; + private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); private BaseWifiDiagnostics mWifiDiagnostics; @@ -196,6 +198,8 @@ public class SoftApManager implements ActiveModeManager { mAllowedClientList = new HashSet<>(softApConfig.getAllowedClientList()); mTimeoutEnabled = softApConfig.isAutoShutdownEnabled(); } + mDefaultShutDownTimeoutMills = mContext.getResources().getInteger( + R.integer.config_wifiFrameworkSoftApShutDownTimeoutMilliseconds); } /** @@ -602,8 +606,7 @@ public class SoftApManager implements ActiveModeManager { } long timeout = mApConfig.getSoftApConfiguration().getShutdownTimeoutMillis(); if (timeout == 0) { - timeout = mContext.getResources().getInteger( - R.integer.config_wifiFrameworkSoftApShutDownTimeoutMilliseconds); + timeout = mDefaultShutDownTimeoutMills; } mSoftApTimeoutMessage.schedule(SystemClock.elapsedRealtime() + timeout); @@ -756,7 +759,14 @@ public class SoftApManager implements ActiveModeManager { // the interface was up, but goes down sendMessage(CMD_INTERFACE_DOWN); } - mWifiMetrics.addSoftApUpChangedEvent(isUp, mApConfig.getTargetMode()); + mWifiMetrics.addSoftApUpChangedEvent(isUp, mApConfig.getTargetMode(), + mDefaultShutDownTimeoutMills); + if (isUp) { + mWifiMetrics.updateSoftApConfiguration(mApConfig.getSoftApConfiguration(), + mApConfig.getTargetMode()); + mWifiMetrics.updateSoftApCapability(mCurrentSoftApCapability, + mApConfig.getTargetMode()); + } } @Override @@ -796,7 +806,8 @@ public class SoftApManager implements ActiveModeManager { // Need this here since we are exiting |Started| state and won't handle any // future CMD_INTERFACE_STATUS_CHANGED events after this point - mWifiMetrics.addSoftApUpChangedEvent(false, mApConfig.getTargetMode()); + mWifiMetrics.addSoftApUpChangedEvent(false, mApConfig.getTargetMode(), + mDefaultShutDownTimeoutMills); updateApState(WifiManager.WIFI_AP_STATE_DISABLED, WifiManager.WIFI_AP_STATE_DISABLING, 0); @@ -904,6 +915,8 @@ public class SoftApManager implements ActiveModeManager { if (mApConfig.getTargetMode() == WifiManager.IFACE_IP_MODE_TETHERED) { SoftApCapability capability = (SoftApCapability) message.obj; mCurrentSoftApCapability = new SoftApCapability(capability); + mWifiMetrics.updateSoftApCapability(mCurrentSoftApCapability, + mApConfig.getTargetMode()); updateClientConnection(); } break; @@ -933,6 +946,9 @@ public class SoftApManager implements ActiveModeManager { cancelTimeoutMessage(); scheduleTimeoutMessage(); } + mWifiMetrics.updateSoftApConfiguration( + mApConfig.getSoftApConfiguration(), + mApConfig.getTargetMode()); } else { Log.d(TAG, "Ignore the config: " + newConfig + " update since it requires restart"); diff --git a/service/java/com/android/server/wifi/WifiMetrics.java b/service/java/com/android/server/wifi/WifiMetrics.java index 8022bd909..f1b2b7d04 100644 --- a/service/java/com/android/server/wifi/WifiMetrics.java +++ b/service/java/com/android/server/wifi/WifiMetrics.java @@ -23,6 +23,8 @@ import android.hardware.wifi.supplicant.V1_0.ISupplicantStaIfaceCallback; import android.net.wifi.EAPConstants; import android.net.wifi.IOnWifiUsabilityStatsListener; import android.net.wifi.ScanResult; +import android.net.wifi.SoftApCapability; +import android.net.wifi.SoftApConfiguration; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; @@ -2195,6 +2197,14 @@ public class WifiMetrics { mSoftApManagerReturnCodeCounts.put( WifiMetricsProto.SoftApReturnCodeCount.SOFT_AP_FAILED_NO_CHANNEL, count + 1); + } else if (failureCode == WifiManager.SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION) { + int count = mSoftApManagerReturnCodeCounts.get( + WifiMetricsProto.SoftApReturnCodeCount + .SOFT_AP_FAILED_UNSUPPORTED_CONFIGURATION); + mSoftApManagerReturnCodeCounts.put( + WifiMetricsProto.SoftApReturnCodeCount + .SOFT_AP_FAILED_UNSUPPORTED_CONFIGURATION, + count + 1); } else { // failure mode not tracked at this time... count as a general error for now. int count = mSoftApManagerReturnCodeCounts.get( @@ -2209,11 +2219,12 @@ public class WifiMetrics { /** * Adds a record indicating the current up state of soft AP */ - public void addSoftApUpChangedEvent(boolean isUp, int mode) { + public void addSoftApUpChangedEvent(boolean isUp, int mode, long defaultShutdownTimeoutMillis) { SoftApConnectedClientsEvent event = new SoftApConnectedClientsEvent(); event.eventType = isUp ? SoftApConnectedClientsEvent.SOFT_AP_UP : SoftApConnectedClientsEvent.SOFT_AP_DOWN; event.numConnectedClients = 0; + event.defaultShutdownTimeoutSetting = defaultShutdownTimeoutMillis; addSoftApConnectedClientsEvent(event, mode); } @@ -2283,6 +2294,66 @@ public class WifiMetrics { } /** + * Updates current soft AP events with softap configuration + */ + public void updateSoftApConfiguration(SoftApConfiguration config, int mode) { + synchronized (mLock) { + List<SoftApConnectedClientsEvent> softApEventList; + switch (mode) { + case WifiManager.IFACE_IP_MODE_TETHERED: + softApEventList = mSoftApEventListTethered; + break; + case WifiManager.IFACE_IP_MODE_LOCAL_ONLY: + softApEventList = mSoftApEventListLocalOnly; + break; + default: + return; + } + + for (int index = softApEventList.size() - 1; index >= 0; index--) { + SoftApConnectedClientsEvent event = softApEventList.get(index); + + if (event != null && event.eventType == SoftApConnectedClientsEvent.SOFT_AP_UP) { + event.maxNumClientsSettingInSoftapConfiguration = + config.getMaxNumberOfClients(); + event.shutdownTimeoutSettingInSoftapConfiguration = + config.getShutdownTimeoutMillis(); + event.clientControlIsEnabled = config.isClientControlByUserEnabled(); + break; + } + } + } + } + + /** + * Updates current soft AP events with softap capability + */ + public void updateSoftApCapability(SoftApCapability capability, int mode) { + synchronized (mLock) { + List<SoftApConnectedClientsEvent> softApEventList; + switch (mode) { + case WifiManager.IFACE_IP_MODE_TETHERED: + softApEventList = mSoftApEventListTethered; + break; + case WifiManager.IFACE_IP_MODE_LOCAL_ONLY: + softApEventList = mSoftApEventListLocalOnly; + break; + default: + return; + } + + for (int index = softApEventList.size() - 1; index >= 0; index--) { + SoftApConnectedClientsEvent event = softApEventList.get(index); + if (event != null && event.eventType == SoftApConnectedClientsEvent.SOFT_AP_UP) { + event.maxNumClientsSettingInSoftapCapability = + capability.getMaxSupportedClients(); + break; + } + } + } + } + + /** * Increment number of times the HAL crashed. */ public void incrementNumHalCrashes() { @@ -2947,6 +3018,10 @@ public class WifiMetrics { WifiMetricsProto.SoftApReturnCodeCount.SOFT_AP_FAILED_GENERAL_ERROR)); pw.println(" FAILED_NO_CHANNEL: " + mSoftApManagerReturnCodeCounts.get( WifiMetricsProto.SoftApReturnCodeCount.SOFT_AP_FAILED_NO_CHANNEL)); + pw.println(" FAILED_UNSUPPORTED_CONFIGURATION: " + + mSoftApManagerReturnCodeCounts.get( + WifiMetricsProto.SoftApReturnCodeCount + .SOFT_AP_FAILED_UNSUPPORTED_CONFIGURATION)); pw.print("\n"); pw.println("mWifiLogProto.numHalCrashes=" + mWifiLogProto.numHalCrashes); @@ -3106,6 +3181,15 @@ public class WifiMetrics { eventLine.append(",num_connected_clients=" + event.numConnectedClients); eventLine.append(",channel_frequency=" + event.channelFrequency); eventLine.append(",channel_bandwidth=" + event.channelBandwidth); + eventLine.append(",max_num_clients_setting_in_softap_configuration=" + + event.maxNumClientsSettingInSoftapConfiguration); + eventLine.append(",max_num_clients_setting_in_softap_capability=" + + event.maxNumClientsSettingInSoftapCapability); + eventLine.append(",shutdown_timeout_setting_in_softap_configuration=" + + event.shutdownTimeoutSettingInSoftapConfiguration); + eventLine.append(",default_shutdown_timeout_setting=" + + event.defaultShutdownTimeoutSetting); + eventLine.append(",client_control_is_enabled=" + event.clientControlIsEnabled); pw.println(eventLine.toString()); } pw.println("mSoftApLocalOnlyEvents:"); @@ -3116,6 +3200,15 @@ public class WifiMetrics { eventLine.append(",num_connected_clients=" + event.numConnectedClients); eventLine.append(",channel_frequency=" + event.channelFrequency); eventLine.append(",channel_bandwidth=" + event.channelBandwidth); + eventLine.append(",max_num_clients_setting_in_softap_configuration=" + + event.maxNumClientsSettingInSoftapConfiguration); + eventLine.append(",max_num_clients_setting_in_softap_capability=" + + event.maxNumClientsSettingInSoftapCapability); + eventLine.append(",shutdown_timeout_setting_in_softap_configuration=" + + event.shutdownTimeoutSettingInSoftapConfiguration); + eventLine.append(",default_shutdown_timeout_setting=" + + event.defaultShutdownTimeoutSetting); + eventLine.append(",client_control_is_enabled=" + event.clientControlIsEnabled); pw.println(eventLine.toString()); } diff --git a/service/proto/src/metrics.proto b/service/proto/src/metrics.proto index 828dcaa19..0a4675d4e 100644 --- a/service/proto/src/metrics.proto +++ b/service/proto/src/metrics.proto @@ -1091,6 +1091,9 @@ message SoftApReturnCodeCount { // SoftAp failed to start due to NO_CHANNEL error SOFT_AP_FAILED_NO_CHANNEL = 3; + + // SoftAp failed to start due to unsupported configuration error + SOFT_AP_FAILED_UNSUPPORTED_CONFIGURATION = 4; } // Historical, no longer used for writing as of 01/2017. @@ -1711,6 +1714,21 @@ message SoftApConnectedClientsEvent { // Channel bandwidth used for Soft AP optional ChannelBandwidth channel_bandwidth = 5; + + // Maximum number of client setting in SoftApConfiguration + optional int32 max_num_clients_setting_in_softap_configuration = 6; + + // Maximum number of client setting in SoftApCapability + optional int32 max_num_clients_setting_in_softap_capability = 7; + + // Auto shutdown timeout setting in SoftApConfiguration + optional int64 shutdown_timeout_setting_in_softap_configuration = 8; + + // Framework default auto shutdown timeout setting + optional int64 default_shutdown_timeout_setting = 9; + + // Indicates if user enabled the client_control + optional bool client_control_is_enabled = 10; } // Wps connection metrics diff --git a/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java b/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java index 21c3305f2..2a3fba014 100644 --- a/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java +++ b/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java @@ -96,6 +96,7 @@ public class SoftApManagerTest extends WifiBaseTest { private static final String TEST_COUNTRY_CODE = "TestCountry"; private static final String TEST_INTERFACE_NAME = "testif0"; private static final String OTHER_INTERFACE_NAME = "otherif"; + private static final long TEST_DEFAULT_SHUTDOWN_TIMEOUT_MILLS = 600_000; private static final MacAddress TEST_MAC_ADDRESS = MacAddress.fromString("22:33:44:55:66:77"); private static final MacAddress TEST_MAC_ADDRESS_2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff"); private static final WifiClient TEST_CONNECTED_CLIENT = new WifiClient(TEST_MAC_ADDRESS); @@ -154,7 +155,7 @@ public class SoftApManagerTest extends WifiBaseTest { .thenReturn(mNotificationManager); when(mResources.getInteger(R.integer.config_wifiFrameworkSoftApShutDownTimeoutMilliseconds)) - .thenReturn(600000); + .thenReturn((int) TEST_DEFAULT_SHUTDOWN_TIMEOUT_MILLS); when(mWifiNative.setCountryCodeHal( TEST_INTERFACE_NAME, TEST_COUNTRY_CODE.toUpperCase(Locale.ROOT))) .thenReturn(true); @@ -1388,8 +1389,6 @@ public class SoftApManagerTest extends WifiBaseTest { new SoftApModeConfiguration(WifiManager.IFACE_IP_MODE_TETHERED, configBuilder.build(), mTestSoftApCapability); startSoftApAndVerifyEnabled(apConfig); - verify(mResources, never()) - .getInteger(R.integer.config_wifiFrameworkSoftApShutDownTimeoutMilliseconds); // Verify timer is scheduled verify(mAlarmManager.getAlarmManager()).setExact(anyInt(), anyLong(), @@ -1775,7 +1774,12 @@ public class SoftApManagerTest extends WifiBaseTest { WIFI_AP_STATE_ENABLING, HOTSPOT_NO_ERROR, TEST_INTERFACE_NAME, softApConfig.getTargetMode()); verify(mListener).onStarted(); - verify(mWifiMetrics).addSoftApUpChangedEvent(true, softApConfig.getTargetMode()); + verify(mWifiMetrics).addSoftApUpChangedEvent(true, softApConfig.getTargetMode(), + TEST_DEFAULT_SHUTDOWN_TIMEOUT_MILLS); + verify(mWifiMetrics).updateSoftApConfiguration(config == null ? mDefaultApConfig : config, + softApConfig.getTargetMode()); + verify(mWifiMetrics).updateSoftApCapability(softApConfig.getCapability(), + softApConfig.getTargetMode()); } private void checkApStateChangedBroadcast(Intent intent, int expectedCurrentState, @@ -1863,6 +1867,8 @@ public class SoftApManagerTest extends WifiBaseTest { verify(mCallback).onStateChanged(WifiManager.WIFI_AP_STATE_ENABLING, 0); verify(mCallback).onStateChanged(WifiManager.WIFI_AP_STATE_FAILED, WifiManager.SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION); + verify(mWifiMetrics).incrementSoftApStartResult(false, + WifiManager.SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION); verify(mListener).onStartFailure(); } @@ -1915,6 +1921,8 @@ public class SoftApManagerTest extends WifiBaseTest { verify(mAlarmManager.getAlarmManager()).setExact(anyInt(), anyLong(), eq(mSoftApManager.SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG), any(), any()); verify(mCallback).onConnectedClientsChanged(new ArrayList<>()); + verify(mWifiMetrics).updateSoftApConfiguration(configBuilder.build(), + WifiManager.IFACE_IP_MODE_TETHERED); mLooper.dispatchAll(); @@ -1927,6 +1935,8 @@ public class SoftApManagerTest extends WifiBaseTest { // Verify timer setup again verify(mAlarmManager.getAlarmManager(), times(2)).setExact(anyInt(), anyLong(), eq(mSoftApManager.SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG), any(), any()); + verify(mWifiMetrics).updateSoftApConfiguration(configBuilder.build(), + WifiManager.IFACE_IP_MODE_TETHERED); } diff --git a/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java b/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java index 1cf3597f8..036ec652f 100644 --- a/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java +++ b/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java @@ -55,6 +55,8 @@ import android.content.Context; import android.net.wifi.EAPConstants; import android.net.wifi.IOnWifiUsabilityStatsListener; import android.net.wifi.ScanResult; +import android.net.wifi.SoftApCapability; +import android.net.wifi.SoftApConfiguration; import android.net.wifi.SupplicantState; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiEnterpriseConfig; @@ -449,6 +451,11 @@ public class WifiMetricsTest extends WifiBaseTest { private static final int NUM_SOFT_AP_ASSOCIATED_STATIONS = 3; private static final int SOFT_AP_CHANNEL_FREQUENCY = 2437; private static final int SOFT_AP_CHANNEL_BANDWIDTH = SoftApConnectedClientsEvent.BANDWIDTH_20; + private static final int SOFT_AP_MAX_CLIENT_SETTING = 10; + private static final int SOFT_AP_MAX_CLIENT_CAPABILITY = 16; + private static final long SOFT_AP_SHUTDOWN_TIMEOUT_SETTING = 10_000; + private static final long SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING = 600_000; + private static final boolean SOFT_AP_CLIENT_CONTROL_ENABLE = true; private static final boolean IS_MAC_RANDOMIZATION_ON = true; private static final int NUM_LINK_SPEED_LEVELS_TO_INCREMENT = 30; private static final int TEST_RSSI_LEVEL = -80; @@ -985,21 +992,39 @@ public class WifiMetricsTest extends WifiBaseTest { private void addSoftApEventsToMetrics() { // Total number of events recorded is NUM_SOFT_AP_EVENT_ENTRIES in both modes - mWifiMetrics.addSoftApUpChangedEvent(true, WifiManager.IFACE_IP_MODE_TETHERED); + mWifiMetrics.addSoftApUpChangedEvent(true, WifiManager.IFACE_IP_MODE_TETHERED, + SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING); mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(NUM_SOFT_AP_ASSOCIATED_STATIONS, WifiManager.IFACE_IP_MODE_TETHERED); mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(NUM_SOFT_AP_ASSOCIATED_STATIONS, WifiManager.IFACE_IP_MODE_UNSPECIFIED); // Should be dropped. - mWifiMetrics.addSoftApUpChangedEvent(false, WifiManager.IFACE_IP_MODE_TETHERED); + mWifiMetrics.addSoftApUpChangedEvent(false, WifiManager.IFACE_IP_MODE_TETHERED, + SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING); // Channel switch info should be added to the last Soft AP UP event in the list mWifiMetrics.addSoftApChannelSwitchedEvent(SOFT_AP_CHANNEL_FREQUENCY, SOFT_AP_CHANNEL_BANDWIDTH, WifiManager.IFACE_IP_MODE_TETHERED); - mWifiMetrics.addSoftApUpChangedEvent(true, WifiManager.IFACE_IP_MODE_LOCAL_ONLY); + SoftApConfiguration testSoftApConfig = new SoftApConfiguration.Builder() + .setSsid("Test_Metric_SSID") + .setMaxNumberOfClients(SOFT_AP_MAX_CLIENT_SETTING) + .setShutdownTimeoutMillis(SOFT_AP_SHUTDOWN_TIMEOUT_SETTING) + .setClientControlByUserEnabled(SOFT_AP_CLIENT_CONTROL_ENABLE) + .build(); + mWifiMetrics.updateSoftApConfiguration(testSoftApConfig, + WifiManager.IFACE_IP_MODE_TETHERED); + SoftApCapability testSoftApCapability = new SoftApCapability(0); + testSoftApCapability.setMaxSupportedClients(SOFT_AP_MAX_CLIENT_CAPABILITY); + mWifiMetrics.updateSoftApCapability(testSoftApCapability, + WifiManager.IFACE_IP_MODE_TETHERED); + + mWifiMetrics.addSoftApUpChangedEvent(true, WifiManager.IFACE_IP_MODE_LOCAL_ONLY, + SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING); mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(NUM_SOFT_AP_ASSOCIATED_STATIONS, WifiManager.IFACE_IP_MODE_LOCAL_ONLY); // Should be dropped. - mWifiMetrics.addSoftApUpChangedEvent(false, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR); - mWifiMetrics.addSoftApUpChangedEvent(false, WifiManager.IFACE_IP_MODE_LOCAL_ONLY); + mWifiMetrics.addSoftApUpChangedEvent(false, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR, + SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING); + mWifiMetrics.addSoftApUpChangedEvent(false, WifiManager.IFACE_IP_MODE_LOCAL_ONLY, + SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING); } private void verifySoftApEventsStoredInProto() { @@ -1012,6 +1037,21 @@ public class WifiMetricsTest extends WifiBaseTest { mDecodedProto.softApConnectedClientsEventsTethered[0].channelFrequency); assertEquals(SOFT_AP_CHANNEL_BANDWIDTH, mDecodedProto.softApConnectedClientsEventsTethered[0].channelBandwidth); + assertEquals(SOFT_AP_MAX_CLIENT_SETTING, + mDecodedProto.softApConnectedClientsEventsTethered[0] + .maxNumClientsSettingInSoftapConfiguration); + assertEquals(SOFT_AP_MAX_CLIENT_CAPABILITY, + mDecodedProto.softApConnectedClientsEventsTethered[0] + .maxNumClientsSettingInSoftapCapability); + assertEquals(SOFT_AP_SHUTDOWN_TIMEOUT_SETTING, + mDecodedProto.softApConnectedClientsEventsTethered[0] + .shutdownTimeoutSettingInSoftapConfiguration); + assertEquals(SOFT_AP_SHUTDOWN_TIMEOUT_DEFAULT_SETTING, + mDecodedProto.softApConnectedClientsEventsTethered[0] + .defaultShutdownTimeoutSetting); + assertEquals(SOFT_AP_CLIENT_CONTROL_ENABLE, + mDecodedProto.softApConnectedClientsEventsTethered[0].clientControlIsEnabled); + assertEquals(SoftApConnectedClientsEvent.NUM_CLIENTS_CHANGED, mDecodedProto.softApConnectedClientsEventsTethered[1].eventType); assertEquals(NUM_SOFT_AP_ASSOCIATED_STATIONS, |