| /* |
| * 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.net.NetworkAgent; |
| import android.net.wifi.WifiInfo; |
| import android.util.Log; |
| |
| 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; |
| |
| // Cache of the last score |
| private int mScore = NetworkAgent.WIFI_BASE_SCORE; |
| |
| private final ScoringParams mScoringParams; |
| private final Clock mClock; |
| private int mSessionNumber = 0; |
| |
| ConnectedScore mAggressiveConnectedScore; |
| VelocityBasedConnectedScore mVelocityBasedConnectedScore; |
| |
| WifiScoreReport(ScoringParams scoringParams, Clock clock) { |
| mScoringParams = scoringParams; |
| mClock = clock; |
| mAggressiveConnectedScore = new AggressiveConnectedScore(scoringParams, clock); |
| mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(scoringParams, clock); |
| } |
| |
| /** |
| * Reset the last calculated score. |
| */ |
| public void reset() { |
| mSessionNumber++; |
| mScore = NetworkAgent.WIFI_BASE_SCORE; |
| mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE; |
| mAggressiveConnectedScore.reset(); |
| mVelocityBasedConnectedScore.reset(); |
| mLastDownwardBreachTimeMillis = 0; |
| 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 provided network agent. |
| * |
| * If the score has changed from the previous value, update the WifiNetworkAgent. |
| * |
| * Called periodically (POLL_RSSI_INTERVAL_MSECS) about every 3 seconds. |
| * |
| * @param wifiInfo WifiInfo instance pointing to the currently connected network. |
| * @param networkAgent NetworkAgent to be notified of new score. |
| * @param wifiMetrics for reporting our scores. |
| */ |
| public void calculateAndReportScore(WifiInfo wifiInfo, NetworkAgent networkAgent, |
| WifiMetrics wifiMetrics) { |
| if (wifiInfo.getRssi() == WifiInfo.INVALID_RSSI) { |
| Log.d(TAG, "Not reporting score because RSSI is invalid"); |
| return; |
| } |
| int score; |
| |
| long millis = mClock.getWallClockMillis(); |
| int netId = 0; |
| |
| if (networkAgent != null) { |
| netId = networkAgent.netId; |
| } |
| |
| mAggressiveConnectedScore.updateUsingWifiInfo(wifiInfo, millis); |
| mVelocityBasedConnectedScore.updateUsingWifiInfo(wifiInfo, millis); |
| |
| int s1 = mAggressiveConnectedScore.generateScore(); |
| int s2 = mVelocityBasedConnectedScore.generateScore(); |
| |
| score = s2; |
| |
| if (wifiInfo.score > ConnectedScore.WIFI_TRANSITION_SCORE |
| && score <= ConnectedScore.WIFI_TRANSITION_SCORE |
| && wifiInfo.txSuccessRate >= mScoringParams.getYippeeSkippyPacketsPerSecond() |
| && wifiInfo.rxSuccessRate >= mScoringParams.getYippeeSkippyPacketsPerSecond()) { |
| score = ConnectedScore.WIFI_TRANSITION_SCORE + 1; |
| } |
| |
| if (wifiInfo.score > 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(wifiInfo.getFrequency()); |
| if (mVelocityBasedConnectedScore.getFilteredRssi() >= entry |
| || wifiInfo.getRssi() >= entry) { |
| // Stay a notch above the transition score to reduce ambiguity. |
| score = ConnectedScore.WIFI_TRANSITION_SCORE + 1; |
| } |
| } |
| |
| if (wifiInfo.score >= ConnectedScore.WIFI_TRANSITION_SCORE |
| && score < ConnectedScore.WIFI_TRANSITION_SCORE) { |
| mLastDownwardBreachTimeMillis = millis; |
| } else if (wifiInfo.score < 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 = wifiInfo.score; |
| } |
| } |
| |
| //sanitize boundaries |
| if (score > NetworkAgent.WIFI_BASE_SCORE) { |
| score = NetworkAgent.WIFI_BASE_SCORE; |
| } |
| if (score < 0) { |
| score = 0; |
| } |
| |
| logLinkMetrics(wifiInfo, millis, netId, s1, s2, score); |
| |
| //report score |
| if (score != wifiInfo.score) { |
| if (mVerboseLoggingEnabled) { |
| Log.d(TAG, "report new wifi score " + score); |
| } |
| wifiInfo.score = score; |
| if (networkAgent != null) { |
| networkAgent.sendNetworkScore(score); |
| } |
| } |
| |
| wifiMetrics.incrementWifiScoreCount(score); |
| mScore = 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() { |
| 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; |
| } |
| // nud is between 1 and 10 at this point |
| double deltaLevel = 11 - nud; |
| // nextNudBreach is the bar the score needs to cross before we ask for NUD |
| double nextNudBreach = ConnectedScore.WIFI_TRANSITION_SCORE; |
| // 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(WifiInfo wifiInfo, long now, int netId, |
| int s1, int s2, int score) { |
| if (now < FIRST_REASONABLE_WALL_CLOCK) return; |
| double rssi = wifiInfo.getRssi(); |
| double filteredRssi = mVelocityBasedConnectedScore.getFilteredRssi(); |
| double rssiThreshold = mVelocityBasedConnectedScore.getAdjustedRssiThreshold(); |
| int freq = wifiInfo.getFrequency(); |
| int txLinkSpeed = wifiInfo.getLinkSpeed(); |
| int rxLinkSpeed = wifiInfo.getRxLinkSpeedMbps(); |
| double txSuccessRate = wifiInfo.txSuccessRate; |
| double txRetriesRate = wifiInfo.txRetriesRate; |
| double txBadRate = wifiInfo.txBadRate; |
| double rxSuccessRate = wifiInfo.rxSuccessRate; |
| 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(); |
| } |
| } |