blob: 8f68627b7558beff378ee347ea84188f02b25a80 [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.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 {
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 static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000;
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();
}
}