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