| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wifi; |
| |
| import android.annotation.NonNull; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.net.wifi.WifiInfo; |
| import android.os.Handler; |
| import android.provider.Settings; |
| import android.util.KeyValueListParser; |
| import android.util.Log; |
| |
| import com.android.internal.R; |
| |
| /** |
| * Holds parameters used for scoring networks. |
| * |
| * Doing this in one place means that there's a better chance of consistency between |
| * connected score and network selection. |
| * |
| */ |
| public class ScoringParams { |
| // A long name that describes itself pretty well |
| public static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000; |
| |
| private static final String TAG = "WifiScoringParams"; |
| private static final int EXIT = 0; |
| private static final int ENTRY = 1; |
| private static final int SUFFICIENT = 2; |
| private static final int GOOD = 3; |
| |
| /** |
| * Parameter values are stored in a separate container so that a new collection of values can |
| * be checked for consistency before activating them. |
| */ |
| private class Values { |
| /** RSSI thresholds for 2.4 GHz band (dBm) */ |
| public static final String KEY_RSSI2 = "rssi2"; |
| public final int[] rssi2 = {-83, -80, -73, -60}; |
| |
| /** RSSI thresholds for 5 GHz band (dBm) */ |
| public static final String KEY_RSSI5 = "rssi5"; |
| public final int[] rssi5 = {-80, -77, -70, -57}; |
| |
| /** Guidelines based on packet rates (packets/sec) */ |
| public static final String KEY_PPS = "pps"; |
| public final int[] pps = {0, 1, 100}; |
| |
| /** Number of seconds for RSSI forecast */ |
| public static final String KEY_HORIZON = "horizon"; |
| public static final int MIN_HORIZON = -9; |
| public static final int MAX_HORIZON = 60; |
| public int horizon = 15; |
| |
| /** Number 0-10 influencing requests for network unreachability detection */ |
| public static final String KEY_NUD = "nud"; |
| public static final int MIN_NUD = 0; |
| public static final int MAX_NUD = 10; |
| public int nud = 8; |
| |
| /** Experiment identifier */ |
| public static final String KEY_EXPID = "expid"; |
| public static final int MIN_EXPID = 0; |
| public static final int MAX_EXPID = Integer.MAX_VALUE; |
| public int expid = 0; |
| |
| Values() { |
| } |
| |
| Values(Values source) { |
| for (int i = 0; i < rssi2.length; i++) { |
| rssi2[i] = source.rssi2[i]; |
| } |
| for (int i = 0; i < rssi5.length; i++) { |
| rssi5[i] = source.rssi5[i]; |
| } |
| for (int i = 0; i < pps.length; i++) { |
| pps[i] = source.pps[i]; |
| } |
| horizon = source.horizon; |
| nud = source.nud; |
| expid = source.expid; |
| } |
| |
| public void validate() throws IllegalArgumentException { |
| validateRssiArray(rssi2); |
| validateRssiArray(rssi5); |
| validateOrderedNonNegativeArray(pps); |
| validateRange(horizon, MIN_HORIZON, MAX_HORIZON); |
| validateRange(nud, MIN_NUD, MAX_NUD); |
| validateRange(expid, MIN_EXPID, MAX_EXPID); |
| } |
| |
| private void validateRssiArray(int[] rssi) throws IllegalArgumentException { |
| int low = WifiInfo.MIN_RSSI; |
| int high = Math.min(WifiInfo.MAX_RSSI, -1); // Stricter than Wifiinfo |
| for (int i = 0; i < rssi.length; i++) { |
| validateRange(rssi[i], low, high); |
| low = rssi[i]; |
| } |
| } |
| |
| private void validateRange(int k, int low, int high) throws IllegalArgumentException { |
| if (k < low || k > high) { |
| throw new IllegalArgumentException(); |
| } |
| } |
| |
| private void validateOrderedNonNegativeArray(int[] a) throws IllegalArgumentException { |
| int low = 0; |
| for (int i = 0; i < a.length; i++) { |
| if (a[i] < low) { |
| throw new IllegalArgumentException(); |
| } |
| low = a[i]; |
| } |
| } |
| |
| public void parseString(String kvList) throws IllegalArgumentException { |
| KeyValueListParser parser = new KeyValueListParser(','); |
| parser.setString(kvList); |
| if (parser.size() != ("" + kvList).split(",").length) { |
| throw new IllegalArgumentException("dup keys"); |
| } |
| updateIntArray(rssi2, parser, KEY_RSSI2); |
| updateIntArray(rssi5, parser, KEY_RSSI5); |
| updateIntArray(pps, parser, KEY_PPS); |
| horizon = updateInt(parser, KEY_HORIZON, horizon); |
| nud = updateInt(parser, KEY_NUD, nud); |
| expid = updateInt(parser, KEY_EXPID, expid); |
| } |
| |
| private int updateInt(KeyValueListParser parser, String key, int defaultValue) |
| throws IllegalArgumentException { |
| String value = parser.getString(key, null); |
| if (value == null) return defaultValue; |
| try { |
| return Integer.parseInt(value); |
| } catch (NumberFormatException e) { |
| throw new IllegalArgumentException(); |
| } |
| } |
| |
| private void updateIntArray(final int[] dest, KeyValueListParser parser, String key) |
| throws IllegalArgumentException { |
| if (parser.getString(key, null) == null) return; |
| int[] ints = parser.getIntArray(key, null); |
| if (ints == null) throw new IllegalArgumentException(); |
| if (ints.length != dest.length) throw new IllegalArgumentException(); |
| for (int i = 0; i < dest.length; i++) { |
| dest[i] = ints[i]; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| appendKey(sb, KEY_RSSI2); |
| appendInts(sb, rssi2); |
| appendKey(sb, KEY_RSSI5); |
| appendInts(sb, rssi5); |
| appendKey(sb, KEY_PPS); |
| appendInts(sb, pps); |
| appendKey(sb, KEY_HORIZON); |
| sb.append(horizon); |
| appendKey(sb, KEY_NUD); |
| sb.append(nud); |
| appendKey(sb, KEY_EXPID); |
| sb.append(expid); |
| return sb.toString(); |
| } |
| |
| private void appendKey(StringBuilder sb, String key) { |
| if (sb.length() != 0) sb.append(","); |
| sb.append(key).append("="); |
| } |
| |
| private void appendInts(StringBuilder sb, final int[] a) { |
| final int n = a.length; |
| for (int i = 0; i < n; i++) { |
| if (i > 0) sb.append(":"); |
| sb.append(a[i]); |
| } |
| } |
| } |
| |
| @NonNull private Values mVal = new Values(); |
| |
| public ScoringParams() { |
| } |
| |
| public ScoringParams(Context context) { |
| loadResources(context); |
| } |
| |
| public ScoringParams(Context context, FrameworkFacade facade, Handler handler) { |
| loadResources(context); |
| setupContentObserver(context, facade, handler); |
| } |
| |
| private void loadResources(Context context) { |
| mVal.rssi2[EXIT] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz); |
| mVal.rssi2[ENTRY] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_24GHz); |
| mVal.rssi2[SUFFICIENT] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz); |
| mVal.rssi2[GOOD] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_24GHz); |
| mVal.rssi5[EXIT] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz); |
| mVal.rssi5[ENTRY] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_5GHz); |
| mVal.rssi5[SUFFICIENT] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz); |
| mVal.rssi5[GOOD] = context.getResources().getInteger( |
| R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_5GHz); |
| try { |
| mVal.validate(); |
| } catch (IllegalArgumentException e) { |
| Log.wtf(TAG, "Inconsistent config_wifi_framework_ resources: " + this, e); |
| } |
| } |
| |
| private void setupContentObserver(Context context, FrameworkFacade facade, Handler handler) { |
| final ScoringParams self = this; |
| String defaults = self.toString(); |
| ContentObserver observer = new ContentObserver(handler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| String params = facade.getStringSetting( |
| context, Settings.Global.WIFI_SCORE_PARAMS); |
| self.update(defaults); |
| if (!self.update(params)) { |
| Log.e(TAG, "Error in " + Settings.Global.WIFI_SCORE_PARAMS + ": " |
| + sanitize(params)); |
| } |
| Log.i(TAG, self.toString()); |
| } |
| }; |
| facade.registerContentObserver(context, |
| Settings.Global.getUriFor(Settings.Global.WIFI_SCORE_PARAMS), |
| true, |
| observer); |
| observer.onChange(false); |
| } |
| |
| private static final String COMMA_KEY_VAL_STAR = "^(,[A-Za-z_][A-Za-z0-9_]*=[0-9.:+-]+)*$"; |
| |
| /** |
| * Updates the parameters from the given parameter string. |
| * If any errors are detected, no change is made. |
| * @param kvList is a comma-separated key=value list. |
| * @return true for success |
| */ |
| public boolean update(String kvList) { |
| if (kvList == null || "".equals(kvList)) { |
| return true; |
| } |
| if (!("," + kvList).matches(COMMA_KEY_VAL_STAR)) { |
| return false; |
| } |
| Values v = new Values(mVal); |
| try { |
| v.parseString(kvList); |
| v.validate(); |
| mVal = v; |
| return true; |
| } catch (IllegalArgumentException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Sanitize a string to make it safe for printing. |
| * @param params is the untrusted string |
| * @return string with questionable characters replaced with question marks |
| */ |
| public String sanitize(String params) { |
| if (params == null) return ""; |
| String printable = params.replaceAll("[^A-Za-z_0-9=,:.+-]", "?"); |
| if (printable.length() > 100) { |
| printable = printable.substring(0, 98) + "..."; |
| } |
| return printable; |
| } |
| |
| /** Constant to denote someplace in the 2.4 GHz band */ |
| public static final int BAND2 = 2400; |
| |
| /** Constant to denote someplace in the 5 GHz band */ |
| public static final int BAND5 = 5000; |
| |
| /** |
| * Returns the RSSI value at which the connection is deemed to be unusable, |
| * in the absence of other indications. |
| */ |
| public int getExitRssi(int frequencyMegaHertz) { |
| return getRssiArray(frequencyMegaHertz)[EXIT]; |
| } |
| |
| /** |
| * Returns the minimum scan RSSI for making a connection attempt. |
| */ |
| public int getEntryRssi(int frequencyMegaHertz) { |
| return getRssiArray(frequencyMegaHertz)[ENTRY]; |
| } |
| |
| /** |
| * Returns a connected RSSI value that indicates the connection is |
| * good enough that we needn't scan for alternatives. |
| */ |
| public int getSufficientRssi(int frequencyMegaHertz) { |
| return getRssiArray(frequencyMegaHertz)[SUFFICIENT]; |
| } |
| |
| /** |
| * Returns a connected RSSI value that indicates a good connection. |
| */ |
| public int getGoodRssi(int frequencyMegaHertz) { |
| return getRssiArray(frequencyMegaHertz)[GOOD]; |
| } |
| |
| /** |
| * Returns the number of seconds to use for rssi forecast. |
| */ |
| public int getHorizonSeconds() { |
| return mVal.horizon; |
| } |
| |
| /** |
| * Returns a packet rate that should be considered acceptable for staying on wifi, |
| * no matter how bad the RSSI gets (packets per second). |
| */ |
| public int getYippeeSkippyPacketsPerSecond() { |
| return mVal.pps[2]; |
| } |
| |
| /** |
| * Returns a number between 0 and 10 inclusive that indicates |
| * how aggressive to be about asking for IP configuration checks |
| * (also known as Network Unreachabilty Detection, or NUD). |
| * |
| * 0 - no nud checks requested by scorer (framework still checks after roam) |
| * 1 - check when score becomes very low |
| * ... |
| * 10 - check when score first breaches threshold, and again as it gets worse |
| * |
| */ |
| public int getNudKnob() { |
| return mVal.nud; |
| } |
| |
| /** |
| * Returns the experiment identifier. |
| * |
| * This value may be used to tag a set of experimental settings. |
| */ |
| public int getExperimentIdentifier() { |
| return mVal.expid; |
| } |
| |
| private int[] getRssiArray(int frequency) { |
| if (frequency < MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) { |
| return mVal.rssi2; |
| } else { |
| return mVal.rssi5; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return mVal.toString(); |
| } |
| } |