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.net.Network;
20 import android.net.NetworkAgent;
21 import android.net.wifi.WifiInfo;
22 import android.util.Log;
23 
24 import java.io.FileDescriptor;
25 import java.io.PrintWriter;
26 import java.text.SimpleDateFormat;
27 import java.util.Date;
28 import java.util.LinkedList;
29 import java.util.Locale;
30 
31 /**
32  * Class used to calculate scores for connected wifi networks and report it to the associated
33  * network agent.
34 */
35 public class WifiScoreReport {
36     private static final String TAG = "WifiScoreReport";
37 
38     private static final int DUMPSYS_ENTRY_COUNT_LIMIT = 3600; // 3 hours on 3 second poll
39 
40     private boolean mVerboseLoggingEnabled = false;
41     private static final long FIRST_REASONABLE_WALL_CLOCK = 1490000000000L; // mid-December 2016
42 
43     private static final long MIN_TIME_TO_KEEP_BELOW_TRANSITION_SCORE_MILLIS = 9000;
44     private long mLastDownwardBreachTimeMillis = 0;
45 
46     // Cache of the last score
47     private int mScore = ConnectedScore.WIFI_MAX_SCORE;
48 
49     private final ScoringParams mScoringParams;
50     private final Clock mClock;
51     private int mSessionNumber = 0;
52 
53     ConnectedScore mAggressiveConnectedScore;
54     VelocityBasedConnectedScore mVelocityBasedConnectedScore;
55 
WifiScoreReport(ScoringParams scoringParams, Clock clock)56     WifiScoreReport(ScoringParams scoringParams, Clock clock) {
57         mScoringParams = scoringParams;
58         mClock = clock;
59         mAggressiveConnectedScore = new AggressiveConnectedScore(scoringParams, clock);
60         mVelocityBasedConnectedScore = new VelocityBasedConnectedScore(scoringParams, clock);
61     }
62 
63     /**
64      * Reset the last calculated score.
65      */
reset()66     public void reset() {
67         mSessionNumber++;
68         mScore = ConnectedScore.WIFI_MAX_SCORE;
69         mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
70         mAggressiveConnectedScore.reset();
71         mVelocityBasedConnectedScore.reset();
72         mLastDownwardBreachTimeMillis = 0;
73         if (mVerboseLoggingEnabled) Log.d(TAG, "reset");
74     }
75 
76     /**
77      * Enable/Disable verbose logging in score report generation.
78      */
enableVerboseLogging(boolean enable)79     public void enableVerboseLogging(boolean enable) {
80         mVerboseLoggingEnabled = enable;
81     }
82 
83     /**
84      * Calculate wifi network score based on updated link layer stats and send the score to
85      * the provided network agent.
86      *
87      * If the score has changed from the previous value, update the WifiNetworkAgent.
88      *
89      * Called periodically (POLL_RSSI_INTERVAL_MSECS) about every 3 seconds.
90      *
91      * @param wifiInfo WifiInfo instance pointing to the currently connected network.
92      * @param networkAgent NetworkAgent to be notified of new score.
93      * @param wifiMetrics for reporting our scores.
94      */
calculateAndReportScore(WifiInfo wifiInfo, NetworkAgent networkAgent, WifiMetrics wifiMetrics)95     public void calculateAndReportScore(WifiInfo wifiInfo, NetworkAgent networkAgent,
96                                         WifiMetrics wifiMetrics) {
97         if (wifiInfo.getRssi() == WifiInfo.INVALID_RSSI) {
98             Log.d(TAG, "Not reporting score because RSSI is invalid");
99             return;
100         }
101         int score;
102 
103         long millis = mClock.getWallClockMillis();
104         int netId = 0;
105 
106         if (networkAgent != null) {
107             final Network network = networkAgent.getNetwork();
108             if (network != null) {
109                 netId = network.netId;
110             }
111         }
112 
113         mAggressiveConnectedScore.updateUsingWifiInfo(wifiInfo, millis);
114         mVelocityBasedConnectedScore.updateUsingWifiInfo(wifiInfo, millis);
115 
116         int s1 = mAggressiveConnectedScore.generateScore();
117         int s2 = mVelocityBasedConnectedScore.generateScore();
118 
119         score = s2;
120 
121         if (wifiInfo.score > ConnectedScore.WIFI_TRANSITION_SCORE
122                  && score <= ConnectedScore.WIFI_TRANSITION_SCORE
123                  && wifiInfo.txSuccessRate >= mScoringParams.getYippeeSkippyPacketsPerSecond()
124                  && wifiInfo.rxSuccessRate >= mScoringParams.getYippeeSkippyPacketsPerSecond()) {
125             score = ConnectedScore.WIFI_TRANSITION_SCORE + 1;
126         }
127 
128         if (wifiInfo.score > ConnectedScore.WIFI_TRANSITION_SCORE
129                  && score <= ConnectedScore.WIFI_TRANSITION_SCORE) {
130             // We don't want to trigger a downward breach unless the rssi is
131             // below the entry threshold.  There is noise in the measured rssi, and
132             // the kalman-filtered rssi is affected by the trend, so check them both.
133             // TODO(b/74613347) skip this if there are other indications to support the low score
134             int entry = mScoringParams.getEntryRssi(wifiInfo.getFrequency());
135             if (mVelocityBasedConnectedScore.getFilteredRssi() >= entry
136                     || wifiInfo.getRssi() >= entry) {
137                 // Stay a notch above the transition score to reduce ambiguity.
138                 score = ConnectedScore.WIFI_TRANSITION_SCORE + 1;
139             }
140         }
141 
142         if (wifiInfo.score >= ConnectedScore.WIFI_TRANSITION_SCORE
143                  && score < ConnectedScore.WIFI_TRANSITION_SCORE) {
144             mLastDownwardBreachTimeMillis = millis;
145         } else if (wifiInfo.score < ConnectedScore.WIFI_TRANSITION_SCORE
146                  && score >= ConnectedScore.WIFI_TRANSITION_SCORE) {
147             // Staying at below transition score for a certain period of time
148             // to prevent going back to wifi network again in a short time.
149             long elapsedMillis = millis - mLastDownwardBreachTimeMillis;
150             if (elapsedMillis < MIN_TIME_TO_KEEP_BELOW_TRANSITION_SCORE_MILLIS) {
151                 score = wifiInfo.score;
152             }
153         }
154 
155         //sanitize boundaries
156         if (score > ConnectedScore.WIFI_MAX_SCORE) {
157             score = ConnectedScore.WIFI_MAX_SCORE;
158         }
159         if (score < 0) {
160             score = 0;
161         }
162 
163         logLinkMetrics(wifiInfo, millis, netId, s1, s2, score);
164 
165         //report score
166         if (score != wifiInfo.score) {
167             if (mVerboseLoggingEnabled) {
168                 Log.d(TAG, "report new wifi score " + score);
169             }
170             wifiInfo.score = score;
171             if (networkAgent != null) {
172                 networkAgent.sendNetworkScore(score);
173             }
174         }
175 
176         wifiMetrics.incrementWifiScoreCount(score);
177         mScore = score;
178     }
179 
180     private static final double TIME_CONSTANT_MILLIS = 30.0e+3;
181     private static final long NUD_THROTTLE_MILLIS = 5000;
182     private long mLastKnownNudCheckTimeMillis = 0;
183     private int mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
184     private int mNudYes = 0;    // Counts when we voted for a NUD
185     private int mNudCount = 0;  // Counts when we were told a NUD was sent
186 
187     /**
188      * Recommends that a layer 3 check be done
189      *
190      * The caller can use this to (help) decide that an IP reachability check
191      * is desirable. The check is not done here; that is the caller's responsibility.
192      *
193      * @return true to indicate that an IP reachability check is recommended
194      */
shouldCheckIpLayer()195     public boolean shouldCheckIpLayer() {
196         int nud = mScoringParams.getNudKnob();
197         if (nud == 0) {
198             return false;
199         }
200         long millis = mClock.getWallClockMillis();
201         long deltaMillis = millis - mLastKnownNudCheckTimeMillis;
202         // Don't ever ask back-to-back - allow at least 5 seconds
203         // for the previous one to finish.
204         if (deltaMillis < NUD_THROTTLE_MILLIS) {
205             return false;
206         }
207         // nud is between 1 and 10 at this point
208         double deltaLevel = 11 - nud;
209         // nextNudBreach is the bar the score needs to cross before we ask for NUD
210         double nextNudBreach = ConnectedScore.WIFI_TRANSITION_SCORE;
211         // If we were below threshold the last time we checked, then compute a new bar
212         // that starts down from there and decays exponentially back up to the steady-state
213         // bar. If 5 time constants have passed, we are 99% of the way there, so skip the math.
214         if (mLastKnownNudCheckScore < ConnectedScore.WIFI_TRANSITION_SCORE
215                 && deltaMillis < 5.0 * TIME_CONSTANT_MILLIS) {
216             double a = Math.exp(-deltaMillis / TIME_CONSTANT_MILLIS);
217             nextNudBreach = a * (mLastKnownNudCheckScore - deltaLevel) + (1.0 - a) * nextNudBreach;
218         }
219         if (mScore >= nextNudBreach) {
220             return false;
221         }
222         mNudYes++;
223         return true;
224     }
225 
226     /**
227      * Should be called when a reachability check has been issued
228      *
229      * When the caller has requested an IP reachability check, calling this will
230      * help to rate-limit requests via shouldCheckIpLayer()
231      */
noteIpCheck()232     public void noteIpCheck() {
233         long millis = mClock.getWallClockMillis();
234         mLastKnownNudCheckTimeMillis = millis;
235         mLastKnownNudCheckScore = mScore;
236         mNudCount++;
237     }
238 
239     /**
240      * Data for dumpsys
241      *
242      * These are stored as csv formatted lines
243      */
244     private LinkedList<String> mLinkMetricsHistory = new LinkedList<String>();
245 
246     /**
247      * Data logging for dumpsys
248      */
logLinkMetrics(WifiInfo wifiInfo, long now, int netId, int s1, int s2, int score)249     private void logLinkMetrics(WifiInfo wifiInfo, long now, int netId,
250                                 int s1, int s2, int score) {
251         if (now < FIRST_REASONABLE_WALL_CLOCK) return;
252         double rssi = wifiInfo.getRssi();
253         double filteredRssi = mVelocityBasedConnectedScore.getFilteredRssi();
254         double rssiThreshold = mVelocityBasedConnectedScore.getAdjustedRssiThreshold();
255         int freq = wifiInfo.getFrequency();
256         int txLinkSpeed = wifiInfo.getLinkSpeed();
257         int rxLinkSpeed = wifiInfo.getRxLinkSpeedMbps();
258         double txSuccessRate = wifiInfo.txSuccessRate;
259         double txRetriesRate = wifiInfo.txRetriesRate;
260         double txBadRate = wifiInfo.txBadRate;
261         double rxSuccessRate = wifiInfo.rxSuccessRate;
262         String s;
263         try {
264             String timestamp = new SimpleDateFormat("MM-dd HH:mm:ss.SSS").format(new Date(now));
265             s = String.format(Locale.US, // Use US to avoid comma/decimal confusion
266                     "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d",
267                     timestamp, mSessionNumber, netId,
268                     rssi, filteredRssi, rssiThreshold, freq, txLinkSpeed, rxLinkSpeed,
269                     txSuccessRate, txRetriesRate, txBadRate, rxSuccessRate,
270                     mNudYes, mNudCount,
271                     s1, s2, score);
272         } catch (Exception e) {
273             Log.e(TAG, "format problem", e);
274             return;
275         }
276         synchronized (mLinkMetricsHistory) {
277             mLinkMetricsHistory.add(s);
278             while (mLinkMetricsHistory.size() > DUMPSYS_ENTRY_COUNT_LIMIT) {
279                 mLinkMetricsHistory.removeFirst();
280             }
281         }
282     }
283 
284     /**
285      * Tag to be used in dumpsys request
286      */
287     public static final String DUMP_ARG = "WifiScoreReport";
288 
289     /**
290      * Dump logged signal strength and traffic measurements.
291      * @param fd unused
292      * @param pw PrintWriter for writing dump to
293      * @param args unused
294      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)295     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
296         LinkedList<String> history;
297         synchronized (mLinkMetricsHistory) {
298             history = new LinkedList<>(mLinkMetricsHistory);
299         }
300         pw.println("time,session,netid,rssi,filtered_rssi,rssi_threshold, freq,txLinkSpeed,"
301                 + "rxLinkSpeed,tx_good,tx_retry,tx_bad,rx_pps,nudrq,nuds,s1,s2,score");
302         for (String line : history) {
303             pw.println(line);
304         }
305         history.clear();
306     }
307 }
308