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