• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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