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