| /* |
| * 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 android.content.Context; |
| import android.database.ContentObserver; |
| import android.net.Network; |
| import android.net.NetworkAgent; |
| import android.net.Uri; |
| import android.net.wifi.IScoreUpdateObserver; |
| import android.net.wifi.IWifiConnectedNetworkScorer; |
| import android.net.wifi.WifiInfo; |
| import android.net.wifi.nl80211.WifiNl80211Manager; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.wifi.resources.R; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.LinkedList; |
| import java.util.Locale; |
| |
| /** |
| * 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; |
| |
| /** |
| * Copy of the settings string. Can't directly use the constant because it is @hide. |
| * See {@link android.provider.Settings.Secure.ADAPTIVE_CONNECTIVITY_ENABLED}. |
| * TODO(b/167709538) remove this hardcoded string and create new API in Wifi mainline. |
| */ |
| @VisibleForTesting |
| public static final String SETTINGS_SECURE_ADAPTIVE_CONNECTIVITY_ENABLED = |
| "adaptive_connectivity_enabled"; |
| |
| // Cache of the last score |
| private int mScore = ConnectedScore.WIFI_MAX_SCORE; |
| |
| private final ScoringParams mScoringParams; |
| private final Clock mClock; |
| private int mSessionNumber = 0; // not to be confused with sessionid, this just counts resets |
| private String mInterfaceName; |
| private final BssidBlocklistMonitor mBssidBlocklistMonitor; |
| private final Context mContext; |
| private long mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| private long mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| |
| ConnectedScore mAggressiveConnectedScore; |
| VelocityBasedConnectedScore mVelocityBasedConnectedScore; |
| |
| NetworkAgent mNetworkAgent; |
| WifiMetrics mWifiMetrics; |
| WifiInfo mWifiInfo; |
| WifiNative mWifiNative; |
| WifiThreadRunner mWifiThreadRunner; |
| DeviceConfigFacade mDeviceConfigFacade; |
| Handler mHandler; |
| FrameworkFacade mFrameworkFacade; |
| |
| /** |
| * Callback proxy. See {@link android.net.wifi.WifiManager.ScoreUpdateObserver}. |
| */ |
| private class ScoreUpdateObserverProxy extends IScoreUpdateObserver.Stub { |
| @Override |
| public void notifyScoreUpdate(int sessionId, int score) { |
| mWifiThreadRunner.post(() -> { |
| 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 (score < ConnectedScore.WIFI_TRANSITION_SCORE) { |
| if (mScore >= ConnectedScore.WIFI_TRANSITION_SCORE) { |
| mLastScoreBreachLowTimeMillis = millis; |
| } |
| } else { |
| mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| } |
| if (score > ConnectedScore.WIFI_TRANSITION_SCORE) { |
| if (mScore <= ConnectedScore.WIFI_TRANSITION_SCORE) { |
| mLastScoreBreachHighTimeMillis = millis; |
| } |
| } else { |
| mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| } |
| reportNetworkScoreToConnectivityServiceIfNecessary(score); |
| mScore = score; |
| updateWifiMetrics(millis, -1, mScore); |
| }); |
| } |
| |
| @Override |
| public void triggerUpdateOfWifiUsabilityStats(int sessionId) { |
| mWifiThreadRunner.post(() -> { |
| 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(mWifiInfo, stats); |
| }); |
| } |
| } |
| |
| /** |
| * Report network score to connectivity service. |
| */ |
| private void reportNetworkScoreToConnectivityServiceIfNecessary(int score) { |
| if (mNetworkAgent == null) { |
| return; |
| } |
| if (mWifiConnectedNetworkScorerHolder == null && score == mWifiInfo.getScore()) { |
| return; |
| } |
| if (mWifiConnectedNetworkScorerHolder != null |
| && mContext.getResources().getBoolean( |
| R.bool.config_wifiMinConfirmationDurationSendNetworkScoreEnabled)) { |
| 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 (!mAdaptiveConnectivityEnabled) { |
| score = ConnectedScore.WIFI_TRANSITION_SCORE + 1; |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, |
| "Adaptive connectivity disabled - Stay a notch above the transition score"); |
| } |
| } |
| mNetworkAgent.sendNetworkScore(score); |
| } |
| |
| /** |
| * 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; |
| |
| 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) { |
| 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; |
| try { |
| mScorer.onStart(sessionId); |
| } 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; |
| try { |
| mScorer.onStop(sessionId); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to stop Wifi connected network scorer " + this, e); |
| revertToDefaultConnectedScorer(); |
| } |
| } |
| } |
| |
| private final ScoreUpdateObserverProxy mScoreUpdateObserver = |
| new ScoreUpdateObserverProxy(); |
| |
| private WifiConnectedNetworkScorerHolder mWifiConnectedNetworkScorerHolder; |
| |
| /** |
| * Observer for adaptive connectivity enable settings changes. |
| * This is enabled by default. Will be toggled off via adb command or a settings |
| * toggle by the user to disable adaptive connectivity. |
| */ |
| private class AdaptiveConnectivityEnabledSettingObserver extends ContentObserver { |
| AdaptiveConnectivityEnabledSettingObserver(Handler handler) { |
| super(handler); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| super.onChange(selfChange); |
| mAdaptiveConnectivityEnabled = getValue(); |
| Log.d(TAG, "Adaptive connectivity status changed: " + mAdaptiveConnectivityEnabled); |
| mWifiMetrics.setAdaptiveConnectivityState(mAdaptiveConnectivityEnabled); |
| mWifiMetrics.logUserActionEvent( |
| mWifiMetrics.convertAdaptiveConnectivityStateToUserActionEventType( |
| mAdaptiveConnectivityEnabled)); |
| } |
| |
| /** |
| * Register settings change observer. |
| */ |
| public void initialize() { |
| Uri uri = Settings.Secure.getUriFor(SETTINGS_SECURE_ADAPTIVE_CONNECTIVITY_ENABLED); |
| if (uri == null) { |
| Log.e(TAG, "Adaptive connectivity user toggle does not exist in Settings"); |
| return; |
| } |
| mFrameworkFacade.registerContentObserver(mContext, uri, true, this); |
| mAdaptiveConnectivityEnabled = mAdaptiveConnectivityEnabledSettingObserver.getValue(); |
| mWifiMetrics.setAdaptiveConnectivityState(mAdaptiveConnectivityEnabled); |
| } |
| |
| public boolean getValue() { |
| return mFrameworkFacade.getSecureIntegerSetting( |
| mContext, SETTINGS_SECURE_ADAPTIVE_CONNECTIVITY_ENABLED, 1) == 1; |
| } |
| } |
| |
| private final AdaptiveConnectivityEnabledSettingObserver |
| mAdaptiveConnectivityEnabledSettingObserver; |
| private boolean mAdaptiveConnectivityEnabled = true; |
| |
| WifiScoreReport(ScoringParams scoringParams, Clock clock, WifiMetrics wifiMetrics, |
| WifiInfo wifiInfo, WifiNative wifiNative, BssidBlocklistMonitor bssidBlocklistMonitor, |
| WifiThreadRunner wifiThreadRunner, DeviceConfigFacade deviceConfigFacade, |
| Context context, Looper looper, FrameworkFacade frameworkFacade) { |
| mScoringParams = scoringParams; |
| mClock = clock; |
| mAggressiveConnectedScore = new AggressiveConnectedScore(scoringParams, clock); |
| mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(scoringParams, clock); |
| mWifiMetrics = wifiMetrics; |
| mWifiInfo = wifiInfo; |
| mWifiNative = wifiNative; |
| mBssidBlocklistMonitor = bssidBlocklistMonitor; |
| mWifiThreadRunner = wifiThreadRunner; |
| mDeviceConfigFacade = deviceConfigFacade; |
| mContext = context; |
| mFrameworkFacade = frameworkFacade; |
| mHandler = new Handler(looper); |
| mAdaptiveConnectivityEnabledSettingObserver = |
| new AdaptiveConnectivityEnabledSettingObserver(mHandler); |
| } |
| |
| /** |
| * Reset the last calculated score. |
| */ |
| public void reset() { |
| mSessionNumber++; |
| mScore = ConnectedScore.WIFI_MAX_SCORE; |
| mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE; |
| mAggressiveConnectedScore.reset(); |
| if (mVelocityBasedConnectedScore != null) { |
| mVelocityBasedConnectedScore.reset(); |
| } |
| mLastDownwardBreachTimeMillis = 0; |
| mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| mLastScoreBreachHighTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| 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; |
| |
| if (mWifiInfo.getScore() > ConnectedScore.WIFI_TRANSITION_SCORE |
| && score <= ConnectedScore.WIFI_TRANSITION_SCORE |
| && mWifiInfo.getSuccessfulTxPacketsPerSecond() |
| >= mScoringParams.getYippeeSkippyPacketsPerSecond() |
| && mWifiInfo.getSuccessfulRxPacketsPerSecond() |
| >= mScoringParams.getYippeeSkippyPacketsPerSecond() |
| ) { |
| score = ConnectedScore.WIFI_TRANSITION_SCORE + 1; |
| } |
| |
| if (mWifiInfo.getScore() > ConnectedScore.WIFI_TRANSITION_SCORE |
| && score <= ConnectedScore.WIFI_TRANSITION_SCORE) { |
| // 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 = ConnectedScore.WIFI_TRANSITION_SCORE + 1; |
| } |
| } |
| |
| if (mWifiInfo.getScore() >= ConnectedScore.WIFI_TRANSITION_SCORE |
| && score < ConnectedScore.WIFI_TRANSITION_SCORE) { |
| mLastDownwardBreachTimeMillis = millis; |
| } else if (mWifiInfo.getScore() < ConnectedScore.WIFI_TRANSITION_SCORE |
| && score >= ConnectedScore.WIFI_TRANSITION_SCORE) { |
| // 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 > ConnectedScore.WIFI_MAX_SCORE) { |
| score = ConnectedScore.WIFI_MAX_SCORE; |
| } |
| if (score < 0) { |
| score = 0; |
| } |
| |
| //report score |
| reportNetworkScoreToConnectivityServiceIfNecessary(score); |
| updateWifiMetrics(millis, s2, score); |
| mScore = score; |
| } |
| |
| private int getCurrentNetId() { |
| int netId = 0; |
| if (mNetworkAgent != null) { |
| final Network network = mNetworkAgent.getNetwork(); |
| if (network != null) { |
| netId = network.getNetId(); |
| } |
| } |
| return netId; |
| } |
| |
| 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 score) { |
| int netId = getCurrentNetId(); |
| |
| mAggressiveConnectedScore.updateUsingWifiInfo(mWifiInfo, now); |
| int s1 = mAggressiveConnectedScore.generateScore(); |
| logLinkMetrics(now, netId, s1, s2, score); |
| |
| if (score != mWifiInfo.getScore()) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "report new wifi score " + score); |
| } |
| mWifiInfo.setScore(score); |
| } |
| mWifiMetrics.incrementWifiScoreCount(score); |
| } |
| |
| 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 (!mAdaptiveConnectivityEnabled) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "Adaptive connectivity disabled - Don't check IP layer"); |
| } |
| return false; |
| } |
| int nud = mScoringParams.getNudKnob(); |
| if (nud == 0) { |
| 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; |
| } |
| // 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 (mScore >= 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 = mScore; |
| mNudCount++; |
| } |
| |
| /** |
| * 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 rssi = mWifiInfo.getRssi(); |
| 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(); |
| double txSuccessRate = mWifiInfo.getSuccessfulTxPacketsPerSecond(); |
| double txRetriesRate = mWifiInfo.getRetriedTxPacketsPerSecond(); |
| double txBadRate = mWifiInfo.getLostTxPacketsPerSecond(); |
| double rxSuccessRate = mWifiInfo.getSuccessfulRxPacketsPerSecond(); |
| String s; |
| try { |
| String timestamp = new SimpleDateFormat("MM-dd HH:mm:ss.SSS").format(new Date(now)); |
| s = String.format(Locale.US, // Use US to avoid comma/decimal confusion |
| "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d", |
| timestamp, mSessionNumber, netId, |
| rssi, filteredRssi, rssiThreshold, freq, txLinkSpeed, rxLinkSpeed, |
| txSuccessRate, txRetriesRate, txBadRate, rxSuccessRate, |
| 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,tx_good,tx_retry,tx_bad,rx_pps,nudrq,nuds,s1,s2,score"); |
| for (String line : history) { |
| pw.println(line); |
| } |
| history.clear(); |
| } |
| |
| /** |
| * 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; |
| |
| try { |
| scorer.onSetScoreUpdateObserver(mScoreUpdateObserver); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to set score update observer " + scorer, e); |
| revertToDefaultConnectedScorer(); |
| return false; |
| } |
| // 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) { |
| startConnectedNetworkScorer(netId); |
| } |
| return true; |
| } |
| |
| /** |
| * Clear an existing scorer for Wi-Fi connected network score handling. |
| */ |
| public void clearWifiConnectedNetworkScorer() { |
| if (mWifiConnectedNetworkScorerHolder == null) { |
| return; |
| } |
| mWifiConnectedNetworkScorerHolder.reset(); |
| revertToDefaultConnectedScorer(); |
| } |
| |
| /** |
| * Start the registered Wi-Fi connected network scorer. |
| * @param netId identifies the current android.net.Network |
| */ |
| public void startConnectedNetworkScorer(int netId) { |
| final int sessionId = getCurrentSessionId(); |
| if (mWifiConnectedNetworkScorerHolder == null |
| || netId != getCurrentNetId() |
| || sessionId == INVALID_SESSION_ID) { |
| Log.w(TAG, "Cannot start external scoring" |
| + " netId=" + netId |
| + " currentNetId=" + getCurrentNetId() |
| + " sessionId=" + sessionId); |
| return; |
| } |
| mWifiInfo.setScore(ConnectedScore.WIFI_MAX_SCORE); |
| mWifiConnectedNetworkScorerHolder.startSession(sessionId); |
| 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)) { |
| mBssidBlocklistMonitor.handleBssidConnectionFailure(mWifiInfo.getBSSID(), |
| mWifiInfo.getSSID(), |
| BssidBlocklistMonitor.REASON_FRAMEWORK_DISCONNECT_CONNECTED_SCORE, |
| mWifiInfo.getRssi()); |
| mLastScoreBreachLowTimeMillis = INVALID_WALL_CLOCK_MILLIS; |
| } |
| } |
| |
| /** |
| * Set NetworkAgent |
| */ |
| public void setNetworkAgent(NetworkAgent agent) { |
| mNetworkAgent = agent; |
| } |
| |
| /** |
| * Get cached score |
| */ |
| public int getScore() { |
| return mScore; |
| } |
| |
| /** |
| * Set interface name |
| * @param ifaceName |
| */ |
| public void setInterfaceName(String ifaceName) { |
| mInterfaceName = ifaceName; |
| } |
| |
| private void revertToDefaultConnectedScorer() { |
| Log.d(TAG, "Using VelocityBasedConnectedScore"); |
| mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(mScoringParams, mClock); |
| mWifiConnectedNetworkScorerHolder = null; |
| mWifiMetrics.setIsExternalWifiScorerOn(false); |
| } |
| |
| /** |
| * Initialize WifiScoreReport |
| */ |
| public void initialize() { |
| mAdaptiveConnectivityEnabledSettingObserver.initialize(); |
| } |
| } |