diff options
author | Kai Shi <kaishi@google.com> | 2020-01-11 00:20:18 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-01-11 00:20:18 +0000 |
commit | 46addc2fbc03652fabbc46d3d70c7649c9463063 (patch) | |
tree | b8f4c2076d5cf8f46bb4bd53e305b4f6eab63bdb /service | |
parent | 6a701444f98de01777658f34e16a5a15c1be4da2 (diff) | |
parent | e2625a2794d3d4262652edafbb87df9eac54ae53 (diff) |
Merge "Wifi: add the initial version of WifiHealthMonitor and related changes."
Diffstat (limited to 'service')
9 files changed, 1938 insertions, 126 deletions
diff --git a/service/java/com/android/server/wifi/ClientModeImpl.java b/service/java/com/android/server/wifi/ClientModeImpl.java index 523515919..5a7d39fe0 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; @@ -791,6 +794,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); @@ -1119,6 +1123,8 @@ public class ClientModeImpl extends StateMachine { mNetworkFactory.enableVerboseLogging(verbose); mLinkProbeManager.enableVerboseLogging(mVerboseLoggingEnabled); mMboOceController.enableVerboseLogging(mVerboseLoggingEnabled); + mWifiScoreCard.enableVerboseLogging(mVerboseLoggingEnabled); + mWifiHealthMonitor.enableVerboseLogging(mVerboseLoggingEnabled); } private boolean setRandomMacOui() { @@ -2579,7 +2585,6 @@ public class ClientModeImpl extends StateMachine { mWifiInjector.getWakeupController().setLastDisconnectInfo(matchInfo); mWifiNetworkSuggestionsManager.handleDisconnect(wifiConfig, getCurrentBSSID()); } - stopRssiMonitoringOffload(); clearTargetBssid("handleNetworkDisconnect"); @@ -2603,7 +2608,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; @@ -2746,21 +2751,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 @@ -2771,12 +2792,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); @@ -3480,6 +3496,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 @@ -3504,6 +3521,7 @@ public class ClientModeImpl extends StateMachine { mWifiInfo.reset(); mWifiInfo.setSupplicantState(SupplicantState.DISCONNECTED); mWifiScoreCard.noteSupplicantStateChanged(mWifiInfo); + mWifiHealthMonitor.setWifiEnabled(false); stopClientMode(); } @@ -3762,7 +3780,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); @@ -5056,6 +5075,7 @@ public class ClientModeImpl extends StateMachine { mLastBssid, config.SSID, BssidBlocklistMonitor .REASON_NETWORK_VALIDATION_FAILURE); + mWifiScoreCard.noteValidationFailure(mWifiInfo); } } } @@ -5107,7 +5127,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); @@ -5140,7 +5161,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; @@ -5678,6 +5700,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 0647ed987..dd3997a28 100644 --- a/service/java/com/android/server/wifi/WifiConfigManager.java +++ b/service/java/com/android/server/wifi/WifiConfigManager.java @@ -2384,6 +2384,19 @@ public class WifiConfigManager { } /** + * 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 * {@link #mScanDetailCaches} for the retrieved network. @@ -3492,4 +3505,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<ScanDetail> 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<WifiConfiguration> 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<WifiConfiguration> 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<WifiConfiguration> 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<ScanDetail> mScanDetails = new ArrayList<ScanDetail>(); + + 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<ScanDetail> 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 { @@ -132,6 +188,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. * * Based on mClock.getElapsedSinceBootMillis(). @@ -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 @@ -165,6 +231,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<Pair<Event, Integer>, 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<byte[]> 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<String, PerNetwork> 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<Map.Entry<MacAddress, PerBssid>> 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); } @@ -764,11 +1482,21 @@ public class WifiScoreCard { } @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 6b5b43128..b9dc498ab 100644 --- a/service/java/com/android/server/wifi/WifiServiceImpl.java +++ b/service/java/com/android/server/wifi/WifiServiceImpl.java @@ -284,6 +284,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; @@ -322,6 +324,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; @@ -2814,6 +2827,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(); } } }; @@ -3140,6 +3155,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; +} |