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