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