1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wifi; 18 19 import static android.net.wifi.WifiInfo.DEFAULT_MAC_ADDRESS; 20 import static android.net.wifi.WifiInfo.INVALID_RSSI; 21 import static android.net.wifi.WifiInfo.LINK_SPEED_UNKNOWN; 22 23 import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS; 24 import static com.android.server.wifi.WifiHealthMonitor.HEALTH_MONITOR_MIN_TX_PACKET_PER_SEC; 25 import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_REJECTION; 26 import static com.android.server.wifi.WifiHealthMonitor.REASON_ASSOC_TIMEOUT; 27 import static com.android.server.wifi.WifiHealthMonitor.REASON_AUTH_FAILURE; 28 import static com.android.server.wifi.WifiHealthMonitor.REASON_CONNECTION_FAILURE; 29 import static com.android.server.wifi.WifiHealthMonitor.REASON_DISCONNECTION_NONLOCAL; 30 import static com.android.server.wifi.WifiHealthMonitor.REASON_NO_FAILURE; 31 import static com.android.server.wifi.WifiHealthMonitor.REASON_SHORT_CONNECTION_NONLOCAL; 32 33 import android.annotation.IntDef; 34 import android.annotation.NonNull; 35 import android.annotation.Nullable; 36 import android.net.MacAddress; 37 import android.net.wifi.SupplicantState; 38 import android.net.wifi.WifiManager; 39 import android.util.ArrayMap; 40 import android.util.Base64; 41 import android.util.Log; 42 import android.util.Pair; 43 import android.util.SparseLongArray; 44 45 import com.android.internal.annotations.VisibleForTesting; 46 import com.android.internal.util.Preconditions; 47 import com.android.server.wifi.BssidBlocklistMonitor.FailureReason; 48 import com.android.server.wifi.WifiHealthMonitor.FailureStats; 49 import com.android.server.wifi.proto.WifiScoreCardProto; 50 import com.android.server.wifi.proto.WifiScoreCardProto.AccessPoint; 51 import com.android.server.wifi.proto.WifiScoreCardProto.ConnectionStats; 52 import com.android.server.wifi.proto.WifiScoreCardProto.Event; 53 import com.android.server.wifi.proto.WifiScoreCardProto.HistogramBucket; 54 import com.android.server.wifi.proto.WifiScoreCardProto.Network; 55 import com.android.server.wifi.proto.WifiScoreCardProto.NetworkList; 56 import com.android.server.wifi.proto.WifiScoreCardProto.NetworkStats; 57 import com.android.server.wifi.proto.WifiScoreCardProto.SecurityType; 58 import com.android.server.wifi.proto.WifiScoreCardProto.Signal; 59 import com.android.server.wifi.proto.WifiScoreCardProto.UnivariateStatistic; 60 import com.android.server.wifi.util.IntHistogram; 61 import com.android.server.wifi.util.LruList; 62 import com.android.server.wifi.util.NativeUtil; 63 64 import com.google.protobuf.ByteString; 65 import com.google.protobuf.InvalidProtocolBufferException; 66 67 import java.lang.annotation.Retention; 68 import java.lang.annotation.RetentionPolicy; 69 import java.nio.ByteBuffer; 70 import java.security.MessageDigest; 71 import java.security.NoSuchAlgorithmException; 72 import java.util.ArrayList; 73 import java.util.Iterator; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.Objects; 77 import java.util.concurrent.atomic.AtomicReference; 78 79 import javax.annotation.concurrent.NotThreadSafe; 80 81 /** 82 * Retains statistical information about the performance of various 83 * access points and networks, as experienced by this device. 84 * 85 * The purpose is to better inform future network selection and switching 86 * by this device and help health monitor detect network issues. 87 */ 88 @NotThreadSafe 89 public class WifiScoreCard { 90 91 public static final String DUMP_ARG = "WifiScoreCard"; 92 93 private static final String TAG = "WifiScoreCard"; 94 private boolean mVerboseLoggingEnabled = false; 95 96 @VisibleForTesting 97 boolean mPersistentHistograms = true; 98 99 private static final int TARGET_IN_MEMORY_ENTRIES = 50; 100 private static final int UNKNOWN_REASON = -1; 101 102 public static final String PER_BSSID_DATA_NAME = "scorecard.proto"; 103 public static final String PER_NETWORK_DATA_NAME = "perNetworkData"; 104 105 static final int INSUFFICIENT_RECENT_STATS = 0; 106 static final int SUFFICIENT_RECENT_STATS_ONLY = 1; 107 static final int SUFFICIENT_RECENT_PREV_STATS = 2; 108 109 private static final int MAX_FREQUENCIES_PER_SSID = 10; 110 111 private final Clock mClock; 112 private final String mL2KeySeed; 113 private MemoryStore mMemoryStore; 114 private final DeviceConfigFacade mDeviceConfigFacade; 115 116 @VisibleForTesting 117 static final int[] RSSI_BUCKETS = intsInRange(-100, -20); 118 intsInRange(int min, int max)119 private static int[] intsInRange(int min, int max) { 120 int[] a = new int[max - min + 1]; 121 for (int i = 0; i < a.length; i++) { 122 a[i] = min + i; 123 } 124 return a; 125 } 126 127 /** Our view of the memory store */ 128 public interface MemoryStore { 129 /** Requests a read, with asynchronous reply */ read(String key, String name, BlobListener blobListener)130 void read(String key, String name, BlobListener blobListener); 131 /** Requests a write, does not wait for completion */ write(String key, String name, byte[] value)132 void write(String key, String name, byte[] value); 133 /** Sets the cluster identifier */ setCluster(String key, String cluster)134 void setCluster(String key, String cluster); 135 /** Requests removal of all entries matching the cluster */ removeCluster(String cluster)136 void removeCluster(String cluster); 137 } 138 /** Asynchronous response to a read request */ 139 public interface BlobListener { 140 /** Provides the previously stored value, or null if none */ onBlobRetrieved(@ullable byte[] value)141 void onBlobRetrieved(@Nullable byte[] value); 142 } 143 144 /** 145 * Installs a memory store. 146 * 147 * Normally this happens just once, shortly after we start. But wifi can 148 * come up before the disk is ready, and we might not yet have a valid wall 149 * clock when we start up, so we need to be prepared to begin recording data 150 * even if the MemoryStore is not yet available. 151 * 152 * When the store is installed for the first time, we want to merge any 153 * recently recorded data together with data already in the store. But if 154 * the store restarts and has to be reinstalled, we don't want to do 155 * this merge, because that would risk double-counting the old data. 156 * 157 */ installMemoryStore(@onNull MemoryStore memoryStore)158 public void installMemoryStore(@NonNull MemoryStore memoryStore) { 159 Preconditions.checkNotNull(memoryStore); 160 if (mMemoryStore == null) { 161 mMemoryStore = memoryStore; 162 Log.i(TAG, "Installing MemoryStore"); 163 requestReadForAllChanged(); 164 } else { 165 mMemoryStore = memoryStore; 166 Log.e(TAG, "Reinstalling MemoryStore"); 167 // Our caller will call doWrites() eventually, so nothing more to do here. 168 } 169 } 170 171 /** 172 * Enable/Disable verbose logging. 173 * 174 * @param verbose true to enable and false to disable. 175 */ enableVerboseLogging(boolean verbose)176 public void enableVerboseLogging(boolean verbose) { 177 mVerboseLoggingEnabled = verbose; 178 } 179 180 /** 181 * Timestamp of the start of the most recent connection attempt. 182 * 183 * Based on mClock.getElapsedSinceBootMillis(). 184 * 185 * This is for calculating the time to connect and the duration of the connection. 186 * Any negative value means we are not currently connected. 187 */ 188 private long mTsConnectionAttemptStart = TS_NONE; 189 @VisibleForTesting 190 static final long TS_NONE = -1; 191 192 /** 193 * Timestamp captured when we find out about a firmware roam 194 */ 195 private long mTsRoam = TS_NONE; 196 197 /** 198 * Becomes true the first time we see a poll with a valid RSSI in a connection 199 */ 200 private boolean mPolled = false; 201 202 /** 203 * Records validation success for the current connection. 204 * 205 * We want to gather statistics only on the first success. 206 */ 207 private boolean mValidatedThisConnectionAtLeastOnce = false; 208 209 /** 210 * A note to ourself that we are attempting a network switch 211 */ 212 private boolean mAttemptingSwitch = false; 213 214 /** 215 * SSID of currently connected or connecting network. Used during disconnection 216 */ 217 private String mSsidCurr = ""; 218 /** 219 * SSID of previously connected network. Used during disconnection when connection attempt 220 * of current network is issued before the disconnection of previous network. 221 */ 222 private String mSsidPrev = ""; 223 /** 224 * A flag that notes that current disconnection is not generated by wpa_supplicant 225 * which may indicate abnormal disconnection. 226 */ 227 private boolean mNonlocalDisconnection = false; 228 private int mDisconnectionReason; 229 230 private long mFirmwareAlertTimeMs = TS_NONE; 231 232 /** 233 * @param clock is the time source 234 * @param l2KeySeed is for making our L2Keys usable only on this device 235 */ WifiScoreCard(Clock clock, String l2KeySeed, DeviceConfigFacade deviceConfigFacade)236 public WifiScoreCard(Clock clock, String l2KeySeed, DeviceConfigFacade deviceConfigFacade) { 237 mClock = clock; 238 mL2KeySeed = l2KeySeed; 239 mDummyPerBssid = new PerBssid("", MacAddress.fromString(DEFAULT_MAC_ADDRESS)); 240 mDummyPerNetwork = new PerNetwork(""); 241 mDeviceConfigFacade = deviceConfigFacade; 242 } 243 244 /** 245 * Gets the L2Key and GroupHint associated with the connection. 246 */ getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo)247 public @NonNull Pair<String, String> getL2KeyAndGroupHint(ExtendedWifiInfo wifiInfo) { 248 PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); 249 if (perBssid == mDummyPerBssid) { 250 return new Pair<>(null, null); 251 } 252 return new Pair<>(perBssid.getL2Key(), groupHintFromSsid(perBssid.ssid)); 253 } 254 255 /** 256 * Computes the GroupHint associated with the given ssid. 257 */ groupHintFromSsid(String ssid)258 public @NonNull String groupHintFromSsid(String ssid) { 259 final long groupIdHash = computeHashLong(ssid, mDummyPerBssid.bssid, mL2KeySeed); 260 return groupHintFromLong(groupIdHash); 261 } 262 263 /** 264 * Handle network disconnection or shutdown event 265 */ resetConnectionState()266 public void resetConnectionState() { 267 String ssidDisconnected = mAttemptingSwitch ? mSsidPrev : mSsidCurr; 268 updatePerNetwork(Event.DISCONNECTION, ssidDisconnected, INVALID_RSSI, LINK_SPEED_UNKNOWN, 269 UNKNOWN_REASON); 270 if (mVerboseLoggingEnabled && mTsConnectionAttemptStart > TS_NONE && !mAttemptingSwitch) { 271 Log.v(TAG, "handleNetworkDisconnect", new Exception()); 272 } 273 resetConnectionStateInternal(true); 274 } 275 276 /** 277 * @param calledFromResetConnectionState says the call is from outside the class, 278 * indicating that we need to respect the value of mAttemptingSwitch. 279 */ resetConnectionStateInternal(boolean calledFromResetConnectionState)280 private void resetConnectionStateInternal(boolean calledFromResetConnectionState) { 281 if (!calledFromResetConnectionState) { 282 mAttemptingSwitch = false; 283 } 284 if (!mAttemptingSwitch) { 285 mTsConnectionAttemptStart = TS_NONE; 286 } 287 mTsRoam = TS_NONE; 288 mPolled = false; 289 mValidatedThisConnectionAtLeastOnce = false; 290 mNonlocalDisconnection = false; 291 mFirmwareAlertTimeMs = TS_NONE; 292 } 293 294 /** 295 * Updates perBssid using relevant parts of WifiInfo 296 * 297 * @param wifiInfo object holding relevant values. 298 */ updatePerBssid(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo)299 private void updatePerBssid(WifiScoreCardProto.Event event, ExtendedWifiInfo wifiInfo) { 300 PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); 301 perBssid.updateEventStats(event, 302 wifiInfo.getFrequency(), 303 wifiInfo.getRssi(), 304 wifiInfo.getLinkSpeed()); 305 perBssid.setNetworkConfigId(wifiInfo.getNetworkId()); 306 logd("BSSID update " + event + " ID: " + perBssid.id + " " + wifiInfo); 307 } 308 309 /** 310 * Updates perNetwork with SSID, current RSSI and failureReason. failureReason is meaningful 311 * only during connection failure. 312 */ updatePerNetwork(WifiScoreCardProto.Event event, String ssid, int rssi, int txSpeed, int failureReason)313 private void updatePerNetwork(WifiScoreCardProto.Event event, String ssid, int rssi, 314 int txSpeed, int failureReason) { 315 PerNetwork perNetwork = lookupNetwork(ssid); 316 logd("network update " + event + ((ssid == null) ? " " : " " 317 + ssid) + " ID: " + perNetwork.id + " RSSI " + rssi + " txSpeed " + txSpeed); 318 perNetwork.updateEventStats(event, rssi, txSpeed, failureReason); 319 } 320 321 /** 322 * Updates the score card after a signal poll 323 * 324 * @param wifiInfo object holding relevant values 325 */ noteSignalPoll(@onNull ExtendedWifiInfo wifiInfo)326 public void noteSignalPoll(@NonNull ExtendedWifiInfo wifiInfo) { 327 if (!mPolled && wifiInfo.getRssi() != INVALID_RSSI) { 328 updatePerBssid(Event.FIRST_POLL_AFTER_CONNECTION, wifiInfo); 329 mPolled = true; 330 } 331 updatePerBssid(Event.SIGNAL_POLL, wifiInfo); 332 int validTxSpeed = geTxLinkSpeedWithSufficientTxRate(wifiInfo); 333 updatePerNetwork(Event.SIGNAL_POLL, wifiInfo.getSSID(), wifiInfo.getRssi(), 334 validTxSpeed, UNKNOWN_REASON); 335 if (mTsRoam > TS_NONE && wifiInfo.getRssi() != INVALID_RSSI) { 336 long duration = mClock.getElapsedSinceBootMillis() - mTsRoam; 337 if (duration >= SUCCESS_MILLIS_SINCE_ROAM) { 338 updatePerBssid(Event.ROAM_SUCCESS, wifiInfo); 339 mTsRoam = TS_NONE; 340 doWritesBssid(); 341 } 342 } 343 } 344 geTxLinkSpeedWithSufficientTxRate(@onNull ExtendedWifiInfo wifiInfo)345 private int geTxLinkSpeedWithSufficientTxRate(@NonNull ExtendedWifiInfo wifiInfo) { 346 int txRate = (int) Math.ceil(wifiInfo.getSuccessfulTxPacketsPerSecond() 347 + wifiInfo.getLostTxPacketsPerSecond() 348 + wifiInfo.getRetriedTxPacketsPerSecond()); 349 int txSpeed = wifiInfo.getTxLinkSpeedMbps(); 350 logd("txRate: " + txRate + " txSpeed: " + txSpeed); 351 return (txRate >= HEALTH_MONITOR_MIN_TX_PACKET_PER_SEC) ? txSpeed : LINK_SPEED_UNKNOWN; 352 } 353 354 /** Wait a few seconds before considering the roam successful */ 355 private static final long SUCCESS_MILLIS_SINCE_ROAM = 4_000; 356 357 /** 358 * Updates the score card after IP configuration 359 * 360 * @param wifiInfo object holding relevant values 361 */ noteIpConfiguration(@onNull ExtendedWifiInfo wifiInfo)362 public void noteIpConfiguration(@NonNull ExtendedWifiInfo wifiInfo) { 363 updatePerBssid(Event.IP_CONFIGURATION_SUCCESS, wifiInfo); 364 mAttemptingSwitch = false; 365 doWrites(); 366 } 367 368 /** 369 * Updates the score card after network validation success. 370 * 371 * @param wifiInfo object holding relevant values 372 */ noteValidationSuccess(@onNull ExtendedWifiInfo wifiInfo)373 public void noteValidationSuccess(@NonNull ExtendedWifiInfo wifiInfo) { 374 if (mValidatedThisConnectionAtLeastOnce) return; // Only once per connection 375 updatePerBssid(Event.VALIDATION_SUCCESS, wifiInfo); 376 mValidatedThisConnectionAtLeastOnce = true; 377 doWrites(); 378 } 379 380 /** 381 * Updates the score card after network validation failure 382 * 383 * @param wifiInfo object holding relevant values 384 */ noteValidationFailure(@onNull ExtendedWifiInfo wifiInfo)385 public void noteValidationFailure(@NonNull ExtendedWifiInfo wifiInfo) { 386 // VALIDATION_FAILURE is not currently recorded. 387 } 388 389 /** 390 * Records the start of a connection attempt 391 * 392 * @param wifiInfo may have state about an existing connection 393 * @param scanRssi is the highest RSSI of recent scan found from scanDetailCache 394 * @param ssid is the network SSID of connection attempt 395 */ noteConnectionAttempt(@onNull ExtendedWifiInfo wifiInfo, int scanRssi, String ssid)396 public void noteConnectionAttempt(@NonNull ExtendedWifiInfo wifiInfo, 397 int scanRssi, String ssid) { 398 // We may or may not be currently connected. If not, simply record the start. 399 // But if we are connected, wrap up the old one first. 400 if (mTsConnectionAttemptStart > TS_NONE) { 401 if (mPolled) { 402 updatePerBssid(Event.LAST_POLL_BEFORE_SWITCH, wifiInfo); 403 } 404 mAttemptingSwitch = true; 405 } 406 mTsConnectionAttemptStart = mClock.getElapsedSinceBootMillis(); 407 mPolled = false; 408 mSsidPrev = mSsidCurr; 409 mSsidCurr = ssid; 410 mFirmwareAlertTimeMs = TS_NONE; 411 412 updatePerNetwork(Event.CONNECTION_ATTEMPT, ssid, scanRssi, LINK_SPEED_UNKNOWN, 413 UNKNOWN_REASON); 414 logd("CONNECTION_ATTEMPT" + (mAttemptingSwitch ? " X " : " ") + wifiInfo); 415 } 416 417 /** 418 * Records a newly assigned NetworkAgent netId. 419 */ noteNetworkAgentCreated(@onNull ExtendedWifiInfo wifiInfo, int networkAgentId)420 public void noteNetworkAgentCreated(@NonNull ExtendedWifiInfo wifiInfo, int networkAgentId) { 421 PerBssid perBssid = lookupBssid(wifiInfo.getSSID(), wifiInfo.getBSSID()); 422 logd("NETWORK_AGENT_ID: " + networkAgentId + " ID: " + perBssid.id); 423 perBssid.mNetworkAgentId = networkAgentId; 424 } 425 426 /** 427 * Record disconnection not initiated by wpa_supplicant in connected mode 428 * @param reason is detailed disconnection reason code 429 */ noteNonlocalDisconnect(int reason)430 public void noteNonlocalDisconnect(int reason) { 431 mNonlocalDisconnection = true; 432 mDisconnectionReason = reason; 433 logd("nonlocal disconnection with reason: " + reason); 434 } 435 436 /** 437 * Record firmware alert timestamp and error code 438 */ noteFirmwareAlert(int errorCode)439 public void noteFirmwareAlert(int errorCode) { 440 mFirmwareAlertTimeMs = mClock.getElapsedSinceBootMillis(); 441 logd("firmware alert with error code: " + errorCode); 442 } 443 444 /** 445 * Updates the score card after a failed connection attempt 446 * 447 * @param wifiInfo object holding relevant values. 448 * @param scanRssi is the highest RSSI of recent scan found from scanDetailCache 449 * @param ssid is the network SSID. 450 * @param failureReason is connection failure reason 451 */ noteConnectionFailure(@onNull ExtendedWifiInfo wifiInfo, int scanRssi, String ssid, @FailureReason int failureReason)452 public void noteConnectionFailure(@NonNull ExtendedWifiInfo wifiInfo, 453 int scanRssi, String ssid, @FailureReason int failureReason) { 454 // TODO: add the breakdown of level2FailureReason 455 updatePerBssid(Event.CONNECTION_FAILURE, wifiInfo); 456 457 updatePerNetwork(Event.CONNECTION_FAILURE, ssid, scanRssi, LINK_SPEED_UNKNOWN, 458 failureReason); 459 resetConnectionStateInternal(false); 460 } 461 462 /** 463 * Updates the score card after network reachability failure 464 * 465 * @param wifiInfo object holding relevant values 466 */ noteIpReachabilityLost(@onNull ExtendedWifiInfo wifiInfo)467 public void noteIpReachabilityLost(@NonNull ExtendedWifiInfo wifiInfo) { 468 if (mTsRoam > TS_NONE) { 469 mTsConnectionAttemptStart = mTsRoam; // just to update elapsed 470 updatePerBssid(Event.ROAM_FAILURE, wifiInfo); 471 } else { 472 updatePerBssid(Event.IP_REACHABILITY_LOST, wifiInfo); 473 } 474 // No need to call resetConnectionStateInternal() because 475 // resetConnectionState() will be called after WifiNative.disconnect() in ClientModeImpl 476 doWrites(); 477 } 478 479 /** 480 * Updates the score card before a roam 481 * 482 * We may have already done a firmware roam, but wifiInfo has not yet 483 * been updated, so we still have the old state. 484 * 485 * @param wifiInfo object holding relevant values 486 */ noteRoam(@onNull ExtendedWifiInfo wifiInfo)487 private void noteRoam(@NonNull ExtendedWifiInfo wifiInfo) { 488 updatePerBssid(Event.LAST_POLL_BEFORE_ROAM, wifiInfo); 489 mTsRoam = mClock.getElapsedSinceBootMillis(); 490 } 491 492 /** 493 * Called when the supplicant state is about to change, before wifiInfo is updated 494 * 495 * @param wifiInfo object holding old values 496 * @param state the new supplicant state 497 */ noteSupplicantStateChanging(@onNull ExtendedWifiInfo wifiInfo, SupplicantState state)498 public void noteSupplicantStateChanging(@NonNull ExtendedWifiInfo wifiInfo, 499 SupplicantState state) { 500 if (state == SupplicantState.COMPLETED && wifiInfo.getSupplicantState() == state) { 501 // Our signal that a firmware roam has occurred 502 noteRoam(wifiInfo); 503 } 504 logd("Changing state to " + state + " " + wifiInfo); 505 } 506 507 /** 508 * Called after the supplicant state changed 509 * 510 * @param wifiInfo object holding old values 511 */ noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo)512 public void noteSupplicantStateChanged(ExtendedWifiInfo wifiInfo) { 513 logd("STATE " + wifiInfo); 514 } 515 516 /** 517 * Updates the score card after wifi is disabled 518 * 519 * @param wifiInfo object holding relevant values 520 */ noteWifiDisabled(@onNull ExtendedWifiInfo wifiInfo)521 public void noteWifiDisabled(@NonNull ExtendedWifiInfo wifiInfo) { 522 updatePerBssid(Event.WIFI_DISABLED, wifiInfo); 523 resetConnectionStateInternal(false); 524 doWrites(); 525 } 526 527 /** 528 * Records the last successful L2 connection timestamp for a BSSID. 529 * @return the previous BSSID connection time. 530 */ setBssidConnectionTimestampMs(String ssid, String bssid, long timeMs)531 public long setBssidConnectionTimestampMs(String ssid, String bssid, long timeMs) { 532 PerBssid perBssid = lookupBssid(ssid, bssid); 533 long prev = perBssid.lastConnectionTimestampMs; 534 perBssid.lastConnectionTimestampMs = timeMs; 535 return prev; 536 } 537 538 /** 539 * Returns the last successful L2 connection time for this BSSID. 540 */ getBssidConnectionTimestampMs(String ssid, String bssid)541 public long getBssidConnectionTimestampMs(String ssid, String bssid) { 542 return lookupBssid(ssid, bssid).lastConnectionTimestampMs; 543 } 544 545 /** 546 * Increment the blocklist streak count for a failure reason on an AP. 547 * @return the updated count 548 */ incrementBssidBlocklistStreak(String ssid, String bssid, @BssidBlocklistMonitor.FailureReason int reason)549 public int incrementBssidBlocklistStreak(String ssid, String bssid, 550 @BssidBlocklistMonitor.FailureReason int reason) { 551 PerBssid perBssid = lookupBssid(ssid, bssid); 552 return ++perBssid.blocklistStreakCount[reason]; 553 } 554 555 /** 556 * Get the blocklist streak count for a failure reason on an AP. 557 * @return the blocklist streak count 558 */ getBssidBlocklistStreak(String ssid, String bssid, @BssidBlocklistMonitor.FailureReason int reason)559 public int getBssidBlocklistStreak(String ssid, String bssid, 560 @BssidBlocklistMonitor.FailureReason int reason) { 561 return lookupBssid(ssid, bssid).blocklistStreakCount[reason]; 562 } 563 564 /** 565 * Clear the blocklist streak count for a failure reason on an AP. 566 */ resetBssidBlocklistStreak(String ssid, String bssid, @BssidBlocklistMonitor.FailureReason int reason)567 public void resetBssidBlocklistStreak(String ssid, String bssid, 568 @BssidBlocklistMonitor.FailureReason int reason) { 569 lookupBssid(ssid, bssid).blocklistStreakCount[reason] = 0; 570 } 571 572 /** 573 * Clear the blocklist streak count for all APs that belong to this SSID. 574 */ resetBssidBlocklistStreakForSsid(@onNull String ssid)575 public void resetBssidBlocklistStreakForSsid(@NonNull String ssid) { 576 Iterator<Map.Entry<MacAddress, PerBssid>> it = mApForBssid.entrySet().iterator(); 577 while (it.hasNext()) { 578 PerBssid perBssid = it.next().getValue(); 579 if (!ssid.equals(perBssid.ssid)) { 580 continue; 581 } 582 for (int i = 0; i < perBssid.blocklistStreakCount.length; i++) { 583 perBssid.blocklistStreakCount[i] = 0; 584 } 585 } 586 } 587 588 /** 589 * Detect abnormal disconnection at high RSSI with a high rate 590 */ detectAbnormalDisconnection()591 public int detectAbnormalDisconnection() { 592 String ssid = mAttemptingSwitch ? mSsidPrev : mSsidCurr; 593 PerNetwork perNetwork = lookupNetwork(ssid); 594 NetworkConnectionStats recentStats = perNetwork.getRecentStats(); 595 if (recentStats.getRecentCountCode() == CNT_SHORT_CONNECTION_NONLOCAL) { 596 return detectAbnormalFailureReason(recentStats, CNT_SHORT_CONNECTION_NONLOCAL, 597 REASON_SHORT_CONNECTION_NONLOCAL, 598 mDeviceConfigFacade.getShortConnectionNonlocalHighThrPercent(), 599 mDeviceConfigFacade.getShortConnectionNonlocalCountMin(), 600 CNT_DISCONNECTION); 601 } else if (recentStats.getRecentCountCode() == CNT_DISCONNECTION_NONLOCAL) { 602 return detectAbnormalFailureReason(recentStats, CNT_DISCONNECTION_NONLOCAL, 603 REASON_DISCONNECTION_NONLOCAL, 604 mDeviceConfigFacade.getDisconnectionNonlocalHighThrPercent(), 605 mDeviceConfigFacade.getDisconnectionNonlocalCountMin(), 606 CNT_DISCONNECTION); 607 } else { 608 return REASON_NO_FAILURE; 609 } 610 } 611 612 /** 613 * Detect abnormal connection failure at high RSSI with a high rate 614 */ detectAbnormalConnectionFailure(String ssid)615 public int detectAbnormalConnectionFailure(String ssid) { 616 PerNetwork perNetwork = lookupNetwork(ssid); 617 NetworkConnectionStats recentStats = perNetwork.getRecentStats(); 618 int recentCountCode = recentStats.getRecentCountCode(); 619 if (recentCountCode == CNT_AUTHENTICATION_FAILURE) { 620 return detectAbnormalFailureReason(recentStats, CNT_AUTHENTICATION_FAILURE, 621 REASON_AUTH_FAILURE, 622 mDeviceConfigFacade.getAuthFailureHighThrPercent(), 623 mDeviceConfigFacade.getAuthFailureCountMin(), 624 CNT_CONNECTION_ATTEMPT); 625 } else if (recentCountCode == CNT_ASSOCIATION_REJECTION) { 626 return detectAbnormalFailureReason(recentStats, CNT_ASSOCIATION_REJECTION, 627 REASON_ASSOC_REJECTION, 628 mDeviceConfigFacade.getAssocRejectionHighThrPercent(), 629 mDeviceConfigFacade.getAssocRejectionCountMin(), 630 CNT_CONNECTION_ATTEMPT); 631 } else if (recentCountCode == CNT_ASSOCIATION_TIMEOUT) { 632 return detectAbnormalFailureReason(recentStats, CNT_ASSOCIATION_TIMEOUT, 633 REASON_ASSOC_TIMEOUT, 634 mDeviceConfigFacade.getAssocTimeoutHighThrPercent(), 635 mDeviceConfigFacade.getAssocTimeoutCountMin(), 636 CNT_CONNECTION_ATTEMPT); 637 } else if (recentCountCode == CNT_CONNECTION_FAILURE) { 638 return detectAbnormalFailureReason(recentStats, CNT_CONNECTION_FAILURE, 639 REASON_CONNECTION_FAILURE, 640 mDeviceConfigFacade.getConnectionFailureHighThrPercent(), 641 mDeviceConfigFacade.getConnectionFailureCountMin(), 642 CNT_CONNECTION_ATTEMPT); 643 } else { 644 return REASON_NO_FAILURE; 645 } 646 } 647 detectAbnormalFailureReason(NetworkConnectionStats stats, int countCode, int reasonCode, int highThresholdPercent, int minCount, int refCountCode)648 private int detectAbnormalFailureReason(NetworkConnectionStats stats, int countCode, 649 int reasonCode, int highThresholdPercent, int minCount, int refCountCode) { 650 // To detect abnormal failure which may trigger bugReport, 651 // increase the detection threshold by thresholdRatio 652 int thresholdRatio = 653 mDeviceConfigFacade.getBugReportThresholdExtraRatio(); 654 if (isHighPercentageAndEnoughCount(stats, countCode, reasonCode, 655 highThresholdPercent * thresholdRatio, 656 minCount * thresholdRatio, 657 refCountCode)) { 658 return reasonCode; 659 } else { 660 return REASON_NO_FAILURE; 661 } 662 } 663 isHighPercentageAndEnoughCount(NetworkConnectionStats stats, int countCode, int reasonCode, int highThresholdPercent, int minCount, int refCountCode)664 private boolean isHighPercentageAndEnoughCount(NetworkConnectionStats stats, int countCode, 665 int reasonCode, int highThresholdPercent, int minCount, int refCountCode) { 666 highThresholdPercent = Math.min(highThresholdPercent, 100); 667 // Use Laplace's rule of succession, useful especially for a small 668 // connection attempt count 669 // R = (f+1)/(n+2) with a pseudo count of 2 (one for f and one for s) 670 return ((stats.getCount(countCode) >= minCount) 671 && ((stats.getCount(countCode) + 1) * 100) 672 >= (highThresholdPercent * (stats.getCount(refCountCode) + 2))); 673 } 674 675 final class PerBssid extends MemoryStoreAccessBase { 676 public int id; 677 public final String ssid; 678 public final MacAddress bssid; 679 public final int[] blocklistStreakCount = 680 new int[BssidBlocklistMonitor.NUMBER_REASON_CODES]; 681 // The wall clock time in milliseconds for the last successful l2 connection. 682 public long lastConnectionTimestampMs; 683 public boolean changed; 684 public boolean referenced; 685 686 private SecurityType mSecurityType = null; 687 private int mNetworkAgentId = Integer.MIN_VALUE; 688 private int mNetworkConfigId = Integer.MIN_VALUE; 689 private final Map<Pair<Event, Integer>, PerSignal> 690 mSignalForEventAndFrequency = new ArrayMap<>(); PerBssid(String ssid, MacAddress bssid)691 PerBssid(String ssid, MacAddress bssid) { 692 super(computeHashLong(ssid, bssid, mL2KeySeed)); 693 this.ssid = ssid; 694 this.bssid = bssid; 695 this.id = idFromLong(); 696 this.changed = false; 697 this.referenced = false; 698 } updateEventStats(Event event, int frequency, int rssi, int linkspeed)699 void updateEventStats(Event event, int frequency, int rssi, int linkspeed) { 700 PerSignal perSignal = lookupSignal(event, frequency); 701 if (rssi != INVALID_RSSI) { 702 perSignal.rssi.update(rssi); 703 changed = true; 704 } 705 if (linkspeed > 0) { 706 perSignal.linkspeed.update(linkspeed); 707 changed = true; 708 } 709 if (perSignal.elapsedMs != null && mTsConnectionAttemptStart > TS_NONE) { 710 long millis = mClock.getElapsedSinceBootMillis() - mTsConnectionAttemptStart; 711 if (millis >= 0) { 712 perSignal.elapsedMs.update(millis); 713 changed = true; 714 } 715 } 716 } lookupSignal(Event event, int frequency)717 PerSignal lookupSignal(Event event, int frequency) { 718 finishPendingRead(); 719 Pair<Event, Integer> key = new Pair<>(event, frequency); 720 PerSignal ans = mSignalForEventAndFrequency.get(key); 721 if (ans == null) { 722 ans = new PerSignal(event, frequency); 723 mSignalForEventAndFrequency.put(key, ans); 724 } 725 return ans; 726 } getSecurityType()727 SecurityType getSecurityType() { 728 finishPendingRead(); 729 return mSecurityType; 730 } setSecurityType(SecurityType securityType)731 void setSecurityType(SecurityType securityType) { 732 finishPendingRead(); 733 if (!Objects.equals(securityType, mSecurityType)) { 734 mSecurityType = securityType; 735 changed = true; 736 } 737 } setNetworkConfigId(int networkConfigId)738 void setNetworkConfigId(int networkConfigId) { 739 // Not serialized, so don't need to set changed, etc. 740 if (networkConfigId >= 0) { 741 mNetworkConfigId = networkConfigId; 742 } 743 } toAccessPoint()744 AccessPoint toAccessPoint() { 745 return toAccessPoint(false); 746 } toAccessPoint(boolean obfuscate)747 AccessPoint toAccessPoint(boolean obfuscate) { 748 finishPendingRead(); 749 AccessPoint.Builder builder = AccessPoint.newBuilder(); 750 builder.setId(id); 751 if (!obfuscate) { 752 builder.setBssid(ByteString.copyFrom(bssid.toByteArray())); 753 } 754 if (mSecurityType != null) { 755 builder.setSecurityType(mSecurityType); 756 } 757 for (PerSignal sig: mSignalForEventAndFrequency.values()) { 758 builder.addEventStats(sig.toSignal()); 759 } 760 return builder.build(); 761 } merge(AccessPoint ap)762 PerBssid merge(AccessPoint ap) { 763 if (ap.hasId() && this.id != ap.getId()) { 764 return this; 765 } 766 if (ap.hasSecurityType()) { 767 SecurityType prev = ap.getSecurityType(); 768 if (mSecurityType == null) { 769 mSecurityType = prev; 770 } else if (!mSecurityType.equals(prev)) { 771 if (mVerboseLoggingEnabled) { 772 Log.i(TAG, "ID: " + id 773 + "SecurityType changed: " + prev + " to " + mSecurityType); 774 } 775 changed = true; 776 } 777 } 778 for (Signal signal: ap.getEventStatsList()) { 779 Pair<Event, Integer> key = new Pair<>(signal.getEvent(), signal.getFrequency()); 780 PerSignal perSignal = mSignalForEventAndFrequency.get(key); 781 if (perSignal == null) { 782 mSignalForEventAndFrequency.put(key, 783 new PerSignal(key.first, key.second).merge(signal)); 784 // No need to set changed for this, since we are in sync with what's stored 785 } else { 786 perSignal.merge(signal); 787 changed = true; 788 } 789 } 790 return this; 791 } 792 793 /** 794 * Handles (when convenient) the arrival of previously stored data. 795 * 796 * The response from IpMemoryStore arrives on a different thread, so we 797 * defer handling it until here, when we're on our favorite thread and 798 * in a good position to deal with it. We may have already collected some 799 * data before now, so we need to be prepared to merge the new and old together. 800 */ finishPendingRead()801 void finishPendingRead() { 802 final byte[] serialized = finishPendingReadBytes(); 803 if (serialized == null) return; 804 AccessPoint ap; 805 try { 806 ap = AccessPoint.parseFrom(serialized); 807 } catch (InvalidProtocolBufferException e) { 808 Log.e(TAG, "Failed to deserialize", e); 809 return; 810 } 811 merge(ap); 812 } 813 814 /** 815 * Estimates the probability of getting internet access, based on the 816 * device experience. 817 * 818 * @return a probability, expressed as a percentage in the range 0 to 100 819 */ estimatePercentInternetAvailability()820 public int estimatePercentInternetAvailability() { 821 // Initialize counts accoring to Laplace's rule of succession 822 int trials = 2; 823 int successes = 1; 824 // Aggregate over all of the frequencies 825 for (PerSignal s : mSignalForEventAndFrequency.values()) { 826 switch (s.event) { 827 case IP_CONFIGURATION_SUCCESS: 828 if (s.elapsedMs != null) { 829 trials += s.elapsedMs.count; 830 } 831 break; 832 case VALIDATION_SUCCESS: 833 if (s.elapsedMs != null) { 834 successes += s.elapsedMs.count; 835 } 836 break; 837 default: 838 break; 839 } 840 } 841 // Note that because of roaming it is possible to count successes 842 // without corresponding trials. 843 return Math.min(Math.max(Math.round(successes * 100.0f / trials), 0), 100); 844 } 845 } 846 847 /** 848 * A class collecting the connection stats of one network or SSID. 849 */ 850 final class PerNetwork extends MemoryStoreAccessBase { 851 public int id; 852 public final String ssid; 853 public boolean changed; 854 private int mLastRssiPoll = INVALID_RSSI; 855 private int mLastTxSpeedPoll = LINK_SPEED_UNKNOWN; 856 private long mLastRssiPollTimeMs = TS_NONE; 857 private long mConnectionSessionStartTimeMs = TS_NONE; 858 private NetworkConnectionStats mRecentStats; 859 private NetworkConnectionStats mStatsCurrBuild; 860 private NetworkConnectionStats mStatsPrevBuild; 861 private LruList<Integer> mFrequencyList; 862 // In memory keep frequency with timestamp last time available, the elapsed time since boot. 863 private SparseLongArray mFreqTimestamp; 864 PerNetwork(String ssid)865 PerNetwork(String ssid) { 866 super(computeHashLong(ssid, MacAddress.fromString(DEFAULT_MAC_ADDRESS), mL2KeySeed)); 867 this.ssid = ssid; 868 this.id = idFromLong(); 869 this.changed = false; 870 mRecentStats = new NetworkConnectionStats(); 871 mStatsCurrBuild = new NetworkConnectionStats(); 872 mStatsPrevBuild = new NetworkConnectionStats(); 873 mFrequencyList = new LruList<>(MAX_FREQUENCIES_PER_SSID); 874 mFreqTimestamp = new SparseLongArray(); 875 } 876 updateEventStats(Event event, int rssi, int txSpeed, int failureReason)877 void updateEventStats(Event event, int rssi, int txSpeed, int failureReason) { 878 finishPendingRead(); 879 long currTimeMs = mClock.getElapsedSinceBootMillis(); 880 switch (event) { 881 case SIGNAL_POLL: 882 mLastRssiPoll = rssi; 883 mLastRssiPollTimeMs = currTimeMs; 884 mLastTxSpeedPoll = txSpeed; 885 changed = true; 886 break; 887 case CONNECTION_ATTEMPT: 888 logd(" scan rssi: " + rssi); 889 if (rssi >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()) { 890 mRecentStats.incrementCount(CNT_CONNECTION_ATTEMPT); 891 } 892 mConnectionSessionStartTimeMs = currTimeMs; 893 changed = true; 894 break; 895 case CONNECTION_FAILURE: 896 mConnectionSessionStartTimeMs = TS_NONE; 897 if (rssi >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm()) { 898 if (failureReason != BssidBlocklistMonitor.REASON_WRONG_PASSWORD) { 899 mRecentStats.incrementCount(CNT_CONNECTION_FAILURE); 900 mRecentStats.incrementCount(CNT_CONSECUTIVE_CONNECTION_FAILURE); 901 } 902 switch (failureReason) { 903 case BssidBlocklistMonitor.REASON_AP_UNABLE_TO_HANDLE_NEW_STA: 904 case BssidBlocklistMonitor.REASON_ASSOCIATION_REJECTION: 905 mRecentStats.incrementCount(CNT_ASSOCIATION_REJECTION); 906 break; 907 case BssidBlocklistMonitor.REASON_ASSOCIATION_TIMEOUT: 908 mRecentStats.incrementCount(CNT_ASSOCIATION_TIMEOUT); 909 break; 910 case BssidBlocklistMonitor.REASON_AUTHENTICATION_FAILURE: 911 case BssidBlocklistMonitor.REASON_EAP_FAILURE: 912 mRecentStats.incrementCount(CNT_AUTHENTICATION_FAILURE); 913 break; 914 case BssidBlocklistMonitor.REASON_WRONG_PASSWORD: 915 case BssidBlocklistMonitor.REASON_DHCP_FAILURE: 916 default: 917 break; 918 } 919 } 920 changed = true; 921 break; 922 case WIFI_DISABLED: 923 case DISCONNECTION: 924 handleDisconnection(); 925 changed = true; 926 break; 927 default: 928 break; 929 } 930 logd(this.toString()); 931 } 932 @Override toString()933 public String toString() { 934 StringBuilder sb = new StringBuilder(); 935 sb.append("SSID: ").append(ssid).append("\n"); 936 if (mLastRssiPollTimeMs != TS_NONE) { 937 sb.append(" LastRssiPollTime: "); 938 sb.append(mLastRssiPollTimeMs); 939 } 940 sb.append(" LastRssiPoll: " + mLastRssiPoll); 941 sb.append(" LastTxSpeedPoll: " + mLastTxSpeedPoll); 942 sb.append("\n"); 943 sb.append(" StatsRecent: ").append(mRecentStats).append("\n"); 944 sb.append(" StatsCurr: ").append(mStatsCurrBuild).append("\n"); 945 sb.append(" StatsPrev: ").append(mStatsPrevBuild); 946 return sb.toString(); 947 } handleDisconnection()948 private void handleDisconnection() { 949 if (mConnectionSessionStartTimeMs > TS_NONE) { 950 long currTimeMs = mClock.getElapsedSinceBootMillis(); 951 int currSessionDurationMs = (int) (currTimeMs - mConnectionSessionStartTimeMs); 952 int currSessionDurationSec = currSessionDurationMs / 1000; 953 mRecentStats.accumulate(CNT_CONNECTION_DURATION_SEC, currSessionDurationSec); 954 long timeSinceLastRssiPollMs = currTimeMs - mLastRssiPollTimeMs; 955 boolean hasRecentRssiPoll = mLastRssiPollTimeMs > TS_NONE 956 && timeSinceLastRssiPollMs <= mDeviceConfigFacade 957 .getHealthMonitorRssiPollValidTimeMs(); 958 if (hasRecentRssiPoll) { 959 mRecentStats.incrementCount(CNT_DISCONNECTION); 960 } 961 int fwAlertValidTimeMs = mDeviceConfigFacade.getHealthMonitorFwAlertValidTimeMs(); 962 long timeSinceLastFirmAlert = currTimeMs - mFirmwareAlertTimeMs; 963 boolean isInvalidFwAlertTime = mFirmwareAlertTimeMs == TS_NONE; 964 boolean disableFwAlertCheck = fwAlertValidTimeMs == -1; 965 boolean passFirmwareAlertCheck = disableFwAlertCheck ? true : (isInvalidFwAlertTime 966 ? false : timeSinceLastFirmAlert < fwAlertValidTimeMs); 967 boolean hasHighRssiOrHighTxSpeed = 968 mLastRssiPoll >= mDeviceConfigFacade.getHealthMonitorMinRssiThrDbm() 969 || mLastTxSpeedPoll >= HEALTH_MONITOR_COUNT_TX_SPEED_MIN_MBPS; 970 if (mNonlocalDisconnection && hasRecentRssiPoll 971 && isAbnormalDisconnectionReason(mDisconnectionReason) 972 && passFirmwareAlertCheck 973 && hasHighRssiOrHighTxSpeed) { 974 mRecentStats.incrementCount(CNT_DISCONNECTION_NONLOCAL); 975 if (currSessionDurationMs <= mDeviceConfigFacade 976 .getHealthMonitorShortConnectionDurationThrMs()) { 977 mRecentStats.incrementCount(CNT_SHORT_CONNECTION_NONLOCAL); 978 } 979 } 980 } 981 // Reset CNT_CONSECUTIVE_CONNECTION_FAILURE here so that it can report the correct 982 // failure count after a connection success 983 mRecentStats.clearCount(CNT_CONSECUTIVE_CONNECTION_FAILURE); 984 mConnectionSessionStartTimeMs = TS_NONE; 985 mLastRssiPollTimeMs = TS_NONE; 986 } 987 isAbnormalDisconnectionReason(int disconnectionReason)988 private boolean isAbnormalDisconnectionReason(int disconnectionReason) { 989 long mask = mDeviceConfigFacade.getAbnormalDisconnectionReasonCodeMask(); 990 return disconnectionReason >= 0 && disconnectionReason <= 63 991 && ((mask >> disconnectionReason) & 0x1) == 0x1; 992 } 993 getRecentStats()994 @NonNull NetworkConnectionStats getRecentStats() { 995 return mRecentStats; 996 } getStatsCurrBuild()997 @NonNull NetworkConnectionStats getStatsCurrBuild() { 998 return mStatsCurrBuild; 999 } getStatsPrevBuild()1000 @NonNull NetworkConnectionStats getStatsPrevBuild() { 1001 return mStatsPrevBuild; 1002 } 1003 1004 /** 1005 * Retrieve the list of frequencies seen for this network, with the most recent first. 1006 * @param ageInMills Max age to filter the channels. 1007 * @return a list of frequencies 1008 */ getFrequencies(Long ageInMills)1009 List<Integer> getFrequencies(Long ageInMills) { 1010 List<Integer> results = new ArrayList<>(); 1011 Long nowInMills = mClock.getElapsedSinceBootMillis(); 1012 for (Integer freq : mFrequencyList.getEntries()) { 1013 if (nowInMills - mFreqTimestamp.get(freq, 0L) > ageInMills) { 1014 continue; 1015 } 1016 results.add(freq); 1017 } 1018 return results; 1019 } 1020 1021 /** 1022 * Add a frequency to the list of frequencies for this network. 1023 * Will evict the least recently added frequency if the cache is full. 1024 */ addFrequency(int frequency)1025 void addFrequency(int frequency) { 1026 mFrequencyList.add(frequency); 1027 mFreqTimestamp.put(frequency, mClock.getElapsedSinceBootMillis()); 1028 } 1029 1030 /** 1031 /* Detect a significant failure stats change with historical data 1032 /* or high failure stats without historical data. 1033 /* @return 0 if recentStats doesn't have sufficient data 1034 * 1 if recentStats has sufficient data while statsPrevBuild doesn't 1035 * 2 if recentStats and statsPrevBuild have sufficient data 1036 */ dailyDetection(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh)1037 int dailyDetection(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh) { 1038 finishPendingRead(); 1039 dailyDetectionDisconnectionEvent(statsDec, statsInc, statsHigh); 1040 return dailyDetectionConnectionEvent(statsDec, statsInc, statsHigh); 1041 } 1042 dailyDetectionConnectionEvent(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh)1043 private int dailyDetectionConnectionEvent(FailureStats statsDec, FailureStats statsInc, 1044 FailureStats statsHigh) { 1045 // Skip daily detection if recentStats is not sufficient 1046 if (!isRecentConnectionStatsSufficient()) return INSUFFICIENT_RECENT_STATS; 1047 if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT) 1048 < mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt()) { 1049 // don't have enough historical data, 1050 // so only detect high failure stats without relying on mStatsPrevBuild. 1051 recentStatsHighDetectionConnection(statsHigh); 1052 return SUFFICIENT_RECENT_STATS_ONLY; 1053 } else { 1054 // mStatsPrevBuild has enough updates, 1055 // detect improvement or degradation 1056 statsDeltaDetectionConnection(statsDec, statsInc); 1057 return SUFFICIENT_RECENT_PREV_STATS; 1058 } 1059 } 1060 dailyDetectionDisconnectionEvent(FailureStats statsDec, FailureStats statsInc, FailureStats statsHigh)1061 private void dailyDetectionDisconnectionEvent(FailureStats statsDec, FailureStats statsInc, 1062 FailureStats statsHigh) { 1063 // Skip daily detection if recentStats is not sufficient 1064 int minConnectAttempt = mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt(); 1065 if (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT) < minConnectAttempt) { 1066 return; 1067 } 1068 if (mStatsPrevBuild.getCount(CNT_CONNECTION_ATTEMPT) < minConnectAttempt) { 1069 recentStatsHighDetectionDisconnection(statsHigh); 1070 } else { 1071 statsDeltaDetectionDisconnection(statsDec, statsInc); 1072 } 1073 } 1074 statsDeltaDetectionConnection(FailureStats statsDec, FailureStats statsInc)1075 private void statsDeltaDetectionConnection(FailureStats statsDec, 1076 FailureStats statsInc) { 1077 statsDeltaDetection(statsDec, statsInc, CNT_CONNECTION_FAILURE, 1078 REASON_CONNECTION_FAILURE, 1079 mDeviceConfigFacade.getConnectionFailureCountMin(), 1080 CNT_CONNECTION_ATTEMPT); 1081 statsDeltaDetection(statsDec, statsInc, CNT_AUTHENTICATION_FAILURE, 1082 REASON_AUTH_FAILURE, 1083 mDeviceConfigFacade.getAuthFailureCountMin(), 1084 CNT_CONNECTION_ATTEMPT); 1085 statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_REJECTION, 1086 REASON_ASSOC_REJECTION, 1087 mDeviceConfigFacade.getAssocRejectionCountMin(), 1088 CNT_CONNECTION_ATTEMPT); 1089 statsDeltaDetection(statsDec, statsInc, CNT_ASSOCIATION_TIMEOUT, 1090 REASON_ASSOC_TIMEOUT, 1091 mDeviceConfigFacade.getAssocTimeoutCountMin(), 1092 CNT_CONNECTION_ATTEMPT); 1093 } 1094 recentStatsHighDetectionConnection(FailureStats statsHigh)1095 private void recentStatsHighDetectionConnection(FailureStats statsHigh) { 1096 recentStatsHighDetection(statsHigh, CNT_CONNECTION_FAILURE, 1097 REASON_CONNECTION_FAILURE, 1098 mDeviceConfigFacade.getConnectionFailureHighThrPercent(), 1099 mDeviceConfigFacade.getConnectionFailureCountMin(), 1100 CNT_CONNECTION_ATTEMPT); 1101 recentStatsHighDetection(statsHigh, CNT_AUTHENTICATION_FAILURE, 1102 REASON_AUTH_FAILURE, 1103 mDeviceConfigFacade.getAuthFailureHighThrPercent(), 1104 mDeviceConfigFacade.getAuthFailureCountMin(), 1105 CNT_CONNECTION_ATTEMPT); 1106 recentStatsHighDetection(statsHigh, CNT_ASSOCIATION_REJECTION, 1107 REASON_ASSOC_REJECTION, 1108 mDeviceConfigFacade.getAssocRejectionHighThrPercent(), 1109 mDeviceConfigFacade.getAssocRejectionCountMin(), 1110 CNT_CONNECTION_ATTEMPT); 1111 recentStatsHighDetection(statsHigh, CNT_ASSOCIATION_TIMEOUT, 1112 REASON_ASSOC_TIMEOUT, 1113 mDeviceConfigFacade.getAssocTimeoutHighThrPercent(), 1114 mDeviceConfigFacade.getAssocTimeoutCountMin(), 1115 CNT_CONNECTION_ATTEMPT); 1116 } 1117 statsDeltaDetectionDisconnection(FailureStats statsDec, FailureStats statsInc)1118 private void statsDeltaDetectionDisconnection(FailureStats statsDec, 1119 FailureStats statsInc) { 1120 statsDeltaDetection(statsDec, statsInc, CNT_SHORT_CONNECTION_NONLOCAL, 1121 REASON_SHORT_CONNECTION_NONLOCAL, 1122 mDeviceConfigFacade.getShortConnectionNonlocalCountMin(), 1123 CNT_CONNECTION_ATTEMPT); 1124 statsDeltaDetection(statsDec, statsInc, CNT_DISCONNECTION_NONLOCAL, 1125 REASON_DISCONNECTION_NONLOCAL, 1126 mDeviceConfigFacade.getDisconnectionNonlocalCountMin(), 1127 CNT_CONNECTION_ATTEMPT); 1128 } 1129 recentStatsHighDetectionDisconnection(FailureStats statsHigh)1130 private void recentStatsHighDetectionDisconnection(FailureStats statsHigh) { 1131 recentStatsHighDetection(statsHigh, CNT_SHORT_CONNECTION_NONLOCAL, 1132 REASON_SHORT_CONNECTION_NONLOCAL, 1133 mDeviceConfigFacade.getShortConnectionNonlocalHighThrPercent(), 1134 mDeviceConfigFacade.getShortConnectionNonlocalCountMin(), 1135 CNT_DISCONNECTION); 1136 recentStatsHighDetection(statsHigh, CNT_DISCONNECTION_NONLOCAL, 1137 REASON_DISCONNECTION_NONLOCAL, 1138 mDeviceConfigFacade.getDisconnectionNonlocalHighThrPercent(), 1139 mDeviceConfigFacade.getDisconnectionNonlocalCountMin(), 1140 CNT_DISCONNECTION); 1141 } 1142 statsDeltaDetection(FailureStats statsDec, FailureStats statsInc, int countCode, int reasonCode, int minCount, int refCountCode)1143 private boolean statsDeltaDetection(FailureStats statsDec, 1144 FailureStats statsInc, int countCode, int reasonCode, 1145 int minCount, int refCountCode) { 1146 if (isRatioAboveThreshold(mRecentStats, mStatsPrevBuild, countCode, refCountCode) 1147 && mRecentStats.getCount(countCode) >= minCount) { 1148 statsInc.incrementCount(reasonCode); 1149 return true; 1150 } 1151 1152 if (isRatioAboveThreshold(mStatsPrevBuild, mRecentStats, countCode, refCountCode) 1153 && mStatsPrevBuild.getCount(countCode) >= minCount) { 1154 statsDec.incrementCount(reasonCode); 1155 return true; 1156 } 1157 return false; 1158 } 1159 recentStatsHighDetection(FailureStats statsHigh, int countCode, int reasonCode, int highThresholdPercent, int minCount, int refCountCode)1160 private boolean recentStatsHighDetection(FailureStats statsHigh, int countCode, 1161 int reasonCode, int highThresholdPercent, int minCount, int refCountCode) { 1162 if (isHighPercentageAndEnoughCount(mRecentStats, countCode, reasonCode, 1163 highThresholdPercent, minCount, refCountCode)) { 1164 statsHigh.incrementCount(reasonCode); 1165 return true; 1166 } 1167 return false; 1168 } 1169 isRatioAboveThreshold(NetworkConnectionStats stats1, NetworkConnectionStats stats2, @ConnectionCountCode int countCode, int refCountCode)1170 private boolean isRatioAboveThreshold(NetworkConnectionStats stats1, 1171 NetworkConnectionStats stats2, 1172 @ConnectionCountCode int countCode, int refCountCode) { 1173 // Also with Laplace's rule of succession discussed above 1174 // R1 = (stats1(countCode) + 1) / (stats1(refCountCode) + 2) 1175 // R2 = (stats2(countCode) + 1) / (stats2(refCountCode) + 2) 1176 // Check R1 / R2 >= ratioThr 1177 return ((stats1.getCount(countCode) + 1) * (stats2.getCount(refCountCode) + 2) 1178 * mDeviceConfigFacade.HEALTH_MONITOR_RATIO_THR_DENOMINATOR) 1179 >= ((stats1.getCount(refCountCode) + 2) * (stats2.getCount(countCode) + 1) 1180 * mDeviceConfigFacade.getHealthMonitorRatioThrNumerator()); 1181 } 1182 isRecentConnectionStatsSufficient()1183 private boolean isRecentConnectionStatsSufficient() { 1184 return (mRecentStats.getCount(CNT_CONNECTION_ATTEMPT) 1185 >= mDeviceConfigFacade.getHealthMonitorMinNumConnectionAttempt()); 1186 } 1187 1188 // Update StatsCurrBuild with recentStats and clear recentStats updateAfterDailyDetection()1189 void updateAfterDailyDetection() { 1190 // Skip update if recentStats is not sufficient since daily detection is also skipped 1191 if (!isRecentConnectionStatsSufficient()) return; 1192 mStatsCurrBuild.accumulateAll(mRecentStats); 1193 mRecentStats.clear(); 1194 changed = true; 1195 } 1196 1197 // Refresh StatsPrevBuild with StatsCurrBuild which is cleared afterwards updateAfterSwBuildChange()1198 void updateAfterSwBuildChange() { 1199 finishPendingRead(); 1200 mStatsPrevBuild.copy(mStatsCurrBuild); 1201 mRecentStats.clear(); 1202 mStatsCurrBuild.clear(); 1203 changed = true; 1204 } 1205 toNetworkStats()1206 NetworkStats toNetworkStats() { 1207 finishPendingRead(); 1208 NetworkStats.Builder builder = NetworkStats.newBuilder(); 1209 builder.setId(id); 1210 builder.setRecentStats(toConnectionStats(mRecentStats)); 1211 builder.setStatsCurrBuild(toConnectionStats(mStatsCurrBuild)); 1212 builder.setStatsPrevBuild(toConnectionStats(mStatsPrevBuild)); 1213 if (mFrequencyList.size() > 0) { 1214 builder.addAllFrequencies(mFrequencyList.getEntries()); 1215 } 1216 return builder.build(); 1217 } 1218 toConnectionStats(NetworkConnectionStats stats)1219 private ConnectionStats toConnectionStats(NetworkConnectionStats stats) { 1220 ConnectionStats.Builder builder = ConnectionStats.newBuilder(); 1221 builder.setNumConnectionAttempt(stats.getCount(CNT_CONNECTION_ATTEMPT)); 1222 builder.setNumConnectionFailure(stats.getCount(CNT_CONNECTION_FAILURE)); 1223 builder.setConnectionDurationSec(stats.getCount(CNT_CONNECTION_DURATION_SEC)); 1224 builder.setNumDisconnectionNonlocal(stats.getCount(CNT_DISCONNECTION_NONLOCAL)); 1225 builder.setNumDisconnection(stats.getCount(CNT_DISCONNECTION)); 1226 builder.setNumShortConnectionNonlocal(stats.getCount(CNT_SHORT_CONNECTION_NONLOCAL)); 1227 builder.setNumAssociationRejection(stats.getCount(CNT_ASSOCIATION_REJECTION)); 1228 builder.setNumAssociationTimeout(stats.getCount(CNT_ASSOCIATION_TIMEOUT)); 1229 builder.setNumAuthenticationFailure(stats.getCount(CNT_AUTHENTICATION_FAILURE)); 1230 return builder.build(); 1231 } 1232 finishPendingRead()1233 void finishPendingRead() { 1234 final byte[] serialized = finishPendingReadBytes(); 1235 if (serialized == null) return; 1236 NetworkStats ns; 1237 try { 1238 ns = NetworkStats.parseFrom(serialized); 1239 } catch (InvalidProtocolBufferException e) { 1240 Log.e(TAG, "Failed to deserialize", e); 1241 return; 1242 } 1243 mergeNetworkStatsFromMemory(ns); 1244 changed = true; 1245 } 1246 mergeNetworkStatsFromMemory(@onNull NetworkStats ns)1247 PerNetwork mergeNetworkStatsFromMemory(@NonNull NetworkStats ns) { 1248 if (ns.hasId() && this.id != ns.getId()) { 1249 return this; 1250 } 1251 if (ns.hasRecentStats()) { 1252 ConnectionStats recentStats = ns.getRecentStats(); 1253 mergeConnectionStats(recentStats, mRecentStats); 1254 } 1255 if (ns.hasStatsCurrBuild()) { 1256 ConnectionStats statsCurr = ns.getStatsCurrBuild(); 1257 mStatsCurrBuild.clear(); 1258 mergeConnectionStats(statsCurr, mStatsCurrBuild); 1259 } 1260 if (ns.hasStatsPrevBuild()) { 1261 ConnectionStats statsPrev = ns.getStatsPrevBuild(); 1262 mStatsPrevBuild.clear(); 1263 mergeConnectionStats(statsPrev, mStatsPrevBuild); 1264 } 1265 if (ns.getFrequenciesList().size() > 0) { 1266 // This merge assumes that whatever data is in memory is more recent that what's 1267 // in store 1268 List<Integer> mergedFrequencyList = mFrequencyList.getEntries(); 1269 mergedFrequencyList.addAll(ns.getFrequenciesList()); 1270 mFrequencyList = new LruList<>(MAX_FREQUENCIES_PER_SSID); 1271 for (int i = mergedFrequencyList.size() - 1; i >= 0; i--) { 1272 mFrequencyList.add(mergedFrequencyList.get(i)); 1273 } 1274 } 1275 return this; 1276 } 1277 mergeConnectionStats(ConnectionStats source, NetworkConnectionStats target)1278 private void mergeConnectionStats(ConnectionStats source, NetworkConnectionStats target) { 1279 if (source.hasNumConnectionAttempt()) { 1280 target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionAttempt()); 1281 } 1282 if (source.hasNumConnectionFailure()) { 1283 target.accumulate(CNT_CONNECTION_ATTEMPT, source.getNumConnectionFailure()); 1284 } 1285 if (source.hasConnectionDurationSec()) { 1286 target.accumulate(CNT_CONNECTION_DURATION_SEC, source.getConnectionDurationSec()); 1287 } 1288 if (source.hasNumDisconnectionNonlocal()) { 1289 target.accumulate(CNT_DISCONNECTION_NONLOCAL, source.getNumDisconnectionNonlocal()); 1290 } 1291 if (source.hasNumDisconnection()) { 1292 target.accumulate(CNT_DISCONNECTION, source.getNumDisconnection()); 1293 } 1294 if (source.hasNumShortConnectionNonlocal()) { 1295 target.accumulate(CNT_SHORT_CONNECTION_NONLOCAL, 1296 source.getNumShortConnectionNonlocal()); 1297 } 1298 if (source.hasNumAssociationRejection()) { 1299 target.accumulate(CNT_ASSOCIATION_REJECTION, source.getNumAssociationRejection()); 1300 } 1301 if (source.hasNumAssociationTimeout()) { 1302 target.accumulate(CNT_ASSOCIATION_TIMEOUT, source.getNumAssociationTimeout()); 1303 } 1304 if (source.hasNumAuthenticationFailure()) { 1305 target.accumulate(CNT_AUTHENTICATION_FAILURE, source.getNumAuthenticationFailure()); 1306 } 1307 } 1308 } 1309 1310 // Codes for various connection related counts 1311 public static final int CNT_INVALID = -1; 1312 public static final int CNT_CONNECTION_ATTEMPT = 0; 1313 public static final int CNT_CONNECTION_FAILURE = 1; 1314 public static final int CNT_CONNECTION_DURATION_SEC = 2; 1315 public static final int CNT_ASSOCIATION_REJECTION = 3; 1316 public static final int CNT_ASSOCIATION_TIMEOUT = 4; 1317 public static final int CNT_AUTHENTICATION_FAILURE = 5; 1318 public static final int CNT_SHORT_CONNECTION_NONLOCAL = 6; 1319 public static final int CNT_DISCONNECTION_NONLOCAL = 7; 1320 public static final int CNT_DISCONNECTION = 8; 1321 public static final int CNT_CONSECUTIVE_CONNECTION_FAILURE = 9; 1322 // Constant being used to keep track of how many counter there are. 1323 public static final int NUMBER_CONNECTION_CNT_CODE = 10; 1324 private static final String[] CONNECTION_CNT_NAME = { 1325 " ConnectAttempt: ", 1326 " ConnectFailure: ", 1327 " ConnectDurSec: ", 1328 " AssocRej: ", 1329 " AssocTimeout: ", 1330 " AuthFailure: ", 1331 " ShortDiscNonlocal: ", 1332 " DisconnectNonlocal: ", 1333 " Disconnect: ", 1334 " ConsecutiveConnectFailure: " 1335 }; 1336 1337 @IntDef(prefix = { "CNT_" }, value = { 1338 CNT_CONNECTION_ATTEMPT, 1339 CNT_CONNECTION_FAILURE, 1340 CNT_CONNECTION_DURATION_SEC, 1341 CNT_ASSOCIATION_REJECTION, 1342 CNT_ASSOCIATION_TIMEOUT, 1343 CNT_AUTHENTICATION_FAILURE, 1344 CNT_SHORT_CONNECTION_NONLOCAL, 1345 CNT_DISCONNECTION_NONLOCAL, 1346 CNT_DISCONNECTION, 1347 CNT_CONSECUTIVE_CONNECTION_FAILURE 1348 }) 1349 @Retention(RetentionPolicy.SOURCE) 1350 public @interface ConnectionCountCode {} 1351 1352 /** 1353 * A class maintaining the connection related statistics of a Wifi network. 1354 */ 1355 public static class NetworkConnectionStats { 1356 private final int[] mCount = new int[NUMBER_CONNECTION_CNT_CODE]; 1357 private int mRecentCountCode = CNT_INVALID; 1358 /** 1359 * Copy all values 1360 * @param src is the source of copy 1361 */ copy(NetworkConnectionStats src)1362 public void copy(NetworkConnectionStats src) { 1363 for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { 1364 mCount[i] = src.getCount(i); 1365 } 1366 mRecentCountCode = src.mRecentCountCode; 1367 } 1368 1369 /** 1370 * Clear all counters 1371 */ clear()1372 public void clear() { 1373 for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { 1374 mCount[i] = 0; 1375 } 1376 mRecentCountCode = CNT_INVALID; 1377 } 1378 1379 /** 1380 * Get counter value 1381 * @param countCode is the selected counter 1382 * @return the value of selected counter 1383 */ getCount(@onnectionCountCode int countCode)1384 public int getCount(@ConnectionCountCode int countCode) { 1385 return mCount[countCode]; 1386 } 1387 1388 /** 1389 * Clear counter value 1390 * @param countCode is the selected counter to be cleared 1391 */ clearCount(@onnectionCountCode int countCode)1392 public void clearCount(@ConnectionCountCode int countCode) { 1393 mCount[countCode] = 0; 1394 } 1395 1396 /** 1397 * Increment count value by 1 1398 * @param countCode is the selected counter 1399 */ incrementCount(@onnectionCountCode int countCode)1400 public void incrementCount(@ConnectionCountCode int countCode) { 1401 mCount[countCode]++; 1402 mRecentCountCode = countCode; 1403 } 1404 1405 /** 1406 * Got the recent incremented count code 1407 */ getRecentCountCode()1408 public int getRecentCountCode() { 1409 return mRecentCountCode; 1410 } 1411 1412 /** 1413 * Decrement count value by 1 1414 * @param countCode is the selected counter 1415 */ decrementCount(@onnectionCountCode int countCode)1416 public void decrementCount(@ConnectionCountCode int countCode) { 1417 mCount[countCode]--; 1418 } 1419 1420 /** 1421 * Add and accumulate the selected counter 1422 * @param countCode is the selected counter 1423 * @param cnt is the value to be added to the counter 1424 */ accumulate(@onnectionCountCode int countCode, int cnt)1425 public void accumulate(@ConnectionCountCode int countCode, int cnt) { 1426 mCount[countCode] += cnt; 1427 } 1428 1429 /** 1430 * Accumulate daily stats to historical data 1431 * @param recentStats are the raw daily counts 1432 */ accumulateAll(NetworkConnectionStats recentStats)1433 public void accumulateAll(NetworkConnectionStats recentStats) { 1434 // 32-bit counter in second can support connection duration up to 68 years. 1435 // Similarly 32-bit counter can support up to continuous connection attempt 1436 // up to 68 years with one attempt per second. 1437 for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { 1438 mCount[i] += recentStats.getCount(i); 1439 } 1440 } 1441 1442 @Override toString()1443 public String toString() { 1444 StringBuilder sb = new StringBuilder(); 1445 for (int i = 0; i < NUMBER_CONNECTION_CNT_CODE; i++) { 1446 sb.append(CONNECTION_CNT_NAME[i]); 1447 sb.append(mCount[i]); 1448 } 1449 return sb.toString(); 1450 } 1451 } 1452 /** 1453 * A base class dealing with common operations of MemoryStore. 1454 */ 1455 public static class MemoryStoreAccessBase { 1456 private final String mL2Key; 1457 private final long mHash; 1458 private static final String TAG = "WifiMemoryStoreAccessBase"; 1459 private final AtomicReference<byte[]> mPendingReadFromStore = new AtomicReference<>(); MemoryStoreAccessBase(long hash)1460 MemoryStoreAccessBase(long hash) { 1461 mHash = hash; 1462 mL2Key = l2KeyFromLong(); 1463 } getL2Key()1464 String getL2Key() { 1465 return mL2Key; 1466 } 1467 l2KeyFromLong()1468 private String l2KeyFromLong() { 1469 return "W" + Long.toHexString(mHash); 1470 } 1471 1472 /** 1473 * Callback function when MemoryStore read is done 1474 * @param serialized is the readback value 1475 */ readBackListener(byte[] serialized)1476 void readBackListener(byte[] serialized) { 1477 if (serialized == null) return; 1478 byte[] old = mPendingReadFromStore.getAndSet(serialized); 1479 if (old != null) { 1480 Log.e(TAG, "More answers than we expected!"); 1481 } 1482 } 1483 1484 /** 1485 * Handles (when convenient) the arrival of previously stored data. 1486 * 1487 * The response from IpMemoryStore arrives on a different thread, so we 1488 * defer handling it until here, when we're on our favorite thread and 1489 * in a good position to deal with it. We may have already collected some 1490 * data before now, so we need to be prepared to merge the new and old together. 1491 */ finishPendingReadBytes()1492 byte[] finishPendingReadBytes() { 1493 return mPendingReadFromStore.getAndSet(null); 1494 } 1495 idFromLong()1496 int idFromLong() { 1497 return (int) mHash & 0x7fffffff; 1498 } 1499 } 1500 logd(String string)1501 private void logd(String string) { 1502 if (mVerboseLoggingEnabled) { 1503 Log.d(TAG, string); 1504 } 1505 } 1506 // Returned by lookupBssid when the BSSID is not available, 1507 // for instance when we are not associated. 1508 private final PerBssid mDummyPerBssid; 1509 1510 private final Map<MacAddress, PerBssid> mApForBssid = new ArrayMap<>(); 1511 private int mApForBssidTargetSize = TARGET_IN_MEMORY_ENTRIES; 1512 private int mApForBssidReferenced = 0; 1513 1514 // TODO should be private, but WifiCandidates needs it lookupBssid(String ssid, String bssid)1515 @NonNull PerBssid lookupBssid(String ssid, String bssid) { 1516 MacAddress mac; 1517 if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid) || bssid == null) { 1518 return mDummyPerBssid; 1519 } 1520 try { 1521 mac = MacAddress.fromString(bssid); 1522 } catch (IllegalArgumentException e) { 1523 return mDummyPerBssid; 1524 } 1525 if (mac.equals(mDummyPerBssid.bssid)) { 1526 return mDummyPerBssid; 1527 } 1528 PerBssid ans = mApForBssid.get(mac); 1529 if (ans == null || !ans.ssid.equals(ssid)) { 1530 ans = new PerBssid(ssid, mac); 1531 PerBssid old = mApForBssid.put(mac, ans); 1532 if (old != null) { 1533 Log.i(TAG, "Discarding stats for score card (ssid changed) ID: " + old.id); 1534 if (old.referenced) mApForBssidReferenced--; 1535 } 1536 requestReadBssid(ans); 1537 } 1538 if (!ans.referenced) { 1539 ans.referenced = true; 1540 mApForBssidReferenced++; 1541 clean(); 1542 } 1543 return ans; 1544 } 1545 requestReadBssid(final PerBssid perBssid)1546 private void requestReadBssid(final PerBssid perBssid) { 1547 if (mMemoryStore != null) { 1548 mMemoryStore.read(perBssid.getL2Key(), PER_BSSID_DATA_NAME, 1549 (value) -> perBssid.readBackListener(value)); 1550 } 1551 } 1552 requestReadForAllChanged()1553 private void requestReadForAllChanged() { 1554 for (PerBssid perBssid : mApForBssid.values()) { 1555 if (perBssid.changed) { 1556 requestReadBssid(perBssid); 1557 } 1558 } 1559 } 1560 1561 // Returned by lookupNetwork when the network is not available, 1562 // for instance when we are not associated. 1563 private final PerNetwork mDummyPerNetwork; 1564 private final Map<String, PerNetwork> mApForNetwork = new ArrayMap<>(); lookupNetwork(String ssid)1565 PerNetwork lookupNetwork(String ssid) { 1566 if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) { 1567 return mDummyPerNetwork; 1568 } 1569 1570 PerNetwork ans = mApForNetwork.get(ssid); 1571 if (ans == null) { 1572 ans = new PerNetwork(ssid); 1573 mApForNetwork.put(ssid, ans); 1574 requestReadNetwork(ans); 1575 } 1576 return ans; 1577 } 1578 1579 /** 1580 * Remove network from cache and memory store 1581 * @param ssid is the network SSID 1582 */ removeNetwork(String ssid)1583 public void removeNetwork(String ssid) { 1584 if (ssid == null || WifiManager.UNKNOWN_SSID.equals(ssid)) { 1585 return; 1586 } 1587 mApForNetwork.remove(ssid); 1588 mApForBssid.entrySet().removeIf(entry -> ssid.equals(entry.getValue().ssid)); 1589 if (mMemoryStore == null) return; 1590 mMemoryStore.removeCluster(groupHintFromSsid(ssid)); 1591 } 1592 requestReadNetwork(final PerNetwork perNetwork)1593 void requestReadNetwork(final PerNetwork perNetwork) { 1594 if (mMemoryStore != null) { 1595 mMemoryStore.read(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME, 1596 (value) -> perNetwork.readBackListener(value)); 1597 } 1598 } 1599 1600 /** 1601 * Issues write requests for all changed entries. 1602 * 1603 * This should be called from time to time to save the state to persistent 1604 * storage. Since we always check internal state first, this does not need 1605 * to be called very often, but it should be called before shutdown. 1606 * 1607 * @returns number of writes issued. 1608 */ doWrites()1609 public int doWrites() { 1610 return doWritesBssid() + doWritesNetwork(); 1611 } 1612 doWritesBssid()1613 private int doWritesBssid() { 1614 if (mMemoryStore == null) return 0; 1615 int count = 0; 1616 int bytes = 0; 1617 for (PerBssid perBssid : mApForBssid.values()) { 1618 if (perBssid.changed) { 1619 perBssid.finishPendingRead(); 1620 byte[] serialized = perBssid.toAccessPoint(/* No BSSID */ true).toByteArray(); 1621 mMemoryStore.setCluster(perBssid.getL2Key(), groupHintFromSsid(perBssid.ssid)); 1622 mMemoryStore.write(perBssid.getL2Key(), PER_BSSID_DATA_NAME, serialized); 1623 1624 perBssid.changed = false; 1625 count++; 1626 bytes += serialized.length; 1627 } 1628 } 1629 if (mVerboseLoggingEnabled && count > 0) { 1630 Log.v(TAG, "Write count: " + count + ", bytes: " + bytes); 1631 } 1632 return count; 1633 } 1634 doWritesNetwork()1635 private int doWritesNetwork() { 1636 if (mMemoryStore == null) return 0; 1637 int count = 0; 1638 int bytes = 0; 1639 for (PerNetwork perNetwork : mApForNetwork.values()) { 1640 if (perNetwork.changed) { 1641 perNetwork.finishPendingRead(); 1642 byte[] serialized = perNetwork.toNetworkStats().toByteArray(); 1643 mMemoryStore.setCluster(perNetwork.getL2Key(), groupHintFromSsid(perNetwork.ssid)); 1644 mMemoryStore.write(perNetwork.getL2Key(), PER_NETWORK_DATA_NAME, serialized); 1645 perNetwork.changed = false; 1646 count++; 1647 bytes += serialized.length; 1648 } 1649 } 1650 if (mVerboseLoggingEnabled && count > 0) { 1651 Log.v(TAG, "Write count: " + count + ", bytes: " + bytes); 1652 } 1653 return count; 1654 } 1655 1656 /** 1657 * Evicts older entries from memory. 1658 * 1659 * This uses an approximate least-recently-used method. When the number of 1660 * referenced entries exceeds the target value, any items that have not been 1661 * referenced since the last round are evicted, and the remaining entries 1662 * are marked as unreferenced. The total count varies between the target 1663 * value and twice the target value. 1664 */ clean()1665 private void clean() { 1666 if (mMemoryStore == null) return; 1667 if (mApForBssidReferenced >= mApForBssidTargetSize) { 1668 doWritesBssid(); // Do not want to evict changed items 1669 // Evict the unreferenced ones, and clear all the referenced bits for the next round. 1670 Iterator<Map.Entry<MacAddress, PerBssid>> it = mApForBssid.entrySet().iterator(); 1671 while (it.hasNext()) { 1672 PerBssid perBssid = it.next().getValue(); 1673 if (perBssid.referenced) { 1674 perBssid.referenced = false; 1675 } else { 1676 it.remove(); 1677 if (mVerboseLoggingEnabled) Log.v(TAG, "Evict " + perBssid.id); 1678 } 1679 } 1680 mApForBssidReferenced = 0; 1681 } 1682 } 1683 1684 /** 1685 * Compute a hash value with the given SSID and MAC address 1686 * @param ssid is the network SSID 1687 * @param mac is the network MAC address 1688 * @param l2KeySeed is the seed for hash generation 1689 * @return 1690 */ computeHashLong(String ssid, MacAddress mac, String l2KeySeed)1691 public static long computeHashLong(String ssid, MacAddress mac, String l2KeySeed) { 1692 byte[][] parts = { 1693 // Our seed keeps the L2Keys specific to this device 1694 l2KeySeed.getBytes(), 1695 // ssid is either quoted utf8 or hex-encoded bytes; turn it into plain bytes. 1696 NativeUtil.byteArrayFromArrayList(NativeUtil.decodeSsid(ssid)), 1697 // And the BSSID 1698 mac.toByteArray() 1699 }; 1700 // Assemble the parts into one, with single-byte lengths before each. 1701 int n = 0; 1702 for (int i = 0; i < parts.length; i++) { 1703 n += 1 + parts[i].length; 1704 } 1705 byte[] mashed = new byte[n]; 1706 int p = 0; 1707 for (int i = 0; i < parts.length; i++) { 1708 byte[] part = parts[i]; 1709 mashed[p++] = (byte) part.length; 1710 for (int j = 0; j < part.length; j++) { 1711 mashed[p++] = part[j]; 1712 } 1713 } 1714 // Finally, turn that into a long 1715 MessageDigest md; 1716 try { 1717 md = MessageDigest.getInstance("SHA-256"); 1718 } catch (NoSuchAlgorithmException e) { 1719 Log.e(TAG, "SHA-256 not supported."); 1720 return 0; 1721 } 1722 ByteBuffer buffer = ByteBuffer.wrap(md.digest(mashed)); 1723 return buffer.getLong(); 1724 } 1725 groupHintFromLong(long hash)1726 private static String groupHintFromLong(long hash) { 1727 return "G" + Long.toHexString(hash); 1728 } 1729 1730 @VisibleForTesting fetchByBssid(MacAddress mac)1731 PerBssid fetchByBssid(MacAddress mac) { 1732 return mApForBssid.get(mac); 1733 } 1734 1735 @VisibleForTesting fetchByNetwork(String ssid)1736 PerNetwork fetchByNetwork(String ssid) { 1737 return mApForNetwork.get(ssid); 1738 } 1739 1740 @VisibleForTesting perBssidFromAccessPoint(String ssid, AccessPoint ap)1741 PerBssid perBssidFromAccessPoint(String ssid, AccessPoint ap) { 1742 MacAddress bssid = MacAddress.fromBytes(ap.getBssid().toByteArray()); 1743 return new PerBssid(ssid, bssid).merge(ap); 1744 } 1745 1746 @VisibleForTesting perNetworkFromNetworkStats(String ssid, NetworkStats ns)1747 PerNetwork perNetworkFromNetworkStats(String ssid, NetworkStats ns) { 1748 return new PerNetwork(ssid).mergeNetworkStatsFromMemory(ns); 1749 } 1750 1751 final class PerSignal { 1752 public final Event event; 1753 public final int frequency; 1754 public final PerUnivariateStatistic rssi; 1755 public final PerUnivariateStatistic linkspeed; 1756 @Nullable public final PerUnivariateStatistic elapsedMs; PerSignal(Event event, int frequency)1757 PerSignal(Event event, int frequency) { 1758 this.event = event; 1759 this.frequency = frequency; 1760 switch (event) { 1761 case SIGNAL_POLL: 1762 case IP_CONFIGURATION_SUCCESS: 1763 case IP_REACHABILITY_LOST: 1764 this.rssi = new PerUnivariateStatistic(RSSI_BUCKETS); 1765 break; 1766 default: 1767 this.rssi = new PerUnivariateStatistic(); 1768 break; 1769 } 1770 this.linkspeed = new PerUnivariateStatistic(); 1771 switch (event) { 1772 case FIRST_POLL_AFTER_CONNECTION: 1773 case IP_CONFIGURATION_SUCCESS: 1774 case VALIDATION_SUCCESS: 1775 case CONNECTION_FAILURE: 1776 case DISCONNECTION: 1777 case WIFI_DISABLED: 1778 case ROAM_FAILURE: 1779 this.elapsedMs = new PerUnivariateStatistic(); 1780 break; 1781 default: 1782 this.elapsedMs = null; 1783 break; 1784 } 1785 } merge(Signal signal)1786 PerSignal merge(Signal signal) { 1787 Preconditions.checkArgument(event == signal.getEvent()); 1788 Preconditions.checkArgument(frequency == signal.getFrequency()); 1789 rssi.merge(signal.getRssi()); 1790 linkspeed.merge(signal.getLinkspeed()); 1791 if (elapsedMs != null && signal.hasElapsedMs()) { 1792 elapsedMs.merge(signal.getElapsedMs()); 1793 } 1794 return this; 1795 } toSignal()1796 Signal toSignal() { 1797 Signal.Builder builder = Signal.newBuilder(); 1798 builder.setEvent(event) 1799 .setFrequency(frequency) 1800 .setRssi(rssi.toUnivariateStatistic()) 1801 .setLinkspeed(linkspeed.toUnivariateStatistic()); 1802 if (elapsedMs != null) { 1803 builder.setElapsedMs(elapsedMs.toUnivariateStatistic()); 1804 } 1805 if (rssi.intHistogram != null 1806 && rssi.intHistogram.numNonEmptyBuckets() > 0) { 1807 logd("Histogram " + event + " RSSI" + rssi.intHistogram); 1808 } 1809 return builder.build(); 1810 } 1811 } 1812 1813 final class PerUnivariateStatistic { 1814 public long count = 0; 1815 public double sum = 0.0; 1816 public double sumOfSquares = 0.0; 1817 public double minValue = Double.POSITIVE_INFINITY; 1818 public double maxValue = Double.NEGATIVE_INFINITY; 1819 public double historicalMean = 0.0; 1820 public double historicalVariance = Double.POSITIVE_INFINITY; 1821 public IntHistogram intHistogram = null; PerUnivariateStatistic()1822 PerUnivariateStatistic() {} PerUnivariateStatistic(int[] bucketBoundaries)1823 PerUnivariateStatistic(int[] bucketBoundaries) { 1824 intHistogram = new IntHistogram(bucketBoundaries); 1825 } update(double value)1826 void update(double value) { 1827 count++; 1828 sum += value; 1829 sumOfSquares += value * value; 1830 minValue = Math.min(minValue, value); 1831 maxValue = Math.max(maxValue, value); 1832 if (intHistogram != null) { 1833 intHistogram.add(Math.round((float) value), 1); 1834 } 1835 } age()1836 void age() { 1837 //TODO Fold the current stats into the historical stats 1838 } merge(UnivariateStatistic stats)1839 void merge(UnivariateStatistic stats) { 1840 if (stats.hasCount()) { 1841 count += stats.getCount(); 1842 sum += stats.getSum(); 1843 sumOfSquares += stats.getSumOfSquares(); 1844 } 1845 if (stats.hasMinValue()) { 1846 minValue = Math.min(minValue, stats.getMinValue()); 1847 } 1848 if (stats.hasMaxValue()) { 1849 maxValue = Math.max(maxValue, stats.getMaxValue()); 1850 } 1851 if (stats.hasHistoricalVariance()) { 1852 if (historicalVariance < Double.POSITIVE_INFINITY) { 1853 // Combine the estimates; c.f. 1854 // Maybeck, Stochasic Models, Estimation, and Control, Vol. 1 1855 // equations (1-3) and (1-4) 1856 double numer1 = stats.getHistoricalVariance(); 1857 double numer2 = historicalVariance; 1858 double denom = numer1 + numer2; 1859 historicalMean = (numer1 * historicalMean 1860 + numer2 * stats.getHistoricalMean()) 1861 / denom; 1862 historicalVariance = numer1 * numer2 / denom; 1863 } else { 1864 historicalMean = stats.getHistoricalMean(); 1865 historicalVariance = stats.getHistoricalVariance(); 1866 } 1867 } 1868 if (intHistogram != null) { 1869 for (HistogramBucket bucket : stats.getBucketsList()) { 1870 long low = bucket.getLow(); 1871 long count = bucket.getNumber(); 1872 if (low != (int) low || count != (int) count || count < 0) { 1873 Log.e(TAG, "Found corrupted histogram! Clearing."); 1874 intHistogram.clear(); 1875 break; 1876 } 1877 intHistogram.add((int) low, (int) count); 1878 } 1879 } 1880 } toUnivariateStatistic()1881 UnivariateStatistic toUnivariateStatistic() { 1882 UnivariateStatistic.Builder builder = UnivariateStatistic.newBuilder(); 1883 if (count != 0) { 1884 builder.setCount(count) 1885 .setSum(sum) 1886 .setSumOfSquares(sumOfSquares) 1887 .setMinValue(minValue) 1888 .setMaxValue(maxValue); 1889 } 1890 if (historicalVariance < Double.POSITIVE_INFINITY) { 1891 builder.setHistoricalMean(historicalMean) 1892 .setHistoricalVariance(historicalVariance); 1893 } 1894 if (mPersistentHistograms 1895 && intHistogram != null && intHistogram.numNonEmptyBuckets() > 0) { 1896 for (IntHistogram.Bucket b : intHistogram) { 1897 if (b.count == 0) continue; 1898 builder.addBuckets( 1899 HistogramBucket.newBuilder().setLow(b.start).setNumber(b.count)); 1900 } 1901 } 1902 return builder.build(); 1903 } 1904 } 1905 1906 /** 1907 * Returns the current scorecard in the form of a protobuf com_android_server_wifi.NetworkList 1908 * 1909 * Synchronization is the caller's responsibility. 1910 * 1911 * @param obfuscate - if true, ssids and bssids are omitted (short id only) 1912 */ getNetworkListByteArray(boolean obfuscate)1913 public byte[] getNetworkListByteArray(boolean obfuscate) { 1914 // These are really grouped by ssid, ignoring the security type. 1915 Map<String, Network.Builder> networks = new ArrayMap<>(); 1916 for (PerBssid perBssid: mApForBssid.values()) { 1917 String key = perBssid.ssid; 1918 Network.Builder network = networks.get(key); 1919 if (network == null) { 1920 network = Network.newBuilder(); 1921 networks.put(key, network); 1922 if (!obfuscate) { 1923 network.setSsid(perBssid.ssid); 1924 } 1925 } 1926 if (perBssid.mNetworkAgentId >= network.getNetworkAgentId()) { 1927 network.setNetworkAgentId(perBssid.mNetworkAgentId); 1928 } 1929 if (perBssid.mNetworkConfigId >= network.getNetworkConfigId()) { 1930 network.setNetworkConfigId(perBssid.mNetworkConfigId); 1931 } 1932 network.addAccessPoints(perBssid.toAccessPoint(obfuscate)); 1933 } 1934 for (PerNetwork perNetwork: mApForNetwork.values()) { 1935 String key = perNetwork.ssid; 1936 Network.Builder network = networks.get(key); 1937 if (network != null) { 1938 network.setNetworkStats(perNetwork.toNetworkStats()); 1939 } 1940 } 1941 NetworkList.Builder builder = NetworkList.newBuilder(); 1942 for (Network.Builder network: networks.values()) { 1943 builder.addNetworks(network); 1944 } 1945 return builder.build().toByteArray(); 1946 } 1947 1948 /** 1949 * Returns the current scorecard as a base64-encoded protobuf 1950 * 1951 * Synchronization is the caller's responsibility. 1952 * 1953 * @param obfuscate - if true, bssids are omitted (short id only) 1954 */ getNetworkListBase64(boolean obfuscate)1955 public String getNetworkListBase64(boolean obfuscate) { 1956 byte[] raw = getNetworkListByteArray(obfuscate); 1957 return Base64.encodeToString(raw, Base64.DEFAULT); 1958 } 1959 1960 /** 1961 * Clears the internal state. 1962 * 1963 * This is called in response to a factoryReset call from Settings. 1964 * The memory store will be called after we are called, to wipe the stable 1965 * storage as well. Since we will have just removed all of our networks, 1966 * it is very unlikely that we're connected, or will connect immediately. 1967 * Any in-flight reads will land in the objects we are dropping here, and 1968 * the memory store should drop the in-flight writes. Ideally we would 1969 * avoid issuing reads until we were sure that the memory store had 1970 * received the factoryReset. 1971 */ clear()1972 public void clear() { 1973 mApForBssid.clear(); 1974 mApForNetwork.clear(); 1975 resetConnectionStateInternal(false); 1976 } 1977 } 1978