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