1 /*
2  * Copyright 2018 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.annotation.NonNull;
20 import android.content.Context;
21 import android.net.wifi.ScanResult;
22 import android.net.wifi.WifiInfo;
23 import android.util.Log;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.server.wifi.util.KeyValueListParser;
27 import com.android.wifi.resources.R;
28 
29 /**
30  * Holds parameters used for scoring networks.
31  *
32  * Doing this in one place means that there's a better chance of consistency between
33  * connected score and network selection.
34  *
35  */
36 public class ScoringParams {
37     private final Context mContext;
38 
39     private static final String TAG = "WifiScoringParams";
40     private static final int EXIT = 0;
41     private static final int ENTRY = 1;
42     private static final int SUFFICIENT = 2;
43     private static final int GOOD = 3;
44 
45     private static final int ACTIVE_TRAFFIC = 1;
46     private static final int HIGH_TRAFFIC = 2;
47     /**
48      * Parameter values are stored in a separate container so that a new collection of values can
49      * be checked for consistency before activating them.
50      */
51     private class Values {
52         /** RSSI thresholds for 2.4 GHz band (dBm) */
53         public static final String KEY_RSSI2 = "rssi2";
54         public final int[] rssi2 = {-83, -80, -73, -60};
55 
56         /** RSSI thresholds for 5 GHz band (dBm) */
57         public static final String KEY_RSSI5 = "rssi5";
58         public final int[] rssi5 = {-80, -77, -70, -57};
59 
60         /** RSSI thresholds for 6 GHz band (dBm) */
61         public static final String KEY_RSSI6 = "rssi6";
62         public final int[] rssi6 = {-80, -77, -70, -57};
63 
64        /** Guidelines based on packet rates (packets/sec) */
65         public static final String KEY_PPS = "pps";
66         public final int[] pps = {0, 1, 100};
67 
68         /** Number of seconds for RSSI forecast */
69         public static final String KEY_HORIZON = "horizon";
70         public static final int MIN_HORIZON = -9;
71         public static final int MAX_HORIZON = 60;
72         public int horizon = 15;
73 
74         /** Number 0-10 influencing requests for network unreachability detection */
75         public static final String KEY_NUD = "nud";
76         public static final int MIN_NUD = 0;
77         public static final int MAX_NUD = 10;
78         public int nud = 8;
79 
80         /** Experiment identifier */
81         public static final String KEY_EXPID = "expid";
82         public static final int MIN_EXPID = 0;
83         public static final int MAX_EXPID = Integer.MAX_VALUE;
84         public int expid = 0;
85 
86         /** CandidateScorer parameters */
87         public int throughputBonusNumerator = 120;
88         public int throughputBonusDenominator = 433;
89         public int throughputBonusLimit = 200;
90         public int savedNetworkBonus = 500;
91         public int unmeteredNetworkBonus = 1000;
92         public int currentNetworkBonusMin = 20;
93         public int currentNetworkBonusPercent = 20;
94         public int secureNetworkBonus = 40;
95         public int lastSelectionMinutes = 480;
96         public static final int MIN_MINUTES = 1;
97         public static final int MAX_MINUTES = Integer.MAX_VALUE / (60 * 1000);
98 
Values()99         Values() {
100         }
101 
Values(Values source)102         Values(Values source) {
103             for (int i = 0; i < rssi2.length; i++) {
104                 rssi2[i] = source.rssi2[i];
105             }
106             for (int i = 0; i < rssi5.length; i++) {
107                 rssi5[i] = source.rssi5[i];
108             }
109             for (int i = 0; i < rssi6.length; i++) {
110                 rssi6[i] = source.rssi6[i];
111             }
112             for (int i = 0; i < pps.length; i++) {
113                 pps[i] = source.pps[i];
114             }
115             horizon = source.horizon;
116             nud = source.nud;
117             expid = source.expid;
118         }
119 
validate()120         public void validate() throws IllegalArgumentException {
121             validateRssiArray(rssi2);
122             validateRssiArray(rssi5);
123             validateRssiArray(rssi6);
124             validateOrderedNonNegativeArray(pps);
125             validateRange(horizon, MIN_HORIZON, MAX_HORIZON);
126             validateRange(nud, MIN_NUD, MAX_NUD);
127             validateRange(expid, MIN_EXPID, MAX_EXPID);
128             validateRange(lastSelectionMinutes, MIN_MINUTES, MAX_MINUTES);
129         }
130 
validateRssiArray(int[] rssi)131         private void validateRssiArray(int[] rssi) throws IllegalArgumentException {
132             int low = WifiInfo.MIN_RSSI;
133             int high = Math.min(WifiInfo.MAX_RSSI, -1); // Stricter than Wifiinfo
134             for (int i = 0; i < rssi.length; i++) {
135                 validateRange(rssi[i], low, high);
136                 low = rssi[i];
137             }
138         }
139 
validateRange(int k, int low, int high)140         private void validateRange(int k, int low, int high) throws IllegalArgumentException {
141             if (k < low || k > high) {
142                 throw new IllegalArgumentException();
143             }
144         }
145 
validateOrderedNonNegativeArray(int[] a)146         private void validateOrderedNonNegativeArray(int[] a) throws IllegalArgumentException {
147             int low = 0;
148             for (int i = 0; i < a.length; i++) {
149                 if (a[i] < low) {
150                     throw new IllegalArgumentException();
151                 }
152                 low = a[i];
153             }
154         }
155 
parseString(String kvList)156         public void parseString(String kvList) throws IllegalArgumentException {
157             KeyValueListParser parser = new KeyValueListParser(',');
158             parser.setString(kvList);
159             if (parser.size() != ("" + kvList).split(",").length) {
160                 throw new IllegalArgumentException("dup keys");
161             }
162             updateIntArray(rssi2, parser, KEY_RSSI2);
163             updateIntArray(rssi5, parser, KEY_RSSI5);
164             updateIntArray(rssi6, parser, KEY_RSSI6);
165             updateIntArray(pps, parser, KEY_PPS);
166             horizon = updateInt(parser, KEY_HORIZON, horizon);
167             nud = updateInt(parser, KEY_NUD, nud);
168             expid = updateInt(parser, KEY_EXPID, expid);
169         }
170 
updateInt(KeyValueListParser parser, String key, int defaultValue)171         private int updateInt(KeyValueListParser parser, String key, int defaultValue)
172                 throws IllegalArgumentException {
173             String value = parser.getString(key, null);
174             if (value == null) return defaultValue;
175             try {
176                 return Integer.parseInt(value);
177             } catch (NumberFormatException e) {
178                 throw new IllegalArgumentException();
179             }
180         }
181 
updateIntArray(final int[] dest, KeyValueListParser parser, String key)182         private void updateIntArray(final int[] dest, KeyValueListParser parser, String key)
183                 throws IllegalArgumentException {
184             if (parser.getString(key, null) == null) return;
185             int[] ints = parser.getIntArray(key, null);
186             if (ints == null) throw new IllegalArgumentException();
187             if (ints.length != dest.length) throw new IllegalArgumentException();
188             for (int i = 0; i < dest.length; i++) {
189                 dest[i] = ints[i];
190             }
191         }
192 
193         @Override
toString()194         public String toString() {
195             StringBuilder sb = new StringBuilder();
196             appendKey(sb, KEY_RSSI2);
197             appendInts(sb, rssi2);
198             appendKey(sb, KEY_RSSI5);
199             appendInts(sb, rssi5);
200             appendKey(sb, KEY_RSSI6);
201             appendInts(sb, rssi6);
202             appendKey(sb, KEY_PPS);
203             appendInts(sb, pps);
204             appendKey(sb, KEY_HORIZON);
205             sb.append(horizon);
206             appendKey(sb, KEY_NUD);
207             sb.append(nud);
208             appendKey(sb, KEY_EXPID);
209             sb.append(expid);
210             return sb.toString();
211         }
212 
appendKey(StringBuilder sb, String key)213         private void appendKey(StringBuilder sb, String key) {
214             if (sb.length() != 0) sb.append(",");
215             sb.append(key).append("=");
216         }
217 
appendInts(StringBuilder sb, final int[] a)218         private void appendInts(StringBuilder sb, final int[] a) {
219             final int n = a.length;
220             for (int i = 0; i < n; i++) {
221                 if (i > 0) sb.append(":");
222                 sb.append(a[i]);
223             }
224         }
225     }
226 
227     @NonNull private Values mVal = null;
228 
229     @VisibleForTesting
ScoringParams()230     public ScoringParams() {
231         mContext = null;
232         mVal = new Values();
233     }
234 
ScoringParams(Context context)235     public ScoringParams(Context context) {
236         mContext = context;
237     }
238 
loadResources(Context context)239     private void loadResources(Context context) {
240         if (mVal != null) return;
241         mVal = new Values();
242         mVal.rssi2[EXIT] = context.getResources().getInteger(
243                 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
244         mVal.rssi2[ENTRY] = context.getResources().getInteger(
245                 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_24GHz);
246         mVal.rssi2[SUFFICIENT] = context.getResources().getInteger(
247                 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
248         mVal.rssi2[GOOD] = context.getResources().getInteger(
249                 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_24GHz);
250         mVal.rssi5[EXIT] = context.getResources().getInteger(
251                 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
252         mVal.rssi5[ENTRY] = context.getResources().getInteger(
253                 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_5GHz);
254         mVal.rssi5[SUFFICIENT] = context.getResources().getInteger(
255                 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
256         mVal.rssi5[GOOD] = context.getResources().getInteger(
257                 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_5GHz);
258         mVal.rssi6[EXIT] = context.getResources().getInteger(
259                 R.integer.config_wifiFrameworkScoreBadRssiThreshold6ghz);
260         mVal.rssi6[ENTRY] = context.getResources().getInteger(
261                 R.integer.config_wifiFrameworkScoreEntryRssiThreshold6ghz);
262         mVal.rssi6[SUFFICIENT] = context.getResources().getInteger(
263                 R.integer.config_wifiFrameworkScoreLowRssiThreshold6ghz);
264         mVal.rssi6[GOOD] = context.getResources().getInteger(
265                 R.integer.config_wifiFrameworkScoreGoodRssiThreshold6ghz);
266         mVal.throughputBonusNumerator = context.getResources().getInteger(
267                 R.integer.config_wifiFrameworkThroughputBonusNumerator);
268         mVal.throughputBonusDenominator = context.getResources().getInteger(
269                 R.integer.config_wifiFrameworkThroughputBonusDenominator);
270         mVal.throughputBonusLimit = context.getResources().getInteger(
271                 R.integer.config_wifiFrameworkThroughputBonusLimit);
272         mVal.savedNetworkBonus = context.getResources().getInteger(
273                 R.integer.config_wifiFrameworkSavedNetworkBonus);
274         mVal.unmeteredNetworkBonus = context.getResources().getInteger(
275                 R.integer.config_wifiFrameworkUnmeteredNetworkBonus);
276         mVal.currentNetworkBonusMin = context.getResources().getInteger(
277                 R.integer.config_wifiFrameworkCurrentNetworkBonusMin);
278         mVal.currentNetworkBonusPercent = context.getResources().getInteger(
279             R.integer.config_wifiFrameworkCurrentNetworkBonusPercent);
280         mVal.secureNetworkBonus = context.getResources().getInteger(
281                 R.integer.config_wifiFrameworkSecureNetworkBonus);
282         mVal.lastSelectionMinutes = context.getResources().getInteger(
283                 R.integer.config_wifiFrameworkLastSelectionMinutes);
284         mVal.pps[ACTIVE_TRAFFIC] = context.getResources().getInteger(
285                 R.integer.config_wifiFrameworkMinPacketPerSecondActiveTraffic);
286         mVal.pps[HIGH_TRAFFIC] = context.getResources().getInteger(
287                 R.integer.config_wifiFrameworkMinPacketPerSecondHighTraffic);
288         try {
289             mVal.validate();
290         } catch (IllegalArgumentException e) {
291             Log.wtf(TAG, "Inconsistent config_wifi_framework_ resources: " + this, e);
292         }
293     }
294 
295     private static final String COMMA_KEY_VAL_STAR = "^(,[A-Za-z_][A-Za-z0-9_]*=[0-9.:+-]+)*$";
296 
297     /**
298      * Updates the parameters from the given parameter string.
299      * If any errors are detected, no change is made.
300      * @param kvList is a comma-separated key=value list.
301      * @return true for success
302      */
303     @VisibleForTesting
update(String kvList)304     public boolean update(String kvList) {
305         if (kvList == null || "".equals(kvList)) {
306             return true;
307         }
308         if (!("," + kvList).matches(COMMA_KEY_VAL_STAR)) {
309             return false;
310         }
311         loadResources(mContext);
312         Values v = new Values(mVal);
313         try {
314             v.parseString(kvList);
315             v.validate();
316             mVal = v;
317             return true;
318         } catch (IllegalArgumentException e) {
319             return false;
320         }
321     }
322 
323     /**
324      * Sanitize a string to make it safe for printing.
325      * @param params is the untrusted string
326      * @return string with questionable characters replaced with question marks
327      */
sanitize(String params)328     public String sanitize(String params) {
329         if (params == null) return "";
330         String printable = params.replaceAll("[^A-Za-z_0-9=,:.+-]", "?");
331         if (printable.length() > 100) {
332             printable = printable.substring(0, 98) + "...";
333         }
334         return printable;
335     }
336 
337     /**
338      * Returns the RSSI value at which the connection is deemed to be unusable,
339      * in the absence of other indications.
340      */
getExitRssi(int frequencyMegaHertz)341     public int getExitRssi(int frequencyMegaHertz) {
342         return getRssiArray(frequencyMegaHertz)[EXIT];
343     }
344 
345     /**
346      * Returns the minimum scan RSSI for making a connection attempt.
347      */
getEntryRssi(int frequencyMegaHertz)348     public int getEntryRssi(int frequencyMegaHertz) {
349         return getRssiArray(frequencyMegaHertz)[ENTRY];
350     }
351 
352     /**
353      * Returns a connected RSSI value that indicates the connection is
354      * good enough that we needn't scan for alternatives.
355      */
getSufficientRssi(int frequencyMegaHertz)356     public int getSufficientRssi(int frequencyMegaHertz) {
357         return getRssiArray(frequencyMegaHertz)[SUFFICIENT];
358     }
359 
360     /**
361      * Returns a connected RSSI value that indicates a good connection.
362      */
getGoodRssi(int frequencyMegaHertz)363     public int getGoodRssi(int frequencyMegaHertz) {
364         return getRssiArray(frequencyMegaHertz)[GOOD];
365     }
366 
367     /**
368      * Returns the number of seconds to use for rssi forecast.
369      */
getHorizonSeconds()370     public int getHorizonSeconds() {
371         loadResources(mContext);
372         return mVal.horizon;
373     }
374 
375     /**
376      * Returns a packet rate that should be considered acceptable for staying on wifi,
377      * no matter how bad the RSSI gets (packets per second).
378      */
getYippeeSkippyPacketsPerSecond()379     public int getYippeeSkippyPacketsPerSecond() {
380         loadResources(mContext);
381         return mVal.pps[HIGH_TRAFFIC];
382     }
383 
384     /**
385      * Returns a packet rate that should be considered acceptable to skip scan or network selection
386      */
getActiveTrafficPacketsPerSecond()387     public int getActiveTrafficPacketsPerSecond() {
388         loadResources(mContext);
389         return mVal.pps[ACTIVE_TRAFFIC];
390     }
391 
392     /**
393      * Returns a number between 0 and 10 inclusive that indicates
394      * how aggressive to be about asking for IP configuration checks
395      * (also known as Network Unreachabilty Detection, or NUD).
396      *
397      * 0 - no nud checks requested by scorer (framework still checks after roam)
398      * 1 - check when score becomes very low
399      *     ...
400      * 10 - check when score first breaches threshold, and again as it gets worse
401      *
402      */
getNudKnob()403     public int getNudKnob() {
404         loadResources(mContext);
405         return mVal.nud;
406     }
407 
408     /**
409      */
getThroughputBonusNumerator()410     public int getThroughputBonusNumerator() {
411         return mVal.throughputBonusNumerator;
412     }
413 
414     /**
415      */
getThroughputBonusDenominator()416     public int getThroughputBonusDenominator() {
417         return mVal.throughputBonusDenominator;
418     }
419 
420     /*
421      * Returns the maximum bonus for the network selection candidate score
422      * for the contribution of the selected score.
423      */
getThroughputBonusLimit()424     public int getThroughputBonusLimit() {
425         return mVal.throughputBonusLimit;
426     }
427 
428     /*
429      * Returns the bonus for the network selection candidate score
430      * for a saved network (i.e., not a suggestion).
431      */
getSavedNetworkBonus()432     public int getSavedNetworkBonus() {
433         return mVal.savedNetworkBonus;
434     }
435 
436     /*
437      * Returns the bonus for the network selection candidate score
438      * for an unmetered network.
439      */
getUnmeteredNetworkBonus()440     public int getUnmeteredNetworkBonus() {
441         return mVal.unmeteredNetworkBonus;
442     }
443 
444     /*
445      * Returns the minimum bonus for the network selection candidate score
446      * for the currently connected network.
447      */
getCurrentNetworkBonusMin()448     public int getCurrentNetworkBonusMin() {
449         return mVal.currentNetworkBonusMin;
450     }
451 
452     /*
453      * Returns the percentage bonus for the network selection candidate score
454      * for the currently connected network. The percent value is applied to rssi score and
455      * throughput score;
456      */
getCurrentNetworkBonusPercent()457     public int getCurrentNetworkBonusPercent() {
458         return mVal.currentNetworkBonusPercent;
459     }
460 
461     /*
462      * Returns the bonus for the network selection candidate score
463      * for a secure network.
464      */
getSecureNetworkBonus()465     public int getSecureNetworkBonus() {
466         return mVal.secureNetworkBonus;
467     }
468 
469     /*
470      * Returns the duration in minutes for a recently selected network
471      * to be strongly favored.
472      */
getLastSelectionMinutes()473     public int getLastSelectionMinutes() {
474         return mVal.lastSelectionMinutes;
475     }
476 
477     /**
478      * Returns the experiment identifier.
479      *
480      * This value may be used to tag a set of experimental settings.
481      */
getExperimentIdentifier()482     public int getExperimentIdentifier() {
483         loadResources(mContext);
484         return mVal.expid;
485     }
486 
getRssiArray(int frequency)487     private int[] getRssiArray(int frequency) {
488         loadResources(mContext);
489         if (ScanResult.is24GHz(frequency)) {
490             return mVal.rssi2;
491         } else if (ScanResult.is5GHz(frequency)) {
492             return mVal.rssi5;
493         } else if (ScanResult.is6GHz(frequency)) {
494             return mVal.rssi6;
495         }
496         // Invalid frequency use
497         Log.e(TAG, "Invalid frequency(" + frequency + "), using 5G as default rssi array");
498         return mVal.rssi5;
499     }
500 
501     @Override
toString()502     public String toString() {
503         loadResources(mContext);
504         return mVal.toString();
505     }
506 }
507