From e2625a2794d3d4262652edafbb87df9eac54ae53 Mon Sep 17 00:00:00 2001 From: Kai Shi Date: Tue, 19 Nov 2019 21:15:57 -0800 Subject: Wifi: add the initial version of WifiHealthMonitor and related changes. 1) Add the new WifiHealthMonitor class for daily and post-boot detection. 2) Add PerNetwork for per-network/SSID connection stats collection in WifiScoreCard. 3) Add NetworkStats in scorecard.proto for persistent storage per-network stats. 4) Update scanDetail cache even if network selection is skipped. 5) Do memory store write after receiving ACTION_SHUTDOWN message. Bug: 141753195 Test: manual test with various phones and APs. 1) Periodically enable and disable WiFi and check daily stats report and stats report with reduced daily detection interval. 2) Change SW build and check post-boot SW build change detection. Test: unit test with atest com.android.server.wifi Change-Id: Ic7c348260ecb7a17e65f8280a4225bb8365c93e1 --- .../com/android/server/wifi/ClientModeImpl.java | 53 +- .../com/android/server/wifi/MemoryStoreImpl.java | 18 +- .../com/android/server/wifi/WifiConfigManager.java | 38 + .../com/android/server/wifi/WifiHealthMonitor.java | 914 ++++++++++++++++++++ .../java/com/android/server/wifi/WifiInjector.java | 11 +- .../android/server/wifi/WifiNetworkSelector.java | 14 +- .../com/android/server/wifi/WifiScoreCard.java | 933 ++++++++++++++++++--- .../com/android/server/wifi/WifiServiceImpl.java | 20 +- service/proto/src/scorecard.proto | 63 ++ 9 files changed, 1938 insertions(+), 126 deletions(-) create mode 100644 service/java/com/android/server/wifi/WifiHealthMonitor.java (limited to 'service') diff --git a/service/java/com/android/server/wifi/ClientModeImpl.java b/service/java/com/android/server/wifi/ClientModeImpl.java index 8957f21fe..d3b34b71d 100644 --- a/service/java/com/android/server/wifi/ClientModeImpl.java +++ b/service/java/com/android/server/wifi/ClientModeImpl.java @@ -24,6 +24,8 @@ import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED; import static android.net.wifi.WifiManager.WIFI_STATE_ENABLING; import static android.net.wifi.WifiManager.WIFI_STATE_UNKNOWN; +import static com.android.server.wifi.WifiHealthMonitor.SCAN_RSSI_VALID_TIME_MS; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -213,6 +215,7 @@ public class ClientModeImpl extends StateMachine { private final BuildProperties mBuildProperties; private final WifiCountryCode mCountryCode; private final WifiScoreCard mWifiScoreCard; + private final WifiHealthMonitor mWifiHealthMonitor; private final WifiScoreReport mWifiScoreReport; private final SarManager mSarManager; private final WifiTrafficPoller mWifiTrafficPoller; @@ -790,6 +793,7 @@ public class ClientModeImpl extends StateMachine { mWifiNetworkSuggestionsManager = mWifiInjector.getWifiNetworkSuggestionsManager(); mProcessingActionListeners = new ExternalCallbackTracker<>(getHandler()); mProcessingTxPacketCountListeners = new ExternalCallbackTracker<>(getHandler()); + mWifiHealthMonitor = mWifiInjector.getWifiHealthMonitor(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_ON); @@ -1118,6 +1122,8 @@ public class ClientModeImpl extends StateMachine { mNetworkFactory.enableVerboseLogging(verbose); mLinkProbeManager.enableVerboseLogging(mVerboseLoggingEnabled); mMboOceController.enableVerboseLogging(mVerboseLoggingEnabled); + mWifiScoreCard.enableVerboseLogging(mVerboseLoggingEnabled); + mWifiHealthMonitor.enableVerboseLogging(mVerboseLoggingEnabled); } private boolean setRandomMacOui() { @@ -2567,7 +2573,6 @@ public class ClientModeImpl extends StateMachine { mWifiInjector.getWakeupController().setLastDisconnectInfo(matchInfo); mWifiNetworkSuggestionsManager.handleDisconnect(wifiConfig, getCurrentBSSID()); } - stopRssiMonitoringOffload(); clearTargetBssid("handleNetworkDisconnect"); @@ -2591,7 +2596,7 @@ public class ClientModeImpl extends StateMachine { /* Clear network properties */ clearLinkProperties(); - /* Cend event to CM & network change broadcast */ + /* Send event to CM & network change broadcast */ sendNetworkStateChangeBroadcast(mLastBssid); mLastBssid = null; @@ -2734,21 +2739,37 @@ public class ClientModeImpl extends StateMachine { */ private void reportConnectionAttemptEnd(int level2FailureCode, int connectivityFailureCode, int level2FailureReason) { + // if connected, this should be non-null. + WifiConfiguration configuration = getCurrentWifiConfiguration(); + if (configuration == null) { + // If not connected, this should be non-null. + configuration = getTargetWifiConfiguration(); + } + if (level2FailureCode != WifiMetrics.ConnectionEvent.FAILURE_NONE) { - mWifiScoreCard.noteConnectionFailure(mWifiInfo, - level2FailureCode, connectivityFailureCode); + + int bssidBlocklistMonitorReason = convertToBssidBlocklistMonitorFailureReason( + level2FailureCode, level2FailureReason); + String bssid = mLastBssid == null ? mTargetRoamBSSID : mLastBssid; String ssid = mWifiInfo.getSSID(); if (WifiManager.UNKNOWN_SSID.equals(ssid)) { ssid = getTargetSsid(); } - int bssidBlocklistMonitorReason = convertToBssidBlocklistMonitorFailureReason( - level2FailureCode, level2FailureReason); + + if (level2FailureCode != WifiMetrics.ConnectionEvent.FAILURE_NETWORK_DISCONNECTION) { + int networkId = (configuration == null) ? WifiConfiguration.INVALID_NETWORK_ID + : configuration.networkId; + int scanRssi = mWifiConfigManager.findScanRssi(networkId, SCAN_RSSI_VALID_TIME_MS); + mWifiScoreCard.noteConnectionFailure(mWifiInfo, scanRssi, ssid, + bssidBlocklistMonitorReason); + } if (bssidBlocklistMonitorReason != -1) { mBssidBlocklistMonitor.handleBssidConnectionFailure(bssid, ssid, bssidBlocklistMonitorReason); } } + boolean isAssociationRejection = level2FailureCode == WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_REJECTION; boolean isAuthenticationFailure = level2FailureCode @@ -2759,12 +2780,7 @@ public class ClientModeImpl extends StateMachine { mConnectionFailureNotifier .showFailedToConnectDueToNoRandomizedMacSupportNotification(mTargetNetworkId); } - // if connected, this should be non-null. - WifiConfiguration configuration = getCurrentWifiConfiguration(); - if (configuration == null) { - // If not connected, this should be non-null. - configuration = getTargetWifiConfiguration(); - } + mWifiMetrics.endConnectionEvent(level2FailureCode, connectivityFailureCode, level2FailureReason); mWifiConnectivityManager.handleConnectionAttemptEnded(level2FailureCode); @@ -3471,6 +3487,7 @@ public class ClientModeImpl extends StateMachine { mWifiMetrics.setWifiState(WifiMetricsProto.WifiLog.WIFI_DISCONNECTED); mWifiMetrics.logStaEvent(StaEvent.TYPE_WIFI_ENABLED); mWifiScoreCard.noteSupplicantStateChanged(mWifiInfo); + mWifiHealthMonitor.setWifiEnabled(true); } @Override @@ -3495,6 +3512,7 @@ public class ClientModeImpl extends StateMachine { mWifiInfo.reset(); mWifiInfo.setSupplicantState(SupplicantState.DISCONNECTED); mWifiScoreCard.noteSupplicantStateChanged(mWifiInfo); + mWifiHealthMonitor.setWifiEnabled(false); stopClientMode(); } @@ -3753,7 +3771,8 @@ public class ClientModeImpl extends StateMachine { break; } // Update scorecard while there is still state from existing connection - mWifiScoreCard.noteConnectionAttempt(mWifiInfo); + int scanRssi = mWifiConfigManager.findScanRssi(netId, SCAN_RSSI_VALID_TIME_MS); + mWifiScoreCard.noteConnectionAttempt(mWifiInfo, scanRssi, config.SSID); mTargetNetworkId = netId; setTargetBssid(config, bssid); mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(config.SSID); @@ -5046,6 +5065,7 @@ public class ClientModeImpl extends StateMachine { mLastBssid, config.SSID, BssidBlocklistMonitor .REASON_NETWORK_VALIDATION_FAILURE); + mWifiScoreCard.noteValidationFailure(mWifiInfo); } } } @@ -5097,7 +5117,8 @@ public class ClientModeImpl extends StateMachine { WifiDiagnostics.REPORT_REASON_UNEXPECTED_DISCONNECT); } boolean localGen = message.arg1 == 1; - if (!localGen) { // ignore disconnects initiatied by wpa_supplicant. + if (!localGen) { // ignore disconnects initiated by wpa_supplicant. + mWifiScoreCard.noteNonlocalDisconnect(message.arg2); mBssidBlocklistMonitor.handleBssidConnectionFailure(mWifiInfo.getBSSID(), mWifiInfo.getSSID(), BssidBlocklistMonitor.REASON_ABNORMAL_DISCONNECT); @@ -5130,7 +5151,8 @@ public class ClientModeImpl extends StateMachine { loge("CMD_START_ROAM and no config, bail out..."); break; } - mWifiScoreCard.noteConnectionAttempt(mWifiInfo); + int scanRssi = mWifiConfigManager.findScanRssi(netId, SCAN_RSSI_VALID_TIME_MS); + mWifiScoreCard.noteConnectionAttempt(mWifiInfo, scanRssi, config.SSID); setTargetBssid(config, bssid); mTargetNetworkId = netId; @@ -5664,6 +5686,7 @@ public class ClientModeImpl extends StateMachine { */ public void setDeviceMobilityState(@DeviceMobilityState int state) { mWifiConnectivityManager.setDeviceMobilityState(state); + mWifiHealthMonitor.setDeviceMobilityState(state); } /** diff --git a/service/java/com/android/server/wifi/MemoryStoreImpl.java b/service/java/com/android/server/wifi/MemoryStoreImpl.java index 3d9e7626b..1b819d46d 100644 --- a/service/java/com/android/server/wifi/MemoryStoreImpl.java +++ b/service/java/com/android/server/wifi/MemoryStoreImpl.java @@ -39,17 +39,17 @@ final class MemoryStoreImpl implements WifiScoreCard.MemoryStore { // The id of the client that stored this data public static final String WIFI_FRAMEWORK_IP_MEMORY_STORE_CLIENT_ID = "com.android.server.wifi"; - // The name of the data - public static final String WIFI_FRAMEWORK_IP_MEMORY_STORE_DATA_NAME = "scorecard.proto"; - @NonNull private final Context mContext; @NonNull private final WifiScoreCard mWifiScoreCard; + @NonNull private final WifiHealthMonitor mWifiHealthMonitor; @NonNull private final WifiInjector mWifiInjector; @Nullable private IpMemoryStore mIpMemoryStore; - MemoryStoreImpl(Context context, WifiInjector wifiInjector, WifiScoreCard wifiScoreCard) { + MemoryStoreImpl(Context context, WifiInjector wifiInjector, WifiScoreCard wifiScoreCard, + WifiHealthMonitor wifiHealthMonitor) { mContext = Preconditions.checkNotNull(context); mWifiScoreCard = Preconditions.checkNotNull(wifiScoreCard); + mWifiHealthMonitor = Preconditions.checkNotNull(wifiHealthMonitor); mWifiInjector = Preconditions.checkNotNull(wifiInjector); mIpMemoryStore = null; } @@ -62,13 +62,13 @@ final class MemoryStoreImpl implements WifiScoreCard.MemoryStore { @Override - public void read(final String key, final BlobListener blobListener) { + public void read(final String key, final String name, final BlobListener blobListener) { if (mBroken) return; try { mIpMemoryStore.retrieveBlob( key, WIFI_FRAMEWORK_IP_MEMORY_STORE_CLIENT_ID, - WIFI_FRAMEWORK_IP_MEMORY_STORE_DATA_NAME, + name, new CatchAFallingBlob(key, blobListener)); } catch (RuntimeException e) { handleException(e); @@ -111,7 +111,7 @@ final class MemoryStoreImpl implements WifiScoreCard.MemoryStore { } @Override - public void write(String key, byte[] value) { + public void write(String key, String name, byte[] value) { if (mBroken) return; final Blob blob = new Blob(); blob.data = value; @@ -119,7 +119,7 @@ final class MemoryStoreImpl implements WifiScoreCard.MemoryStore { mIpMemoryStore.storeBlob( key, WIFI_FRAMEWORK_IP_MEMORY_STORE_CLIENT_ID, - WIFI_FRAMEWORK_IP_MEMORY_STORE_DATA_NAME, + name, blob, null /* no listener for now, just fire and forget */); } catch (RuntimeException e) { @@ -140,6 +140,7 @@ final class MemoryStoreImpl implements WifiScoreCard.MemoryStore { return; } mWifiScoreCard.installMemoryStore(this); + mWifiHealthMonitor.installMemoryStoreSetUpDetectionAlarm(this); } /** @@ -148,6 +149,7 @@ final class MemoryStoreImpl implements WifiScoreCard.MemoryStore { public void stop() { if (mIpMemoryStore == null) return; mWifiScoreCard.doWrites(); + mWifiHealthMonitor.doWrites(); // TODO - Should wait for writes to complete (or time out) Log.i(TAG, "Disconnecting from IpMemoryStore service"); mIpMemoryStore = null; diff --git a/service/java/com/android/server/wifi/WifiConfigManager.java b/service/java/com/android/server/wifi/WifiConfigManager.java index f2655e9c0..1781717d9 100644 --- a/service/java/com/android/server/wifi/WifiConfigManager.java +++ b/service/java/com/android/server/wifi/WifiConfigManager.java @@ -2385,6 +2385,19 @@ public class WifiConfigManager { return config; } + /** + * Caches the provided |scanDetail| into the corresponding scan detail cache entry + * {@link #mScanDetailCaches} for the retrieved network. + * + * @param scanDetail input a scanDetail from the scan result + */ + public void updateScanDetailCacheFromScanDetail(ScanDetail scanDetail) { + WifiConfiguration network = getConfiguredNetworkForScanDetail(scanDetail); + if (network == null) { + return; + } + saveToScanDetailCacheForNetwork(network, scanDetail); + } /** * Retrieves a configured network corresponding to the provided scan detail if one exists and * caches the provided |scanDetail| into the corresponding scan detail cache entry @@ -3494,4 +3507,29 @@ public class WifiConfigManager { } config.recentFailure.clear(); } + + /** + * Find the highest RSSI among all valid scanDetails in current network's scanDetail cache. + * If scanDetail is too old, it is not considered to be valid. + * @param netId The network ID of the config to find scan RSSI + * @params scanRssiValidTimeMs The valid time for scan RSSI + * @return The highest RSSI in dBm found with current network's scanDetail cache. + */ + public int findScanRssi(int netId, int scanRssiValidTimeMs) { + int scanMaxRssi = WifiInfo.INVALID_RSSI; + ScanDetailCache scanDetailCache = getScanDetailCacheForNetwork(netId); + if (scanDetailCache == null || scanDetailCache.size() == 0) return scanMaxRssi; + long nowInMillis = mClock.getWallClockMillis(); + for (ScanDetail scanDetail : scanDetailCache.values()) { + ScanResult result = scanDetail.getScanResult(); + if (result == null) continue; + boolean valid = (nowInMillis - result.seen) < scanRssiValidTimeMs; + + if (valid) { + scanMaxRssi = Math.max(scanMaxRssi, result.level); + } + } + return scanMaxRssi; + } + } diff --git a/service/java/com/android/server/wifi/WifiHealthMonitor.java b/service/java/com/android/server/wifi/WifiHealthMonitor.java new file mode 100644 index 000000000..f545c0985 --- /dev/null +++ b/service/java/com/android/server/wifi/WifiHealthMonitor.java @@ -0,0 +1,914 @@ +/* + * Copyright 2019 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 com.android.server.wifi; + +import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; + +import static com.android.server.wifi.WifiScoreCard.TS_NONE; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.AlarmManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.MacAddress; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.DeviceMobilityState; +import android.net.wifi.WifiScanner; +import android.os.Build; +import android.os.Handler; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.wifi.WifiScoreCard.MemoryStore; +import com.android.server.wifi.WifiScoreCard.MemoryStoreAccessBase; +import com.android.server.wifi.WifiScoreCard.PerNetwork; +import com.android.server.wifi.proto.WifiScoreCardProto.SoftwareBuildInfo; +import com.android.server.wifi.proto.WifiScoreCardProto.SystemInfoStats; +import com.android.server.wifi.util.ScanResultUtil; + +import com.google.protobuf.InvalidProtocolBufferException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import javax.annotation.concurrent.NotThreadSafe; + +/** + * Monitor and detect potential WiFi health issue when RSSI is sufficiently high. + * There are two detections, daily detection and post-boot detection. + * Post-boot detection is to detect abnormal scan/connection behavior change after device reboot + * and/or SW build change. + * Daily detection is to detect connection and other behavior changes especially after SW change. + */ + +@NotThreadSafe +public class WifiHealthMonitor { + private static final String TAG = "WifiHealthMonitor"; + private boolean mVerboseLoggingEnabled = false; + public static final String DAILY_DETECTION_TIMER_TAG = + "WifiHealthMonitor Schedule Daily Detection Timer"; + public static final String POST_BOOT_DETECTION_TIMER_TAG = + "WifiHealthMonitor Schedule Post-Boot Detection Timer"; + // Package name of WiFi mainline module found from the following adb command + // adb shell pm list packages --apex-only| grep wifi + private static final String WIFI_APK_PACKAGE_NAME = "com.google.android.wifi"; + private static final String SYSTEM_INFO_DATA_NAME = "systemInfoData"; + // The time that device waits after device boot before triggering post-boot detection. + // This needs be long enough so that memory read can complete before post-boot detection. + private static final int POST_BOOT_DETECTION_WAIT_TIME_MS = 25_000; + // 0 - ELAPSED_REAL_TIME, 1 - RTC + private static final int DAILY_DETECTION_ALARM_TYPE = 1; + // The time interval between two daily detections + private static final int DAILY_DETECTION_INTERVAL_MS = 24 * 3600_000; + private static final int DAILY_DETECTION_HOUR = 24; + // Max interval between pre-boot scan and post-boot scan to qualify post-boot scan detection + private static final long MAX_INTERVAL_BETWEEN_TWO_SCAN_MS = 60_000; + // The minimum number of BSSIDs that should be found during a normal scan to trigger detection + // of an abnormal scan which happens either before or after the normal scan within a short time. + // Minimum number of BSSIDs found at 2G with a normal scan + private static final int MIN_NUM_BSSID_SCAN_2G = 2; + // Minimum number of BSSIDs found above 2G with a normal scan + private static final int MIN_NUM_BSSID_SCAN_ABOVE_2G = 2; + // Maximum scan RSSI valid time for scan RSSI search which is done by finding + // the maximum RSSI found among all valid scan detail entries of each network's scanDetailCache + // If a scanDetail is seen more than SCAN_RSSI_VALID_TIME_MS ago, + // it will not be considered valid anymore. + static final int SCAN_RSSI_VALID_TIME_MS = 6_000; + // Minimum RSSI in dBm for connection stats collection + // Connection or disconnection events with RSSI below this threshold are not + // included in connection stats collection. + static final int HEALTH_MONITOR_COUNT_RSSI_MIN_DBM = -68; + // Minimum Tx speed in Mbps for disconnection stats collection + // Disconnection events with Tx speed below this threshold are not + // included in connection stats collection. + static final int HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS = 54; + static final int HEALTH_MONITOR_COUNT_MIN_TX_RATE = 6; + private final Context mContext; + private final WifiConfigManager mWifiConfigManager; + private final WifiScoreCard mWifiScoreCard; + private final Clock mClock; + private final AlarmManager mAlarmManager; + private final Handler mHandler; + private final WifiNative mWifiNative; + private final WifiInjector mWifiInjector; + private final DeviceConfigFacade mDeviceConfigFacade; + private WifiScanner mScanner; + private MemoryStore mMemoryStore; + private boolean mWifiEnabled; + private WifiSystemInfoStats mWifiSystemInfoStats; + private ScanStats mFirstScanStats = new ScanStats(); + // Detected significant increase of failure stats between daily data and historical data + private FailureStats mFailureStatsIncrease = new FailureStats(); + // Detected significant decrease of failure stats between daily data and historical data + private FailureStats mFailureStatsDecrease = new FailureStats(); + // Detected high failure stats from daily data without historical data + private FailureStats mFailureStatsHigh = new FailureStats(); + + WifiHealthMonitor(Context context, WifiInjector wifiInjector, Clock clock, + WifiConfigManager wifiConfigManager, WifiScoreCard wifiScoreCard, Handler handler, + WifiNative wifiNative, String l2KeySeed, DeviceConfigFacade deviceConfigFacade) { + mContext = context; + mWifiInjector = wifiInjector; + mClock = clock; + mWifiConfigManager = wifiConfigManager; + mWifiScoreCard = wifiScoreCard; + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mHandler = handler; + mWifiNative = wifiNative; + mDeviceConfigFacade = deviceConfigFacade; + mWifiEnabled = false; + mWifiSystemInfoStats = new WifiSystemInfoStats(l2KeySeed); + mWifiConfigManager.addOnNetworkUpdateListener(new OnNetworkUpdateListener()); + } + + /** + * Enable/Disable verbose logging. + * + * @param verbose true to enable and false to disable. + */ + public void enableVerboseLogging(boolean verbose) { + mVerboseLoggingEnabled = verbose; + } + + private final AlarmManager.OnAlarmListener mDailyDetectionListener = + new AlarmManager.OnAlarmListener() { + public void onAlarm() { + dailyDetectionHandler(); + } + }; + + private final AlarmManager.OnAlarmListener mPostBootDetectionListener = + new AlarmManager.OnAlarmListener() { + public void onAlarm() { + postBootDetectionHandler(); + } + }; + + /** + * Installs a memory store, request read for post-boot detection and set up detection alarms. + */ + public void installMemoryStoreSetUpDetectionAlarm(@NonNull MemoryStore memoryStore) { + if (mMemoryStore == null) { + mMemoryStore = memoryStore; + Log.i(TAG, "Installing MemoryStore"); + } else { + mMemoryStore = memoryStore; + Log.e(TAG, "Reinstalling MemoryStore"); + } + requestReadForPostBootDetection(); + setDailyDetectionAlarm(); + setPostBootDetectionAlarm(); + } + + /** + * Set WiFi enable state. + * During the off->on transition, retrieve scanner. + * During the on->off transition, issue MemoryStore write to save data. + */ + public void setWifiEnabled(boolean enable) { + mWifiEnabled = enable; + logd("Set WiFi " + (enable ? "enabled" : "disabled")); + if (enable) { + retrieveWifiScanner(); + } else { + doWrites(); + } + } + + /** + * Issue MemoryStore write. This should be called from time to time + * to save the state to persistent storage. + */ + public void doWrites() { + mWifiSystemInfoStats.writeToMemory(); + } + + /** + * Set device mobility state to assist abnormal scan failure detection + */ + public void setDeviceMobilityState(@DeviceMobilityState int newState) { + logd("Device mobility state: " + newState); + mWifiSystemInfoStats.setMobilityState(newState); + } + + /** + * Issue read request to prepare for post-boot detection. + */ + private void requestReadForPostBootDetection() { + mWifiSystemInfoStats.readFromMemory(); + // Potential SW change detection may require to update all networks. + // Thus read all networks. + requestReadAllNetworks(); + } + + /** + * Helper method to populate WifiScanner handle. This is done lazily because + * WifiScanningService is started after WifiService. + */ + private void retrieveWifiScanner() { + if (mScanner != null) return; + mScanner = mWifiInjector.getWifiScanner(); + if (mScanner == null) return; + // Register for all single scan results + mScanner.registerScanListener(new ScanListener()); + } + + /** + * Handle scan results when scan results come back from WiFi scanner. + */ + private void handleScanResults(List scanDetails) { + ScanStats scanStats = mWifiSystemInfoStats.getCurrScanStats(); + scanStats.clear(); + scanStats.setLastScanTimeMs(mClock.getWallClockMillis()); + for (ScanDetail scanDetail : scanDetails) { + ScanResult scanResult = scanDetail.getScanResult(); + if (scanResult.is24GHz()) { + scanStats.incrementNumBssidLastScan2g(); + } else { + scanStats.incrementNumBssidLastScanAbove2g(); + } + } + if (mFirstScanStats.getLastScanTimeMs() == TS_NONE) { + mFirstScanStats.copy(scanStats); + } + mWifiSystemInfoStats.setChanged(true); + logd(" 2G scanResult count: " + scanStats.getNumBssidLastScan2g() + + ", Above2g scanResult count: " + scanStats.getNumBssidLastScanAbove2g()); + } + + private void setDailyDetectionAlarm() { + if (DAILY_DETECTION_ALARM_TYPE == 0) { + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mClock.getElapsedSinceBootMillis() + DAILY_DETECTION_INTERVAL_MS, + DAILY_DETECTION_TIMER_TAG, + mDailyDetectionListener, mHandler); + } else { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(System.currentTimeMillis()); + calendar.set(Calendar.HOUR_OF_DAY, DAILY_DETECTION_HOUR); + calendar.set(Calendar.MINUTE, 0); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, + calendar.getTimeInMillis(), + DAILY_DETECTION_TIMER_TAG, + mDailyDetectionListener, mHandler); + } + } + + private void setPostBootDetectionAlarm() { + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mClock.getElapsedSinceBootMillis() + POST_BOOT_DETECTION_WAIT_TIME_MS, + POST_BOOT_DETECTION_TIMER_TAG, + mPostBootDetectionListener, mHandler); + } + + private void dailyDetectionHandler() { + logd("Run daily detection"); + // Clear daily detection result + mFailureStatsDecrease.clear(); + mFailureStatsIncrease.clear(); + mFailureStatsHigh.clear(); + int connectionDurationSec = 0; + // Set the alarm for the next day + setDailyDetectionAlarm(); + int numNetworkSufficientRecentStatsOnly = 0; + int numNetworkSufficientRecentPrevStats = 0; + List configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); + for (WifiConfiguration network : configuredNetworks) { + if (isInValidConfiguredNetwork(network)) { + continue; + } + logd(network.SSID); + PerNetwork perNetwork = mWifiScoreCard.lookupNetwork(network.SSID); + logd("before daily update: " + perNetwork.toString()); + + int detectionFlag = perNetwork.dailyDetection(mFailureStatsDecrease, + mFailureStatsIncrease, mFailureStatsHigh); + if (detectionFlag == WifiScoreCard.SUFFICIENT_RECENT_STATS_ONLY) { + numNetworkSufficientRecentStatsOnly++; + } + if (detectionFlag == WifiScoreCard.SUFFICIENT_RECENT_PREV_STATS) { + numNetworkSufficientRecentPrevStats++; + } + connectionDurationSec += perNetwork.getRecentStats().getCount( + WifiScoreCard.CNT_CONNECTION_DURATION_SEC); + // Update historical stats with dailyStats and clear dailyStats + perNetwork.updateAfterDailyDetection(); + logd("after daily update: " + perNetwork.toString()); + } + logd("total connection duration: " + connectionDurationSec); + logd("#networks w/ sufficient recent stats: " + numNetworkSufficientRecentStatsOnly); + logd("#networks w/ sufficient recent/prev stats: " + numNetworkSufficientRecentPrevStats); + // TODO: Report numNetworkSufficientRecentStatsOnly, numNetworkSufficientRecentPrevStats + // mFailureStatsDecrease, mFailureStatsIncrease and mFailureStatsHigh to metrics + doWrites(); + mWifiScoreCard.doWrites(); + } + + private boolean isInValidConfiguredNetwork(WifiConfiguration config) { + return (config == null || WifiManager.UNKNOWN_SSID.equals(config.SSID) + || config.SSID == null); + } + + private void postBootDetectionHandler() { + logd("Run post-boot detection"); + postBootSwBuildCheck(); + mWifiSystemInfoStats.postBootAbnormalScanDetection(mFirstScanStats); + logd(" postBootAbnormalScanDetection: " + mWifiSystemInfoStats.getScanFailure()); + // TODO: Check if scan is not empty but all high RSSI connection attempts failed + // while connection attempt with the same network succeeded before boot. + doWrites(); + } + + private void postBootSwBuildCheck() { + WifiSoftwareBuildInfo currSoftwareBuildInfo = extractCurrentSoftwareBuildInfo(); + if (currSoftwareBuildInfo == null) return; + logd(currSoftwareBuildInfo.toString()); + + mWifiSystemInfoStats.finishPendingRead(); + if (mWifiSystemInfoStats.getCurrSoftwareBuildInfo() == null) { + logd("Miss current software build info from memory"); + mWifiSystemInfoStats.setCurrSoftwareBuildInfo(currSoftwareBuildInfo); + return; + } + if (mWifiSystemInfoStats.detectSwBuildChange(currSoftwareBuildInfo)) { + logd("Detect SW build change"); + updateAllNetworkAfterSwBuildChange(); + mWifiSystemInfoStats.updateBuildInfoAfterSwBuildChange(currSoftwareBuildInfo); + } else { + logd("Detect no SW build change"); + } + } + + /** + * Issue NetworkStats read request for all configured networks. + */ + private void requestReadAllNetworks() { + List configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); + for (WifiConfiguration network : configuredNetworks) { + if (isInValidConfiguredNetwork(network)) { + continue; + } + logd(network.SSID); + WifiScoreCard.PerNetwork perNetwork = mWifiScoreCard.fetchByNetwork(network.SSID); + if (perNetwork == null) { + // This network is not in cache. Move it to cache and read it out from MemoryStore. + mWifiScoreCard.lookupNetwork(network.SSID); + } else { + // This network is already in cache before memoryStore is stalled. + mWifiScoreCard.requestReadNetwork(perNetwork); + } + } + } + + /** + * Update NetworkStats of all configured networks after a SW build change is detected + */ + private void updateAllNetworkAfterSwBuildChange() { + List configuredNetworks = mWifiConfigManager.getConfiguredNetworks(); + for (WifiConfiguration network : configuredNetworks) { + if (isInValidConfiguredNetwork(network)) { + continue; + } + logd(network.SSID); + WifiScoreCard.PerNetwork perNetwork = mWifiScoreCard.lookupNetwork(network.SSID); + + logd("before SW build update: " + perNetwork.toString()); + perNetwork.updateAfterSwBuildChange(); + logd("after SW build update: " + perNetwork.toString()); + } + } + + /** + * Extract current software build information from the running software. + */ + private WifiSoftwareBuildInfo extractCurrentSoftwareBuildInfo() { + PackageManager packageManager = mContext.getPackageManager(); + int wifiStackVersion = 0; + try { + wifiStackVersion = packageManager.getPackageInfo( + WIFI_APK_PACKAGE_NAME, PackageManager.MATCH_APEX).versionCode; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, " hit PackageManager nameNotFoundException"); + } + String osBuildVersion = replaceNullByEmptyString(Build.DISPLAY); + if (mWifiNative == null) { + return null; + } + String driverVersion = replaceNullByEmptyString(mWifiNative.getDriverVersion()); + String firmwareVersion = replaceNullByEmptyString(mWifiNative.getFirmwareVersion()); + return (new WifiSoftwareBuildInfo(osBuildVersion, + wifiStackVersion, driverVersion, firmwareVersion)); + } + + private String replaceNullByEmptyString(String str) { + return str == null ? "" : str; + } + + /** + * Clears the internal state. + * This is called in response to a factoryReset call from Settings. + */ + public void clear() { + mWifiSystemInfoStats.clearAll(); + } + + public static final int REASON_ASSOC_REJECTION = 0; + public static final int REASON_ASSOC_TIMEOUT = 1; + public static final int REASON_AUTH_FAILURE = 2; + public static final int REASON_CONNECTION_FAILURE = 3; + public static final int REASON_DISCONNECTION_NONLOCAL = 4; + public static final int REASON_SHORT_CONNECTION_NONLOCAL = 5; + public static final int NUMBER_FAILURE_REASON_CODE = 6; + @IntDef(prefix = { "REASON_" }, value = { + REASON_ASSOC_REJECTION, + REASON_ASSOC_TIMEOUT, + REASON_AUTH_FAILURE, + REASON_CONNECTION_FAILURE, + REASON_DISCONNECTION_NONLOCAL, + REASON_SHORT_CONNECTION_NONLOCAL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FailureReasonCode {} + + /** + * A class maintaining the number of networks with high failure rate or + * with a significant change of failure rate + */ + public static class FailureStats { + private final int[] mCount = new int[NUMBER_FAILURE_REASON_CODE]; + void clear() { + for (int i = 0; i < NUMBER_FAILURE_REASON_CODE; i++) { + mCount[i] = 0; + } + } + + int getCount(@FailureReasonCode int reasonCode) { + return mCount[reasonCode]; + } + + void setCount(@FailureReasonCode int reasonCode, int cnt) { + mCount[reasonCode] = cnt; + } + + void incrementCount(@FailureReasonCode int reasonCode) { + mCount[reasonCode]++; + } + } + /** + * A class maintaining current OS, Wifi APK, Wifi driver and firmware build version information. + */ + final class WifiSoftwareBuildInfo { + private String mOsBuildVersion; + private int mWifiStackVersion; + private String mWifiDriverVersion; + private String mWifiFirmwareVersion; + WifiSoftwareBuildInfo(@NonNull String osBuildVersion, int wifiStackVersion, + @NonNull String wifiDriverVersion, @NonNull String wifiFirmwareVersion) { + mOsBuildVersion = osBuildVersion; + mWifiStackVersion = wifiStackVersion; + mWifiDriverVersion = wifiDriverVersion; + mWifiFirmwareVersion = wifiFirmwareVersion; + } + WifiSoftwareBuildInfo(@NonNull WifiSoftwareBuildInfo wifiSoftwareBuildInfo) { + mOsBuildVersion = wifiSoftwareBuildInfo.getOsBuildVersion(); + mWifiStackVersion = wifiSoftwareBuildInfo.getWifiStackVersion(); + mWifiDriverVersion = wifiSoftwareBuildInfo.getWifiDriverVersion(); + mWifiFirmwareVersion = wifiSoftwareBuildInfo.getWifiFirmwareVersion(); + } + String getOsBuildVersion() { + return mOsBuildVersion; + } + int getWifiStackVersion() { + return mWifiStackVersion; + } + String getWifiDriverVersion() { + return mWifiDriverVersion; + } + String getWifiFirmwareVersion() { + return mWifiFirmwareVersion; + } + @Override + public boolean equals(Object otherObj) { + if (this == otherObj) { + return true; + } + if (!(otherObj instanceof WifiSoftwareBuildInfo)) { + return false; + } + if (otherObj == null) { + return false; + } + WifiSoftwareBuildInfo other = (WifiSoftwareBuildInfo) otherObj; + return mOsBuildVersion.equals(other.getOsBuildVersion()) + && mWifiStackVersion == other.getWifiStackVersion() + && mWifiDriverVersion.equals(other.getWifiDriverVersion()) + && mWifiFirmwareVersion.equals(other.getWifiFirmwareVersion()); + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("OS build version: "); + sb.append(mOsBuildVersion); + sb.append(" Wifi stack version: "); + sb.append(mWifiStackVersion); + sb.append(" Wifi driver version: "); + sb.append(mWifiDriverVersion); + sb.append(" Wifi firmware version: "); + sb.append(mWifiFirmwareVersion); + return sb.toString(); + } + } + + /** + * A class maintaining various WiFi system information and statistics. + */ + final class WifiSystemInfoStats extends MemoryStoreAccessBase { + private WifiSoftwareBuildInfo mCurrSoftwareBuildInfo; + private WifiSoftwareBuildInfo mPrevSoftwareBuildInfo; + private ScanStats mCurrScanStats = new ScanStats(); + private ScanStats mPrevScanStats = new ScanStats(); + private int mScanFailure; + private @DeviceMobilityState int mMobilityState; + private boolean mChanged = false; + WifiSystemInfoStats(String l2KeySeed) { + super(WifiScoreCard.computeHashLong( + "", MacAddress.fromString(DEFAULT_MAC_ADDRESS), l2KeySeed)); + } + + ScanStats getCurrScanStats() { + return mCurrScanStats; + } + + void setChanged(boolean changed) { + mChanged = changed; + } + + void setCurrSoftwareBuildInfo(WifiSoftwareBuildInfo currSoftwareBuildInfo) { + mCurrSoftwareBuildInfo = currSoftwareBuildInfo; + mChanged = true; + } + + void setMobilityState(@DeviceMobilityState int mobilityState) { + mMobilityState = mobilityState; + } + + WifiSoftwareBuildInfo getCurrSoftwareBuildInfo() { + return mCurrSoftwareBuildInfo; + } + + WifiSoftwareBuildInfo getPrevSoftwareBuildInfo() { + return mPrevSoftwareBuildInfo; + } + + void clearAll() { + mCurrSoftwareBuildInfo = null; + mPrevSoftwareBuildInfo = null; + mCurrScanStats.clear(); + mPrevScanStats.clear(); + mChanged = true; + } + + /** + * Detect if there is a SW build change by comparing current SW build version vs. SW build + * version previously saved in MemoryStore. + * @param currSoftwareBuildInfo is current SW build info derived from running SW + * @return true if a SW build change is detected, false if no change is detected. + */ + boolean detectSwBuildChange(@NonNull WifiSoftwareBuildInfo currSoftwareBuildInfo) { + if (mCurrSoftwareBuildInfo == null) { + return false; + } + + logd(" from Memory: " + mCurrSoftwareBuildInfo.toString()); + logd(" from SW: " + currSoftwareBuildInfo.toString()); + return (!mCurrSoftwareBuildInfo.equals(currSoftwareBuildInfo)); + } + + void updateBuildInfoAfterSwBuildChange(@NonNull WifiSoftwareBuildInfo currBuildInfo) { + mPrevSoftwareBuildInfo = new WifiSoftwareBuildInfo(mCurrSoftwareBuildInfo); + mCurrSoftwareBuildInfo = new WifiSoftwareBuildInfo(currBuildInfo); + mChanged = true; + } + + void readFromMemory() { + if (mMemoryStore != null) { + mMemoryStore.read(getL2Key(), SYSTEM_INFO_DATA_NAME, + (value) -> readBackListener(value)); + } + } + + // Read may not be completed in theory when finishPendingRead() is called. + // Currently it relies on the fact that memory read is issued right after boot complete + // while finishPendingRead() is called only POST_BOOT_DETECTION_WAIT_TIME_MS after that. + private void finishPendingRead() { + final byte[] serialized = finishPendingReadBytes(); + if (serialized == null) { + logd("Fail to read systemInfoStats from memory"); + return; + } + SystemInfoStats systemInfoStats; + try { + systemInfoStats = SystemInfoStats.parseFrom(serialized); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Failed to deserialize", e); + return; + } + readFromMemory(systemInfoStats); + } + + private void readFromMemory(@NonNull SystemInfoStats systemInfoStats) { + if (systemInfoStats.hasCurrSoftwareBuildInfo()) { + mCurrSoftwareBuildInfo = fromSoftwareBuildInfo( + systemInfoStats.getCurrSoftwareBuildInfo()); + } + if (systemInfoStats.hasPrevSoftwareBuildInfo()) { + mPrevSoftwareBuildInfo = fromSoftwareBuildInfo( + systemInfoStats.getPrevSoftwareBuildInfo()); + } + if (systemInfoStats.hasNumBssidLastScan2G()) { + mPrevScanStats.setNumBssidLastScan2g(systemInfoStats.getNumBssidLastScan2G()); + } + if (systemInfoStats.hasNumBssidLastScanAbove2G()) { + mPrevScanStats.setNumBssidLastScanAbove2g(systemInfoStats + .getNumBssidLastScanAbove2G()); + } + if (systemInfoStats.hasLastScanTimeMs()) { + mPrevScanStats.setLastScanTimeMs(systemInfoStats.getLastScanTimeMs()); + } + } + + void writeToMemory() { + if (mMemoryStore == null || !mChanged) return; + byte[] serialized = toSystemInfoStats().toByteArray(); + mMemoryStore.write(getL2Key(), SYSTEM_INFO_DATA_NAME, serialized); + mChanged = false; + } + + SystemInfoStats toSystemInfoStats() { + SystemInfoStats.Builder builder = SystemInfoStats.newBuilder(); + if (mCurrSoftwareBuildInfo != null) { + builder.setCurrSoftwareBuildInfo(toSoftwareBuildInfo(mCurrSoftwareBuildInfo)); + } + if (mPrevSoftwareBuildInfo != null) { + builder.setPrevSoftwareBuildInfo(toSoftwareBuildInfo(mPrevSoftwareBuildInfo)); + } + builder.setLastScanTimeMs(mCurrScanStats.getLastScanTimeMs()); + builder.setNumBssidLastScan2G(mCurrScanStats.getNumBssidLastScan2g()); + builder.setNumBssidLastScanAbove2G(mCurrScanStats.getNumBssidLastScanAbove2g()); + return builder.build(); + } + + private SoftwareBuildInfo toSoftwareBuildInfo( + @NonNull WifiSoftwareBuildInfo softwareBuildInfo) { + SoftwareBuildInfo.Builder builder = SoftwareBuildInfo.newBuilder(); + builder.setOsBuildVersion(softwareBuildInfo.getOsBuildVersion()); + builder.setWifiStackVersion(softwareBuildInfo.getWifiStackVersion()); + builder.setWifiDriverVersion(softwareBuildInfo.getWifiDriverVersion()); + builder.setWifiFirmwareVersion(softwareBuildInfo.getWifiFirmwareVersion()); + return builder.build(); + } + + WifiSoftwareBuildInfo fromSoftwareBuildInfo( + @NonNull SoftwareBuildInfo softwareBuildInfo) { + String osBuildVersion = softwareBuildInfo.hasOsBuildVersion() + ? softwareBuildInfo.getOsBuildVersion() : "NA"; + int stackVersion = softwareBuildInfo.hasWifiStackVersion() + ? softwareBuildInfo.getWifiStackVersion() : 0; + String driverVersion = softwareBuildInfo.hasWifiDriverVersion() + ? softwareBuildInfo.getWifiDriverVersion() : "NA"; + String firmwareVersion = softwareBuildInfo.hasWifiFirmwareVersion() + ? softwareBuildInfo.getWifiFirmwareVersion() : "NA"; + return new WifiSoftwareBuildInfo(osBuildVersion, stackVersion, + driverVersion, firmwareVersion); + } + /** + * Detect pre-boot or post-boot detection failure. + * @return 0 if no failure is found or a positive integer if failure is found where + * b0 for pre-boot 2G scan failure + * b1 for pre-boot Above2g scan failure + * b2 for post-boot 2G scan failure + * b3 for post-boot Above2g scan failure + */ + void postBootAbnormalScanDetection(ScanStats firstScanStats) { + long preBootScanTimeMs = mPrevScanStats.getLastScanTimeMs(); + long postBootScanTimeMs = firstScanStats.getLastScanTimeMs(); + logd(" preBootScanTimeMs: " + preBootScanTimeMs); + logd(" postBootScanTimeMs: " + postBootScanTimeMs); + int preBootNumBssid2g = mPrevScanStats.getNumBssidLastScan2g(); + int preBootNumBssidAbove2g = mPrevScanStats.getNumBssidLastScanAbove2g(); + int postBootNumBssid2g = firstScanStats.getNumBssidLastScan2g(); + int postBootNumBssidAbove2g = firstScanStats.getNumBssidLastScanAbove2g(); + logd(" preBootScan 2G count: " + preBootNumBssid2g + + ", Above2G count: " + preBootNumBssidAbove2g); + logd(" postBootScan 2G count: " + postBootNumBssid2g + + ", Above2G count: " + postBootNumBssidAbove2g); + mScanFailure = 0; + if (postBootScanTimeMs == TS_NONE || preBootScanTimeMs == TS_NONE) return; + if ((postBootScanTimeMs - preBootScanTimeMs) > MAX_INTERVAL_BETWEEN_TWO_SCAN_MS) { + return; + } + if (preBootNumBssid2g == 0 && postBootNumBssid2g >= MIN_NUM_BSSID_SCAN_2G) { + mScanFailure += 1; + } + if (preBootNumBssidAbove2g == 0 && postBootNumBssidAbove2g + >= MIN_NUM_BSSID_SCAN_ABOVE_2G) { + mScanFailure += 2; + } + if (postBootNumBssid2g == 0 && preBootNumBssid2g >= MIN_NUM_BSSID_SCAN_2G) { + mScanFailure += 4; + } + if (postBootNumBssidAbove2g == 0 && preBootNumBssidAbove2g + >= MIN_NUM_BSSID_SCAN_ABOVE_2G) { + mScanFailure += 8; + } + } + + int getScanFailure() { + return mScanFailure; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (mCurrSoftwareBuildInfo != null) { + sb.append("current SW build: "); + sb.append(mCurrSoftwareBuildInfo.toString()); + } + if (mPrevSoftwareBuildInfo != null) { + sb.append(" previous SW build: "); + sb.append(mPrevSoftwareBuildInfo.toString()); + } + sb.append(" currScanStats: "); + sb.append(mCurrScanStats.toString()); + sb.append(" prevScanStats: "); + sb.append(mPrevScanStats.toString()); + return sb.toString(); + } + } + + final class ScanStats { + private long mLastScanTimeMs = TS_NONE; + private int mNumBssidLastScan2g; + private int mNumBssidLastScanAbove2g; + void copy(ScanStats source) { + mLastScanTimeMs = source.mLastScanTimeMs; + mNumBssidLastScan2g = source.mNumBssidLastScan2g; + mNumBssidLastScanAbove2g = source.mNumBssidLastScanAbove2g; + } + void setLastScanTimeMs(long lastScanTimeMs) { + mLastScanTimeMs = lastScanTimeMs; + } + void setNumBssidLastScan2g(int numBssidLastScan2g) { + mNumBssidLastScan2g = numBssidLastScan2g; + } + void setNumBssidLastScanAbove2g(int numBssidLastScanAbove2g) { + mNumBssidLastScanAbove2g = numBssidLastScanAbove2g; + } + long getLastScanTimeMs() { + return mLastScanTimeMs; + } + int getNumBssidLastScan2g() { + return mNumBssidLastScan2g; + } + int getNumBssidLastScanAbove2g() { + return mNumBssidLastScanAbove2g; + } + void incrementNumBssidLastScan2g() { + mNumBssidLastScan2g++; + } + void incrementNumBssidLastScanAbove2g() { + mNumBssidLastScanAbove2g++; + } + void clear() { + mLastScanTimeMs = TS_NONE; + mNumBssidLastScan2g = 0; + mNumBssidLastScanAbove2g = 0; + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("last scan time: "); + sb.append(mLastScanTimeMs); + sb.append(" APs found at 2G: "); + sb.append(mNumBssidLastScan2g); + sb.append(" APs found above 2g: "); + sb.append(mNumBssidLastScanAbove2g); + return sb.toString(); + } + } + + @VisibleForTesting + WifiSystemInfoStats getWifiSystemInfoStats() { + return mWifiSystemInfoStats; + } + + private void logd(String string) { + if (mVerboseLoggingEnabled) { + Log.d(TAG, string); + } + } + + /** + * Listener for config manager network config related events. + */ + private class OnNetworkUpdateListener implements + WifiConfigManager.OnNetworkUpdateListener { + + @Override + public void onNetworkAdded(WifiConfiguration config) { + if (config == null) return; + mWifiScoreCard.lookupNetwork(config.SSID); + } + + @Override + public void onNetworkEnabled(WifiConfiguration config) { + } + + @Override + public void onNetworkPermanentlyDisabled(WifiConfiguration config, int disableReason) { + } + + @Override + public void onNetworkRemoved(WifiConfiguration config) { + if (config == null) return; + mWifiScoreCard.removeNetwork(config.SSID); + } + + @Override + public void onNetworkTemporarilyDisabled(WifiConfiguration config, int disableReason) { + } + + @Override + public void onNetworkUpdated(WifiConfiguration config) { + } + } + + /** + * Scan listener for any full band scan. + */ + private class ScanListener implements WifiScanner.ScanListener { + private List mScanDetails = new ArrayList(); + + public void clearScanDetails() { + mScanDetails.clear(); + } + + @Override + public void onSuccess() { + } + + @Override + public void onFailure(int reason, String description) { + logd("registerScanListener onFailure:" + + " reason: " + reason + " description: " + description); + } + + @Override + public void onPeriodChanged(int periodInMs) { + } + + @Override + public void onResults(WifiScanner.ScanData[] results) { + if (!mWifiEnabled) { + clearScanDetails(); + return; + } + + boolean isFullBandScanResults = + results[0].getBandScanned() == WifiScanner.WIFI_BAND_BOTH_WITH_DFS + || results[0].getBandScanned() == WifiScanner.WIFI_BAND_BOTH; + if (isFullBandScanResults) { + handleScanResults(mScanDetails); + } + clearScanDetails(); + } + + @Override + public void onFullResult(ScanResult fullScanResult) { + if (!mWifiEnabled) { + return; + } + mScanDetails.add(ScanResultUtil.toScanDetail(fullScanResult)); + } + } +} diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java index 3fbd58ed5..51436eaad 100644 --- a/service/java/com/android/server/wifi/WifiInjector.java +++ b/service/java/com/android/server/wifi/WifiInjector.java @@ -157,6 +157,7 @@ public class WifiInjector { private final ConnectionFailureNotificationBuilder mConnectionFailureNotificationBuilder; private final ThroughputPredictor mThroughputPredictor; private NetdWrapper mNetdWrapper; + private final WifiHealthMonitor mWifiHealthMonitor; public WifiInjector(Context context) { if (context == null) { @@ -185,8 +186,6 @@ public class WifiInjector { mConnectionFailureNotificationBuilder = new ConnectionFailureNotificationBuilder( mContext, getWifiStackPackageName(), mFrameworkFacade); mBatteryStats = context.getSystemService(BatteryStatsManager.class); - mWifiScoreCard = new WifiScoreCard(mClock, - Secure.getString(mContext.getContentResolver(), Secure.ANDROID_ID)); mSettingsStore = new WifiSettingsStore(mContext); mWifiPermissionsWrapper = new WifiPermissionsWrapper(mContext); mNetworkScoreManager = mContext.getSystemService(NetworkScoreManager.class); @@ -260,6 +259,8 @@ public class WifiInjector { new NetworkListUserStoreData(mContext), new DeletedEphemeralSsidsStoreData(mClock), new RandomizedMacStoreData(), mFrameworkFacade, wifiHandler, mDeviceConfigFacade); + String l2KeySeed = Secure.getString(mContext.getContentResolver(), Secure.ANDROID_ID); + mWifiScoreCard = new WifiScoreCard(mClock, l2KeySeed); mWifiMetrics.setWifiConfigManager(mWifiConfigManager); mWifiApConfigStore = new WifiApConfigStore( @@ -321,6 +322,8 @@ public class WifiInjector { SupplicantStateTracker supplicantStateTracker = new SupplicantStateTracker( mContext, mWifiConfigManager, mBatteryStats, wifiHandler); mMboOceController = new MboOceController(makeTelephonyManager(), mWifiNative); + mWifiHealthMonitor = new WifiHealthMonitor(mContext, this, mClock, mWifiConfigManager, + mWifiScoreCard, wifiHandler, mWifiNative, l2KeySeed, mDeviceConfigFacade); mClientModeImpl = new ClientModeImpl(mContext, mFrameworkFacade, wifiLooper, mUserManager, this, mBackupManagerProxy, mCountryCode, mWifiNative, @@ -790,4 +793,8 @@ public class WifiInjector { public WifiCondManager getWifiCondManager() { return mWifiCondManager; } + + public WifiHealthMonitor getWifiHealthMonitor() { + return mWifiHealthMonitor; + } } diff --git a/service/java/com/android/server/wifi/WifiNetworkSelector.java b/service/java/com/android/server/wifi/WifiNetworkSelector.java index fec8e0dde..3d3a48147 100644 --- a/service/java/com/android/server/wifi/WifiNetworkSelector.java +++ b/service/java/com/android/server/wifi/WifiNetworkSelector.java @@ -655,7 +655,7 @@ public class WifiNetworkSelector { } /** - * Select the best network from the ones in range. + * Select the best network from the ones in range. Scan detail cache is also updated here. * * @param scanDetails List of ScanDetail for all the APs in range * @param bssidBlacklist Blacklisted BSSIDs @@ -685,6 +685,9 @@ public class WifiNetworkSelector { // Shall we start network selection at all? if (!isNetworkSelectionNeeded(scanDetails, wifiInfo, connected, disconnected)) { + // If network selection is skipped, update scan detail cache before exit. + // Otherwise, scan detail cache will be updated in each nominator. + updateScanDetailCache(scanDetails); return null; } @@ -700,6 +703,9 @@ public class WifiNetworkSelector { mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist, connected && wifiInfo.getScore() >= WIFI_POOR_SCORE, currentBssid); if (mFilteredNetworks.size() == 0) { + // If network selection is skipped, update scan detail cache before exit. + // Otherwise, scan detail cache will be updated in each nominator. + updateScanDetailCache(scanDetails); return null; } @@ -831,6 +837,12 @@ public class WifiNetworkSelector { } } + private void updateScanDetailCache(List scanDetails) { + for (ScanDetail scanDetail : scanDetails) { + mWifiConfigManager.updateScanDetailCacheFromScanDetail(scanDetail); + } + } + private static int toProtoNominatorId(@NetworkNominator.NominatorId int nominatorId) { switch (nominatorId) { case NetworkNominator.NOMINATOR_ID_SAVED: diff --git a/service/java/com/android/server/wifi/WifiScoreCard.java b/service/java/com/android/server/wifi/WifiScoreCard.java index cc5f1285f..4b15a97c3 100644 --- a/service/java/com/android/server/wifi/WifiScoreCard.java +++ b/service/java/com/android/server/wifi/WifiScoreCard.java @@ -18,7 +18,19 @@ package com.android.server.wifi; import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; import static android.net.wifi.WifiInfo.INVALID_RSSI; - +import static android.net.wifi.WifiInfo.LINK_SPEED_UNKNOWN; + +import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_COUNT_MIN_TX_RATE; +import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_COUNT_RSSI_MIN_DBM; +import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS; +import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_REJECTION; +import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_TIMEOUT; +import static com.android.server.wifi.WifiHealthMonitor.REASON_AUTH_FAILURE; +import static com.android.server.wifi.WifiHealthMonitor.REASON_CONNECTION_FAILURE; +import static com.android.server.wifi.WifiHealthMonitor.REASON_DISCONNECTION_NONLOCAL; +import static com.android.server.wifi.WifiHealthMonitor.REASON_SHORT_CONNECTION_NONLOCAL; + +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.MacAddress; @@ -31,12 +43,16 @@ import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.server.wifi.BssidBlocklistMonitor.FailureReason; +import com.android.server.wifi.WifiHealthMonitor.FailureStats; import com.android.server.wifi.proto.WifiScoreCardProto; import com.android.server.wifi.proto.WifiScoreCardProto.AccessPoint; +import com.android.server.wifi.proto.WifiScoreCardProto.ConnectionStats; import com.android.server.wifi.proto.WifiScoreCardProto.Event; import com.android.server.wifi.proto.WifiScoreCardProto.HistogramBucket; import com.android.server.wifi.proto.WifiScoreCardProto.Network; import com.android.server.wifi.proto.WifiScoreCardProto.NetworkList; +import com.android.server.wifi.proto.WifiScoreCardProto.NetworkStats; import com.android.server.wifi.proto.WifiScoreCardProto.SecurityType; import com.android.server.wifi.proto.WifiScoreCardProto.Signal; import com.android.server.wifi.proto.WifiScoreCardProto.UnivariateStatistic; @@ -46,6 +62,8 @@ import com.android.server.wifi.util.NativeUtil; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -58,10 +76,10 @@ import javax.annotation.concurrent.NotThreadSafe; /** * Retains statistical information about the performance of various - * access points, as experienced by this device. + * access points and networks, as experienced by this device. * * The purpose is to better inform future network selection and switching - * by this device. + * by this device and help health monitor detect network issues. */ @NotThreadSafe public class WifiScoreCard { @@ -69,12 +87,50 @@ public class WifiScoreCard { public static final String DUMP_ARG = "WifiScoreCard"; private static final String TAG = "WifiScoreCard"; - private static final boolean DBG = false; + private boolean mVerboseLoggingEnabled = false; @VisibleForTesting boolean mPersistentHistograms = true; private static final int TARGET_IN_MEMORY_ENTRIES = 50; + private static final int UNKNOWN_REASON = -1; + + public static final String PER_BSSID_DATA_NAME = "scorecard.proto"; + public static final String PER_NETWORK_DATA_NAME = "perNetworkData"; + // Maximum connection duration in seconds to qualify short connection + private static final int SHORT_CONNECTION_DURATION_MAX_SEC = 20; + // Maximum interval between last RSSI poll and disconnection to qualify + // disconnection stats collection. + private static final int LAST_RSSI_POLL_MAX_INTERVAL_MS = 3_100; + // Minimum number of connection attempts to qualify daily detection + @VisibleForTesting + static final int MIN_NUM_CONNECTION_ATTEMPT = 20; + // Minimum number of connection attempts for historical data + private static final int MIN_NUM_CONNECTION_ATTEMPT_PREV = 40; + private static final int MIN_NUM_DISCONNECTION = 20; + private static final int MIN_NUM_DISCONNECTION_PREV = 40; + + static final int INSUFFICIENT_RECENT_STATS = 0; + static final int SUFFICIENT_RECENT_STATS_ONLY = 1; + static final int SUFFICIENT_RECENT_PREV_STATS = 2; + + // High and low threshold values for connection failure rate. All of them are in percent with + // respect to connection attempts + private static final int ONE_HUNDRED_PERCENT = 100; + private static final int CONNECTION_FAILURE_PERCENT_HIGH_THRESHOLD = 30; + private static final int CONNECTION_FAILURE_PERCENT_LOW_THRESHOLD = 5; + private static final int ASSOC_REJECTION_PERCENT_HIGH_THRESHOLD = 10; + private static final int ASSOC_REJECTION_PERCENT_LOW_THRESHOLD = 1; + private static final int ASSOC_TIMEOUT_PERCENT_HIGH_THRESHOLD = 10; + private static final int ASSOC_TIMEOUT_PERCENT_LOW_THRESHOLD = 2; + private static final int AUTH_FAILURE_PERCENT_HIGH_THRESHOLD = 10; + private static final int AUTH_FAILURE_PERCENT_LOW_THRESHOLD = 2; + // High and low threshold values for non-local disconnection rate at high RSSI or high Tx speed + // with respect to CNT_DISCONNECTION count (with a recent RSSI poll) + private static final int SHORT_CONNECTION_NONLOCAL_PERCENT_HIGH_THRESHOLD = 10; + private static final int SHORT_CONNECTION_NONLOCAL_PERCENT_LOW_THRESHOLD = 1; + private static final int DISCONNECTION_NONLOCAL_PERCENT_HIGH_THRESHOLD = 15; + private static final int DISCONNECTION_NONLOCAL_PERCENT_LOW_THRESHOLD = 1; private final Clock mClock; private final String mL2KeySeed; @@ -94,9 +150,9 @@ public class WifiScoreCard { /** Our view of the memory store */ public interface MemoryStore { /** Requests a read, with asynchronous reply */ - void read(String key, BlobListener blobListener); + void read(String key, String name, BlobListener blobListener); /** Requests a write, does not wait for completion */ - void write(String key, byte[] value); + void write(String key, String name, byte[] value); } /** Asynchronous response to a read request */ public interface BlobListener { @@ -131,6 +187,15 @@ public class WifiScoreCard { } } + /** + * Enable/Disable verbose logging. + * + * @param verbose true to enable and false to disable. + */ + public void enableVerboseLogging(boolean verbose) { + mVerboseLoggingEnabled = verbose; + } + /** * Timestamp of the start of the most recent connection attempt. * @@ -140,7 +205,8 @@ public class WifiScoreCard { * Any negative value means we are not currently connected. */ private long mTsConnectionAttemptStart = TS_NONE; - private static final long TS_NONE = -1; + @VisibleForTesting + static final long TS_NONE = -1; /** * Timestamp captured when we find out about a firmware roam @@ -164,6 +230,22 @@ public class WifiScoreCard { */ private boolean mAttemptingSwitch = false; + /** + * SSID of currently connected or connecting network. Used during disconnection + */ + private String mSsidCurr = ""; + /** + * SSID of previously connected network. Used during disconnection when connection attempt + * of current network is issued before the disconnection of previous network. + */ + private String mSsidPrev = ""; + /** + * A flag that notes that current disconnection is not generated by wpa_supplicant + * which may indicate abnormal disconnection. + */ + private boolean mNonlocalDisconnection = false; + private int mDisconnectionReason; + /** * @param clock is the time source * @param l2KeySeed is for making our L2Keys usable only on this device @@ -172,6 +254,7 @@ public class WifiScoreCard { mClock = clock; mL2KeySeed = l2KeySeed; mDummyPerBssid = new PerBssid("", MacAddress.fromString(DEFAULT_MAC_ADDRESS)); + mDummyPerNetwork = new PerNetwork(""); } /** @@ -182,23 +265,27 @@ public class WifiScoreCard { if (perBssid == mDummyPerBssid) { return new Pair<>(null, null); } - final long groupIdHash = computeHashLong(perBssid.ssid, mDummyPerBssid.bssid); - return new Pair<>(perBssid.l2Key, groupHintFromLong(groupIdHash)); + final long groupIdHash = computeHashLong( + perBssid.ssid, mDummyPerBssid.bssid, mL2KeySeed); + return new Pair<>(perBssid.getL2Key(), groupHintFromLong(groupIdHash)); } /** - * Resets the connection state + * Handle network disconnection or shutdown event */ public void resetConnectionState() { - if (DBG && mTsConnectionAttemptStart > TS_NONE && !mAttemptingSwitch) { - Log.v(TAG, "resetConnectionState", new Exception()); + String ssidDisconnected = (mAttemptingSwitch) ? mSsidPrev : mSsidCurr; + updatePerNetwork(Event.DISCONNECTION, ssidDisconnected, INVALID_RSSI, LINK_SPEED_UNKNOWN, + UNKNOWN_REASON); + if (mVerboseLoggingEnabled && mTsConnectionAttemptStart > TS_NONE && !mAttemptingSwitch) { + Log.v(TAG, "handleNetworkDisconnect", new Exception()); } resetConnectionStateInternal(true); } /** * @param calledFromResetConnectionState says the call is from outside the class, - * indicating that we need to resepect the value of mAttemptingSwitch. + * indicating that we need to respect the value of mAttemptingSwitch. */ private void resetConnectionStateInternal(boolean calledFromResetConnectionState) { if (!calledFromResetConnectionState) { @@ -210,22 +297,34 @@ public class WifiScoreCard { mTsRoam = TS_NONE; mPolled = false; mValidated = false; + mNonlocalDisconnection = false; } /** - * Updates the score card using relevant parts of WifiInfo + * Updates perBssid using relevant parts of WifiInfo * * @param wifiInfo object holding relevant values. */ - private void update(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) { + private void updatePerBssid(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) { PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); perBssid.updateEventStats(event, wifiInfo.getFrequency(), wifiInfo.getRssi(), wifiInfo.getLinkSpeed()); perBssid.setNetworkConfigId(wifiInfo.getNetworkId()); + logd("BSSID update " + event.toString() + " ID: " + perBssid.id + " " + wifiInfo); + } - if (DBG) Log.d(TAG, event.toString() + " ID: " + perBssid.id + " " + wifiInfo); + /** + * Updates perNetwork with SSID, current RSSI and failureReason. failureReason is meaningful + * only during connection failure. + */ + private void updatePerNetwork(WifiScoreCardProto.Event event, String ssid, int rssi, + int txSpeed, int failureReason) { + PerNetwork perNetwork = lookupNetwork(ssid); + logd("network update " + event.toString() + ((ssid == null) ? " " : " " + + ssid) + " ID: " + perNetwork.id + " RSSI " + rssi + " txSpeed " + txSpeed); + perNetwork.updateEventStats(event, rssi, txSpeed, failureReason); } /** @@ -233,21 +332,33 @@ public class WifiScoreCard { * * @param wifiInfo object holding relevant values */ - public void noteSignalPoll(ExtendedWifiInfo wifiInfo) { + public void noteSignalPoll(@NonNull ExtendedWifiInfo wifiInfo) { if (!mPolled && wifiInfo.getRssi() != INVALID_RSSI) { - update(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo); + updatePerBssid(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo); mPolled = true; } - update(Event.SIGNAL_POLL, wifiInfo); + updatePerBssid(Event.SIGNAL_POLL, wifiInfo); + int validTxSpeed = geTxLinkSpeedWithSufficientTxRate(wifiInfo); + updatePerNetwork(Event.SIGNAL_POLL, wifiInfo.getSSID(), wifiInfo.getRssi(), + validTxSpeed, UNKNOWN_REASON); if (mTsRoam > TS_NONE && wifiInfo.getRssi() != INVALID_RSSI) { long duration = mClock.getElapsedSinceBootMillis() - mTsRoam; if (duration >= SUCCESS_MILLIS_SINCE_ROAM) { - update(Event.ROAM_SUCCESS, wifiInfo); + updatePerBssid(Event.ROAM_SUCCESS, wifiInfo); mTsRoam = TS_NONE; - doWrites(); + doWritesBssid(); } } } + + private int geTxLinkSpeedWithSufficientTxRate(@NonNull ExtendedWifiInfo wifiInfo) { + double txRate = wifiInfo.getTxSuccessRate() + wifiInfo.getTxBadRate() + + wifiInfo.getTxRetriesRate(); + int txSpeed = wifiInfo.getTxLinkSpeedMbps(); + logd("txRate: " + txRate + " txSpeed: " + txSpeed); + return (txRate >= HEALTH_MONITOR_COUNT_MIN_TX_RATE) ? txSpeed : LINK_SPEED_UNKNOWN; + } + /** Wait a few seconds before considering the roam successful */ private static final long SUCCESS_MILLIS_SINCE_ROAM = 4_000; @@ -256,8 +367,8 @@ public class WifiScoreCard { * * @param wifiInfo object holding relevant values */ - public void noteIpConfiguration(ExtendedWifiInfo wifiInfo) { - update(Event.IP_CONFIGURATION_SUCCESS, wifiInfo); + public void noteIpConfiguration(@NonNull ExtendedWifiInfo wifiInfo) { + updatePerBssid(Event.IP_CONFIGURATION_SUCCESS, wifiInfo); mAttemptingSwitch = false; doWrites(); } @@ -267,58 +378,83 @@ public class WifiScoreCard { * * @param wifiInfo object holding relevant values */ - public void noteValidationSuccess(ExtendedWifiInfo wifiInfo) { + public void noteValidationSuccess(@NonNull ExtendedWifiInfo wifiInfo) { if (mValidated) return; // Only once per connection - update(Event.VALIDATION_SUCCESS, wifiInfo); + updatePerBssid(Event.VALIDATION_SUCCESS, wifiInfo); mValidated = true; doWrites(); } + /** + * Updates the score card after network validation failure + * + * @param wifiInfo object holding relevant values + */ + public void noteValidationFailure(@NonNull ExtendedWifiInfo wifiInfo) { + mValidated = false; + } + /** * Records the start of a connection attempt * * @param wifiInfo may have state about an existing connection + * @param scanRssi is the highest RSSI of recent scan found from scanDetailCache + * @param ssid is the network SSID of connection attempt */ - public void noteConnectionAttempt(ExtendedWifiInfo wifiInfo) { + public void noteConnectionAttempt(@NonNull ExtendedWifiInfo wifiInfo, + int scanRssi, String ssid) { // We may or may not be currently connected. If not, simply record the start. // But if we are connected, wrap up the old one first. if (mTsConnectionAttemptStart > TS_NONE) { if (mPolled) { - update(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo); + updatePerBssid(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo); } mAttemptingSwitch = true; } mTsConnectionAttemptStart = mClock.getElapsedSinceBootMillis(); mPolled = false; + mSsidPrev = mSsidCurr; + mSsidCurr = ssid; - if (DBG) Log.d(TAG, "CONNECTION_ATTEMPT" + (mAttemptingSwitch ? " X " : " ") + wifiInfo); + updatePerNetwork(Event.CONNECTION_ATTEMPT, ssid, scanRssi, LINK_SPEED_UNKNOWN, + UNKNOWN_REASON); + logd("CONNECTION_ATTEMPT" + (mAttemptingSwitch ? " X " : " ") + wifiInfo); } /** * Records a newly assigned NetworkAgent netId. */ - public void noteNetworkAgentCreated(ExtendedWifiInfo wifiInfo, int networkAgentId) { + public void noteNetworkAgentCreated(@NonNull ExtendedWifiInfo wifiInfo, int networkAgentId) { PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); - if (DBG) { - Log.d(TAG, "NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id); - } + logd("NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id); perBssid.mNetworkAgentId = networkAgentId; } + /** + * Record disconnection not initiated by wpa_supplicant in connected mode + * @param reason is detailed disconnection reason code + */ + public void noteNonlocalDisconnect(int reason) { + mNonlocalDisconnection = true; + mDisconnectionReason = reason; + logd("nonlocal disconnection with reason: " + reason); + } + /** * Updates the score card after a failed connection attempt * - * @param wifiInfo object holding relevant values + * @param wifiInfo object holding relevant values. + * @param scanRssi is the highest RSSI of recent scan found from scanDetailCache + * @param ssid is the network SSID. + * @param failureReason is connection failure reason */ - public void noteConnectionFailure(ExtendedWifiInfo wifiInfo, - int codeMetrics, int codeMetricsProto) { - if (DBG) { - Log.d(TAG, "noteConnectionFailure(..., " + codeMetrics + ", " + codeMetricsProto + ")"); - } - // TODO(b/112196799) Need to sort out the reasons better. Also, we get here - // when we disconnect from below, so it should sometimes get counted as a - // disconnection rather than a connection failure. - update(Event.CONNECTION_FAILURE, wifiInfo); + public void noteConnectionFailure(@NonNull ExtendedWifiInfo wifiInfo, + int scanRssi, String ssid, @FailureReason int failureReason) { + // TODO: add the breakdown of level2FailureReason + updatePerBssid(Event.CONNECTION_FAILURE, wifiInfo); + + updatePerNetwork(Event.CONNECTION_FAILURE, ssid, scanRssi, LINK_SPEED_UNKNOWN, + failureReason); resetConnectionStateInternal(false); } @@ -327,13 +463,14 @@ public class WifiScoreCard { * * @param wifiInfo object holding relevant values */ - public void noteIpReachabilityLost(ExtendedWifiInfo wifiInfo) { - update(Event.IP_REACHABILITY_LOST, wifiInfo); + public void noteIpReachabilityLost(@NonNull ExtendedWifiInfo wifiInfo) { + updatePerBssid(Event.IP_REACHABILITY_LOST, wifiInfo); if (mTsRoam > TS_NONE) { mTsConnectionAttemptStart = mTsRoam; // just to update elapsed - update(Event.ROAM_FAILURE, wifiInfo); + updatePerBssid(Event.ROAM_FAILURE, wifiInfo); } - resetConnectionStateInternal(false); + // No need to call resetConnectionStateInternal() because + // resetConnectionState() will be called after WifiNative.disconnect() in ClientModeImpl doWrites(); } @@ -345,8 +482,8 @@ public class WifiScoreCard { * * @param wifiInfo object holding relevant values */ - public void noteRoam(ExtendedWifiInfo wifiInfo) { - update(Event.LAST_POLL_BEFORE_ROAM, wifiInfo); + public void noteRoam(@NonNull ExtendedWifiInfo wifiInfo) { + updatePerBssid(Event.LAST_POLL_BEFORE_ROAM, wifiInfo); mTsRoam = mClock.getElapsedSinceBootMillis(); } @@ -356,10 +493,9 @@ public class WifiScoreCard { * @param wifiInfo object holding old values * @param state the new supplicant state */ - public void noteSupplicantStateChanging(ExtendedWifiInfo wifiInfo, SupplicantState state) { - if (DBG) { - Log.d(TAG, "Changing state to " + state + " " + wifiInfo); - } + public void noteSupplicantStateChanging(@NonNull ExtendedWifiInfo wifiInfo, + SupplicantState state) { + logd("Changing state to " + state + " " + wifiInfo); } /** @@ -368,9 +504,7 @@ public class WifiScoreCard { * @param wifiInfo object holding old values */ public void noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo) { - if (DBG) { - Log.d(TAG, "STATE " + wifiInfo); - } + logd("STATE " + wifiInfo); } /** @@ -378,8 +512,8 @@ public class WifiScoreCard { * * @param wifiInfo object holding relevant values */ - public void noteWifiDisabled(ExtendedWifiInfo wifiInfo) { - update(Event.WIFI_DISABLED, wifiInfo); + public void noteWifiDisabled(@NonNull ExtendedWifiInfo wifiInfo) { + updatePerBssid(Event.WIFI_DISABLED, wifiInfo); resetConnectionStateInternal(false); doWrites(); } @@ -445,9 +579,8 @@ public class WifiScoreCard { } } - final class PerBssid { + final class PerBssid extends MemoryStoreAccessBase { public int id; - public final String l2Key; public final String ssid; public final MacAddress bssid; public final int[] blocklistStreakCount = @@ -463,11 +596,10 @@ public class WifiScoreCard { private final Map, PerSignal> mSignalForEventAndFrequency = new ArrayMap<>(); PerBssid(String ssid, MacAddress bssid) { + super(computeHashLong(ssid, bssid, mL2KeySeed)); this.ssid = ssid; this.bssid = bssid; - final long hash = computeHashLong(ssid, bssid); - this.l2Key = l2KeyFromLong(hash); - this.id = idFromLong(hash); + this.id = idFromLong(); this.changed = false; this.referenced = false; } @@ -543,7 +675,7 @@ public class WifiScoreCard { if (mSecurityType == null) { mSecurityType = prev; } else if (!mSecurityType.equals(prev)) { - if (DBG) { + if (mVerboseLoggingEnabled) { Log.i(TAG, "ID: " + id + "SecurityType changed: " + prev + " to " + mSecurityType); } @@ -564,19 +696,7 @@ public class WifiScoreCard { } return this; } - String getL2Key() { - return l2Key.toString(); - } - /** - * Called when the (asynchronous) answer to a read request comes back. - */ - void lazyMerge(byte[] serialized) { - if (serialized == null) return; - byte[] old = mPendingReadFromStore.getAndSet(serialized); - if (old != null) { - Log.e(TAG, "More answers than we expected!"); - } - } + /** * Handles (when convenient) the arrival of previously stored data. * @@ -586,7 +706,7 @@ public class WifiScoreCard { * data before now, so we need to be prepared to merge the new and old together. */ void finishPendingRead() { - final byte[] serialized = mPendingReadFromStore.getAndSet(null); + final byte[] serialized = finishPendingReadBytes(); if (serialized == null) return; AccessPoint ap; try { @@ -597,9 +717,543 @@ public class WifiScoreCard { } merge(ap); } + } + + /** + * A class collecting the connection stats of one network or SSID. + */ + final class PerNetwork extends MemoryStoreAccessBase { + public int id; + public final String ssid; + public boolean changed; + private int mLastRssiPoll = INVALID_RSSI; + private int mLastTxSpeedPoll = LINK_SPEED_UNKNOWN; + private long mLastRssiPollTimeMs = TS_NONE; + private long mConnectionSessionStartTimeMs = TS_NONE; + private NetworkConnectionStats mRecentStats; + private NetworkConnectionStats mStatsCurrBuild; + private NetworkConnectionStats mStatsPrevBuild; + + PerNetwork(String ssid) { + super(computeHashLong(ssid, MacAddress.fromString(DEFAULT_MAC_ADDRESS), mL2KeySeed)); + this.ssid = ssid; + this.id = idFromLong(); + this.changed = false; + mRecentStats = new NetworkConnectionStats(); + mStatsCurrBuild = new NetworkConnectionStats(); + mStatsPrevBuild = new NetworkConnectionStats(); + } + + void updateEventStats(Event event, int rssi, int txSpeed, int failureReason) { + finishPendingRead(); + long currTimeMs = mClock.getWallClockMillis(); + switch (event) { + case SIGNAL_POLL: + mLastRssiPoll = rssi; + mLastRssiPollTimeMs = currTimeMs; + mLastTxSpeedPoll = txSpeed; + changed = true; + break; + case CONNECTION_ATTEMPT: + logd(" scan rssi: " + rssi); + if (rssi >= HEALTH_MONITOR_COUNT_RSSI_MIN_DBM) { + mRecentStats.incrementCount(CNT_CONNECTION_ATTEMPT); + } + mConnectionSessionStartTimeMs = currTimeMs; + changed = true; + break; + case CONNECTION_FAILURE: + mConnectionSessionStartTimeMs = TS_NONE; + if (rssi >= HEALTH_MONITOR_COUNT_RSSI_MIN_DBM) { + if (failureReason != BssidBlocklistMonitor.REASON_WRONG_PASSWORD) { + mRecentStats.incrementCount(CNT_CONNECTION_FAILURE); + } + switch (failureReason) { + case BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA: + case BssidBlocklistMonitor.REASON_ASSOCIATION_REJECTION: + mRecentStats.incrementCount(CNT_ASSOCIATION_REJECTION); + break; + case BssidBlocklistMonitor.REASON_ASSOCIATION_TIMEOUT: + mRecentStats.incrementCount(CNT_ASSOCIATION_TIMEOUT); + break; + case BssidBlocklistMonitor.REASON_AUTHENTICATION_FAILURE: + case BssidBlocklistMonitor.REASON_EAP_FAILURE: + mRecentStats.incrementCount(CNT_AUTHENTICATION_FAILURE); + break; + case BssidBlocklistMonitor.REASON_WRONG_PASSWORD: + case BssidBlocklistMonitor.REASON_DHCP_FAILURE: + default: + break; + } + } + changed = true; + break; + case WIFI_DISABLED: + case DISCONNECTION: + handleDisconnection(); + changed = true; + break; + default: + break; + } + logd(this.toString()); + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(" LastRssiPollTime: " + mLastRssiPollTimeMs); + sb.append(" LastRssiPoll: " + mLastRssiPoll); + sb.append(" LastTxSpeedPoll: " + mLastTxSpeedPoll); + sb.append("\n"); + sb.append(" StatsRecent: " + mRecentStats.toString()); + sb.append("\n"); + sb.append(" StatsCurr: " + mStatsCurrBuild.toString()); + sb.append("\n"); + sb.append(" StatsPrev: " + mStatsPrevBuild.toString()); + return sb.toString(); + } + private void handleDisconnection() { + if (mConnectionSessionStartTimeMs > TS_NONE) { + long currTimeMs = mClock.getWallClockMillis(); + int currSessionDurationSec = (int) ((currTimeMs + - mConnectionSessionStartTimeMs) / 1000); + mRecentStats.accumulate(CNT_CONNECTION_DURATION_SEC, currSessionDurationSec); + long timeSinceLastRssiPollMs = currTimeMs - mLastRssiPollTimeMs; + boolean hasRecentRssiPoll = (mLastRssiPollTimeMs > TS_NONE + && timeSinceLastRssiPollMs <= LAST_RSSI_POLL_MAX_INTERVAL_MS); + if (hasRecentRssiPoll) { + mRecentStats.incrementCount(CNT_DISCONNECTION); + } + if (mNonlocalDisconnection && hasRecentRssiPoll + && (mLastRssiPoll >= HEALTH_MONITOR_COUNT_RSSI_MIN_DBM + || mLastTxSpeedPoll >= HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS)) { + mRecentStats.incrementCount(CNT_DISCONNECTION_NONLOCAL); + if (currSessionDurationSec <= SHORT_CONNECTION_DURATION_MAX_SEC) { + mRecentStats.incrementCount(CNT_SHORT_CONNECTION_NONLOCAL); + } + } + } + mConnectionSessionStartTimeMs = TS_NONE; + mLastRssiPollTimeMs = TS_NONE; + } + @NonNull NetworkConnectionStats getRecentStats() { + return mRecentStats; + } + @NonNull NetworkConnectionStats getStatsCurrBuild() { + return mStatsCurrBuild; + } + @NonNull NetworkConnectionStats getStatsPrevBuild() { + return mStatsPrevBuild; + } + + /** + /* Detect a significant failure stats change with historical data + /* or high failure stats without historical data. + /* @return 0 if recentStats doesn't have sufficient data + * 1 if recentStats has sufficient data while statsPrevBuild doesn't + * 2 if recentStats and statsPrevBuild have sufficient data + */ + int dailyDetection(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh) { + finishPendingRead(); + dailyDetectionDisconnectionEvent(statsDec, statsInc, statsHigh); + return dailyDetectionConnectionEvent(statsDec, statsInc, statsHigh); + } + + private int dailyDetectionConnectionEvent(FailureStats statsDec, FailureStats statsInc, + FailureStats statsHigh) { + // Skip daily detection if recentStats is not sufficient + if (!isRecentConnectionStatsSufficient()) return INSUFFICIENT_RECENT_STATS; + if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT) + < MIN_NUM_CONNECTION_ATTEMPT_PREV) { + // don't have enough historical data, + // so only detect high failure stats without relying on mStatsPrevBuild. + // Increase low threshold so that mStatsPrevBuild is always below it + // statsHigh only depends on mRecentStats. + FailureStats statsDummy = new FailureStats(); + statsDeltaDetectionConnection(statsDummy, statsHigh, ONE_HUNDRED_PERCENT); + return SUFFICIENT_RECENT_STATS_ONLY; + } else { + // mStatsPrevBuild has enough updates, + // detect improvement or degradation with normal threshold values. + statsDeltaDetectionConnection(statsDec, statsInc, /* thresholdLowOffset */ 0); + return SUFFICIENT_RECENT_PREV_STATS; + } + } + + private void dailyDetectionDisconnectionEvent(FailureStats statsDec, FailureStats statsInc, + FailureStats statsHigh) { + // Skip daily detection if recentStats is not sufficient + if (mRecentStats.getCount(CNT_DISCONNECTION) < MIN_NUM_DISCONNECTION) return; + if (mStatsPrevBuild.getCount(CNT_DISCONNECTION) < MIN_NUM_DISCONNECTION_PREV) { + FailureStats statsDummy = new FailureStats(); + statsDeltaDetectionDisconnection(statsDummy, statsHigh, ONE_HUNDRED_PERCENT); + } else { + statsDeltaDetectionDisconnection(statsDec, statsInc, /* thresholdLowOffset */ 0); + } + } + + private void statsDeltaDetectionConnection(FailureStats statsDec, + FailureStats statsInc, int thresholdLowOffset) { + statsDeltaDetection(statsDec, statsInc, CNT_CONNECTION_FAILURE, + REASON_CONNECTION_FAILURE, + CONNECTION_FAILURE_PERCENT_HIGH_THRESHOLD, + CONNECTION_FAILURE_PERCENT_LOW_THRESHOLD + thresholdLowOffset, + CNT_CONNECTION_ATTEMPT); + statsDeltaDetection(statsDec, statsInc, CNT_AUTHENTICATION_FAILURE, + REASON_AUTH_FAILURE, + AUTH_FAILURE_PERCENT_HIGH_THRESHOLD, + AUTH_FAILURE_PERCENT_LOW_THRESHOLD + thresholdLowOffset, + CNT_CONNECTION_ATTEMPT); + statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_REJECTION, + REASON_ASSOC_REJECTION, + ASSOC_REJECTION_PERCENT_HIGH_THRESHOLD, + ASSOC_REJECTION_PERCENT_LOW_THRESHOLD + thresholdLowOffset, + CNT_CONNECTION_ATTEMPT); + statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_TIMEOUT, + REASON_ASSOC_TIMEOUT, + ASSOC_TIMEOUT_PERCENT_HIGH_THRESHOLD, + ASSOC_TIMEOUT_PERCENT_LOW_THRESHOLD + thresholdLowOffset, + CNT_CONNECTION_ATTEMPT); + } + + private void statsDeltaDetectionDisconnection(FailureStats statsDec, + FailureStats statsInc, int thresholdLowOffset) { + statsDeltaDetection(statsDec, statsInc, CNT_SHORT_CONNECTION_NONLOCAL, + REASON_SHORT_CONNECTION_NONLOCAL, + SHORT_CONNECTION_NONLOCAL_PERCENT_HIGH_THRESHOLD, + SHORT_CONNECTION_NONLOCAL_PERCENT_LOW_THRESHOLD + thresholdLowOffset, + CNT_DISCONNECTION); + statsDeltaDetection(statsDec, statsInc, CNT_DISCONNECTION_NONLOCAL, + REASON_DISCONNECTION_NONLOCAL, + DISCONNECTION_NONLOCAL_PERCENT_HIGH_THRESHOLD, + DISCONNECTION_NONLOCAL_PERCENT_LOW_THRESHOLD + thresholdLowOffset, + CNT_DISCONNECTION); + } + + private boolean statsDeltaDetection(FailureStats statsDec, + FailureStats statsInc, int countCode, int reasonCode, + int highThreshold, int lowThreshold, int refCountCode) { + if (isRateBelowThreshold(mStatsPrevBuild, countCode, lowThreshold, refCountCode) + && isRateAboveThreshold(mRecentStats, countCode, highThreshold, refCountCode)) { + statsInc.incrementCount(reasonCode); + return true; + } + if (isRateAboveThreshold(mStatsPrevBuild, countCode, highThreshold, refCountCode) + && isRateBelowThreshold(mRecentStats, countCode, lowThreshold, refCountCode)) { + statsDec.incrementCount(reasonCode); + return true; + } + return false; + } + + private boolean isRateAboveThreshold(NetworkConnectionStats stats, + @ConnectionCountCode int countCode, int threshold, int refCountCode) { + return (stats.getCount(countCode) * ONE_HUNDRED_PERCENT) + >= (threshold * stats.getCount(refCountCode)); + } + + private boolean isRateBelowThreshold(NetworkConnectionStats stats, + @ConnectionCountCode int countCode, int threshold, int refCountCode) { + return (stats.getCount(countCode) * ONE_HUNDRED_PERCENT) + <= (threshold * stats.getCount(refCountCode)); + } + + private boolean isRecentConnectionStatsSufficient() { + return (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT) >= MIN_NUM_CONNECTION_ATTEMPT); + } + + // Update StatsCurrBuild with recentStats and clear recentStats + void updateAfterDailyDetection() { + // Skip update if recentStats is not sufficient since daily detection is also skipped + if (!isRecentConnectionStatsSufficient()) return; + mStatsCurrBuild.accumulateAll(mRecentStats); + mRecentStats.clear(); + changed = true; + } + + // Refresh StatsPrevBuild with StatsCurrBuild which is cleared afterwards + void updateAfterSwBuildChange() { + finishPendingRead(); + mStatsPrevBuild.copy(mStatsCurrBuild); + mRecentStats.clear(); + mStatsCurrBuild.clear(); + changed = true; + } + + NetworkStats toNetworkStats() { + finishPendingRead(); + NetworkStats.Builder builder = NetworkStats.newBuilder(); + builder.setId(id); + builder.setRecentStats(toConnectionStats(mRecentStats)); + builder.setStatsCurrBuild(toConnectionStats(mStatsCurrBuild)); + builder.setStatsPrevBuild(toConnectionStats(mStatsPrevBuild)); + return builder.build(); + } + + private ConnectionStats toConnectionStats(NetworkConnectionStats stats) { + ConnectionStats.Builder builder = ConnectionStats.newBuilder(); + builder.setNumConnectionAttempt(stats.getCount(CNT_CONNECTION_ATTEMPT)); + builder.setNumConnectionFailure(stats.getCount(CNT_CONNECTION_FAILURE)); + builder.setConnectionDurationSec(stats.getCount(CNT_CONNECTION_DURATION_SEC)); + builder.setNumDisconnectionNonlocal(stats.getCount(CNT_DISCONNECTION_NONLOCAL)); + builder.setNumDisconnection(stats.getCount(CNT_DISCONNECTION)); + builder.setNumShortConnectionNonlocal(stats.getCount(CNT_SHORT_CONNECTION_NONLOCAL)); + builder.setNumAssociationRejection(stats.getCount(CNT_ASSOCIATION_REJECTION)); + builder.setNumAssociationTimeout(stats.getCount(CNT_ASSOCIATION_TIMEOUT)); + builder.setNumAuthenticationFailure(stats.getCount(CNT_AUTHENTICATION_FAILURE)); + return builder.build(); + } + + void finishPendingRead() { + final byte[] serialized = finishPendingReadBytes(); + if (serialized == null) return; + NetworkStats ns; + try { + ns = NetworkStats.parseFrom(serialized); + } catch (InvalidProtocolBufferException e) { + Log.e(TAG, "Failed to deserialize", e); + return; + } + mergeNetworkStatsFromMemory(ns); + changed = true; + } + + PerNetwork mergeNetworkStatsFromMemory(@NonNull NetworkStats ns) { + if (ns.hasId() && this.id != ns.getId()) { + return this; + } + if (ns.hasRecentStats()) { + ConnectionStats recentStats = ns.getRecentStats(); + mergeConnectionStats(recentStats, mRecentStats); + } + if (ns.hasStatsCurrBuild()) { + ConnectionStats statsCurr = ns.getStatsCurrBuild(); + mStatsCurrBuild.clear(); + mergeConnectionStats(statsCurr, mStatsCurrBuild); + } + if (ns.hasStatsPrevBuild()) { + ConnectionStats statsPrev = ns.getStatsPrevBuild(); + mStatsPrevBuild.clear(); + mergeConnectionStats(statsPrev, mStatsPrevBuild); + } + return this; + } + + private void mergeConnectionStats(ConnectionStats source, NetworkConnectionStats target) { + if (source.hasNumConnectionAttempt()) { + target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionAttempt()); + } + if (source.hasNumConnectionFailure()) { + target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionFailure()); + } + if (source.hasConnectionDurationSec()) { + target.accumulate(CNT_CONNECTION_DURATION_SEC, source.getConnectionDurationSec()); + } + if (source.hasNumDisconnectionNonlocal()) { + target.accumulate(CNT_DISCONNECTION_NONLOCAL, source.getNumDisconnectionNonlocal()); + } + if (source.hasNumDisconnection()) { + target.accumulate(CNT_DISCONNECTION, source.getNumDisconnection()); + } + if (source.hasNumShortConnectionNonlocal()) { + target.accumulate(CNT_SHORT_CONNECTION_NONLOCAL, + source.getNumShortConnectionNonlocal()); + } + if (source.hasNumAssociationRejection()) { + target.accumulate(CNT_ASSOCIATION_REJECTION, source.getNumAssociationRejection()); + } + if (source.hasNumAssociationTimeout()) { + target.accumulate(CNT_ASSOCIATION_TIMEOUT, source.getNumAssociationTimeout()); + } + if (source.hasNumAuthenticationFailure()) { + target.accumulate(CNT_AUTHENTICATION_FAILURE, source.getNumAuthenticationFailure()); + } + } + } + + // Codes for various connection related counts + public static final int CNT_CONNECTION_ATTEMPT = 0; + public static final int CNT_CONNECTION_FAILURE = 1; + public static final int CNT_CONNECTION_DURATION_SEC = 2; + public static final int CNT_ASSOCIATION_REJECTION = 3; + public static final int CNT_ASSOCIATION_TIMEOUT = 4; + public static final int CNT_AUTHENTICATION_FAILURE = 5; + public static final int CNT_SHORT_CONNECTION_NONLOCAL = 6; + public static final int CNT_DISCONNECTION_NONLOCAL = 7; + public static final int CNT_DISCONNECTION = 8; + // Constant being used to keep track of how many counter there are. + public static final int NUMBER_CONNECTION_CNT_CODE = 9; + private static final String[] CONNECTION_CNT_NAME = { + " ConnectAttempt: ", + " ConnectFailure: ", + " ConnectDurSec: ", + " AssocRej: ", + " AssocTimeout: ", + " AuthFailure: ", + " ShortDiscNonlocal: ", + " DisconnectNonlocal: ", + " Disconnect: " + }; + + @IntDef(prefix = { "CNT_" }, value = { + CNT_CONNECTION_ATTEMPT, + CNT_CONNECTION_FAILURE, + CNT_CONNECTION_DURATION_SEC, + CNT_ASSOCIATION_REJECTION, + CNT_ASSOCIATION_TIMEOUT, + CNT_AUTHENTICATION_FAILURE, + CNT_SHORT_CONNECTION_NONLOCAL, + CNT_DISCONNECTION_NONLOCAL, + CNT_DISCONNECTION + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ConnectionCountCode {} + + /** + * A class maintaining the connection related statistics of a Wifi network. + */ + public static class NetworkConnectionStats { + private final int[] mCount = new int[NUMBER_CONNECTION_CNT_CODE]; + + /** + * Copy all values + * @param src is the source of copy + */ + public void copy(NetworkConnectionStats src) { + for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { + mCount[i] = src.getCount(i); + } + } + + /** + * Clear all counters + */ + public void clear() { + for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { + mCount[i] = 0; + } + } + + /** + * Get counter value + * @param countCode is the selected counter + * @return the value of selected counter + */ + public int getCount(@ConnectionCountCode int countCode) { + return mCount[countCode]; + } + + /** + * Set counterer value + * @param countCode is the selected counter + * @param cnt is the value set to the selected counter + */ + public void setCount(@ConnectionCountCode int countCode, int cnt) { + mCount[countCode] = cnt; + } + + /** + * Increment count value by 1 + * @param countCode is the selected counter + */ + public void incrementCount(@ConnectionCountCode int countCode) { + mCount[countCode]++; + } + + /** + * Decrement count value by 1 + * @param countCode is the selected counter + */ + public void decrementCount(@ConnectionCountCode int countCode) { + mCount[countCode]--; + } + + /** + * Add and accumulate the selected counter + * @param countCode is the selected counter + * @param cnt is the value to be added to the counter + */ + public void accumulate(@ConnectionCountCode int countCode, int cnt) { + mCount[countCode] += cnt; + } + + /** + * Accumulate daily stats to historical data + * @param recentStats are the raw daily counts + */ + public void accumulateAll(NetworkConnectionStats recentStats) { + // 32-bit counter in second can support connection duration up to 68 years. + // Similarly 32-bit counter can support up to continuous connection attempt + // up to 68 years with one attempt per second. + for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { + mCount[i] += recentStats.getCount(i); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { + sb.append(CONNECTION_CNT_NAME[i]); + sb.append(mCount[i]); + } + return sb.toString(); + } + } + /** + * A base class dealing with common operations of MemoryStore. + */ + public static class MemoryStoreAccessBase { + private final String mL2Key; + private final long mHash; + private static final String TAG = "WifiMemoryStoreAccessBase"; private final AtomicReference mPendingReadFromStore = new AtomicReference<>(); + MemoryStoreAccessBase(long hash) { + mHash = hash; + mL2Key = l2KeyFromLong(); + } + String getL2Key() { + return mL2Key; + } + + private String l2KeyFromLong() { + return "W" + Long.toHexString(mHash); + } + + /** + * Callback function when MemoryStore read is done + * @param serialized is the readback value + */ + void readBackListener(byte[] serialized) { + if (serialized == null) return; + byte[] old = mPendingReadFromStore.getAndSet(serialized); + if (old != null) { + Log.e(TAG, "More answers than we expected!"); + } + } + /** + * Handles (when convenient) the arrival of previously stored data. + * + * The response from IpMemoryStore arrives on a different thread, so we + * defer handling it until here, when we're on our favorite thread and + * in a good position to deal with it. We may have already collected some + * data before now, so we need to be prepared to merge the new and old together. + */ + byte[] finishPendingReadBytes() { + return mPendingReadFromStore.getAndSet(null); + } + + + int idFromLong() { + return (int) mHash & 0x7fffffff; + } } + private void logd(String string) { + if (mVerboseLoggingEnabled) { + Log.d(TAG, string); + } + } // Returned by lookupBssid when the BSSID is not available, // for instance when we are not associated. private final PerBssid mDummyPerBssid; @@ -630,7 +1284,7 @@ public class WifiScoreCard { Log.i(TAG, "Discarding stats for score card (ssid changed) ID: " + old.id); if (old.referenced) mApForBssidReferenced--; } - requestReadForPerBssid(ans); + requestReadBssid(ans); } if (!ans.referenced) { ans.referenced = true; @@ -640,20 +1294,61 @@ public class WifiScoreCard { return ans; } - private void requestReadForPerBssid(final PerBssid perBssid) { + private void requestReadBssid(final PerBssid perBssid) { if (mMemoryStore != null) { - mMemoryStore.read(perBssid.getL2Key(), (value) -> perBssid.lazyMerge(value)); + mMemoryStore.read(perBssid.getL2Key(), PER_BSSID_DATA_NAME, + (value) -> perBssid.readBackListener(value)); } } private void requestReadForAllChanged() { for (PerBssid perBssid : mApForBssid.values()) { if (perBssid.changed) { - requestReadForPerBssid(perBssid); + requestReadBssid(perBssid); } } } + // Returned by lookupNetwork when the network is not available, + // for instance when we are not associated. + private final PerNetwork mDummyPerNetwork; + private final Map mApForNetwork = new ArrayMap<>(); + PerNetwork lookupNetwork(String ssid) { + if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) { + return mDummyPerNetwork; + } + + PerNetwork ans = mApForNetwork.get(ssid); + if (ans == null) { + ans = new PerNetwork(ssid); + mApForNetwork.put(ssid, ans); + requestReadNetwork(ans); + } + return ans; + } + + /** + * Remove network from cache and memory store + * @param ssid is the network SSID + */ + public void removeNetwork(String ssid) { + if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) { + return; + } + mApForNetwork.remove(ssid); + if (mMemoryStore == null) return; + PerNetwork ans = new PerNetwork(ssid); + byte[] serialized = {}; + mMemoryStore.write(ans.getL2Key(), PER_NETWORK_DATA_NAME, serialized); + } + + void requestReadNetwork(final PerNetwork perNetwork) { + if (mMemoryStore != null) { + mMemoryStore.read(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME, + (value) -> perNetwork.readBackListener(value)); + } + } + /** * Issues write requests for all changed entries. * @@ -664,6 +1359,10 @@ public class WifiScoreCard { * @returns number of writes issued. */ public int doWrites() { + return doWritesBssid() + doWritesNetwork(); + } + + private int doWritesBssid() { if (mMemoryStore == null) return 0; int count = 0; int bytes = 0; @@ -671,13 +1370,33 @@ public class WifiScoreCard { if (perBssid.changed) { perBssid.finishPendingRead(); byte[] serialized = perBssid.toAccessPoint(/* No BSSID */ true).toByteArray(); - mMemoryStore.write(perBssid.getL2Key(), serialized); + mMemoryStore.write(perBssid.getL2Key(), PER_BSSID_DATA_NAME, serialized); perBssid.changed = false; count++; bytes += serialized.length; } } - if (DBG && count > 0) { + if (mVerboseLoggingEnabled && count > 0) { + Log.v(TAG, "Write count: " + count + ", bytes: " + bytes); + } + return count; + } + + private int doWritesNetwork() { + if (mMemoryStore == null) return 0; + int count = 0; + int bytes = 0; + for (PerNetwork perNetwork : mApForNetwork.values()) { + if (perNetwork.changed) { + perNetwork.finishPendingRead(); + byte[] serialized = perNetwork.toNetworkStats().toByteArray(); + mMemoryStore.write(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME, serialized); + perNetwork.changed = false; + count++; + bytes += serialized.length; + } + } + if (mVerboseLoggingEnabled && count > 0) { Log.v(TAG, "Write count: " + count + ", bytes: " + bytes); } return count; @@ -695,7 +1414,7 @@ public class WifiScoreCard { private void clean() { if (mMemoryStore == null) return; if (mApForBssidReferenced >= mApForBssidTargetSize) { - doWrites(); // Do not want to evict changed items + doWritesBssid(); // Do not want to evict changed items // Evict the unreferenced ones, and clear all the referenced bits for the next round. Iterator> it = mApForBssid.entrySet().iterator(); while (it.hasNext()) { @@ -704,17 +1423,24 @@ public class WifiScoreCard { perBssid.referenced = false; } else { it.remove(); - if (DBG) Log.v(TAG, "Evict " + perBssid.id); + if (mVerboseLoggingEnabled) Log.v(TAG, "Evict " + perBssid.id); } } mApForBssidReferenced = 0; } } - private long computeHashLong(String ssid, MacAddress mac) { + /** + * Compute a hash value with the given SSID and MAC address + * @param ssid is the network SSID + * @param mac is the network MAC address + * @param l2KeySeed is the seed for hash generation + * @return + */ + public static long computeHashLong(String ssid, MacAddress mac, String l2KeySeed) { byte[][] parts = { // Our seed keeps the L2Keys specific to this device - mL2KeySeed.getBytes(), + l2KeySeed.getBytes(), // ssid is either quoted utf8 or hex-encoded bytes; turn it into plain bytes. NativeUtil.byteArrayFromArrayList(NativeUtil.decodeSsid(ssid)), // And the BSSID @@ -746,14 +1472,6 @@ public class WifiScoreCard { return buffer.getLong(); } - private static int idFromLong(long hash) { - return (int) hash & 0x7fffffff; - } - - private static String l2KeyFromLong(long hash) { - return "W" + Long.toHexString(hash); - } - private static String groupHintFromLong(long hash) { return "G" + Long.toHexString(hash); } @@ -763,12 +1481,22 @@ public class WifiScoreCard { return mApForBssid.get(mac); } + @VisibleForTesting + PerNetwork fetchByNetwork(String ssid) { + return mApForNetwork.get(ssid); + } + @VisibleForTesting PerBssid perBssidFromAccessPoint(String ssid, AccessPoint ap) { MacAddress bssid = MacAddress.fromBytes(ap.getBssid().toByteArray()); return new PerBssid(ssid, bssid).merge(ap); } + @VisibleForTesting + PerNetwork perNetworkFromNetworkStats(String ssid, NetworkStats ns) { + return new PerNetwork(ssid).mergeNetworkStatsFromMemory(ns); + } + final class PerSignal { public final Event event; public final int frequency; @@ -794,6 +1522,7 @@ public class WifiScoreCard { case IP_CONFIGURATION_SUCCESS: case VALIDATION_SUCCESS: case CONNECTION_FAILURE: + case DISCONNECTION: case WIFI_DISABLED: case ROAM_FAILURE: this.elapsedMs = new PerUnivariateStatistic(); @@ -822,8 +1551,9 @@ public class WifiScoreCard { if (elapsedMs != null) { builder.setElapsedMs(elapsedMs.toUnivariateStatistic()); } - if (DBG && rssi.intHistogram != null && rssi.intHistogram.numNonEmptyBuckets() > 0) { - Log.d(TAG, "Histogram " + event + " RSSI" + rssi.intHistogram); + if (rssi.intHistogram != null + && rssi.intHistogram.numNonEmptyBuckets() > 0) { + logd("Histogram " + event + " RSSI" + rssi.intHistogram); } return builder.build(); } @@ -950,6 +1680,13 @@ public class WifiScoreCard { } network.addAccessPoints(perBssid.toAccessPoint(obfuscate)); } + for (PerNetwork perNetwork: mApForNetwork.values()) { + String key = perNetwork.ssid; + Network.Builder network = networks.get(key); + if (network != null) { + network.setNetworkStats(perNetwork.toNetworkStats()); + } + } NetworkList.Builder builder = NetworkList.newBuilder(); for (Network.Builder network: networks.values()) { builder.addNetworks(network); @@ -983,7 +1720,7 @@ public class WifiScoreCard { */ public void clear() { mApForBssid.clear(); + mApForNetwork.clear(); resetConnectionStateInternal(false); } - } diff --git a/service/java/com/android/server/wifi/WifiServiceImpl.java b/service/java/com/android/server/wifi/WifiServiceImpl.java index 8b3a3213f..36308fa21 100644 --- a/service/java/com/android/server/wifi/WifiServiceImpl.java +++ b/service/java/com/android/server/wifi/WifiServiceImpl.java @@ -283,6 +283,8 @@ public class WifiServiceImpl extends BaseWifiService { private final DppManager mDppManager; private final WifiApConfigStore mWifiApConfigStore; private final WifiThreadRunner mWifiThreadRunner; + private final MemoryStoreImpl mMemoryStoreImpl; + private final WifiScoreCard mWifiScoreCard; public WifiServiceImpl(Context context, WifiInjector wifiInjector, AsyncChannel asyncChannel) { mContext = context; @@ -321,6 +323,9 @@ public class WifiServiceImpl extends BaseWifiService { mWifiThreadRunner = mWifiInjector.getWifiThreadRunner(); mWifiConfigManager = mWifiInjector.getWifiConfigManager(); mPasspointManager = mWifiInjector.getPasspointManager(); + mWifiScoreCard = mWifiInjector.getWifiScoreCard(); + mMemoryStoreImpl = new MemoryStoreImpl(mContext, mWifiInjector, + mWifiScoreCard, mWifiInjector.getWifiHealthMonitor()); } /** @@ -390,14 +395,14 @@ public class WifiServiceImpl extends BaseWifiService { intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); intentFilter.addAction(TelephonyManager.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED); intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + intentFilter.addAction(Intent.ACTION_SHUTDOWN); boolean trackEmergencyCallState = mContext.getResources().getBoolean( R.bool.config_wifi_turn_off_during_emergency_call); if (trackEmergencyCallState) { intentFilter.addAction(TelephonyManager.ACTION_EMERGENCY_CALL_STATE_CHANGED); } mContext.registerReceiver(mReceiver, intentFilter); - - new MemoryStoreImpl(mContext, mWifiInjector, mWifiInjector.getWifiScoreCard()).start(); + mMemoryStoreImpl.start(); if (!mWifiConfigManager.loadFromStore()) { Log.e(TAG, "Failed to load from config store"); } @@ -529,6 +534,14 @@ public class WifiServiceImpl extends BaseWifiService { } } + private void handleShutDown() { + // There is no explicit disconnection event in clientModeImpl during shutdown. + // Call resetConnectionState() so that connection duration is calculated correctly + // before memory store write triggered by mMemoryStoreImpl.stop(). + mWifiScoreCard.resetConnectionState(); + mMemoryStoreImpl.stop(); + } + private boolean checkNetworkSettingsPermission(int pid, int uid) { return mContext.checkPermission(android.Manifest.permission.NETWORK_SETTINGS, pid, uid) == PERMISSION_GRANTED; @@ -2760,6 +2773,8 @@ public class WifiServiceImpl extends BaseWifiService { mActiveModeWarden.emergencyCallStateChanged(inCall); } else if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) { handleIdleModeChanged(); + } else if (action.equals(Intent.ACTION_SHUTDOWN)) { + handleShutDown(); } } }; @@ -3084,6 +3099,7 @@ public class WifiServiceImpl extends BaseWifiService { mClientModeImpl.clearNetworkRequestUserApprovedAccessPoints(); mWifiNetworkSuggestionsManager.clear(); mWifiInjector.getWifiScoreCard().clear(); + mWifiInjector.getWifiHealthMonitor().clear(); notifyFactoryReset(); }); } diff --git a/service/proto/src/scorecard.proto b/service/proto/src/scorecard.proto index 7e42287f4..b924d3d4f 100644 --- a/service/proto/src/scorecard.proto +++ b/service/proto/src/scorecard.proto @@ -41,6 +41,7 @@ message Network { repeated AccessPoint access_points = 3; // The list of related APs optional int32 network_config_id = 4; // The networkId of WifiConfiguration optional int32 network_agent_id = 5; // Latest NetworkAgent netId + optional NetworkStats network_stats = 6; // Network stats of current SSID }; // Describes an access point (single BSSID) @@ -123,4 +124,66 @@ enum Event { ROAM_FAILURE = 11; LAST_POLL_BEFORE_SWITCH = 12; VALIDATION_SUCCESS = 13; + DISCONNECTION = 14; + CONNECTION_ATTEMPT = 15; + VALIDATION_FAILURE = 16; }; + +message SystemInfoStats { + // Current software build information + optional SoftwareBuildInfo curr_software_build_info = 1; + // Previous software build information + optional SoftwareBuildInfo prev_software_build_info = 2; + // Most recent WiFi scan time + optional int64 last_scan_time_ms = 3; + // Number of access points found in most recent WiFi scan at 2G + optional int32 num_bssid_last_scan_2g = 4; + // Number of access points found in most recent WiFi scan above 2G + optional int32 num_bssid_last_scan_above_2g = 5; +} + +message SoftwareBuildInfo { + // Android OS build version + optional string os_build_version = 1; + // WiFi stack APK version, 0 means not available. + optional int32 wifi_stack_version = 2; + // WiFi driver version + optional string wifi_driver_version = 3; + // WiFi firmware version + optional string wifi_firmware_version = 4; +} + +message NetworkStats { + optional int32 id = 1; // Concise id + // The most recent connection stats with current SW build that may be collected over days + optional ConnectionStats recent_stats = 2; + // Accumulated connection stats with current SW build + optional ConnectionStats stats_curr_build = 3; + // Accumulated connection stats with previous SW build + optional ConnectionStats stats_prev_build = 4; + +}; + +message ConnectionStats { + // Number of connection attempts at high RSSI + optional int32 num_connection_attempt = 1; + // Number of connection failures at high RSSI + // Does not include wrong password but does include DHCP + optional int32 num_connection_failure = 2; + // Total connection duration in seconds + optional int32 connection_duration_sec = 3; + // Number of association rejections at high RSSI + optional int32 num_association_rejection = 4; + // Number of association timeouts at high RSSI + optional int32 num_association_timeout = 5; + // Number of authentication failures (excluding wrong password) at high RSSI + optional int32 num_authentication_failure = 6; + // Number of short connections caused by nonlocal disconnection at high RSSI + // or at high Tx speed with a recent RSSI poll + optional int32 num_short_connection_nonlocal = 7; + // Number of non-locally generated disconnections at high RSSI or Tx speed + // with a recent RSSI poll + optional int32 num_disconnection_nonlocal = 8; + // Number of disconnections with a recent RSSI poll + optional int32 num_disconnection = 9; +} -- cgit v1.2.3