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