blob: c707e8bcfd706c88995143716bb1e609760d2851 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wifi;
import static com.android.server.wifi.ClientModeImpl.WIFI_WORK_SOURCE;
import android.annotation.Nullable;
import android.content.Context;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkScore;
import android.net.wifi.IWifiConnectedNetworkScorer;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConnectedSessionInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.nl80211.WifiNl80211Manager;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.server.wifi.ActiveModeManager.ClientRole;
import com.android.wifi.resources.R;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Calendar;
import java.util.LinkedList;
/**
* Class used to calculate scores for connected wifi networks and report it to the associated
* network agent.
*/
public class WifiScoreReport {
private static final String TAG = "WifiScoreReport";
private static final int DUMPSYS_ENTRY_COUNT_LIMIT = 3600; // 3 hours on 3 second poll
private boolean mVerboseLoggingEnabled = false;
private static final long FIRST_REASONABLE_WALL_CLOCK = 1490000000000L; // mid-December 2016
private static final long MIN_TIME_TO_KEEP_BELOW_TRANSITION_SCORE_MILLIS = 9000;
private long mLastDownwardBreachTimeMillis = 0;
private static final int WIFI_CONNECTED_NETWORK_SCORER_IDENTIFIER = 0;
private static final int INVALID_SESSION_ID = -1;
private static final long MIN_TIME_TO_WAIT_BEFORE_BLOCKLIST_BSSID_MILLIS = 29000;
private static final long INVALID_WALL_CLOCK_MILLIS = -1;
private static final int WIFI_SCORE_TO_TERMINATE_CONNECTION_BLOCKLIST_BSSID = -2;
/**
* Set lingering score to be artificially lower than all other scores so that it will force
* ConnectivityService to prefer any other network over this one.
*/
@VisibleForTesting
static final int LINGERING_SCORE = 1;
// Cache of the last score
private int mLegacyIntScore = ConnectedScore.WIFI_INITIAL_SCORE;
// Cache of the last usability status
private boolean mIsUsable = true;
/**
* If true, indicates that the associated {@link ClientModeImpl} instance is lingering
* as a part of make before break STA + STA use-case, and will always send
* {@link #LINGERING_SCORE} to NetworkAgent.
*/
private boolean mShouldReduceNetworkScore = false;
/** The current role of the ClientModeManager which owns this instance of WifiScoreReport. */
@Nullable
private ClientRole mCurrentRole = null;
private final ScoringParams mScoringParams;
private final Clock mClock;
private int mSessionNumber = 0; // not to be confused with sessionid, this just counts resets
private final String mInterfaceName;
private final WifiBlocklistMonitor mWifiBlocklistMonitor;
private final WifiScoreCard mWifiScoreCard;
private final Context mContext;
private long mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS;
private long mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS;
private final ConnectedScore mAggressiveConnectedScore;
private VelocityBasedConnectedScore mVelocityBasedConnectedScore;
private final WifiSettingsStore mWifiSettingsStore;
private int mSessionIdNoReset = INVALID_SESSION_ID;
// Indicate whether current network is selected by the user
private boolean mIsUserSelected = false;
@Nullable
private WifiNetworkAgent mNetworkAgent;
private final WifiMetrics mWifiMetrics;
private final ExtendedWifiInfo mWifiInfo;
private final WifiNative mWifiNative;
private final WifiThreadRunner mWifiThreadRunner;
private final DeviceConfigFacade mDeviceConfigFacade;
private final ExternalScoreUpdateObserverProxy mExternalScoreUpdateObserverProxy;
private final WifiInfo mWifiInfoNoReset;
private final WifiGlobals mWifiGlobals;
private final ActiveModeWarden mActiveModeWarden;
private final WifiConnectivityManager mWifiConnectivityManager;
private final WifiConfigManager mWifiConfigManager;
private long mLastLowScoreScanTimestampMs = -1;
private WifiConfiguration mCurrentWifiConfiguration;
/**
* Callback from {@link ExternalScoreUpdateObserverProxy}
*/
private class ScoreUpdateObserverProxy implements WifiManager.ScoreUpdateObserver {
@Override
public void notifyScoreUpdate(int sessionId, int score) {
if (mWifiConnectedNetworkScorerHolder == null
|| sessionId == INVALID_SESSION_ID
|| sessionId != getCurrentSessionId()) {
Log.w(TAG, "Ignoring stale/invalid external score"
+ " sessionId=" + sessionId
+ " currentSessionId=" + getCurrentSessionId()
+ " score=" + score);
return;
}
long millis = mClock.getWallClockMillis();
if (SdkLevel.isAtLeastS()) {
mLegacyIntScore = score;
// Only primary network can have external scorer.
updateWifiMetrics(millis, -1);
return;
}
// TODO (b/207058915): Disconnect WiFi and blocklist current BSSID. This is to
// maintain backward compatibility for WiFi mainline module on Android 11, and can be
// removed when the WiFi mainline module is no longer updated on Android 11.
if (score == WIFI_SCORE_TO_TERMINATE_CONNECTION_BLOCKLIST_BSSID) {
mWifiBlocklistMonitor.handleBssidConnectionFailure(mWifiInfoNoReset.getBSSID(),
mCurrentWifiConfiguration,
WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE,
mWifiInfoNoReset.getRssi());
return;
}
if (score > ConnectedScore.WIFI_MAX_SCORE
|| score < ConnectedScore.WIFI_MIN_SCORE) {
Log.e(TAG, "Invalid score value from external scorer: " + score);
return;
}
if (score < ConnectedScore.WIFI_TRANSITION_SCORE) {
if (mLegacyIntScore >= ConnectedScore.WIFI_TRANSITION_SCORE) {
mLastScoreBreachLowTimeMillis = millis;
}
} else {
mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS;
}
if (score > ConnectedScore.WIFI_TRANSITION_SCORE) {
if (mLegacyIntScore <= ConnectedScore.WIFI_TRANSITION_SCORE) {
mLastScoreBreachHighTimeMillis = millis;
}
} else {
mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS;
}
mLegacyIntScore = score;
reportNetworkScoreToConnectivityServiceIfNecessary();
updateWifiMetrics(millis, -1);
}
@Override
public void triggerUpdateOfWifiUsabilityStats(int sessionId) {
if (mWifiConnectedNetworkScorerHolder == null
|| sessionId == INVALID_SESSION_ID
|| sessionId != getCurrentSessionId()
|| mInterfaceName == null) {
Log.w(TAG, "Ignoring triggerUpdateOfWifiUsabilityStats"
+ " sessionId=" + sessionId
+ " currentSessionId=" + getCurrentSessionId()
+ " interfaceName=" + mInterfaceName);
return;
}
WifiLinkLayerStats stats = mWifiNative.getWifiLinkLayerStats(mInterfaceName);
// update mWifiInfo
// TODO(b/153075963): Better coordinate this class and ClientModeImpl to remove
// redundant codes below and in ClientModeImpl#fetchRssiLinkSpeedAndFrequencyNative.
WifiNl80211Manager.SignalPollResult pollResult =
mWifiNative.signalPoll(mInterfaceName);
if (pollResult != null) {
int newRssi = pollResult.currentRssiDbm;
int newTxLinkSpeed = pollResult.txBitrateMbps;
int newFrequency = pollResult.associationFrequencyMHz;
int newRxLinkSpeed = pollResult.rxBitrateMbps;
if (newRssi > WifiInfo.INVALID_RSSI && newRssi < WifiInfo.MAX_RSSI) {
if (newRssi > (WifiInfo.INVALID_RSSI + 256)) {
Log.wtf(TAG, "Error! +ve value RSSI: " + newRssi);
newRssi -= 256;
}
mWifiInfo.setRssi(newRssi);
} else {
mWifiInfo.setRssi(WifiInfo.INVALID_RSSI);
}
/*
* set Tx link speed only if it is valid
*/
if (newTxLinkSpeed > 0) {
mWifiInfo.setLinkSpeed(newTxLinkSpeed);
mWifiInfo.setTxLinkSpeedMbps(newTxLinkSpeed);
}
/*
* set Rx link speed only if it is valid
*/
if (newRxLinkSpeed > 0) {
mWifiInfo.setRxLinkSpeedMbps(newRxLinkSpeed);
}
if (newFrequency > 0) {
mWifiInfo.setFrequency(newFrequency);
}
}
// TODO(b/153075963): This should not be plumbed through WifiMetrics
mWifiMetrics.updateWifiUsabilityStatsEntries(mInterfaceName, mWifiInfo, stats);
}
@Override
public void notifyStatusUpdate(int sessionId, boolean isUsable) {
if (mWifiConnectedNetworkScorerHolder == null
|| sessionId == INVALID_SESSION_ID
|| sessionId != getCurrentSessionId()) {
Log.w(TAG, "Ignoring stale/invalid external status"
+ " sessionId=" + sessionId
+ " currentSessionId=" + getCurrentSessionId()
+ " isUsable=" + isUsable);
return;
}
if (mNetworkAgent == null) {
return;
}
if (mShouldReduceNetworkScore) {
return;
}
mIsUsable = isUsable;
// Wifi is set to be usable if adaptive connectivity is disabled.
if (!mAdaptiveConnectivityEnabledSettingObserver.get()
|| !mWifiSettingsStore.isWifiScoringEnabled()) {
mIsUsable = true;
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Wifi scoring disabled - Notify that Wifi is usable");
}
}
// Send `exiting` to NetworkScore, but don't update and send mLegacyIntScore
// and don't change any other fields. All we want to do is relay to ConnectivityService
// whether the current network is usable.
if (SdkLevel.isAtLeastS()) {
mNetworkAgent.sendNetworkScore(
getScoreBuilder()
.setLegacyInt(mLegacyIntScore)
.setExiting(!mIsUsable)
.build());
} else {
mNetworkAgent.sendNetworkScore(mIsUsable ? ConnectedScore.WIFI_TRANSITION_SCORE + 1
: ConnectedScore.WIFI_TRANSITION_SCORE - 1);
}
mWifiInfo.setUsable(mIsUsable);
}
@Override
public void requestNudOperation(int sessionId) {
if (mWifiConnectedNetworkScorerHolder == null
|| sessionId == INVALID_SESSION_ID
|| sessionId != getCurrentSessionId()) {
Log.w(TAG, "Ignoring stale/invalid external input for NUD triggering"
+ " sessionId=" + sessionId
+ " currentSessionId=" + getCurrentSessionId());
return;
}
if (!mAdaptiveConnectivityEnabledSettingObserver.get()
|| !mWifiSettingsStore.isWifiScoringEnabled()) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Wifi scoring disabled - Cannot request a NUD operation");
}
return;
}
mWifiConnectedNetworkScorerHolder.setShouldCheckIpLayerOnce(true);
}
@Override
public void blocklistCurrentBssid(int sessionId) {
if (mWifiConnectedNetworkScorerHolder == null
|| sessionId == INVALID_SESSION_ID
|| sessionId != mSessionIdNoReset) {
Log.w(TAG, "Ignoring stale/invalid external input for blocklisting"
+ " sessionId=" + sessionId
+ " mSessionIdNoReset=" + mSessionIdNoReset);
return;
}
if (!mAdaptiveConnectivityEnabledSettingObserver.get()
|| !mWifiSettingsStore.isWifiScoringEnabled()) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Wifi scoring disabled - Cannot blocklist current BSSID");
}
return;
}
if (mWifiInfoNoReset.getBSSID() != null) {
mWifiBlocklistMonitor.handleBssidConnectionFailure(mWifiInfoNoReset.getBSSID(),
mCurrentWifiConfiguration,
WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE,
mWifiInfoNoReset.getRssi());
}
}
}
/**
* If true, put the WifiScoreReport in lingering mode. A very low score is reported to
* NetworkAgent, and the real score is never reported.
*/
public void setShouldReduceNetworkScore(boolean shouldReduceNetworkScore) {
Log.d(TAG, "setShouldReduceNetworkScore=" + shouldReduceNetworkScore
+ " mNetworkAgent is null? " + (mNetworkAgent == null));
mShouldReduceNetworkScore = shouldReduceNetworkScore;
// inform the external scorer that ongoing session has ended (since the score is no longer
// under their control)
if (mShouldReduceNetworkScore && mWifiConnectedNetworkScorerHolder != null) {
mWifiConnectedNetworkScorerHolder.stopSession();
}
// if set to true, send score below disconnect threshold to start lingering
sendNetworkScore();
}
/**
* Report network score to connectivity service.
*/
private void reportNetworkScoreToConnectivityServiceIfNecessary() {
if (mNetworkAgent == null) {
return;
}
if (mWifiConnectedNetworkScorerHolder == null && mLegacyIntScore == mWifiInfo.getScore()) {
return;
}
// only send network score if not lingering. If lingering, would have already sent score at
// start of lingering.
if (mShouldReduceNetworkScore) {
return;
}
if (mWifiConnectedNetworkScorerHolder != null
&& mContext.getResources().getBoolean(
R.bool.config_wifiMinConfirmationDurationSendNetworkScoreEnabled)
/// Turn off hysteresis/dampening for shell commands.
&& !mWifiConnectedNetworkScorerHolder.isShellCommandScorer()) {
long millis = mClock.getWallClockMillis();
if (mLastScoreBreachLowTimeMillis != INVALID_WALL_CLOCK_MILLIS) {
if (mWifiInfo.getRssi()
>= mDeviceConfigFacade.getRssiThresholdNotSendLowScoreToCsDbm()) {
Log.d(TAG, "Not reporting low score because RSSI is high "
+ mWifiInfo.getRssi());
return;
}
if ((millis - mLastScoreBreachLowTimeMillis)
< mDeviceConfigFacade.getMinConfirmationDurationSendLowScoreMs()) {
Log.d(TAG, "Not reporting low score because elapsed time is shorter than "
+ "the minimum confirmation duration");
return;
}
}
if (mLastScoreBreachHighTimeMillis != INVALID_WALL_CLOCK_MILLIS
&& (millis - mLastScoreBreachHighTimeMillis)
< mDeviceConfigFacade.getMinConfirmationDurationSendHighScoreMs()) {
Log.d(TAG, "Not reporting high score because elapsed time is shorter than "
+ "the minimum confirmation duration");
return;
}
}
// Stay a notch above the transition score if adaptive connectivity is disabled.
if (!mAdaptiveConnectivityEnabledSettingObserver.get()
|| !mWifiSettingsStore.isWifiScoringEnabled()) {
mLegacyIntScore = ConnectedScore.WIFI_TRANSITION_SCORE + 1;
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Wifi scoring disabled - Stay a notch above the transition score");
}
}
sendNetworkScore();
}
/**
* Container for storing info about external scorer and tracking its death.
*/
private final class WifiConnectedNetworkScorerHolder implements IBinder.DeathRecipient {
private final IBinder mBinder;
private final IWifiConnectedNetworkScorer mScorer;
private int mSessionId = INVALID_SESSION_ID;
private boolean mShouldCheckIpLayerOnce = false;
WifiConnectedNetworkScorerHolder(IBinder binder, IWifiConnectedNetworkScorer scorer) {
mBinder = binder;
mScorer = scorer;
}
/**
* Link WiFi connected scorer to death listener.
*/
public boolean linkScorerToDeath() {
try {
mBinder.linkToDeath(this, 0);
} catch (RemoteException e) {
Log.e(TAG, "Unable to linkToDeath Wifi connected network scorer " + mScorer, e);
return false;
}
return true;
}
/**
* App hosting the binder has died.
*/
@Override
public void binderDied() {
mWifiThreadRunner.post(() -> revertToDefaultConnectedScorer());
}
/**
* Unlink this object from binder death.
*/
public void reset() {
mBinder.unlinkToDeath(this, 0);
}
/**
* Starts a new scoring session.
*/
public void startSession(int sessionId, boolean isUserSelected) {
if (sessionId == INVALID_SESSION_ID) {
throw new IllegalArgumentException();
}
if (mSessionId != INVALID_SESSION_ID) {
// This is not expected to happen, log if it does
Log.e(TAG, "Stopping session " + mSessionId + " before starting " + sessionId);
stopSession();
}
// Bail now if the scorer has gone away
if (this != mWifiConnectedNetworkScorerHolder) {
return;
}
mSessionId = sessionId;
mSessionIdNoReset = sessionId;
try {
WifiConnectedSessionInfo sessionInfo =
new WifiConnectedSessionInfo.Builder(sessionId)
.setUserSelected(isUserSelected)
.build();
mScorer.onStart(sessionInfo);
} catch (RemoteException e) {
Log.e(TAG, "Unable to start Wifi connected network scorer " + this, e);
revertToDefaultConnectedScorer();
}
}
public void stopSession() {
final int sessionId = mSessionId;
if (sessionId == INVALID_SESSION_ID) return;
mSessionId = INVALID_SESSION_ID;
mShouldCheckIpLayerOnce = false;
try {
mScorer.onStop(sessionId);
} catch (RemoteException e) {
Log.e(TAG, "Unable to stop Wifi connected network scorer " + this, e);
revertToDefaultConnectedScorer();
}
}
public boolean isShellCommandScorer() {
return mScorer instanceof WifiShellCommand.WifiScorer;
}
private void setShouldCheckIpLayerOnce(boolean shouldCheckIpLayerOnce) {
mShouldCheckIpLayerOnce = shouldCheckIpLayerOnce;
}
private boolean getShouldCheckIpLayerOnce() {
return mShouldCheckIpLayerOnce;
}
}
private final ScoreUpdateObserverProxy mScoreUpdateObserverCallback =
new ScoreUpdateObserverProxy();
@Nullable
private WifiConnectedNetworkScorerHolder mWifiConnectedNetworkScorerHolder;
private final AdaptiveConnectivityEnabledSettingObserver
mAdaptiveConnectivityEnabledSettingObserver;
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,
WifiGlobals wifiGlobals,
ActiveModeWarden activeModeWarden,
WifiConnectivityManager wifiConnectivityManager,
WifiConfigManager wifiConfigManager) {
mScoringParams = scoringParams;
mClock = clock;
mAdaptiveConnectivityEnabledSettingObserver = adaptiveConnectivityEnabledSettingObserver;
mAggressiveConnectedScore = new AggressiveConnectedScore(scoringParams, clock);
mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(scoringParams, clock);
mWifiMetrics = wifiMetrics;
mWifiInfo = wifiInfo;
mWifiNative = wifiNative;
mWifiBlocklistMonitor = wifiBlocklistMonitor;
mWifiThreadRunner = wifiThreadRunner;
mWifiScoreCard = wifiScoreCard;
mDeviceConfigFacade = deviceConfigFacade;
mContext = context;
mInterfaceName = interfaceName;
mExternalScoreUpdateObserverProxy = externalScoreUpdateObserverProxy;
mWifiSettingsStore = wifiSettingsStore;
mWifiInfoNoReset = new WifiInfo(mWifiInfo);
mWifiGlobals = wifiGlobals;
mActiveModeWarden = activeModeWarden;
mWifiConnectivityManager = wifiConnectivityManager;
mWifiConfigManager = wifiConfigManager;
}
/** Returns whether this scores primary network based on the role */
private boolean isPrimary() {
return mCurrentRole != null && mCurrentRole == ActiveModeManager.ROLE_CLIENT_PRIMARY;
}
/**
* Reset the last calculated score.
*/
public void reset() {
mSessionNumber++;
mLegacyIntScore = isPrimary() ? ConnectedScore.WIFI_INITIAL_SCORE
: ConnectedScore.WIFI_SECONDARY_INITIAL_SCORE;
mIsUsable = true;
mLastKnownNudCheckScore = isPrimary() ? ConnectedScore.WIFI_TRANSITION_SCORE
: ConnectedScore.WIFI_SECONDARY_TRANSITION_SCORE;
mAggressiveConnectedScore.reset();
if (mVelocityBasedConnectedScore != null) {
mVelocityBasedConnectedScore.reset();
}
mLastDownwardBreachTimeMillis = 0;
mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS;
mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS;
mLastLowScoreScanTimestampMs = -1;
if (mVerboseLoggingEnabled) Log.d(TAG, "reset");
}
/**
* Enable/Disable verbose logging in score report generation.
*/
public void enableVerboseLogging(boolean enable) {
mVerboseLoggingEnabled = enable;
}
/**
* Calculate wifi network score based on updated link layer stats and send the score to
* the WifiNetworkAgent.
*
* If the score has changed from the previous value, update the WifiNetworkAgent.
*
* Called periodically (POLL_RSSI_INTERVAL_MSECS) about every 3 seconds.
*/
public void calculateAndReportScore() {
// Bypass AOSP scorer if Wifi connected network scorer is set
if (mWifiConnectedNetworkScorerHolder != null) {
return;
}
if (mWifiInfo.getRssi() == mWifiInfo.INVALID_RSSI) {
Log.d(TAG, "Not reporting score because RSSI is invalid");
return;
}
int score;
long millis = mClock.getWallClockMillis();
mVelocityBasedConnectedScore.updateUsingWifiInfo(mWifiInfo, millis);
int s2 = mVelocityBasedConnectedScore.generateScore();
score = s2;
final int transitionScore = isPrimary() ? ConnectedScore.WIFI_TRANSITION_SCORE
: ConnectedScore.WIFI_SECONDARY_TRANSITION_SCORE;
final int maxScore = isPrimary() ? ConnectedScore.WIFI_MAX_SCORE
: ConnectedScore.WIFI_MAX_SCORE - ConnectedScore.WIFI_SECONDARY_DELTA_SCORE;
if (mWifiInfo.getScore() > transitionScore && score <= transitionScore
&& mWifiInfo.getSuccessfulTxPacketsPerSecond()
>= mScoringParams.getYippeeSkippyPacketsPerSecond()
&& mWifiInfo.getSuccessfulRxPacketsPerSecond()
>= mScoringParams.getYippeeSkippyPacketsPerSecond()
) {
score = transitionScore + 1;
}
if (mWifiInfo.getScore() > transitionScore && score <= transitionScore) {
// We don't want to trigger a downward breach unless the rssi is
// below the entry threshold. There is noise in the measured rssi, and
// the kalman-filtered rssi is affected by the trend, so check them both.
// TODO(b/74613347) skip this if there are other indications to support the low score
int entry = mScoringParams.getEntryRssi(mWifiInfo.getFrequency());
if (mVelocityBasedConnectedScore.getFilteredRssi() >= entry
|| mWifiInfo.getRssi() >= entry) {
// Stay a notch above the transition score to reduce ambiguity.
score = transitionScore + 1;
}
}
if (mWifiInfo.getScore() >= transitionScore && score < transitionScore) {
mLastDownwardBreachTimeMillis = millis;
} else if (mWifiInfo.getScore() < transitionScore && score >= transitionScore) {
// Staying at below transition score for a certain period of time
// to prevent going back to wifi network again in a short time.
long elapsedMillis = millis - mLastDownwardBreachTimeMillis;
if (elapsedMillis < MIN_TIME_TO_KEEP_BELOW_TRANSITION_SCORE_MILLIS) {
score = mWifiInfo.getScore();
}
}
//sanitize boundaries
if (score > maxScore) {
score = maxScore;
}
if (score < 0) {
score = 0;
}
if (score < mWifiGlobals.getWifiLowConnectedScoreThresholdToTriggerScanForMbb()
&& enoughTimePassedSinceLastLowConnectedScoreScan()
&& mActiveModeWarden.canRequestSecondaryTransientClientModeManager()) {
mLastLowScoreScanTimestampMs = mClock.getElapsedSinceBootMillis();
mWifiConnectivityManager.forceConnectivityScan(WIFI_WORK_SOURCE);
}
// report score
mLegacyIntScore = score;
reportNetworkScoreToConnectivityServiceIfNecessary();
updateWifiMetrics(millis, s2);
}
private boolean enoughTimePassedSinceLastLowConnectedScoreScan() {
return mLastLowScoreScanTimestampMs == -1
|| mClock.getElapsedSinceBootMillis() - mLastLowScoreScanTimestampMs
> (mWifiGlobals.getWifiLowConnectedScoreScanPeriodSeconds() * 1000);
}
private int getCurrentNetId() {
int netId = 0;
if (mNetworkAgent != null) {
final Network network = mNetworkAgent.getNetwork();
if (network != null) {
netId = network.getNetId();
}
}
return netId;
}
@Nullable
private NetworkCapabilities getCurrentNetCapabilities() {
return mNetworkAgent == null ? null : mNetworkAgent.getCurrentNetworkCapabilities();
}
private int getCurrentSessionId() {
return sessionIdFromNetId(getCurrentNetId());
}
/**
* Encodes a network id into a scoring session id.
*
* We use a different numeric value for session id and the network id
* to make it clear that these are not the same thing. However, for
* easier debugging, the network id can be recovered by dropping the
* last decimal digit (at least until they get very, very, large).
*/
public static int sessionIdFromNetId(final int netId) {
if (netId <= 0) return INVALID_SESSION_ID;
return (int) (((long) netId * 10 + (8 - (netId % 9))) % Integer.MAX_VALUE + 1);
}
private void updateWifiMetrics(long now, int s2) {
int netId = getCurrentNetId();
mAggressiveConnectedScore.updateUsingWifiInfo(mWifiInfo, now);
int s1 = ((AggressiveConnectedScore) mAggressiveConnectedScore).generateScore();
logLinkMetrics(now, netId, s1, s2, mLegacyIntScore);
if (mLegacyIntScore != mWifiInfo.getScore()) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "report new wifi score " + mLegacyIntScore);
}
mWifiInfo.setScore(mLegacyIntScore);
}
mWifiMetrics.incrementWifiScoreCount(mInterfaceName, mLegacyIntScore);
}
private static final double TIME_CONSTANT_MILLIS = 30.0e+3;
private static final long NUD_THROTTLE_MILLIS = 5000;
private long mLastKnownNudCheckTimeMillis = 0;
private int mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
private int mNudYes = 0; // Counts when we voted for a NUD
private int mNudCount = 0; // Counts when we were told a NUD was sent
/**
* Recommends that a layer 3 check be done
*
* The caller can use this to (help) decide that an IP reachability check
* is desirable. The check is not done here; that is the caller's responsibility.
*
* @return true to indicate that an IP reachability check is recommended
*/
public boolean shouldCheckIpLayer() {
// Don't recommend if adaptive connectivity is disabled.
if (!mAdaptiveConnectivityEnabledSettingObserver.get()
|| !mWifiSettingsStore.isWifiScoringEnabled()) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Wifi scoring disabled - Don't check IP layer");
}
return false;
}
long millis = mClock.getWallClockMillis();
long deltaMillis = millis - mLastKnownNudCheckTimeMillis;
// Don't ever ask back-to-back - allow at least 5 seconds
// for the previous one to finish.
if (deltaMillis < NUD_THROTTLE_MILLIS) {
return false;
}
if (SdkLevel.isAtLeastS() && mWifiConnectedNetworkScorerHolder != null) {
if (!mWifiConnectedNetworkScorerHolder.getShouldCheckIpLayerOnce()) {
return false;
}
mNudYes++;
return true;
}
int nud = mScoringParams.getNudKnob();
if (nud == 0) {
return false;
}
// nextNudBreach is the bar the score needs to cross before we ask for NUD
double nextNudBreach = ConnectedScore.WIFI_TRANSITION_SCORE;
if (mWifiConnectedNetworkScorerHolder == null) {
// nud is between 1 and 10 at this point
double deltaLevel = 11 - nud;
// If we were below threshold the last time we checked, then compute a new bar
// that starts down from there and decays exponentially back up to the steady-state
// bar. If 5 time constants have passed, we are 99% of the way there, so skip the math.
if (mLastKnownNudCheckScore < ConnectedScore.WIFI_TRANSITION_SCORE
&& deltaMillis < 5.0 * TIME_CONSTANT_MILLIS) {
double a = Math.exp(-deltaMillis / TIME_CONSTANT_MILLIS);
nextNudBreach =
a * (mLastKnownNudCheckScore - deltaLevel) + (1.0 - a) * nextNudBreach;
}
}
if (mLegacyIntScore >= nextNudBreach) {
return false;
}
mNudYes++;
return true;
}
/**
* Should be called when a reachability check has been issued
*
* When the caller has requested an IP reachability check, calling this will
* help to rate-limit requests via shouldCheckIpLayer()
*/
public void noteIpCheck() {
long millis = mClock.getWallClockMillis();
mLastKnownNudCheckTimeMillis = millis;
mLastKnownNudCheckScore = mLegacyIntScore;
mNudCount++;
// Make sure that only one NUD operation can be triggered.
if (mWifiConnectedNetworkScorerHolder != null) {
mWifiConnectedNetworkScorerHolder.setShouldCheckIpLayerOnce(false);
}
}
/**
* Data for dumpsys
*
* These are stored as csv formatted lines
*/
private LinkedList<String> mLinkMetricsHistory = new LinkedList<String>();
/**
* Data logging for dumpsys
*/
private void logLinkMetrics(long now, int netId, int s1, int s2, int score) {
if (now < FIRST_REASONABLE_WALL_CLOCK) return;
double filteredRssi = -1;
double rssiThreshold = -1;
if (mWifiConnectedNetworkScorerHolder == null) {
filteredRssi = mVelocityBasedConnectedScore.getFilteredRssi();
rssiThreshold = mVelocityBasedConnectedScore.getAdjustedRssiThreshold();
}
int freq = mWifiInfo.getFrequency();
int txLinkSpeed = mWifiInfo.getLinkSpeed();
int rxLinkSpeed = mWifiInfo.getRxLinkSpeedMbps();
WifiScoreCard.PerNetwork network = mWifiScoreCard.lookupNetwork(mWifiInfo.getSSID());
int txThroughputMbps = network.getTxLinkBandwidthKbps() / 1000;
int rxThroughputMbps = network.getRxLinkBandwidthKbps() / 1000;
double txSuccessRate = mWifiInfo.getSuccessfulTxPacketsPerSecond();
double txRetriesRate = mWifiInfo.getRetriedTxPacketsPerSecond();
double txBadRate = mWifiInfo.getLostTxPacketsPerSecond();
double rxSuccessRate = mWifiInfo.getSuccessfulRxPacketsPerSecond();
long totalBeaconRx = mWifiMetrics.getTotalBeaconRxCount();
String s;
try {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(now);
// Date format: "%tm-%td %tH:%tM:%tS.%tL"
String timestamp = new StringBuilder().append(c.get(Calendar.MONTH)).append("-")
.append(c.get(Calendar.DAY_OF_MONTH)).append(" ")
.append(c.get(Calendar.HOUR_OF_DAY)).append(":")
.append(c.get(Calendar.MINUTE)).append(":")
.append(c.get(Calendar.SECOND)).append(".")
.append(c.get(Calendar.MILLISECOND)).toString();
s = timestamp + "," + mSessionNumber + "," + netId + "," + mWifiInfo.getRssi()
+ "," + Math.round(filteredRssi * 100) / 100 + "," + rssiThreshold
+ "," + freq + "," + txLinkSpeed
+ "," + rxLinkSpeed + "," + txThroughputMbps
+ "," + rxThroughputMbps + "," + totalBeaconRx
+ "," + Math.round(txSuccessRate * 100) / 100
+ "," + Math.round(txRetriesRate * 100) / 100
+ "," + Math.round(txBadRate * 100) / 100
+ "," + Math.round(rxSuccessRate * 100) / 100
+ "," + mNudYes + "," + mNudCount + "," + s1 + "," + s2 + "," + score;
} catch (Exception e) {
Log.e(TAG, "format problem", e);
return;
}
synchronized (mLinkMetricsHistory) {
mLinkMetricsHistory.add(s);
while (mLinkMetricsHistory.size() > DUMPSYS_ENTRY_COUNT_LIMIT) {
mLinkMetricsHistory.removeFirst();
}
}
}
/**
* Tag to be used in dumpsys request
*/
public static final String DUMP_ARG = "WifiScoreReport";
/**
* Dump logged signal strength and traffic measurements.
* @param fd unused
* @param pw PrintWriter for writing dump to
* @param args unused
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
LinkedList<String> history;
synchronized (mLinkMetricsHistory) {
history = new LinkedList<>(mLinkMetricsHistory);
}
pw.println("time,session,netid,rssi,filtered_rssi,rssi_threshold,freq,txLinkSpeed,"
+ "rxLinkSpeed,txTput,rxTput,bcnCnt,tx_good,tx_retry,tx_bad,rx_pps,nudrq,nuds,"
+ "s1,s2,score");
for (String line : history) {
pw.println(line);
}
history.clear();
pw.println("externalScorerActive=" + (mWifiConnectedNetworkScorerHolder != null));
pw.println("mShouldReduceNetworkScore=" + mShouldReduceNetworkScore);
}
/**
* Set a scorer for Wi-Fi connected network score handling.
* @param binder
* @param scorer
*/
public boolean setWifiConnectedNetworkScorer(IBinder binder,
IWifiConnectedNetworkScorer scorer) {
if (binder == null || scorer == null) return false;
// Enforce that only a single scorer can be set successfully.
if (mWifiConnectedNetworkScorerHolder != null) {
Log.e(TAG, "Failed to set current scorer because one scorer is already set");
return false;
}
WifiConnectedNetworkScorerHolder scorerHolder =
new WifiConnectedNetworkScorerHolder(binder, scorer);
if (!scorerHolder.linkScorerToDeath()) {
return false;
}
mWifiConnectedNetworkScorerHolder = scorerHolder;
mWifiGlobals.setUsingExternalScorer(true);
// Register to receive updates from external scorer.
mExternalScoreUpdateObserverProxy.registerCallback(mScoreUpdateObserverCallback);
// Disable AOSP scorer
mVelocityBasedConnectedScore = null;
mWifiMetrics.setIsExternalWifiScorerOn(true);
// If there is already a connection, start a new session
final int netId = getCurrentNetId();
if (netId > 0 && !mShouldReduceNetworkScore) {
startConnectedNetworkScorer(netId, mIsUserSelected);
}
return true;
}
/**
* Clear an existing scorer for Wi-Fi connected network score handling.
*/
public void clearWifiConnectedNetworkScorer() {
if (mWifiConnectedNetworkScorerHolder == null) {
return;
}
mWifiConnectedNetworkScorerHolder.reset();
revertToDefaultConnectedScorer();
}
/**
* If this connection is not going to be the default route on the device when cellular is
* present, don't send this connection to external scorer for scoring (since scoring only makes
* sense if we need to score wifi vs cellular to determine the default network).
*
* Hence, we ignore local only or restricted wifi connections.
* @return true if the connection is local only or restricted, false otherwise.
*/
private boolean isLocalOnlyOrRestrictedConnection() {
final NetworkCapabilities nc = getCurrentNetCapabilities();
if (nc == null) return false;
if (SdkLevel.isAtLeastS()) {
// restricted connection support only added in S.
if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID)
|| nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)) {
// restricted connection.
Log.v(TAG, "Restricted connection, ignore.");
return true;
}
}
if (!nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
// local only connection.
Log.v(TAG, "Local only connection, ignore.");
return true;
}
return false;
}
/**
* Start the registered Wi-Fi connected network scorer.
* @param netId identifies the current android.net.Network
*/
public void startConnectedNetworkScorer(int netId, boolean isUserSelected) {
mIsUserSelected = isUserSelected;
final int sessionId = getCurrentSessionId();
if (mWifiConnectedNetworkScorerHolder == null
|| netId != getCurrentNetId()
|| isLocalOnlyOrRestrictedConnection()
|| sessionId == INVALID_SESSION_ID) {
Log.w(TAG, "Cannot start external scoring"
+ " netId=" + netId
+ " currentNetId=" + getCurrentNetId()
+ " currentNetCapabilities=" + getCurrentNetCapabilities()
+ " sessionId=" + sessionId);
return;
}
mCurrentWifiConfiguration = mWifiConfigManager.getConfiguredNetwork(
mWifiInfo.getNetworkId());
mWifiInfo.setScore(isPrimary() ? ConnectedScore.WIFI_MAX_SCORE
: ConnectedScore.WIFI_SECONDARY_MAX_SCORE);
mWifiConnectedNetworkScorerHolder.startSession(sessionId, mIsUserSelected);
mWifiInfoNoReset.setBSSID(mWifiInfo.getBSSID());
mWifiInfoNoReset.setSSID(mWifiInfo.getWifiSsid());
mWifiInfoNoReset.setRssi(mWifiInfo.getRssi());
mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS;
mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS;
}
/**
* Stop the registered Wi-Fi connected network scorer.
*/
public void stopConnectedNetworkScorer() {
mNetworkAgent = null;
if (mWifiConnectedNetworkScorerHolder == null) {
return;
}
mWifiConnectedNetworkScorerHolder.stopSession();
long millis = mClock.getWallClockMillis();
// Blocklist the current BSS
if ((mLastScoreBreachLowTimeMillis != INVALID_WALL_CLOCK_MILLIS)
&& ((millis - mLastScoreBreachLowTimeMillis)
>= MIN_TIME_TO_WAIT_BEFORE_BLOCKLIST_BSSID_MILLIS)) {
mWifiBlocklistMonitor.handleBssidConnectionFailure(mWifiInfo.getBSSID(),
mCurrentWifiConfiguration,
WifiBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE,
mWifiInfo.getRssi());
mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS;
}
}
/**
* Set NetworkAgent
*/
public void setNetworkAgent(WifiNetworkAgent agent) {
WifiNetworkAgent oldAgent = mNetworkAgent;
mNetworkAgent = agent;
// if mNetworkAgent was null previously, then the score wasn't sent to ConnectivityService.
// Send it now that the NetworkAgent has been set.
if (oldAgent == null && mNetworkAgent != null) {
sendNetworkScore();
}
}
/** Get cached score */
@VisibleForTesting
@RequiresApi(Build.VERSION_CODES.S)
public NetworkScore getScore() {
return getScoreBuilder().build();
}
@RequiresApi(Build.VERSION_CODES.S)
private NetworkScore.Builder getScoreBuilder() {
// We should force keep connected for a MBB CMM which is not lingering.
boolean shouldForceKeepConnected =
mCurrentRole == ActiveModeManager.ROLE_CLIENT_SECONDARY_TRANSIENT
&& !mShouldReduceNetworkScore;
int keepConnectedReason =
shouldForceKeepConnected
? NetworkScore.KEEP_CONNECTED_FOR_HANDOVER
: NetworkScore.KEEP_CONNECTED_NONE;
boolean exiting = SdkLevel.isAtLeastS() && mWifiConnectedNetworkScorerHolder != null
? !mIsUsable : mLegacyIntScore < ConnectedScore.WIFI_TRANSITION_SCORE;
return new NetworkScore.Builder()
.setLegacyInt(mShouldReduceNetworkScore ? LINGERING_SCORE : mLegacyIntScore)
.setTransportPrimary(mCurrentRole == ActiveModeManager.ROLE_CLIENT_PRIMARY)
.setExiting(exiting)
.setKeepConnectedReason(keepConnectedReason);
}
/** Get legacy int score. */
@VisibleForTesting
public int getLegacyIntScore() {
// When S Wifi module is run on R:
// - mShouldReduceNetworkScore is useless since MBB doesn't exist on R, so there isn't any
// forced lingering.
// - mIsUsable can't be set as notifyStatusUpdate() for external scorer didn't exist on R
// SDK (assume that only R platform + S Wifi module + R external scorer is possible,
// and R platform + S Wifi module + S external scorer is not possible)
// Thus, it's ok to return the raw int score on R.
return mLegacyIntScore;
}
/** Get counts when we voted for a NUD. */
@VisibleForTesting
public int getNudYes() {
return mNudYes;
}
private void revertToDefaultConnectedScorer() {
Log.d(TAG, "Using VelocityBasedConnectedScore");
mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(mScoringParams, mClock);
mWifiConnectedNetworkScorerHolder = null;
mWifiGlobals.setUsingExternalScorer(false);
mExternalScoreUpdateObserverProxy.unregisterCallback(mScoreUpdateObserverCallback);
mWifiMetrics.setIsExternalWifiScorerOn(false);
}
/**
* This is a function of {@link #mCurrentRole} {@link #mShouldReduceNetworkScore}, and
* {@link #mLegacyIntScore}, and should be called when any of them changes.
*/
private void sendNetworkScore() {
if (mNetworkAgent == null) {
return;
}
if (SdkLevel.isAtLeastS()) {
// NetworkScore was introduced in S
mNetworkAgent.sendNetworkScore(getScore());
} else {
mNetworkAgent.sendNetworkScore(getLegacyIntScore());
}
}
/** Called when the owner {@link ConcreteClientModeManager}'s role changes. */
public void onRoleChanged(@Nullable ClientRole role) {
mCurrentRole = role;
if (mAggressiveConnectedScore != null) mAggressiveConnectedScore.onRoleChanged(role);
if (mVelocityBasedConnectedScore != null) mVelocityBasedConnectedScore.onRoleChanged(role);
sendNetworkScore();
}
}