/* * 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 mLinkMetricsHistory = new LinkedList(); /** * 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 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(); } }