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