ScoringParams.java revision 358daf5df00f5563253ba3465fbc7029f7ac1056
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 17package com.android.server.wifi; 18 19import android.annotation.NonNull; 20import android.content.Context; 21import android.database.ContentObserver; 22import android.net.wifi.WifiInfo; 23import android.os.Handler; 24import android.provider.Settings; 25import android.util.KeyValueListParser; 26import android.util.Log; 27 28import 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 */ 37public class ScoringParams { 38 private static final String TAG = "WifiScoringParams"; 39 private static final int EXIT = 0; 40 private static final int ENTRY = 1; 41 private static final int SUFFICIENT = 2; 42 private static final int GOOD = 3; 43 44 /** 45 * Parameter values are stored in a separate container so that a new collection of values can 46 * be checked for consistency before activating them. 47 */ 48 private class Values { 49 /** RSSI thresholds for 2.4 GHz band (dBm) */ 50 public static final String KEY_RSSI2 = "rssi2"; 51 public final int[] rssi2 = {-83, -80, -73, -60}; 52 53 /** RSSI thresholds for 5 GHz band (dBm) */ 54 public static final String KEY_RSSI5 = "rssi5"; 55 public final int[] rssi5 = {-80, -77, -70, -57}; 56 57 /** Guidelines based on packet rates (packets/sec) */ 58 public static final String KEY_PPS = "pps"; 59 public final int[] pps = {0, 1, 100}; 60 61 /** Number of seconds for RSSI forecast */ 62 public static final String KEY_HORIZON = "horizon"; 63 public static final int MIN_HORIZON = -9; 64 public static final int MAX_HORIZON = 60; 65 public int horizon = 15; 66 67 Values() { 68 } 69 70 Values(Values source) { 71 for (int i = 0; i < rssi2.length; i++) { 72 rssi2[i] = source.rssi2[i]; 73 } 74 for (int i = 0; i < rssi5.length; i++) { 75 rssi5[i] = source.rssi5[i]; 76 } 77 for (int i = 0; i < pps.length; i++) { 78 pps[i] = source.pps[i]; 79 } 80 horizon = source.horizon; 81 } 82 83 public void validate() throws IllegalArgumentException { 84 validateRssiArray(rssi2); 85 validateRssiArray(rssi5); 86 validateOrderedNonNegativeArray(pps); 87 validateRange(horizon, MIN_HORIZON, MAX_HORIZON); 88 } 89 90 private void validateRssiArray(int[] rssi) throws IllegalArgumentException { 91 int low = WifiInfo.MIN_RSSI; 92 int high = Math.min(WifiInfo.MAX_RSSI, -1); // Stricter than Wifiinfo 93 for (int i = 0; i < rssi.length; i++) { 94 validateRange(rssi[i], low, high); 95 low = rssi[i]; 96 } 97 } 98 99 private void validateRange(int k, int low, int high) throws IllegalArgumentException { 100 if (k < low || k > high) { 101 throw new IllegalArgumentException(); 102 } 103 } 104 105 private void validateOrderedNonNegativeArray(int[] a) throws IllegalArgumentException { 106 int low = 0; 107 for (int i = 0; i < a.length; i++) { 108 if (a[i] < low) { 109 throw new IllegalArgumentException(); 110 } 111 low = a[i]; 112 } 113 } 114 115 public void parseString(String kvList) throws IllegalArgumentException { 116 KeyValueListParser parser = new KeyValueListParser(','); 117 parser.setString(kvList); 118 if (parser.size() != ("" + kvList).split(",").length) { 119 throw new IllegalArgumentException("dup keys"); 120 } 121 updateIntArray(rssi2, parser, KEY_RSSI2); 122 updateIntArray(rssi5, parser, KEY_RSSI5); 123 updateIntArray(pps, parser, KEY_PPS); 124 horizon = updateInt(parser, KEY_HORIZON, horizon); 125 } 126 127 private int updateInt(KeyValueListParser parser, String key, int defaultValue) 128 throws IllegalArgumentException { 129 String value = parser.getString(key, null); 130 if (value == null) return defaultValue; 131 try { 132 return Integer.parseInt(value); 133 } catch (NumberFormatException e) { 134 throw new IllegalArgumentException(); 135 } 136 } 137 138 private void updateIntArray(final int[] dest, KeyValueListParser parser, String key) 139 throws IllegalArgumentException { 140 if (parser.getString(key, null) == null) return; 141 int[] ints = parser.getIntArray(key, null); 142 if (ints == null) throw new IllegalArgumentException(); 143 if (ints.length != dest.length) throw new IllegalArgumentException(); 144 for (int i = 0; i < dest.length; i++) { 145 dest[i] = ints[i]; 146 } 147 } 148 149 @Override 150 public String toString() { 151 StringBuilder sb = new StringBuilder(); 152 appendKey(sb, KEY_RSSI2); 153 appendInts(sb, rssi2); 154 appendKey(sb, KEY_RSSI5); 155 appendInts(sb, rssi5); 156 //TODO(b/74613347) - leave these out, pending unit test updates 157 // appendKey(sb, KEY_PPS); 158 // appendInts(sb, pps); 159 appendKey(sb, KEY_HORIZON); 160 sb.append(horizon); 161 return sb.toString(); 162 } 163 164 private void appendKey(StringBuilder sb, String key) { 165 if (sb.length() != 0) sb.append(","); 166 sb.append(key).append("="); 167 } 168 169 private void appendInts(StringBuilder sb, final int[] a) { 170 final int n = a.length; 171 for (int i = 0; i < n; i++) { 172 if (i > 0) sb.append(":"); 173 sb.append(a[i]); 174 } 175 } 176 } 177 178 @NonNull private Values mVal = new Values(); 179 180 public ScoringParams() { 181 } 182 183 public ScoringParams(Context context) { 184 loadResources(context); 185 } 186 187 public ScoringParams(Context context, FrameworkFacade facade, Handler handler) { 188 loadResources(context); 189 setupContentObserver(context, facade, handler); 190 } 191 192 private void loadResources(Context context) { 193 mVal.rssi2[EXIT] = context.getResources().getInteger( 194 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz); 195 mVal.rssi2[ENTRY] = context.getResources().getInteger( 196 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_24GHz); 197 mVal.rssi2[SUFFICIENT] = context.getResources().getInteger( 198 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz); 199 mVal.rssi2[GOOD] = context.getResources().getInteger( 200 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_24GHz); 201 mVal.rssi5[EXIT] = context.getResources().getInteger( 202 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz); 203 mVal.rssi5[ENTRY] = context.getResources().getInteger( 204 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_5GHz); 205 mVal.rssi5[SUFFICIENT] = context.getResources().getInteger( 206 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz); 207 mVal.rssi5[GOOD] = context.getResources().getInteger( 208 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_5GHz); 209 try { 210 mVal.validate(); 211 } catch (IllegalArgumentException e) { 212 Log.wtf(TAG, "Inconsistent config_wifi_framework_ resources: " + this, e); 213 } 214 } 215 216 private void setupContentObserver(Context context, FrameworkFacade facade, Handler handler) { 217 final ScoringParams self = this; 218 String defaults = self.toString(); 219 ContentObserver observer = new ContentObserver(handler) { 220 @Override 221 public void onChange(boolean selfChange) { 222 String params = facade.getStringSetting( 223 context, Settings.Global.WIFI_SCORE_PARAMS); 224 self.update(defaults); 225 if (!self.update(params)) { 226 Log.e(TAG, "Error in " + Settings.Global.WIFI_SCORE_PARAMS + ": " 227 + sanitize(params)); 228 } 229 Log.i(TAG, self.toString()); 230 } 231 }; 232 facade.registerContentObserver(context, 233 Settings.Global.getUriFor(Settings.Global.WIFI_SCORE_PARAMS), 234 true, 235 observer); 236 observer.onChange(false); 237 } 238 239 private static final String COMMA_KEY_VAL_STAR = "^(,[A-Za-z_][A-Za-z0-9_]*=[0-9.:+-]+)*$"; 240 241 /** 242 * Updates the parameters from the given parameter string. 243 * If any errors are detected, no change is made. 244 * @param kvList is a comma-separated key=value list. 245 * @return true for success 246 */ 247 public boolean update(String kvList) { 248 if (kvList == null || "".equals(kvList)) { 249 return true; 250 } 251 if (!("," + kvList).matches(COMMA_KEY_VAL_STAR)) { 252 return false; 253 } 254 Values v = new Values(mVal); 255 try { 256 v.parseString(kvList); 257 v.validate(); 258 mVal = v; 259 return true; 260 } catch (IllegalArgumentException e) { 261 return false; 262 } 263 } 264 265 /** 266 * Sanitize a string to make it safe for printing. 267 * @param params is the untrusted string 268 * @return string with questionable characters replaced with question marks 269 */ 270 public String sanitize(String params) { 271 if (params == null) return ""; 272 String printable = params.replaceAll("[^A-Za-z_0-9=,:.+-]", "?"); 273 if (printable.length() > 100) { 274 printable = printable.substring(0, 98) + "..."; 275 } 276 return printable; 277 } 278 279 /** Constant to denote someplace in the 2.4 GHz band */ 280 public static final int BAND2 = 2400; 281 282 /** Constant to denote someplace in the 5 GHz band */ 283 public static final int BAND5 = 5000; 284 285 /** 286 * Returns the RSSI value at which the connection is deemed to be unusable, 287 * in the absence of other indications. 288 */ 289 public int getExitRssi(int frequencyMegaHertz) { 290 return getRssiArray(frequencyMegaHertz)[EXIT]; 291 } 292 293 /** 294 * Returns the minimum scan RSSI for making a connection attempt. 295 */ 296 public int getEntryRssi(int frequencyMegaHertz) { 297 return getRssiArray(frequencyMegaHertz)[ENTRY]; 298 } 299 300 /** 301 * Returns a connected RSSI value that indicates the connection is 302 * good enough that we needn't scan for alternatives. 303 */ 304 public int getSufficientRssi(int frequencyMegaHertz) { 305 return getRssiArray(frequencyMegaHertz)[SUFFICIENT]; 306 } 307 308 /** 309 * Returns a connected RSSI value that indicates a good connection. 310 */ 311 public int getGoodRssi(int frequencyMegaHertz) { 312 return getRssiArray(frequencyMegaHertz)[GOOD]; 313 } 314 315 /** 316 * Returns the number of seconds to use for rssi forecast. 317 */ 318 public int getHorizonSeconds() { 319 return mVal.horizon; 320 } 321 322 /** 323 * Returns a packet rate that should be considered acceptable for staying on wifi, 324 * no matter how bad the RSSI gets (packets per second). 325 */ 326 public int getYippeeSkippyPacketsPerSecond() { 327 return mVal.pps[2]; 328 } 329 330 private static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000; 331 332 private int[] getRssiArray(int frequency) { 333 if (frequency < MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) { 334 return mVal.rssi2; 335 } else { 336 return mVal.rssi5; 337 } 338 } 339 340 @Override 341 public String toString() { 342 return mVal.toString(); 343 } 344} 345