WifiScoreReport.java revision 279abf6c5af5d42f6deb8e8738c7f030521976ef
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
17package com.android.server.wifi;
18
19import android.content.Context;
20import android.net.NetworkAgent;
21import android.net.wifi.WifiConfiguration;
22import android.net.wifi.WifiInfo;
23import android.util.Log;
24
25import com.android.internal.R;
26
27import java.io.FileDescriptor;
28import java.io.PrintWriter;
29import java.text.SimpleDateFormat;
30import java.util.Date;
31import java.util.LinkedList;
32import java.util.Locale;
33
34/**
35 * Class used to calculate scores for connected wifi networks and report it to the associated
36 * network agent.
37*/
38public class WifiScoreReport {
39    private static final String TAG = "WifiScoreReport";
40
41    private static final int STARTING_SCORE = 56;
42
43    private static final int SCAN_CACHE_VISIBILITY_MS = 12000;
44    private static final int HOME_VISIBLE_NETWORK_MAX_COUNT = 6;
45    private static final int SCAN_CACHE_COUNT_PENALTY = 2;
46    private static final int AGGRESSIVE_HANDOVER_PENALTY = 6;
47    private static final int MAX_SUCCESS_RATE_OF_STUCK_LINK = 3; // proportional to packets per sec
48    private static final int MAX_STUCK_LINK_COUNT = 5;
49    private static final int MAX_BAD_RSSI_COUNT = 7;
50    private static final int BAD_RSSI_COUNT_PENALTY = 2;
51    private static final int MAX_LOW_RSSI_COUNT = 1;
52    private static final double MIN_TX_FAILURE_RATE_FOR_WORKING_LINK = 0.3;
53    private static final int MIN_SUSTAINED_LINK_STUCK_COUNT = 1;
54    private static final int LINK_STUCK_PENALTY = 2;
55    private static final int BAD_LINKSPEED_PENALTY = 4;
56    private static final int GOOD_LINKSPEED_BONUS = 4;
57    private static final int DUMPSYS_ENTRY_COUNT_LIMIT = 14400; // 12 hours on 3 second poll
58
59    // Device configs. The values are examples.
60    private final int mThresholdMinimumRssi5;      // -82
61    private final int mThresholdQualifiedRssi5;    // -70
62    private final int mThresholdSaturatedRssi5;    // -57
63    private final int mThresholdMinimumRssi24;     // -85
64    private final int mThresholdQualifiedRssi24;   // -73
65    private final int mThresholdSaturatedRssi24;   // -60
66    private final int mBadLinkSpeed24;             //  6 Mbps
67    private final int mBadLinkSpeed5;              // 12 Mbps
68    private final int mGoodLinkSpeed24;            // 24 Mbps
69    private final int mGoodLinkSpeed5;             // 36 Mbps
70
71    private final WifiConfigManager mWifiConfigManager;
72    private boolean mVerboseLoggingEnabled = false;
73
74    // Cache of the last score report.
75    private String mReport;
76    private boolean mReportValid = false;
77
78    // State set by updateScoringState
79    private boolean mMultiBandScanResults;
80    private boolean mIsHomeNetwork;
81
82    private final Clock mClock;
83    private int mSessionNumber = 0;
84
85    WifiScoreReport(Context context, WifiConfigManager wifiConfigManager, Clock clock) {
86        // Fetch all the device configs.
87        mThresholdMinimumRssi5 = context.getResources().getInteger(
88                R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
89        mThresholdQualifiedRssi5 = context.getResources().getInteger(
90                R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
91        mThresholdSaturatedRssi5 = context.getResources().getInteger(
92                R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_5GHz);
93        mThresholdMinimumRssi24 = context.getResources().getInteger(
94                R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
95        mThresholdQualifiedRssi24 = context.getResources().getInteger(
96                R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
97        mThresholdSaturatedRssi24 = context.getResources().getInteger(
98                R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_24GHz);
99        mBadLinkSpeed24 = context.getResources().getInteger(
100                R.integer.config_wifi_framework_wifi_score_bad_link_speed_24);
101        mBadLinkSpeed5 = context.getResources().getInteger(
102                R.integer.config_wifi_framework_wifi_score_bad_link_speed_5);
103        mGoodLinkSpeed24 = context.getResources().getInteger(
104                R.integer.config_wifi_framework_wifi_score_good_link_speed_24);
105        mGoodLinkSpeed5 = context.getResources().getInteger(
106                R.integer.config_wifi_framework_wifi_score_good_link_speed_5);
107
108        mWifiConfigManager = wifiConfigManager;
109        mClock = clock;
110    }
111
112    /**
113     * Method returning the String representation of the last score report.
114     *
115     *  @return String score report
116     */
117    public String getLastReport() {
118        return mReport;
119    }
120
121    /**
122     * Reset the last calculated score.
123     */
124    public void reset() {
125        mReport = "";
126        mReportValid = false;
127        mSessionNumber++;
128        if (mVerboseLoggingEnabled) Log.d(TAG, "reset");
129    }
130
131    /**
132     * Checks if the last report data is valid or not. This will be cleared when {@link #reset()} is
133     * invoked.
134     *
135     * @return true if valid, false otherwise.
136     */
137    public boolean isLastReportValid() {
138        return mReportValid;
139    }
140
141    /**
142     * Enable/Disable verbose logging in score report generation.
143     */
144    public void enableVerboseLogging(boolean enable) {
145        mVerboseLoggingEnabled = enable;
146    }
147
148    /**
149     * Calculate wifi network score based on updated link layer stats and send the score to
150     * the provided network agent.
151     *
152     * If the score has changed from the previous value, update the WifiNetworkAgent.
153     *
154     * Called periodically (POLL_RSSI_INTERVAL_MSECS) about every 3 seconds.
155     *
156     * @param wifiInfo WifiInfo instance pointing to the currently connected network.
157     * @param networkAgent NetworkAgent to be notified of new score.
158     * @param aggressiveHandover int current aggressiveHandover setting.
159     * @param wifiMetrics for reporting our scores.
160     */
161    public void calculateAndReportScore(WifiInfo wifiInfo, NetworkAgent networkAgent,
162                                        int aggressiveHandover, WifiMetrics wifiMetrics) {
163        int score;
164
165        logLinkMetrics(wifiInfo);
166        if (aggressiveHandover == 0) {
167            // Use the old method
168            updateScoringState(wifiInfo, aggressiveHandover);
169            score = calculateScore(wifiInfo, aggressiveHandover);
170        } else {
171            score = calculateAlternativeScore(wifiInfo);
172        }
173
174        //sanitize boundaries
175        if (score > NetworkAgent.WIFI_BASE_SCORE) {
176            score = NetworkAgent.WIFI_BASE_SCORE;
177        }
178        if (score < 0) {
179            score = 0;
180        }
181
182        //report score
183        if (score != wifiInfo.score) {
184            if (mVerboseLoggingEnabled) {
185                Log.d(TAG, " report new wifi score " + score);
186            }
187            wifiInfo.score = score;
188            if (networkAgent != null) {
189                networkAgent.sendNetworkScore(score);
190            }
191        }
192
193        mReport = String.format(Locale.US, " score=%d", score);
194        mReportValid = true;
195        wifiMetrics.incrementWifiScoreCount(score);
196    }
197
198    /**
199     * Updates the state.
200     */
201    private void updateScoringState(WifiInfo wifiInfo, int aggressiveHandover) {
202        mMultiBandScanResults = multiBandScanResults(wifiInfo);
203        mIsHomeNetwork = isHomeNetwork(wifiInfo);
204
205        int rssiThreshBad = mThresholdMinimumRssi24;
206        int rssiThreshLow = mThresholdQualifiedRssi24;
207
208        if (wifiInfo.is5GHz() && !mMultiBandScanResults) {
209            rssiThreshBad = mThresholdMinimumRssi5;
210            rssiThreshLow = mThresholdQualifiedRssi5;
211        }
212
213        int rssi =  wifiInfo.getRssi();
214        if (aggressiveHandover != 0) {
215            rssi -= AGGRESSIVE_HANDOVER_PENALTY * aggressiveHandover;
216        }
217        if (mIsHomeNetwork) {
218            rssi += WifiConfiguration.HOME_NETWORK_RSSI_BOOST;
219        }
220
221        if ((wifiInfo.txBadRate >= 1)
222                && (wifiInfo.txSuccessRate < MAX_SUCCESS_RATE_OF_STUCK_LINK)
223                && rssi < rssiThreshLow) {
224            // Link is stuck
225            if (wifiInfo.linkStuckCount < MAX_STUCK_LINK_COUNT) {
226                wifiInfo.linkStuckCount += 1;
227            }
228        } else if (wifiInfo.txBadRate < MIN_TX_FAILURE_RATE_FOR_WORKING_LINK) {
229            if (wifiInfo.linkStuckCount > 0) {
230                wifiInfo.linkStuckCount -= 1;
231            }
232        }
233
234        if (rssi < rssiThreshBad) {
235            if (wifiInfo.badRssiCount < MAX_BAD_RSSI_COUNT) {
236                wifiInfo.badRssiCount += 1;
237            }
238        } else if (rssi < rssiThreshLow) {
239            wifiInfo.lowRssiCount = MAX_LOW_RSSI_COUNT; // Dont increment the lowRssi count above 1
240            if (wifiInfo.badRssiCount > 0) {
241                // Decrement bad Rssi count
242                wifiInfo.badRssiCount -= 1;
243            }
244        } else {
245            wifiInfo.badRssiCount = 0;
246            wifiInfo.lowRssiCount = 0;
247        }
248
249    }
250
251    /**
252     * Calculates the score, without all the cruft.
253     */
254    private int calculateScore(WifiInfo wifiInfo, int aggressiveHandover) {
255        int score = STARTING_SCORE;
256
257        int rssiThreshSaturated = mThresholdSaturatedRssi24;
258        int linkspeedThreshBad = mBadLinkSpeed24;
259        int linkspeedThreshGood = mGoodLinkSpeed24;
260
261        if (wifiInfo.is5GHz()) {
262            if (!mMultiBandScanResults) {
263                rssiThreshSaturated = mThresholdSaturatedRssi5;
264            }
265            linkspeedThreshBad = mBadLinkSpeed5;
266            linkspeedThreshGood = mGoodLinkSpeed5;
267        }
268
269        int rssi =  wifiInfo.getRssi();
270        if (aggressiveHandover != 0) {
271            rssi -= AGGRESSIVE_HANDOVER_PENALTY * aggressiveHandover;
272        }
273        if (mIsHomeNetwork) {
274            rssi += WifiConfiguration.HOME_NETWORK_RSSI_BOOST;
275        }
276
277        int linkSpeed = wifiInfo.getLinkSpeed();
278
279        if (wifiInfo.linkStuckCount > MIN_SUSTAINED_LINK_STUCK_COUNT) {
280            // Once link gets stuck for more than 3 seconds, start reducing the score
281            score = score - LINK_STUCK_PENALTY * (wifiInfo.linkStuckCount - 1);
282        }
283
284        if (linkSpeed < linkspeedThreshBad) {
285            score -= BAD_LINKSPEED_PENALTY;
286        } else if ((linkSpeed >= linkspeedThreshGood) && (wifiInfo.txSuccessRate > 5)) {
287            score += GOOD_LINKSPEED_BONUS; // So as bad rssi alone doesn't kill us
288        }
289
290        score -= wifiInfo.badRssiCount * BAD_RSSI_COUNT_PENALTY + wifiInfo.lowRssiCount;
291
292        if (rssi >= rssiThreshSaturated) score += 5;
293
294        if (score > NetworkAgent.WIFI_BASE_SCORE) score = NetworkAgent.WIFI_BASE_SCORE;
295        if (score < 0) score = 0;
296
297        return score;
298    }
299
300    /**
301     * Determines if we can see both 2.4GHz and 5GHz for current config
302     */
303    private boolean multiBandScanResults(WifiInfo wifiInfo) {
304        WifiConfiguration currentConfiguration =
305                mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
306        if (currentConfiguration == null) return false;
307        ScanDetailCache scanDetailCache =
308                mWifiConfigManager.getScanDetailCacheForNetwork(wifiInfo.getNetworkId());
309        if (scanDetailCache == null) return false;
310        // Nasty that we change state here...
311        currentConfiguration.setVisibility(scanDetailCache.getVisibility(SCAN_CACHE_VISIBILITY_MS));
312        if (currentConfiguration.visibility == null) return false;
313        if (currentConfiguration.visibility.rssi24 == WifiConfiguration.INVALID_RSSI) return false;
314        if (currentConfiguration.visibility.rssi5 == WifiConfiguration.INVALID_RSSI) return false;
315        // N.B. this does not do exactly what is claimed!
316        if (currentConfiguration.visibility.rssi24
317                >= currentConfiguration.visibility.rssi5 - SCAN_CACHE_COUNT_PENALTY) {
318            return true;
319        }
320        return false;
321    }
322
323    /**
324     * Decides whether the current network is a "home" network
325     */
326    private boolean isHomeNetwork(WifiInfo wifiInfo) {
327        WifiConfiguration currentConfiguration =
328                mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
329        if (currentConfiguration == null) return false;
330        // This seems like it will only return true for really old routers!
331        if (currentConfiguration.allowedKeyManagement.cardinality() != 1) return false;
332        if (!currentConfiguration.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
333            return false;
334        }
335        ScanDetailCache scanDetailCache =
336                mWifiConfigManager.getScanDetailCacheForNetwork(wifiInfo.getNetworkId());
337        if (scanDetailCache == null) return false;
338        if (scanDetailCache.size() <= HOME_VISIBLE_NETWORK_MAX_COUNT) {
339            return true;
340        }
341        return false;
342    }
343
344    /**
345     * Experimental scorer, used when aggressive handover preference is set
346     */
347    private int calculateAlternativeScore(WifiInfo wifiInfo) {
348        double rssi = wifiInfo.getRssi();
349        double badRssi = wifiInfo.is5GHz() ? mThresholdQualifiedRssi5 : mThresholdQualifiedRssi24;
350
351        double baseScore = 50.0; // The score to beat to be chosen over mobile data
352        double score = (rssi - badRssi) + baseScore;
353        return (int) Math.round(score);
354    }
355
356    /**
357     * Data for dumpsys
358     *
359     * These are stored as csv formatted lines
360     */
361    private LinkedList<String> mLinkMetricsHistory = new LinkedList<String>();
362
363    /**
364     * Data logging for dumpsys
365     */
366    private void logLinkMetrics(WifiInfo wifiInfo) {
367        long now = mClock.getWallClockMillis();
368        double rssi = wifiInfo.getRssi();
369        int freq = wifiInfo.getFrequency();
370        int linkSpeed = wifiInfo.getLinkSpeed();
371        double txSuccessRate = wifiInfo.txSuccessRate;
372        double txRetriesRate = wifiInfo.txRetriesRate;
373        double txBadRate = wifiInfo.txBadRate;
374        double rxSuccessRate = wifiInfo.rxSuccessRate;
375        try {
376            String timestamp = new SimpleDateFormat("MM-dd HH:mm:ss.SSS").format(new Date(now));
377            String s = String.format(Locale.US, // Use US to avoid comma/decimal confusion
378                    "%s,%d,%.1f,%d,%d,%.2f,%.2f,%.2f,%.2f",
379                    timestamp, mSessionNumber, rssi, freq, linkSpeed,
380                    txSuccessRate, txRetriesRate, txBadRate, rxSuccessRate);
381            mLinkMetricsHistory.add(s);
382        } catch (Exception e) {
383            Log.e(TAG, "format problem", e);
384        }
385        while (mLinkMetricsHistory.size() > DUMPSYS_ENTRY_COUNT_LIMIT) {
386            mLinkMetricsHistory.removeFirst();
387        }
388    }
389
390    /**
391     * Tag to be used in dumpsys request
392     */
393    public static final String DUMP_ARG = "WifiScoreReport";
394
395    /**
396     * Dump logged signal strength and traffic measurements.
397     * @param fd unused
398     * @param pw PrintWriter for writing dump to
399     * @param args unused
400     */
401    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
402        pw.println("time,session,rssi,freq,linkspeed,tx_good,tx_retry,tx_bad,rx");
403        for (String line : mLinkMetricsHistory) {
404            pw.println(line);
405        }
406    }
407}
408