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.database.ContentObserver;
22 import android.net.wifi.WifiInfo;
23 import android.os.Handler;
24 import android.provider.Settings;
25 import android.util.KeyValueListParser;
26 import android.util.Log;
27 
28 import com.android.internal.R;
29 
30 /**
31  * Holds parameters used for scoring networks.
32  *
33  * Doing this in one place means that there's a better chance of consistency between
34  * connected score and network selection.
35  *
36  */
37 public class ScoringParams {
38     // A long name that describes itself pretty well
39     public static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000;
40 
41     private static final String TAG = "WifiScoringParams";
42     private static final int EXIT = 0;
43     private static final int ENTRY = 1;
44     private static final int SUFFICIENT = 2;
45     private static final int GOOD = 3;
46 
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         /** Guidelines based on packet rates (packets/sec) */
61         public static final String KEY_PPS = "pps";
62         public final int[] pps = {0, 1, 100};
63 
64         /** Number of seconds for RSSI forecast */
65         public static final String KEY_HORIZON = "horizon";
66         public static final int MIN_HORIZON = -9;
67         public static final int MAX_HORIZON = 60;
68         public int horizon = 15;
69 
70         /** Number 0-10 influencing requests for network unreachability detection */
71         public static final String KEY_NUD = "nud";
72         public static final int MIN_NUD = 0;
73         public static final int MAX_NUD = 10;
74         public int nud = 8;
75 
76         /** Experiment identifier */
77         public static final String KEY_EXPID = "expid";
78         public static final int MIN_EXPID = 0;
79         public static final int MAX_EXPID = Integer.MAX_VALUE;
80         public int expid = 0;
81 
Values()82         Values() {
83         }
84 
Values(Values source)85         Values(Values source) {
86             for (int i = 0; i < rssi2.length; i++) {
87                 rssi2[i] = source.rssi2[i];
88             }
89             for (int i = 0; i < rssi5.length; i++) {
90                 rssi5[i] = source.rssi5[i];
91             }
92             for (int i = 0; i < pps.length; i++) {
93                 pps[i] = source.pps[i];
94             }
95             horizon = source.horizon;
96             nud = source.nud;
97             expid = source.expid;
98         }
99 
validate()100         public void validate() throws IllegalArgumentException {
101             validateRssiArray(rssi2);
102             validateRssiArray(rssi5);
103             validateOrderedNonNegativeArray(pps);
104             validateRange(horizon, MIN_HORIZON, MAX_HORIZON);
105             validateRange(nud, MIN_NUD, MAX_NUD);
106             validateRange(expid, MIN_EXPID, MAX_EXPID);
107         }
108 
validateRssiArray(int[] rssi)109         private void validateRssiArray(int[] rssi) throws IllegalArgumentException {
110             int low = WifiInfo.MIN_RSSI;
111             int high = Math.min(WifiInfo.MAX_RSSI, -1); // Stricter than Wifiinfo
112             for (int i = 0; i < rssi.length; i++) {
113                 validateRange(rssi[i], low, high);
114                 low = rssi[i];
115             }
116         }
117 
validateRange(int k, int low, int high)118         private void validateRange(int k, int low, int high) throws IllegalArgumentException {
119             if (k < low || k > high) {
120                 throw new IllegalArgumentException();
121             }
122         }
123 
validateOrderedNonNegativeArray(int[] a)124         private void validateOrderedNonNegativeArray(int[] a) throws IllegalArgumentException {
125             int low = 0;
126             for (int i = 0; i < a.length; i++) {
127                 if (a[i] < low) {
128                     throw new IllegalArgumentException();
129                 }
130                 low = a[i];
131             }
132         }
133 
parseString(String kvList)134         public void parseString(String kvList) throws IllegalArgumentException {
135             KeyValueListParser parser = new KeyValueListParser(',');
136             parser.setString(kvList);
137             if (parser.size() != ("" + kvList).split(",").length) {
138                 throw new IllegalArgumentException("dup keys");
139             }
140             updateIntArray(rssi2, parser, KEY_RSSI2);
141             updateIntArray(rssi5, parser, KEY_RSSI5);
142             updateIntArray(pps, parser, KEY_PPS);
143             horizon = updateInt(parser, KEY_HORIZON, horizon);
144             nud = updateInt(parser, KEY_NUD, nud);
145             expid = updateInt(parser, KEY_EXPID, expid);
146         }
147 
updateInt(KeyValueListParser parser, String key, int defaultValue)148         private int updateInt(KeyValueListParser parser, String key, int defaultValue)
149                 throws IllegalArgumentException {
150             String value = parser.getString(key, null);
151             if (value == null) return defaultValue;
152             try {
153                 return Integer.parseInt(value);
154             } catch (NumberFormatException e) {
155                 throw new IllegalArgumentException();
156             }
157         }
158 
updateIntArray(final int[] dest, KeyValueListParser parser, String key)159         private void updateIntArray(final int[] dest, KeyValueListParser parser, String key)
160                 throws IllegalArgumentException {
161             if (parser.getString(key, null) == null) return;
162             int[] ints = parser.getIntArray(key, null);
163             if (ints == null) throw new IllegalArgumentException();
164             if (ints.length != dest.length) throw new IllegalArgumentException();
165             for (int i = 0; i < dest.length; i++) {
166                 dest[i] = ints[i];
167             }
168         }
169 
170         @Override
toString()171         public String toString() {
172             StringBuilder sb = new StringBuilder();
173             appendKey(sb, KEY_RSSI2);
174             appendInts(sb, rssi2);
175             appendKey(sb, KEY_RSSI5);
176             appendInts(sb, rssi5);
177             appendKey(sb, KEY_PPS);
178             appendInts(sb, pps);
179             appendKey(sb, KEY_HORIZON);
180             sb.append(horizon);
181             appendKey(sb, KEY_NUD);
182             sb.append(nud);
183             appendKey(sb, KEY_EXPID);
184             sb.append(expid);
185             return sb.toString();
186         }
187 
appendKey(StringBuilder sb, String key)188         private void appendKey(StringBuilder sb, String key) {
189             if (sb.length() != 0) sb.append(",");
190             sb.append(key).append("=");
191         }
192 
appendInts(StringBuilder sb, final int[] a)193         private void appendInts(StringBuilder sb, final int[] a) {
194             final int n = a.length;
195             for (int i = 0; i < n; i++) {
196                 if (i > 0) sb.append(":");
197                 sb.append(a[i]);
198             }
199         }
200     }
201 
202     @NonNull private Values mVal = new Values();
203 
ScoringParams()204     public ScoringParams() {
205     }
206 
ScoringParams(Context context)207     public ScoringParams(Context context) {
208         loadResources(context);
209     }
210 
ScoringParams(Context context, FrameworkFacade facade, Handler handler)211     public ScoringParams(Context context, FrameworkFacade facade, Handler handler) {
212         loadResources(context);
213         setupContentObserver(context, facade, handler);
214     }
215 
loadResources(Context context)216     private void loadResources(Context context) {
217         mVal.rssi2[EXIT] = context.getResources().getInteger(
218                 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
219         mVal.rssi2[ENTRY] = context.getResources().getInteger(
220                 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_24GHz);
221         mVal.rssi2[SUFFICIENT] = context.getResources().getInteger(
222                 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
223         mVal.rssi2[GOOD] = context.getResources().getInteger(
224                 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_24GHz);
225         mVal.rssi5[EXIT] = context.getResources().getInteger(
226                 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
227         mVal.rssi5[ENTRY] = context.getResources().getInteger(
228                 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_5GHz);
229         mVal.rssi5[SUFFICIENT] = context.getResources().getInteger(
230                 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
231         mVal.rssi5[GOOD] = context.getResources().getInteger(
232                 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_5GHz);
233         try {
234             mVal.validate();
235         } catch (IllegalArgumentException e) {
236             Log.wtf(TAG, "Inconsistent config_wifi_framework_ resources: " + this, e);
237         }
238     }
239 
setupContentObserver(Context context, FrameworkFacade facade, Handler handler)240     private void setupContentObserver(Context context, FrameworkFacade facade, Handler handler) {
241         final ScoringParams self = this;
242         String defaults = self.toString();
243         ContentObserver observer = new ContentObserver(handler) {
244             @Override
245             public void onChange(boolean selfChange) {
246                 String params = facade.getStringSetting(
247                         context, Settings.Global.WIFI_SCORE_PARAMS);
248                 self.update(defaults);
249                 if (!self.update(params)) {
250                     Log.e(TAG, "Error in " + Settings.Global.WIFI_SCORE_PARAMS + ": "
251                             + sanitize(params));
252                 }
253                 Log.i(TAG, self.toString());
254             }
255         };
256         facade.registerContentObserver(context,
257                 Settings.Global.getUriFor(Settings.Global.WIFI_SCORE_PARAMS),
258                 true,
259                 observer);
260         observer.onChange(false);
261     }
262 
263     private static final String COMMA_KEY_VAL_STAR = "^(,[A-Za-z_][A-Za-z0-9_]*=[0-9.:+-]+)*$";
264 
265     /**
266      * Updates the parameters from the given parameter string.
267      * If any errors are detected, no change is made.
268      * @param kvList is a comma-separated key=value list.
269      * @return true for success
270      */
update(String kvList)271     public boolean update(String kvList) {
272         if (kvList == null || "".equals(kvList)) {
273             return true;
274         }
275         if (!("," + kvList).matches(COMMA_KEY_VAL_STAR)) {
276             return false;
277         }
278         Values v = new Values(mVal);
279         try {
280             v.parseString(kvList);
281             v.validate();
282             mVal = v;
283             return true;
284         } catch (IllegalArgumentException e) {
285             return false;
286         }
287     }
288 
289     /**
290      * Sanitize a string to make it safe for printing.
291      * @param params is the untrusted string
292      * @return string with questionable characters replaced with question marks
293      */
sanitize(String params)294     public String sanitize(String params) {
295         if (params == null) return "";
296         String printable = params.replaceAll("[^A-Za-z_0-9=,:.+-]", "?");
297         if (printable.length() > 100) {
298             printable = printable.substring(0, 98) + "...";
299         }
300         return printable;
301     }
302 
303     /** Constant to denote someplace in the 2.4 GHz band */
304     public static final int BAND2 = 2400;
305 
306     /** Constant to denote someplace in the 5 GHz band */
307     public static final int BAND5 = 5000;
308 
309     /**
310      * Returns the RSSI value at which the connection is deemed to be unusable,
311      * in the absence of other indications.
312      */
getExitRssi(int frequencyMegaHertz)313     public int getExitRssi(int frequencyMegaHertz) {
314         return getRssiArray(frequencyMegaHertz)[EXIT];
315     }
316 
317     /**
318      * Returns the minimum scan RSSI for making a connection attempt.
319      */
getEntryRssi(int frequencyMegaHertz)320     public int getEntryRssi(int frequencyMegaHertz) {
321         return getRssiArray(frequencyMegaHertz)[ENTRY];
322     }
323 
324     /**
325      * Returns a connected RSSI value that indicates the connection is
326      * good enough that we needn't scan for alternatives.
327      */
getSufficientRssi(int frequencyMegaHertz)328     public int getSufficientRssi(int frequencyMegaHertz) {
329         return getRssiArray(frequencyMegaHertz)[SUFFICIENT];
330     }
331 
332     /**
333      * Returns a connected RSSI value that indicates a good connection.
334      */
getGoodRssi(int frequencyMegaHertz)335     public int getGoodRssi(int frequencyMegaHertz) {
336         return getRssiArray(frequencyMegaHertz)[GOOD];
337     }
338 
339     /**
340      * Returns the number of seconds to use for rssi forecast.
341      */
getHorizonSeconds()342     public int getHorizonSeconds() {
343         return mVal.horizon;
344     }
345 
346     /**
347      * Returns a packet rate that should be considered acceptable for staying on wifi,
348      * no matter how bad the RSSI gets (packets per second).
349      */
getYippeeSkippyPacketsPerSecond()350     public int getYippeeSkippyPacketsPerSecond() {
351         return mVal.pps[2];
352     }
353 
354     /**
355      * Returns a number between 0 and 10 inclusive that indicates
356      * how aggressive to be about asking for IP configuration checks
357      * (also known as Network Unreachabilty Detection, or NUD).
358      *
359      * 0 - no nud checks requested by scorer (framework still checks after roam)
360      * 1 - check when score becomes very low
361      *     ...
362      * 10 - check when score first breaches threshold, and again as it gets worse
363      *
364      */
getNudKnob()365     public int getNudKnob() {
366         return mVal.nud;
367     }
368 
369     /**
370      * Returns the experiment identifier.
371      *
372      * This value may be used to tag a set of experimental settings.
373      */
getExperimentIdentifier()374     public int getExperimentIdentifier() {
375         return mVal.expid;
376     }
377 
getRssiArray(int frequency)378     private int[] getRssiArray(int frequency) {
379         if (frequency < MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) {
380             return mVal.rssi2;
381         } else {
382             return mVal.rssi5;
383         }
384     }
385 
386     @Override
toString()387     public String toString() {
388         return mVal.toString();
389     }
390 }
391