blob: a860542f84054af41358e29cd4a35e4bc85f54b1 [file] [log] [blame]
/*
* 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.ScanResult;
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 {
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 = 40;
public int lastSelectionMinutes = 480;
public int estimateRssiErrorMargin = 5;
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.estimateRssiErrorMargin = context.getResources().getInteger(
R.integer.config_wifiEstimateRssiErrorMarginDb);
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;
}
/**
* 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;
}
/**
* Returns the estimate rssi error margin to account minor differences in the environment
* and the device's orientation.
*
*/
public int getEstimateRssiErrorMargin() {
return mVal.estimateRssiErrorMargin;
}
/**
*/
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 (ScanResult.is24GHz(frequency)) {
return mVal.rssi2;
} else if (ScanResult.is5GHz(frequency)) {
return mVal.rssi5;
} else if (ScanResult.is6GHz(frequency)) {
return mVal.rssi6;
}
// Invalid frequency use
Log.e(TAG, "Invalid frequency(" + frequency + "), using 5G as default rssi array");
return mVal.rssi5;
}
@Override
public String toString() {
loadResources(mContext);
return mVal.toString();
}
}