| /* |
| * 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.net.wifi.WifiInfo; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.server.wifi.util.KeyValueListParser; |
| import com.android.wifi.resources.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; |
| public static final int MINIMUM_6GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5925; |
| |
| private final Context mContext; |
| |
| 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; |
| |
| private static final int ACTIVE_TRAFFIC = 1; |
| private static final int HIGH_TRAFFIC = 2; |
| /** |
| * 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}; |
| |
| /** RSSI thresholds for 6 GHz band (dBm) */ |
| public static final String KEY_RSSI6 = "rssi6"; |
| public final int[] rssi6 = {-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; |
| |
| /** CandidateScorer parameters */ |
| public int throughputBonusNumerator = 120; |
| public int throughputBonusDenominator = 433; |
| public int throughputBonusLimit = 200; |
| public int savedNetworkBonus = 500; |
| public int unmeteredNetworkBonus = 1000; |
| public int currentNetworkBonusMin = 20; |
| public int currentNetworkBonusPercent = 20; |
| public int secureNetworkBonus = 10; |
| public int lastSelectionMinutes = 480; |
| public static final int MIN_MINUTES = 1; |
| public static final int MAX_MINUTES = Integer.MAX_VALUE / (60 * 1000); |
| |
| 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 < rssi6.length; i++) { |
| rssi6[i] = source.rssi6[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); |
| validateRssiArray(rssi6); |
| validateOrderedNonNegativeArray(pps); |
| validateRange(horizon, MIN_HORIZON, MAX_HORIZON); |
| validateRange(nud, MIN_NUD, MAX_NUD); |
| validateRange(expid, MIN_EXPID, MAX_EXPID); |
| validateRange(lastSelectionMinutes, MIN_MINUTES, MAX_MINUTES); |
| } |
| |
| 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(rssi6, parser, KEY_RSSI6); |
| 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_RSSI6); |
| appendInts(sb, rssi6); |
| 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 = null; |
| |
| @VisibleForTesting |
| public ScoringParams() { |
| mContext = null; |
| mVal = new Values(); |
| } |
| |
| public ScoringParams(Context context) { |
| mContext = context; |
| } |
| |
| private void loadResources(Context context) { |
| if (mVal != null) return; |
| mVal = new Values(); |
| 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); |
| mVal.rssi6[EXIT] = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkScoreBadRssiThreshold6ghz); |
| mVal.rssi6[ENTRY] = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkScoreEntryRssiThreshold6ghz); |
| mVal.rssi6[SUFFICIENT] = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkScoreLowRssiThreshold6ghz); |
| mVal.rssi6[GOOD] = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkScoreGoodRssiThreshold6ghz); |
| mVal.throughputBonusNumerator = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkThroughputBonusNumerator); |
| mVal.throughputBonusDenominator = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkThroughputBonusDenominator); |
| mVal.throughputBonusLimit = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkThroughputBonusLimit); |
| mVal.savedNetworkBonus = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkSavedNetworkBonus); |
| mVal.unmeteredNetworkBonus = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkUnmeteredNetworkBonus); |
| mVal.currentNetworkBonusMin = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkCurrentNetworkBonusMin); |
| mVal.currentNetworkBonusPercent = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkCurrentNetworkBonusPercent); |
| mVal.secureNetworkBonus = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkSecureNetworkBonus); |
| mVal.lastSelectionMinutes = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkLastSelectionMinutes); |
| mVal.pps[ACTIVE_TRAFFIC] = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkMinPacketPerSecondActiveTraffic); |
| mVal.pps[HIGH_TRAFFIC] = context.getResources().getInteger( |
| R.integer.config_wifiFrameworkMinPacketPerSecondHighTraffic); |
| try { |
| mVal.validate(); |
| } catch (IllegalArgumentException e) { |
| Log.wtf(TAG, "Inconsistent config_wifi_framework_ resources: " + this, e); |
| } |
| } |
| |
| 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 |
| */ |
| @VisibleForTesting |
| public boolean update(String kvList) { |
| if (kvList == null || "".equals(kvList)) { |
| return true; |
| } |
| if (!("," + kvList).matches(COMMA_KEY_VAL_STAR)) { |
| return false; |
| } |
| loadResources(mContext); |
| 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; |
| |
| /** Constant to denote someplace in the 6 GHz band */ |
| public static final int BAND6 = 6000; |
| |
| /** |
| * 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() { |
| loadResources(mContext); |
| 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() { |
| loadResources(mContext); |
| return mVal.pps[HIGH_TRAFFIC]; |
| } |
| |
| /** |
| * Returns a packet rate that should be considered acceptable to skip scan or network selection |
| */ |
| public int getActiveTrafficPacketsPerSecond() { |
| loadResources(mContext); |
| return mVal.pps[ACTIVE_TRAFFIC]; |
| } |
| |
| /** |
| * 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() { |
| loadResources(mContext); |
| return mVal.nud; |
| } |
| |
| /** |
| */ |
| public int getThroughputBonusNumerator() { |
| return mVal.throughputBonusNumerator; |
| } |
| |
| /** |
| */ |
| public int getThroughputBonusDenominator() { |
| return mVal.throughputBonusDenominator; |
| } |
| |
| /* |
| * Returns the maximum bonus for the network selection candidate score |
| * for the contribution of the selected score. |
| */ |
| public int getThroughputBonusLimit() { |
| return mVal.throughputBonusLimit; |
| } |
| |
| /* |
| * Returns the bonus for the network selection candidate score |
| * for a saved network (i.e., not a suggestion). |
| */ |
| public int getSavedNetworkBonus() { |
| return mVal.savedNetworkBonus; |
| } |
| |
| /* |
| * Returns the bonus for the network selection candidate score |
| * for an unmetered network. |
| */ |
| public int getUnmeteredNetworkBonus() { |
| return mVal.unmeteredNetworkBonus; |
| } |
| |
| /* |
| * Returns the minimum bonus for the network selection candidate score |
| * for the currently connected network. |
| */ |
| public int getCurrentNetworkBonusMin() { |
| return mVal.currentNetworkBonusMin; |
| } |
| |
| /* |
| * Returns the percentage bonus for the network selection candidate score |
| * for the currently connected network. The percent value is applied to rssi score and |
| * throughput score; |
| */ |
| public int getCurrentNetworkBonusPercent() { |
| return mVal.currentNetworkBonusPercent; |
| } |
| |
| /* |
| * Returns the bonus for the network selection candidate score |
| * for a secure network. |
| */ |
| public int getSecureNetworkBonus() { |
| return mVal.secureNetworkBonus; |
| } |
| |
| /* |
| * Returns the duration in minutes for a recently selected network |
| * to be strongly favored. |
| */ |
| public int getLastSelectionMinutes() { |
| return mVal.lastSelectionMinutes; |
| } |
| |
| /** |
| * Returns the experiment identifier. |
| * |
| * This value may be used to tag a set of experimental settings. |
| */ |
| public int getExperimentIdentifier() { |
| loadResources(mContext); |
| return mVal.expid; |
| } |
| |
| private int[] getRssiArray(int frequency) { |
| loadResources(mContext); |
| if (frequency < MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) { |
| return mVal.rssi2; |
| } else if (frequency < MINIMUM_6GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) { |
| return mVal.rssi5; |
| } else { |
| return mVal.rssi6; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| loadResources(mContext); |
| return mVal.toString(); |
| } |
| } |