Home | History | Annotate | Download | only in wifi
      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 
     17 package com.android.server.wifi;
     18 
     19 import android.annotation.NonNull;
     20 import android.content.Context;
     21 import android.database.ContentObserver;
     22 import android.net.wifi.WifiInfo;
     23 import android.os.Handler;
     24 import android.provider.Settings;
     25 import android.util.KeyValueListParser;
     26 import android.util.Log;
     27 
     28 import 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  */
     37 public 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         /** Guidelines based on packet rates (packets/sec) */
     58         public static final String KEY_PPS = "pps";
     59         public final int[] pps = {0, 1, 100};
     60 
     61         /** Number of seconds for RSSI forecast */
     62         public static final String KEY_HORIZON = "horizon";
     63         public static final int MIN_HORIZON = -9;
     64         public static final int MAX_HORIZON = 60;
     65         public int horizon = 15;
     66 
     67         /** Number 0-10 influencing requests for network unreachability detection */
     68         public static final String KEY_NUD = "nud";
     69         public static final int MIN_NUD = 0;
     70         public static final int MAX_NUD = 10;
     71         public int nud = 8;
     72 
     73         /** Experiment identifier */
     74         public static final String KEY_EXPID = "expid";
     75         public static final int MIN_EXPID = 0;
     76         public static final int MAX_EXPID = Integer.MAX_VALUE;
     77         public int expid = 0;
     78 
     79         Values() {
     80         }
     81 
     82         Values(Values source) {
     83             for (int i = 0; i < rssi2.length; i++) {
     84                 rssi2[i] = source.rssi2[i];
     85             }
     86             for (int i = 0; i < rssi5.length; i++) {
     87                 rssi5[i] = source.rssi5[i];
     88             }
     89             for (int i = 0; i < pps.length; i++) {
     90                 pps[i] = source.pps[i];
     91             }
     92             horizon = source.horizon;
     93             nud = source.nud;
     94             expid = source.expid;
     95         }
     96 
     97         public void validate() throws IllegalArgumentException {
     98             validateRssiArray(rssi2);
     99             validateRssiArray(rssi5);
    100             validateOrderedNonNegativeArray(pps);
    101             validateRange(horizon, MIN_HORIZON, MAX_HORIZON);
    102             validateRange(nud, MIN_NUD, MAX_NUD);
    103             validateRange(expid, MIN_EXPID, MAX_EXPID);
    104         }
    105 
    106         private void validateRssiArray(int[] rssi) throws IllegalArgumentException {
    107             int low = WifiInfo.MIN_RSSI;
    108             int high = Math.min(WifiInfo.MAX_RSSI, -1); // Stricter than Wifiinfo
    109             for (int i = 0; i < rssi.length; i++) {
    110                 validateRange(rssi[i], low, high);
    111                 low = rssi[i];
    112             }
    113         }
    114 
    115         private void validateRange(int k, int low, int high) throws IllegalArgumentException {
    116             if (k < low || k > high) {
    117                 throw new IllegalArgumentException();
    118             }
    119         }
    120 
    121         private void validateOrderedNonNegativeArray(int[] a) throws IllegalArgumentException {
    122             int low = 0;
    123             for (int i = 0; i < a.length; i++) {
    124                 if (a[i] < low) {
    125                     throw new IllegalArgumentException();
    126                 }
    127                 low = a[i];
    128             }
    129         }
    130 
    131         public void parseString(String kvList) throws IllegalArgumentException {
    132             KeyValueListParser parser = new KeyValueListParser(',');
    133             parser.setString(kvList);
    134             if (parser.size() != ("" + kvList).split(",").length) {
    135                 throw new IllegalArgumentException("dup keys");
    136             }
    137             updateIntArray(rssi2, parser, KEY_RSSI2);
    138             updateIntArray(rssi5, parser, KEY_RSSI5);
    139             updateIntArray(pps, parser, KEY_PPS);
    140             horizon = updateInt(parser, KEY_HORIZON, horizon);
    141             nud = updateInt(parser, KEY_NUD, nud);
    142             expid = updateInt(parser, KEY_EXPID, expid);
    143         }
    144 
    145         private int updateInt(KeyValueListParser parser, String key, int defaultValue)
    146                 throws IllegalArgumentException {
    147             String value = parser.getString(key, null);
    148             if (value == null) return defaultValue;
    149             try {
    150                 return Integer.parseInt(value);
    151             } catch (NumberFormatException e) {
    152                 throw new IllegalArgumentException();
    153             }
    154         }
    155 
    156         private void updateIntArray(final int[] dest, KeyValueListParser parser, String key)
    157                 throws IllegalArgumentException {
    158             if (parser.getString(key, null) == null) return;
    159             int[] ints = parser.getIntArray(key, null);
    160             if (ints == null) throw new IllegalArgumentException();
    161             if (ints.length != dest.length) throw new IllegalArgumentException();
    162             for (int i = 0; i < dest.length; i++) {
    163                 dest[i] = ints[i];
    164             }
    165         }
    166 
    167         @Override
    168         public String toString() {
    169             StringBuilder sb = new StringBuilder();
    170             appendKey(sb, KEY_RSSI2);
    171             appendInts(sb, rssi2);
    172             appendKey(sb, KEY_RSSI5);
    173             appendInts(sb, rssi5);
    174             appendKey(sb, KEY_PPS);
    175             appendInts(sb, pps);
    176             appendKey(sb, KEY_HORIZON);
    177             sb.append(horizon);
    178             appendKey(sb, KEY_NUD);
    179             sb.append(nud);
    180             appendKey(sb, KEY_EXPID);
    181             sb.append(expid);
    182             return sb.toString();
    183         }
    184 
    185         private void appendKey(StringBuilder sb, String key) {
    186             if (sb.length() != 0) sb.append(",");
    187             sb.append(key).append("=");
    188         }
    189 
    190         private void appendInts(StringBuilder sb, final int[] a) {
    191             final int n = a.length;
    192             for (int i = 0; i < n; i++) {
    193                 if (i > 0) sb.append(":");
    194                 sb.append(a[i]);
    195             }
    196         }
    197     }
    198 
    199     @NonNull private Values mVal = new Values();
    200 
    201     public ScoringParams() {
    202     }
    203 
    204     public ScoringParams(Context context) {
    205         loadResources(context);
    206     }
    207 
    208     public ScoringParams(Context context, FrameworkFacade facade, Handler handler) {
    209         loadResources(context);
    210         setupContentObserver(context, facade, handler);
    211     }
    212 
    213     private void loadResources(Context context) {
    214         mVal.rssi2[EXIT] = context.getResources().getInteger(
    215                 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_24GHz);
    216         mVal.rssi2[ENTRY] = context.getResources().getInteger(
    217                 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_24GHz);
    218         mVal.rssi2[SUFFICIENT] = context.getResources().getInteger(
    219                 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
    220         mVal.rssi2[GOOD] = context.getResources().getInteger(
    221                 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_24GHz);
    222         mVal.rssi5[EXIT] = context.getResources().getInteger(
    223                 R.integer.config_wifi_framework_wifi_score_bad_rssi_threshold_5GHz);
    224         mVal.rssi5[ENTRY] = context.getResources().getInteger(
    225                 R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_5GHz);
    226         mVal.rssi5[SUFFICIENT] = context.getResources().getInteger(
    227                 R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_5GHz);
    228         mVal.rssi5[GOOD] = context.getResources().getInteger(
    229                 R.integer.config_wifi_framework_wifi_score_good_rssi_threshold_5GHz);
    230         try {
    231             mVal.validate();
    232         } catch (IllegalArgumentException e) {
    233             Log.wtf(TAG, "Inconsistent config_wifi_framework_ resources: " + this, e);
    234         }
    235     }
    236 
    237     private void setupContentObserver(Context context, FrameworkFacade facade, Handler handler) {
    238         final ScoringParams self = this;
    239         String defaults = self.toString();
    240         ContentObserver observer = new ContentObserver(handler) {
    241             @Override
    242             public void onChange(boolean selfChange) {
    243                 String params = facade.getStringSetting(
    244                         context, Settings.Global.WIFI_SCORE_PARAMS);
    245                 self.update(defaults);
    246                 if (!self.update(params)) {
    247                     Log.e(TAG, "Error in " + Settings.Global.WIFI_SCORE_PARAMS + ": "
    248                             + sanitize(params));
    249                 }
    250                 Log.i(TAG, self.toString());
    251             }
    252         };
    253         facade.registerContentObserver(context,
    254                 Settings.Global.getUriFor(Settings.Global.WIFI_SCORE_PARAMS),
    255                 true,
    256                 observer);
    257         observer.onChange(false);
    258     }
    259 
    260     private static final String COMMA_KEY_VAL_STAR = "^(,[A-Za-z_][A-Za-z0-9_]*=[0-9.:+-]+)*$";
    261 
    262     /**
    263      * Updates the parameters from the given parameter string.
    264      * If any errors are detected, no change is made.
    265      * @param kvList is a comma-separated key=value list.
    266      * @return true for success
    267      */
    268     public boolean update(String kvList) {
    269         if (kvList == null || "".equals(kvList)) {
    270             return true;
    271         }
    272         if (!("," + kvList).matches(COMMA_KEY_VAL_STAR)) {
    273             return false;
    274         }
    275         Values v = new Values(mVal);
    276         try {
    277             v.parseString(kvList);
    278             v.validate();
    279             mVal = v;
    280             return true;
    281         } catch (IllegalArgumentException e) {
    282             return false;
    283         }
    284     }
    285 
    286     /**
    287      * Sanitize a string to make it safe for printing.
    288      * @param params is the untrusted string
    289      * @return string with questionable characters replaced with question marks
    290      */
    291     public String sanitize(String params) {
    292         if (params == null) return "";
    293         String printable = params.replaceAll("[^A-Za-z_0-9=,:.+-]", "?");
    294         if (printable.length() > 100) {
    295             printable = printable.substring(0, 98) + "...";
    296         }
    297         return printable;
    298     }
    299 
    300     /** Constant to denote someplace in the 2.4 GHz band */
    301     public static final int BAND2 = 2400;
    302 
    303     /** Constant to denote someplace in the 5 GHz band */
    304     public static final int BAND5 = 5000;
    305 
    306     /**
    307      * Returns the RSSI value at which the connection is deemed to be unusable,
    308      * in the absence of other indications.
    309      */
    310     public int getExitRssi(int frequencyMegaHertz) {
    311         return getRssiArray(frequencyMegaHertz)[EXIT];
    312     }
    313 
    314     /**
    315      * Returns the minimum scan RSSI for making a connection attempt.
    316      */
    317     public int getEntryRssi(int frequencyMegaHertz) {
    318         return getRssiArray(frequencyMegaHertz)[ENTRY];
    319     }
    320 
    321     /**
    322      * Returns a connected RSSI value that indicates the connection is
    323      * good enough that we needn't scan for alternatives.
    324      */
    325     public int getSufficientRssi(int frequencyMegaHertz) {
    326         return getRssiArray(frequencyMegaHertz)[SUFFICIENT];
    327     }
    328 
    329     /**
    330      * Returns a connected RSSI value that indicates a good connection.
    331      */
    332     public int getGoodRssi(int frequencyMegaHertz) {
    333         return getRssiArray(frequencyMegaHertz)[GOOD];
    334     }
    335 
    336     /**
    337      * Returns the number of seconds to use for rssi forecast.
    338      */
    339     public int getHorizonSeconds() {
    340         return mVal.horizon;
    341     }
    342 
    343     /**
    344      * Returns a packet rate that should be considered acceptable for staying on wifi,
    345      * no matter how bad the RSSI gets (packets per second).
    346      */
    347     public int getYippeeSkippyPacketsPerSecond() {
    348         return mVal.pps[2];
    349     }
    350 
    351     /**
    352      * Returns a number between 0 and 10 inclusive that indicates
    353      * how aggressive to be about asking for IP configuration checks
    354      * (also known as Network Unreachabilty Detection, or NUD).
    355      *
    356      * 0 - no nud checks requested by scorer (framework still checks after roam)
    357      * 1 - check when score becomes very low
    358      *     ...
    359      * 10 - check when score first breaches threshold, and again as it gets worse
    360      *
    361      */
    362     public int getNudKnob() {
    363         return mVal.nud;
    364     }
    365 
    366     /**
    367      * Returns the experiment identifier.
    368      *
    369      * This value may be used to tag a set of experimental settings.
    370      */
    371     public int getExperimentIdentifier() {
    372         return mVal.expid;
    373     }
    374 
    375     private static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000;
    376 
    377     private int[] getRssiArray(int frequency) {
    378         if (frequency < MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ) {
    379             return mVal.rssi2;
    380         } else {
    381             return mVal.rssi5;
    382         }
    383     }
    384 
    385     @Override
    386     public String toString() {
    387         return mVal.toString();
    388     }
    389 }
    390