1 /* 2 * Copyright (C) 2016 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 android.annotation.Nullable; 20 import android.content.Context; 21 import android.net.Network; 22 import android.net.NetworkCapabilities; 23 import android.net.NetworkScore; 24 import android.net.wifi.IWifiConnectedNetworkScorer; 25 import android.net.wifi.WifiConnectedSessionInfo; 26 import android.net.wifi.WifiInfo; 27 import android.net.wifi.WifiManager; 28 import android.net.wifi.nl80211.WifiNl80211Manager; 29 import android.os.Build; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 import android.util.Log; 33 34 import androidx.annotation.RequiresApi; 35 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.modules.utils.build.SdkLevel; 38 import com.android.server.wifi.ActiveModeManager.ClientRole; 39 import com.android.wifi.resources.R; 40 41 import java.io.FileDescriptor; 42 import java.io.PrintWriter; 43 import java.text.SimpleDateFormat; 44 import java.util.Date; 45 import java.util.LinkedList; 46 import java.util.Locale; 47 48 /** 49 * Class used to calculate scores for connected wifi networks and report it to the associated 50 * network agent. 51 */ 52 public class WifiScoreReport { 53 private static final String TAG = "WifiScoreReport"; 54 55 private static final int DUMPSYS_ENTRY_COUNT_LIMIT = 3600; // 3 hours on 3 second poll 56 57 private boolean mVerboseLoggingEnabled = false; 58 private static final long FIRST_REASONABLE_WALL_CLOCK = 1490000000000L; // mid-December 2016 59 60 private static final long MIN_TIME_TO_KEEP_BELOW_TRANSITION_SCORE_MILLIS = 9000; 61 private long mLastDownwardBreachTimeMillis = 0; 62 63 private static final int WIFI_CONNECTED_NETWORK_SCORER_IDENTIFIER = 0; 64 private static final int INVALID_SESSION_ID = -1; 65 private static final long MIN_TIME_TO_WAIT_BEFORE_BLOCKLIST_BSSID_MILLIS = 29000; 66 private static final long INVALID_WALL_CLOCK_MILLIS = -1; 67 68 /** 69 * Set lingering score to be artificially lower than all other scores so that it will force 70 * ConnectivityService to prefer any other network over this one. 71 */ 72 @VisibleForTesting 73 static final int LINGERING_SCORE = 1; 74 75 // Cache of the last score 76 private int mLegacyIntScore = ConnectedScore.WIFI_INITIAL_SCORE; 77 // Cache of the last usability status 78 private boolean mIsUsable = true; 79 80 /** 81 * If true, indicates that the associated {@link ClientModeImpl} instance is lingering 82 * as a part of make before break STA + STA use-case, and will always send 83 * {@link #LINGERING_SCORE} to NetworkAgent. 84 */ 85 private boolean mShouldReduceNetworkScore = false; 86 87 /** The current role of the ClientModeManager which owns this instance of WifiScoreReport. */ 88 @Nullable 89 private ClientRole mCurrentRole = null; 90 91 private final ScoringParams mScoringParams; 92 private final Clock mClock; 93 private int mSessionNumber = 0; // not to be confused with sessionid, this just counts resets 94 private final String mInterfaceName; 95 private final WifiBlocklistMonitor mWifiBlocklistMonitor; 96 private final WifiScoreCard mWifiScoreCard; 97 private final Context mContext; 98 private long mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; 99 private long mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; 100 101 private final ConnectedScore mAggressiveConnectedScore; 102 private VelocityBasedConnectedScore mVelocityBasedConnectedScore; 103 private final WifiSettingsStore mWifiSettingsStore; 104 private int mSessionIdNoReset = INVALID_SESSION_ID; 105 // Indicate whether current network is selected by the user 106 private boolean mIsUserSelected = false; 107 108 @Nullable 109 private WifiNetworkAgent mNetworkAgent; 110 private final WifiMetrics mWifiMetrics; 111 private final ExtendedWifiInfo mWifiInfo; 112 private final WifiNative mWifiNative; 113 private final WifiThreadRunner mWifiThreadRunner; 114 private final DeviceConfigFacade mDeviceConfigFacade; 115 private final ExternalScoreUpdateObserverProxy mExternalScoreUpdateObserverProxy; 116 private final WifiInfo mWifiInfoNoReset; 117 118 /** 119 * Callback from {@link ExternalScoreUpdateObserverProxy} 120 */ 121 private class ScoreUpdateObserverProxy implements WifiManager.ScoreUpdateObserver { 122 @Override notifyScoreUpdate(int sessionId, int score)123 public void notifyScoreUpdate(int sessionId, int score) { 124 if (mWifiConnectedNetworkScorerHolder == null 125 || sessionId == INVALID_SESSION_ID 126 || sessionId != getCurrentSessionId()) { 127 Log.w(TAG, "Ignoring stale/invalid external score" 128 + " sessionId=" + sessionId 129 + " currentSessionId=" + getCurrentSessionId() 130 + " score=" + score); 131 return; 132 } 133 long millis = mClock.getWallClockMillis(); 134 if (SdkLevel.isAtLeastS()) { 135 mLegacyIntScore = score; 136 updateWifiMetrics(millis, -1); 137 return; 138 } 139 if (score < ConnectedScore.WIFI_TRANSITION_SCORE) { 140 if (mLegacyIntScore >= ConnectedScore.WIFI_TRANSITION_SCORE) { 141 mLastScoreBreachLowTimeMillis = millis; 142 } 143 } else { 144 mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; 145 } 146 if (score > ConnectedScore.WIFI_TRANSITION_SCORE) { 147 if (mLegacyIntScore <= ConnectedScore.WIFI_TRANSITION_SCORE) { 148 mLastScoreBreachHighTimeMillis = millis; 149 } 150 } else { 151 mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; 152 } 153 mLegacyIntScore = score; 154 reportNetworkScoreToConnectivityServiceIfNecessary(); 155 updateWifiMetrics(millis, -1); 156 } 157 158 @Override triggerUpdateOfWifiUsabilityStats(int sessionId)159 public void triggerUpdateOfWifiUsabilityStats(int sessionId) { 160 if (mWifiConnectedNetworkScorerHolder == null 161 || sessionId == INVALID_SESSION_ID 162 || sessionId != getCurrentSessionId() 163 || mInterfaceName == null) { 164 Log.w(TAG, "Ignoring triggerUpdateOfWifiUsabilityStats" 165 + " sessionId=" + sessionId 166 + " currentSessionId=" + getCurrentSessionId() 167 + " interfaceName=" + mInterfaceName); 168 return; 169 } 170 WifiLinkLayerStats stats = mWifiNative.getWifiLinkLayerStats(mInterfaceName); 171 172 // update mWifiInfo 173 // TODO(b/153075963): Better coordinate this class and ClientModeImpl to remove 174 // redundant codes below and in ClientModeImpl#fetchRssiLinkSpeedAndFrequencyNative. 175 WifiNl80211Manager.SignalPollResult pollResult = 176 mWifiNative.signalPoll(mInterfaceName); 177 if (pollResult != null) { 178 int newRssi = pollResult.currentRssiDbm; 179 int newTxLinkSpeed = pollResult.txBitrateMbps; 180 int newFrequency = pollResult.associationFrequencyMHz; 181 int newRxLinkSpeed = pollResult.rxBitrateMbps; 182 183 if (newRssi > WifiInfo.INVALID_RSSI && newRssi < WifiInfo.MAX_RSSI) { 184 if (newRssi > (WifiInfo.INVALID_RSSI + 256)) { 185 Log.wtf(TAG, "Error! +ve value RSSI: " + newRssi); 186 newRssi -= 256; 187 } 188 mWifiInfo.setRssi(newRssi); 189 } else { 190 mWifiInfo.setRssi(WifiInfo.INVALID_RSSI); 191 } 192 /* 193 * set Tx link speed only if it is valid 194 */ 195 if (newTxLinkSpeed > 0) { 196 mWifiInfo.setLinkSpeed(newTxLinkSpeed); 197 mWifiInfo.setTxLinkSpeedMbps(newTxLinkSpeed); 198 } 199 /* 200 * set Rx link speed only if it is valid 201 */ 202 if (newRxLinkSpeed > 0) { 203 mWifiInfo.setRxLinkSpeedMbps(newRxLinkSpeed); 204 } 205 if (newFrequency > 0) { 206 mWifiInfo.setFrequency(newFrequency); 207 } 208 } 209 210 // TODO(b/153075963): This should not be plumbed through WifiMetrics 211 mWifiMetrics.updateWifiUsabilityStatsEntries(mInterfaceName, mWifiInfo, stats); 212 } 213 214 @Override notifyStatusUpdate(int sessionId, boolean isUsable)215 public void notifyStatusUpdate(int sessionId, boolean isUsable) { 216 if (mWifiConnectedNetworkScorerHolder == null 217 || sessionId == INVALID_SESSION_ID 218 || sessionId != getCurrentSessionId()) { 219 Log.w(TAG, "Ignoring stale/invalid external status" 220 + " sessionId=" + sessionId 221 + " currentSessionId=" + getCurrentSessionId() 222 + " isUsable=" + isUsable); 223 return; 224 } 225 if (mNetworkAgent == null) { 226 return; 227 } 228 if (mShouldReduceNetworkScore) { 229 return; 230 } 231 mIsUsable = isUsable; 232 // Wifi is set to be usable if adaptive connectivity is disabled. 233 if (!mAdaptiveConnectivityEnabledSettingObserver.get() 234 || !mWifiSettingsStore.isWifiScoringEnabled()) { 235 mIsUsable = true; 236 if (mVerboseLoggingEnabled) { 237 Log.d(TAG, "Wifi scoring disabled - Notify that Wifi is usable"); 238 } 239 } 240 // Send `exiting` to NetworkScore, but don't update and send mLegacyIntScore 241 // and don't change any other fields. All we want to do is relay to ConnectivityService 242 // whether the current network is usable. 243 if (SdkLevel.isAtLeastS()) { 244 mNetworkAgent.sendNetworkScore( 245 getScoreBuilder() 246 .setLegacyInt(mLegacyIntScore) 247 .setExiting(!mIsUsable) 248 .build()); 249 } else { 250 mNetworkAgent.sendNetworkScore(mIsUsable ? ConnectedScore.WIFI_TRANSITION_SCORE + 1 251 : ConnectedScore.WIFI_TRANSITION_SCORE - 1); 252 } 253 } 254 255 @Override requestNudOperation(int sessionId)256 public void requestNudOperation(int sessionId) { 257 if (mWifiConnectedNetworkScorerHolder == null 258 || sessionId == INVALID_SESSION_ID 259 || sessionId != getCurrentSessionId()) { 260 Log.w(TAG, "Ignoring stale/invalid external input for NUD triggering" 261 + " sessionId=" + sessionId 262 + " currentSessionId=" + getCurrentSessionId()); 263 return; 264 } 265 if (!mAdaptiveConnectivityEnabledSettingObserver.get() 266 || !mWifiSettingsStore.isWifiScoringEnabled()) { 267 if (mVerboseLoggingEnabled) { 268 Log.d(TAG, "Wifi scoring disabled - Cannot request a NUD operation"); 269 } 270 return; 271 } 272 mWifiConnectedNetworkScorerHolder.setShouldCheckIpLayerOnce(true); 273 } 274 275 @Override blocklistCurrentBssid(int sessionId)276 public void blocklistCurrentBssid(int sessionId) { 277 if (mWifiConnectedNetworkScorerHolder == null 278 || sessionId == INVALID_SESSION_ID 279 || sessionId != mSessionIdNoReset) { 280 Log.w(TAG, "Ignoring stale/invalid external input for blocklisting" 281 + " sessionId=" + sessionId 282 + " mSessionIdNoReset=" + mSessionIdNoReset); 283 return; 284 } 285 if (!mAdaptiveConnectivityEnabledSettingObserver.get() 286 || !mWifiSettingsStore.isWifiScoringEnabled()) { 287 if (mVerboseLoggingEnabled) { 288 Log.d(TAG, "Wifi scoring disabled - Cannot blocklist current BSSID"); 289 } 290 return; 291 } 292 if (mWifiInfoNoReset.getBSSID() != null) { 293 mWifiBlocklistMonitor.handleBssidConnectionFailure(mWifiInfoNoReset.getBSSID(), 294 mWifiInfoNoReset.getSSID(), 295 WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE, 296 mWifiInfoNoReset.getRssi()); 297 } 298 } 299 } 300 301 /** 302 * If true, put the WifiScoreReport in lingering mode. A very low score is reported to 303 * NetworkAgent, and the real score is never reported. 304 */ setShouldReduceNetworkScore(boolean shouldReduceNetworkScore)305 public void setShouldReduceNetworkScore(boolean shouldReduceNetworkScore) { 306 Log.d(TAG, "setShouldReduceNetworkScore=" + shouldReduceNetworkScore 307 + " mNetworkAgent is null? " + (mNetworkAgent == null)); 308 mShouldReduceNetworkScore = shouldReduceNetworkScore; 309 // inform the external scorer that ongoing session has ended (since the score is no longer 310 // under their control) 311 if (mShouldReduceNetworkScore && mWifiConnectedNetworkScorerHolder != null) { 312 mWifiConnectedNetworkScorerHolder.stopSession(); 313 } 314 // if set to true, send score below disconnect threshold to start lingering 315 sendNetworkScore(); 316 } 317 318 /** 319 * Report network score to connectivity service. 320 */ reportNetworkScoreToConnectivityServiceIfNecessary()321 private void reportNetworkScoreToConnectivityServiceIfNecessary() { 322 if (mNetworkAgent == null) { 323 return; 324 } 325 if (mWifiConnectedNetworkScorerHolder == null && mLegacyIntScore == mWifiInfo.getScore()) { 326 return; 327 } 328 // only send network score if not lingering. If lingering, would have already sent score at 329 // start of lingering. 330 if (mShouldReduceNetworkScore) { 331 return; 332 } 333 if (mWifiConnectedNetworkScorerHolder != null 334 && mContext.getResources().getBoolean( 335 R.bool.config_wifiMinConfirmationDurationSendNetworkScoreEnabled) 336 /// Turn off hysteresis/dampening for shell commands. 337 && !mWifiConnectedNetworkScorerHolder.isShellCommandScorer()) { 338 long millis = mClock.getWallClockMillis(); 339 if (mLastScoreBreachLowTimeMillis != INVALID_WALL_CLOCK_MILLIS) { 340 if (mWifiInfo.getRssi() 341 >= mDeviceConfigFacade.getRssiThresholdNotSendLowScoreToCsDbm()) { 342 Log.d(TAG, "Not reporting low score because RSSI is high " 343 + mWifiInfo.getRssi()); 344 return; 345 } 346 if ((millis - mLastScoreBreachLowTimeMillis) 347 < mDeviceConfigFacade.getMinConfirmationDurationSendLowScoreMs()) { 348 Log.d(TAG, "Not reporting low score because elapsed time is shorter than " 349 + "the minimum confirmation duration"); 350 return; 351 } 352 } 353 if (mLastScoreBreachHighTimeMillis != INVALID_WALL_CLOCK_MILLIS 354 && (millis - mLastScoreBreachHighTimeMillis) 355 < mDeviceConfigFacade.getMinConfirmationDurationSendHighScoreMs()) { 356 Log.d(TAG, "Not reporting high score because elapsed time is shorter than " 357 + "the minimum confirmation duration"); 358 return; 359 } 360 } 361 // Stay a notch above the transition score if adaptive connectivity is disabled. 362 if (!mAdaptiveConnectivityEnabledSettingObserver.get() 363 || !mWifiSettingsStore.isWifiScoringEnabled()) { 364 mLegacyIntScore = ConnectedScore.WIFI_TRANSITION_SCORE + 1; 365 if (mVerboseLoggingEnabled) { 366 Log.d(TAG, "Wifi scoring disabled - Stay a notch above the transition score"); 367 } 368 } 369 sendNetworkScore(); 370 } 371 372 /** 373 * Container for storing info about external scorer and tracking its death. 374 */ 375 private final class WifiConnectedNetworkScorerHolder implements IBinder.DeathRecipient { 376 private final IBinder mBinder; 377 private final IWifiConnectedNetworkScorer mScorer; 378 private int mSessionId = INVALID_SESSION_ID; 379 private boolean mShouldCheckIpLayerOnce = false; 380 WifiConnectedNetworkScorerHolder(IBinder binder, IWifiConnectedNetworkScorer scorer)381 WifiConnectedNetworkScorerHolder(IBinder binder, IWifiConnectedNetworkScorer scorer) { 382 mBinder = binder; 383 mScorer = scorer; 384 } 385 386 /** 387 * Link WiFi connected scorer to death listener. 388 */ linkScorerToDeath()389 public boolean linkScorerToDeath() { 390 try { 391 mBinder.linkToDeath(this, 0); 392 } catch (RemoteException e) { 393 Log.e(TAG, "Unable to linkToDeath Wifi connected network scorer " + mScorer, e); 394 return false; 395 } 396 return true; 397 } 398 399 /** 400 * App hosting the binder has died. 401 */ 402 @Override binderDied()403 public void binderDied() { 404 mWifiThreadRunner.post(() -> revertToDefaultConnectedScorer()); 405 } 406 407 /** 408 * Unlink this object from binder death. 409 */ reset()410 public void reset() { 411 mBinder.unlinkToDeath(this, 0); 412 } 413 414 /** 415 * Starts a new scoring session. 416 */ startSession(int sessionId, boolean isUserSelected)417 public void startSession(int sessionId, boolean isUserSelected) { 418 if (sessionId == INVALID_SESSION_ID) { 419 throw new IllegalArgumentException(); 420 } 421 if (mSessionId != INVALID_SESSION_ID) { 422 // This is not expected to happen, log if it does 423 Log.e(TAG, "Stopping session " + mSessionId + " before starting " + sessionId); 424 stopSession(); 425 } 426 // Bail now if the scorer has gone away 427 if (this != mWifiConnectedNetworkScorerHolder) { 428 return; 429 } 430 mSessionId = sessionId; 431 mSessionIdNoReset = sessionId; 432 try { 433 WifiConnectedSessionInfo sessionInfo = 434 new WifiConnectedSessionInfo.Builder(sessionId) 435 .setUserSelected(isUserSelected) 436 .build(); 437 mScorer.onStart(sessionInfo); 438 } catch (RemoteException e) { 439 Log.e(TAG, "Unable to start Wifi connected network scorer " + this, e); 440 revertToDefaultConnectedScorer(); 441 } 442 } stopSession()443 public void stopSession() { 444 final int sessionId = mSessionId; 445 if (sessionId == INVALID_SESSION_ID) return; 446 mSessionId = INVALID_SESSION_ID; 447 mShouldCheckIpLayerOnce = false; 448 try { 449 mScorer.onStop(sessionId); 450 } catch (RemoteException e) { 451 Log.e(TAG, "Unable to stop Wifi connected network scorer " + this, e); 452 revertToDefaultConnectedScorer(); 453 } 454 } 455 isShellCommandScorer()456 public boolean isShellCommandScorer() { 457 return mScorer instanceof WifiShellCommand.WifiScorer; 458 } 459 setShouldCheckIpLayerOnce(boolean shouldCheckIpLayerOnce)460 private void setShouldCheckIpLayerOnce(boolean shouldCheckIpLayerOnce) { 461 mShouldCheckIpLayerOnce = shouldCheckIpLayerOnce; 462 } 463 getShouldCheckIpLayerOnce()464 private boolean getShouldCheckIpLayerOnce() { 465 return mShouldCheckIpLayerOnce; 466 } 467 } 468 469 private final ScoreUpdateObserverProxy mScoreUpdateObserverCallback = 470 new ScoreUpdateObserverProxy(); 471 472 @Nullable 473 private WifiConnectedNetworkScorerHolder mWifiConnectedNetworkScorerHolder; 474 475 private final AdaptiveConnectivityEnabledSettingObserver 476 mAdaptiveConnectivityEnabledSettingObserver; 477 WifiScoreReport(ScoringParams scoringParams, Clock clock, WifiMetrics wifiMetrics, ExtendedWifiInfo wifiInfo, WifiNative wifiNative, WifiBlocklistMonitor wifiBlocklistMonitor, WifiThreadRunner wifiThreadRunner, WifiScoreCard wifiScoreCard, DeviceConfigFacade deviceConfigFacade, Context context, AdaptiveConnectivityEnabledSettingObserver adaptiveConnectivityEnabledSettingObserver, String interfaceName, ExternalScoreUpdateObserverProxy externalScoreUpdateObserverProxy, WifiSettingsStore wifiSettingsStore)478 WifiScoreReport(ScoringParams scoringParams, Clock clock, WifiMetrics wifiMetrics, 479 ExtendedWifiInfo wifiInfo, WifiNative wifiNative, 480 WifiBlocklistMonitor wifiBlocklistMonitor, 481 WifiThreadRunner wifiThreadRunner, WifiScoreCard wifiScoreCard, 482 DeviceConfigFacade deviceConfigFacade, Context context, 483 AdaptiveConnectivityEnabledSettingObserver adaptiveConnectivityEnabledSettingObserver, 484 String interfaceName, 485 ExternalScoreUpdateObserverProxy externalScoreUpdateObserverProxy, 486 WifiSettingsStore wifiSettingsStore) { 487 mScoringParams = scoringParams; 488 mClock = clock; 489 mAdaptiveConnectivityEnabledSettingObserver = adaptiveConnectivityEnabledSettingObserver; 490 mAggressiveConnectedScore = new AggressiveConnectedScore(scoringParams, clock); 491 mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(scoringParams, clock); 492 mWifiMetrics = wifiMetrics; 493 mWifiInfo = wifiInfo; 494 mWifiNative = wifiNative; 495 mWifiBlocklistMonitor = wifiBlocklistMonitor; 496 mWifiThreadRunner = wifiThreadRunner; 497 mWifiScoreCard = wifiScoreCard; 498 mDeviceConfigFacade = deviceConfigFacade; 499 mContext = context; 500 mInterfaceName = interfaceName; 501 mExternalScoreUpdateObserverProxy = externalScoreUpdateObserverProxy; 502 mWifiSettingsStore = wifiSettingsStore; 503 mWifiInfoNoReset = new WifiInfo(mWifiInfo); 504 } 505 506 /** 507 * Reset the last calculated score. 508 */ reset()509 public void reset() { 510 mSessionNumber++; 511 mLegacyIntScore = ConnectedScore.WIFI_INITIAL_SCORE; 512 mIsUsable = true; 513 mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE; 514 mAggressiveConnectedScore.reset(); 515 if (mVelocityBasedConnectedScore != null) { 516 mVelocityBasedConnectedScore.reset(); 517 } 518 mLastDownwardBreachTimeMillis = 0; 519 mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; 520 mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; 521 if (mVerboseLoggingEnabled) Log.d(TAG, "reset"); 522 } 523 524 /** 525 * Enable/Disable verbose logging in score report generation. 526 */ enableVerboseLogging(boolean enable)527 public void enableVerboseLogging(boolean enable) { 528 mVerboseLoggingEnabled = enable; 529 } 530 531 /** 532 * Calculate wifi network score based on updated link layer stats and send the score to 533 * the WifiNetworkAgent. 534 * 535 * If the score has changed from the previous value, update the WifiNetworkAgent. 536 * 537 * Called periodically (POLL_RSSI_INTERVAL_MSECS) about every 3 seconds. 538 */ calculateAndReportScore()539 public void calculateAndReportScore() { 540 // Bypass AOSP scorer if Wifi connected network scorer is set 541 if (mWifiConnectedNetworkScorerHolder != null) { 542 return; 543 } 544 545 if (mWifiInfo.getRssi() == mWifiInfo.INVALID_RSSI) { 546 Log.d(TAG, "Not reporting score because RSSI is invalid"); 547 return; 548 } 549 int score; 550 551 long millis = mClock.getWallClockMillis(); 552 mVelocityBasedConnectedScore.updateUsingWifiInfo(mWifiInfo, millis); 553 554 int s2 = mVelocityBasedConnectedScore.generateScore(); 555 score = s2; 556 557 if (mWifiInfo.getScore() > ConnectedScore.WIFI_TRANSITION_SCORE 558 && score <= ConnectedScore.WIFI_TRANSITION_SCORE 559 && mWifiInfo.getSuccessfulTxPacketsPerSecond() 560 >= mScoringParams.getYippeeSkippyPacketsPerSecond() 561 && mWifiInfo.getSuccessfulRxPacketsPerSecond() 562 >= mScoringParams.getYippeeSkippyPacketsPerSecond() 563 ) { 564 score = ConnectedScore.WIFI_TRANSITION_SCORE + 1; 565 } 566 567 if (mWifiInfo.getScore() > ConnectedScore.WIFI_TRANSITION_SCORE 568 && score <= ConnectedScore.WIFI_TRANSITION_SCORE) { 569 // We don't want to trigger a downward breach unless the rssi is 570 // below the entry threshold. There is noise in the measured rssi, and 571 // the kalman-filtered rssi is affected by the trend, so check them both. 572 // TODO(b/74613347) skip this if there are other indications to support the low score 573 int entry = mScoringParams.getEntryRssi(mWifiInfo.getFrequency()); 574 if (mVelocityBasedConnectedScore.getFilteredRssi() >= entry 575 || mWifiInfo.getRssi() >= entry) { 576 // Stay a notch above the transition score to reduce ambiguity. 577 score = ConnectedScore.WIFI_TRANSITION_SCORE + 1; 578 } 579 } 580 581 if (mWifiInfo.getScore() >= ConnectedScore.WIFI_TRANSITION_SCORE 582 && score < ConnectedScore.WIFI_TRANSITION_SCORE) { 583 mLastDownwardBreachTimeMillis = millis; 584 } else if (mWifiInfo.getScore() < ConnectedScore.WIFI_TRANSITION_SCORE 585 && score >= ConnectedScore.WIFI_TRANSITION_SCORE) { 586 // Staying at below transition score for a certain period of time 587 // to prevent going back to wifi network again in a short time. 588 long elapsedMillis = millis - mLastDownwardBreachTimeMillis; 589 if (elapsedMillis < MIN_TIME_TO_KEEP_BELOW_TRANSITION_SCORE_MILLIS) { 590 score = mWifiInfo.getScore(); 591 } 592 } 593 //sanitize boundaries 594 if (score > ConnectedScore.WIFI_MAX_SCORE) { 595 score = ConnectedScore.WIFI_MAX_SCORE; 596 } 597 if (score < 0) { 598 score = 0; 599 } 600 601 // report score 602 mLegacyIntScore = score; 603 reportNetworkScoreToConnectivityServiceIfNecessary(); 604 updateWifiMetrics(millis, s2); 605 } 606 getCurrentNetId()607 private int getCurrentNetId() { 608 int netId = 0; 609 if (mNetworkAgent != null) { 610 final Network network = mNetworkAgent.getNetwork(); 611 if (network != null) { 612 netId = network.getNetId(); 613 } 614 } 615 return netId; 616 } 617 618 @Nullable getCurrentNetCapabilities()619 private NetworkCapabilities getCurrentNetCapabilities() { 620 return mNetworkAgent == null ? null : mNetworkAgent.getCurrentNetworkCapabilities(); 621 } 622 getCurrentSessionId()623 private int getCurrentSessionId() { 624 return sessionIdFromNetId(getCurrentNetId()); 625 } 626 627 /** 628 * Encodes a network id into a scoring session id. 629 * 630 * We use a different numeric value for session id and the network id 631 * to make it clear that these are not the same thing. However, for 632 * easier debugging, the network id can be recovered by dropping the 633 * last decimal digit (at least until they get very, very, large). 634 */ sessionIdFromNetId(final int netId)635 public static int sessionIdFromNetId(final int netId) { 636 if (netId <= 0) return INVALID_SESSION_ID; 637 return (int) (((long) netId * 10 + (8 - (netId % 9))) % Integer.MAX_VALUE + 1); 638 } 639 updateWifiMetrics(long now, int s2)640 private void updateWifiMetrics(long now, int s2) { 641 int netId = getCurrentNetId(); 642 643 mAggressiveConnectedScore.updateUsingWifiInfo(mWifiInfo, now); 644 int s1 = mAggressiveConnectedScore.generateScore(); 645 logLinkMetrics(now, netId, s1, s2, mLegacyIntScore); 646 647 if (mLegacyIntScore != mWifiInfo.getScore()) { 648 if (mVerboseLoggingEnabled) { 649 Log.d(TAG, "report new wifi score " + mLegacyIntScore); 650 } 651 mWifiInfo.setScore(mLegacyIntScore); 652 } 653 mWifiMetrics.incrementWifiScoreCount(mInterfaceName, mLegacyIntScore); 654 } 655 656 private static final double TIME_CONSTANT_MILLIS = 30.0e+3; 657 private static final long NUD_THROTTLE_MILLIS = 5000; 658 private long mLastKnownNudCheckTimeMillis = 0; 659 private int mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE; 660 private int mNudYes = 0; // Counts when we voted for a NUD 661 private int mNudCount = 0; // Counts when we were told a NUD was sent 662 663 /** 664 * Recommends that a layer 3 check be done 665 * 666 * The caller can use this to (help) decide that an IP reachability check 667 * is desirable. The check is not done here; that is the caller's responsibility. 668 * 669 * @return true to indicate that an IP reachability check is recommended 670 */ shouldCheckIpLayer()671 public boolean shouldCheckIpLayer() { 672 // Don't recommend if adaptive connectivity is disabled. 673 if (!mAdaptiveConnectivityEnabledSettingObserver.get() 674 || !mWifiSettingsStore.isWifiScoringEnabled()) { 675 if (mVerboseLoggingEnabled) { 676 Log.d(TAG, "Wifi scoring disabled - Don't check IP layer"); 677 } 678 return false; 679 } 680 long millis = mClock.getWallClockMillis(); 681 long deltaMillis = millis - mLastKnownNudCheckTimeMillis; 682 // Don't ever ask back-to-back - allow at least 5 seconds 683 // for the previous one to finish. 684 if (deltaMillis < NUD_THROTTLE_MILLIS) { 685 return false; 686 } 687 if (SdkLevel.isAtLeastS() && mWifiConnectedNetworkScorerHolder != null) { 688 if (!mWifiConnectedNetworkScorerHolder.getShouldCheckIpLayerOnce()) { 689 return false; 690 } 691 mNudYes++; 692 return true; 693 } 694 int nud = mScoringParams.getNudKnob(); 695 if (nud == 0) { 696 return false; 697 } 698 // nextNudBreach is the bar the score needs to cross before we ask for NUD 699 double nextNudBreach = ConnectedScore.WIFI_TRANSITION_SCORE; 700 if (mWifiConnectedNetworkScorerHolder == null) { 701 // nud is between 1 and 10 at this point 702 double deltaLevel = 11 - nud; 703 // If we were below threshold the last time we checked, then compute a new bar 704 // that starts down from there and decays exponentially back up to the steady-state 705 // bar. If 5 time constants have passed, we are 99% of the way there, so skip the math. 706 if (mLastKnownNudCheckScore < ConnectedScore.WIFI_TRANSITION_SCORE 707 && deltaMillis < 5.0 * TIME_CONSTANT_MILLIS) { 708 double a = Math.exp(-deltaMillis / TIME_CONSTANT_MILLIS); 709 nextNudBreach = 710 a * (mLastKnownNudCheckScore - deltaLevel) + (1.0 - a) * nextNudBreach; 711 } 712 } 713 if (mLegacyIntScore >= nextNudBreach) { 714 return false; 715 } 716 mNudYes++; 717 return true; 718 } 719 720 /** 721 * Should be called when a reachability check has been issued 722 * 723 * When the caller has requested an IP reachability check, calling this will 724 * help to rate-limit requests via shouldCheckIpLayer() 725 */ noteIpCheck()726 public void noteIpCheck() { 727 long millis = mClock.getWallClockMillis(); 728 mLastKnownNudCheckTimeMillis = millis; 729 mLastKnownNudCheckScore = mLegacyIntScore; 730 mNudCount++; 731 // Make sure that only one NUD operation can be triggered. 732 if (mWifiConnectedNetworkScorerHolder != null) { 733 mWifiConnectedNetworkScorerHolder.setShouldCheckIpLayerOnce(false); 734 } 735 } 736 737 /** 738 * Data for dumpsys 739 * 740 * These are stored as csv formatted lines 741 */ 742 private LinkedList<String> mLinkMetricsHistory = new LinkedList<String>(); 743 744 /** 745 * Data logging for dumpsys 746 */ logLinkMetrics(long now, int netId, int s1, int s2, int score)747 private void logLinkMetrics(long now, int netId, int s1, int s2, int score) { 748 if (now < FIRST_REASONABLE_WALL_CLOCK) return; 749 double rssi = mWifiInfo.getRssi(); 750 double filteredRssi = -1; 751 double rssiThreshold = -1; 752 if (mWifiConnectedNetworkScorerHolder == null) { 753 filteredRssi = mVelocityBasedConnectedScore.getFilteredRssi(); 754 rssiThreshold = mVelocityBasedConnectedScore.getAdjustedRssiThreshold(); 755 } 756 int freq = mWifiInfo.getFrequency(); 757 int txLinkSpeed = mWifiInfo.getLinkSpeed(); 758 int rxLinkSpeed = mWifiInfo.getRxLinkSpeedMbps(); 759 WifiScoreCard.PerNetwork network = mWifiScoreCard.lookupNetwork(mWifiInfo.getSSID()); 760 int txThroughputMbps = network.getTxLinkBandwidthKbps() / 1000; 761 int rxThroughputMbps = network.getRxLinkBandwidthKbps() / 1000; 762 double txSuccessRate = mWifiInfo.getSuccessfulTxPacketsPerSecond(); 763 double txRetriesRate = mWifiInfo.getRetriedTxPacketsPerSecond(); 764 double txBadRate = mWifiInfo.getLostTxPacketsPerSecond(); 765 double rxSuccessRate = mWifiInfo.getSuccessfulRxPacketsPerSecond(); 766 long totalBeaconRx = mWifiMetrics.getTotalBeaconRxCount(); 767 String s; 768 try { 769 String timestamp = new SimpleDateFormat("MM-dd HH:mm:ss.SSS").format(new Date(now)); 770 s = String.format(Locale.US, // Use US to avoid comma/decimal confusion 771 "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%d,%d,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d", 772 timestamp, mSessionNumber, netId, 773 rssi, filteredRssi, rssiThreshold, freq, txLinkSpeed, rxLinkSpeed, 774 txThroughputMbps, rxThroughputMbps, totalBeaconRx, 775 txSuccessRate, txRetriesRate, txBadRate, rxSuccessRate, 776 mNudYes, mNudCount, 777 s1, s2, score); 778 779 } catch (Exception e) { 780 Log.e(TAG, "format problem", e); 781 return; 782 } 783 synchronized (mLinkMetricsHistory) { 784 mLinkMetricsHistory.add(s); 785 while (mLinkMetricsHistory.size() > DUMPSYS_ENTRY_COUNT_LIMIT) { 786 mLinkMetricsHistory.removeFirst(); 787 } 788 } 789 } 790 791 /** 792 * Tag to be used in dumpsys request 793 */ 794 public static final String DUMP_ARG = "WifiScoreReport"; 795 796 /** 797 * Dump logged signal strength and traffic measurements. 798 * @param fd unused 799 * @param pw PrintWriter for writing dump to 800 * @param args unused 801 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)802 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 803 LinkedList<String> history; 804 synchronized (mLinkMetricsHistory) { 805 history = new LinkedList<>(mLinkMetricsHistory); 806 } 807 pw.println("time,session,netid,rssi,filtered_rssi,rssi_threshold,freq,txLinkSpeed," 808 + "rxLinkSpeed,txTput,rxTput,bcnCnt,tx_good,tx_retry,tx_bad,rx_pps,nudrq,nuds," 809 + "s1,s2,score"); 810 for (String line : history) { 811 pw.println(line); 812 } 813 history.clear(); 814 pw.println("externalScorerActive=" + (mWifiConnectedNetworkScorerHolder != null)); 815 pw.println("mShouldReduceNetworkScore=" + mShouldReduceNetworkScore); 816 } 817 818 /** 819 * Set a scorer for Wi-Fi connected network score handling. 820 * @param binder 821 * @param scorer 822 */ setWifiConnectedNetworkScorer(IBinder binder, IWifiConnectedNetworkScorer scorer)823 public boolean setWifiConnectedNetworkScorer(IBinder binder, 824 IWifiConnectedNetworkScorer scorer) { 825 if (binder == null || scorer == null) return false; 826 // Enforce that only a single scorer can be set successfully. 827 if (mWifiConnectedNetworkScorerHolder != null) { 828 Log.e(TAG, "Failed to set current scorer because one scorer is already set"); 829 return false; 830 } 831 WifiConnectedNetworkScorerHolder scorerHolder = 832 new WifiConnectedNetworkScorerHolder(binder, scorer); 833 if (!scorerHolder.linkScorerToDeath()) { 834 return false; 835 } 836 mWifiConnectedNetworkScorerHolder = scorerHolder; 837 838 // Register to receive updates from external scorer. 839 mExternalScoreUpdateObserverProxy.registerCallback(mScoreUpdateObserverCallback); 840 841 // Disable AOSP scorer 842 mVelocityBasedConnectedScore = null; 843 mWifiMetrics.setIsExternalWifiScorerOn(true); 844 // If there is already a connection, start a new session 845 final int netId = getCurrentNetId(); 846 if (netId > 0 && !mShouldReduceNetworkScore) { 847 startConnectedNetworkScorer(netId, mIsUserSelected); 848 } 849 return true; 850 } 851 852 /** 853 * Clear an existing scorer for Wi-Fi connected network score handling. 854 */ clearWifiConnectedNetworkScorer()855 public void clearWifiConnectedNetworkScorer() { 856 if (mWifiConnectedNetworkScorerHolder == null) { 857 return; 858 } 859 mWifiConnectedNetworkScorerHolder.reset(); 860 revertToDefaultConnectedScorer(); 861 } 862 863 /** 864 * If this connection is not going to be the default route on the device when cellular is 865 * present, don't send this connection to external scorer for scoring (since scoring only makes 866 * sense if we need to score wifi vs cellular to determine the default network). 867 * 868 * Hence, we ignore local only or restricted wifi connections. 869 * @return true if the connection is local only or restricted, false otherwise. 870 */ isLocalOnlyOrRestrictedConnection()871 private boolean isLocalOnlyOrRestrictedConnection() { 872 final NetworkCapabilities nc = getCurrentNetCapabilities(); 873 if (nc == null) return false; 874 if (SdkLevel.isAtLeastS()) { 875 // restricted connection support only added in S. 876 if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID) 877 || nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)) { 878 // restricted connection. 879 Log.v(TAG, "Restricted connection, ignore."); 880 return true; 881 } 882 } 883 if (!nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { 884 // local only connection. 885 Log.v(TAG, "Local only connection, ignore."); 886 return true; 887 } 888 return false; 889 } 890 891 /** 892 * Start the registered Wi-Fi connected network scorer. 893 * @param netId identifies the current android.net.Network 894 */ startConnectedNetworkScorer(int netId, boolean isUserSelected)895 public void startConnectedNetworkScorer(int netId, boolean isUserSelected) { 896 mIsUserSelected = isUserSelected; 897 final int sessionId = getCurrentSessionId(); 898 if (mWifiConnectedNetworkScorerHolder == null 899 || netId != getCurrentNetId() 900 || isLocalOnlyOrRestrictedConnection() 901 || sessionId == INVALID_SESSION_ID) { 902 Log.w(TAG, "Cannot start external scoring" 903 + " netId=" + netId 904 + " currentNetId=" + getCurrentNetId() 905 + " currentNetCapabilities=" + getCurrentNetCapabilities() 906 + " sessionId=" + sessionId); 907 return; 908 } 909 mWifiInfo.setScore(ConnectedScore.WIFI_MAX_SCORE); 910 mWifiConnectedNetworkScorerHolder.startSession(sessionId, mIsUserSelected); 911 mWifiInfoNoReset.setBSSID(mWifiInfo.getBSSID()); 912 mWifiInfoNoReset.setSSID(mWifiInfo.getWifiSsid()); 913 mWifiInfoNoReset.setRssi(mWifiInfo.getRssi()); 914 mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; 915 mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; 916 } 917 918 /** 919 * Stop the registered Wi-Fi connected network scorer. 920 */ stopConnectedNetworkScorer()921 public void stopConnectedNetworkScorer() { 922 mNetworkAgent = null; 923 if (mWifiConnectedNetworkScorerHolder == null) { 924 return; 925 } 926 mWifiConnectedNetworkScorerHolder.stopSession(); 927 928 long millis = mClock.getWallClockMillis(); 929 // Blocklist the current BSS 930 if ((mLastScoreBreachLowTimeMillis != INVALID_WALL_CLOCK_MILLIS) 931 && ((millis - mLastScoreBreachLowTimeMillis) 932 >= MIN_TIME_TO_WAIT_BEFORE_BLOCKLIST_BSSID_MILLIS)) { 933 mWifiBlocklistMonitor.handleBssidConnectionFailure(mWifiInfo.getBSSID(), 934 mWifiInfo.getSSID(), 935 WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE, 936 mWifiInfo.getRssi()); 937 mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; 938 } 939 } 940 941 /** 942 * Set NetworkAgent 943 */ setNetworkAgent(WifiNetworkAgent agent)944 public void setNetworkAgent(WifiNetworkAgent agent) { 945 WifiNetworkAgent oldAgent = mNetworkAgent; 946 mNetworkAgent = agent; 947 // if mNetworkAgent was null previously, then the score wasn't sent to ConnectivityService. 948 // Send it now that the NetworkAgent has been set. 949 if (oldAgent == null && mNetworkAgent != null) { 950 sendNetworkScore(); 951 } 952 } 953 954 /** Get cached score */ 955 @VisibleForTesting 956 @RequiresApi(Build.VERSION_CODES.S) getScore()957 public NetworkScore getScore() { 958 return getScoreBuilder().build(); 959 } 960 961 @RequiresApi(Build.VERSION_CODES.S) getScoreBuilder()962 private NetworkScore.Builder getScoreBuilder() { 963 // We should force keep connected for a MBB CMM which is not lingering. 964 boolean shouldForceKeepConnected = 965 mCurrentRole == ActiveModeManager.ROLE_CLIENT_SECONDARY_TRANSIENT 966 && !mShouldReduceNetworkScore; 967 int keepConnectedReason = 968 shouldForceKeepConnected 969 ? NetworkScore.KEEP_CONNECTED_FOR_HANDOVER 970 : NetworkScore.KEEP_CONNECTED_NONE; 971 boolean exiting = SdkLevel.isAtLeastS() && mWifiConnectedNetworkScorerHolder != null 972 ? !mIsUsable : mLegacyIntScore < ConnectedScore.WIFI_TRANSITION_SCORE; 973 return new NetworkScore.Builder() 974 .setLegacyInt(mShouldReduceNetworkScore ? LINGERING_SCORE : mLegacyIntScore) 975 .setTransportPrimary(mCurrentRole == ActiveModeManager.ROLE_CLIENT_PRIMARY) 976 .setExiting(exiting) 977 .setKeepConnectedReason(keepConnectedReason); 978 } 979 980 /** Get legacy int score. */ 981 @VisibleForTesting 982 public int getLegacyIntScore() { 983 // When S Wifi module is run on R: 984 // - mShouldReduceNetworkScore is useless since MBB doesn't exist on R, so there isn't any 985 // forced lingering. 986 // - mIsUsable can't be set as notifyStatusUpdate() for external scorer didn't exist on R 987 // SDK (assume that only R platform + S Wifi module + R external scorer is possible, 988 // and R platform + S Wifi module + S external scorer is not possible) 989 // Thus, it's ok to return the raw int score on R. 990 return mLegacyIntScore; 991 } 992 993 /** Get counts when we voted for a NUD. */ 994 @VisibleForTesting 995 public int getNudYes() { 996 return mNudYes; 997 } 998 999 private void revertToDefaultConnectedScorer() { 1000 Log.d(TAG, "Using VelocityBasedConnectedScore"); 1001 mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(mScoringParams, mClock); 1002 mWifiConnectedNetworkScorerHolder = null; 1003 mExternalScoreUpdateObserverProxy.unregisterCallback(mScoreUpdateObserverCallback); 1004 mWifiMetrics.setIsExternalWifiScorerOn(false); 1005 } 1006 1007 /** 1008 * This is a function of {@link #mCurrentRole} {@link #mShouldReduceNetworkScore}, and 1009 * {@link #mLegacyIntScore}, and should be called when any of them changes. 1010 */ 1011 private void sendNetworkScore() { 1012 if (mNetworkAgent == null) { 1013 return; 1014 } 1015 if (SdkLevel.isAtLeastS()) { 1016 // NetworkScore was introduced in S 1017 mNetworkAgent.sendNetworkScore(getScore()); 1018 } else { 1019 mNetworkAgent.sendNetworkScore(getLegacyIntScore()); 1020 } 1021 } 1022 1023 /** Called when the owner {@link ConcreteClientModeManager}'s role changes. */ 1024 public void onRoleChanged(@Nullable ClientRole role) { 1025 mCurrentRole = role; 1026 sendNetworkScore(); 1027 } 1028 } 1029