WifiQualifiedNetworkSelector.java revision 60969bf2c849011fb585cf6dc914dbd779dfb8cf
1/*
2 * Copyright (C) 2015 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.content.Context;
20import android.net.NetworkKey;
21import android.net.NetworkScoreManager;
22import android.net.WifiKey;
23import android.net.wifi.ScanResult;
24import android.net.wifi.WifiConfiguration;
25import android.net.wifi.WifiInfo;
26import android.net.wifi.WifiManager;
27import android.text.TextUtils;
28import android.util.LocalLog;
29import android.util.Log;
30
31import com.android.internal.R;
32import com.android.internal.annotations.VisibleForTesting;
33
34import java.io.FileDescriptor;
35import java.io.PrintWriter;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.Iterator;
39import java.util.List;
40import java.util.Map;
41
42/**
43 * This class looks at all the connectivity scan results then
44 * select an network for the phone to connect/roam to.
45 */
46public class WifiQualifiedNetworkSelector {
47    private WifiConfigManager mWifiConfigManager;
48    private WifiInfo mWifiInfo;
49    private NetworkScoreManager mScoreManager;
50    private WifiNetworkScoreCache mNetworkScoreCache;
51    private Clock mClock;
52    private static final String TAG = "WifiQualifiedNetworkSelector:";
53    private boolean mDbg = true;
54    private WifiConfiguration mCurrentConnectedNetwork = null;
55    private String mCurrentBssid = null;
56    //buffer most recent scan results
57    private List<ScanDetail> mScanDetails = null;
58    //buffer of filtered scan results (Scan results considered by network selection)
59    private volatile List<ScanDetail> mFilteredScanDetails = null;
60
61    //Minimum time gap between last successful Qualified Network Selection and new selection attempt
62    //usable only when current state is connected state   default 10 s
63    private static final int MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL = 10 * 1000;
64
65    //if current network is on 2.4GHz band and has a RSSI over this, need not new network selection
66    public static final int QUALIFIED_RSSI_24G_BAND = -73;
67    //if current network is on 5GHz band and has a RSSI over this, need not new network selection
68    public static final int QUALIFIED_RSSI_5G_BAND = -70;
69    //any RSSI larger than this will benefit the traffic very limited
70    public static final int RSSI_SATURATION_2G_BAND = -60;
71    public static final int RSSI_SATURATION_5G_BAND = -57;
72    //Any value below this will be considered not usable
73    public static final int MINIMUM_2G_ACCEPT_RSSI = -85;
74    public static final int MINIMUM_5G_ACCEPT_RSSI = -82;
75
76    public static final int RSSI_SCORE_SLOPE = 4;
77    public static final int RSSI_SCORE_OFFSET = 85;
78
79    public static final int BAND_AWARD_5GHz = 40;
80    public static final int SAME_NETWORK_AWARD = 16;
81
82    public static final int SAME_BSSID_AWARD = 24;
83    public static final int LAST_SELECTION_AWARD = 480;
84    public static final int PASSPOINT_SECURITY_AWARD = 40;
85    public static final int SECURITY_AWARD = 80;
86    public static final int BSSID_BLACKLIST_THRESHOLD = 3;
87    public static final int BSSID_BLACKLIST_EXPIRE_TIME = 30 * 60 * 1000;
88    private final int mNoIntnetPenalty;
89    //TODO: check whether we still need this one when we update the scan manager
90    public static final int SCAN_RESULT_MAXIMUNM_AGE = 40000;
91    private static final int INVALID_TIME_STAMP = -1;
92    private long mLastQualifiedNetworkSelectionTimeStamp = INVALID_TIME_STAMP;
93
94    // Temporarily, for dog food
95    private final LocalLog mLocalLog = new LocalLog(16384);
96    private int mRssiScoreSlope = RSSI_SCORE_SLOPE;
97    private int mRssiScoreOffset = RSSI_SCORE_OFFSET;
98    private int mSameBssidAward = SAME_BSSID_AWARD;
99    private int mLastSelectionAward = LAST_SELECTION_AWARD;
100    private int mPasspointSecurityAward = PASSPOINT_SECURITY_AWARD;
101    private int mSecurityAward = SECURITY_AWARD;
102    private int mUserPreferedBand = WifiManager.WIFI_FREQUENCY_BAND_AUTO;
103    private Map<String, BssidBlacklistStatus> mBssidBlacklist =
104            new HashMap<String, BssidBlacklistStatus>();
105
106    /**
107     * class save the blacklist status of a given BSSID
108     */
109    private static class BssidBlacklistStatus {
110        //how many times it is requested to be blacklisted (association rejection trigger this)
111        int mCounter;
112        boolean mIsBlacklisted;
113        long mBlacklistedTimeStamp = INVALID_TIME_STAMP;
114    }
115
116    private void qnsLog(String log) {
117        if (mDbg) {
118            mLocalLog.log(log);
119        }
120    }
121
122    private void qnsLoge(String log) {
123        mLocalLog.log(log);
124    }
125
126    @VisibleForTesting
127    void setWifiNetworkScoreCache(WifiNetworkScoreCache cache) {
128        mNetworkScoreCache = cache;
129    }
130
131    /**
132     * @return current target connected network
133     */
134    public WifiConfiguration getConnetionTargetNetwork() {
135        return mCurrentConnectedNetwork;
136    }
137
138    /**
139     * @return the list of ScanDetails scored as potential candidates by the last run of
140     * selectQualifiedNetwork, this will be empty if QNS determined no selection was needed on last
141     * run. This includes scan details of sufficient signal strength, and had an associated
142     * WifiConfiguration.
143     */
144    public List<ScanDetail> getFilteredScanDetails() {
145        return mFilteredScanDetails;
146    }
147
148    /**
149     * set the user selected preferred band
150     *
151     * @param band preferred band user selected
152     */
153    public void setUserPreferredBand(int band) {
154        mUserPreferedBand = band;
155    }
156
157    WifiQualifiedNetworkSelector(WifiConfigManager configureStore, Context context,
158            WifiInfo wifiInfo, Clock clock) {
159        mWifiConfigManager = configureStore;
160        mWifiInfo = wifiInfo;
161        mClock = clock;
162        mScoreManager =
163                (NetworkScoreManager) context.getSystemService(Context.NETWORK_SCORE_SERVICE);
164        if (mScoreManager != null) {
165            mNetworkScoreCache = new WifiNetworkScoreCache(context);
166            mScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI, mNetworkScoreCache);
167        } else {
168            qnsLoge("No network score service: Couldn't register as a WiFi score Manager, type="
169                    + NetworkKey.TYPE_WIFI + " service= " + Context.NETWORK_SCORE_SERVICE);
170            mNetworkScoreCache = null;
171        }
172
173        mRssiScoreSlope = context.getResources().getInteger(
174                R.integer.config_wifi_framework_RSSI_SCORE_SLOPE);
175        mRssiScoreOffset = context.getResources().getInteger(
176                R.integer.config_wifi_framework_RSSI_SCORE_OFFSET);
177        mSameBssidAward = context.getResources().getInteger(
178                R.integer.config_wifi_framework_SAME_BSSID_AWARD);
179        mLastSelectionAward = context.getResources().getInteger(
180                R.integer.config_wifi_framework_LAST_SELECTION_AWARD);
181        mPasspointSecurityAward = context.getResources().getInteger(
182                R.integer.config_wifi_framework_PASSPOINT_SECURITY_AWARD);
183        mSecurityAward = context.getResources().getInteger(
184                R.integer.config_wifi_framework_SECURITY_AWARD);
185        mNoIntnetPenalty = (mWifiConfigManager.mThresholdSaturatedRssi24.get() + mRssiScoreOffset)
186                * mRssiScoreSlope + mWifiConfigManager.mBandAward5Ghz.get()
187                + mWifiConfigManager.mCurrentNetworkBoost.get() + mSameBssidAward + mSecurityAward;
188    }
189
190    void enableVerboseLogging(int verbose) {
191        mDbg = verbose > 0;
192    }
193
194    private String getNetworkString(WifiConfiguration network) {
195        if (network == null) {
196            return null;
197        }
198
199        return (network.SSID + ":" + network.networkId);
200
201    }
202
203    /**
204     * check whether current network is good enough we need not consider any potential switch
205     *
206     * @param currentNetwork -- current connected network
207     * @return true -- qualified and do not consider potential network switch
208     *         false -- not good enough and should try potential network switch
209     */
210    private boolean isNetworkQualified(WifiConfiguration currentNetwork) {
211
212        if (currentNetwork == null) {
213            qnsLog("Disconnected");
214            return false;
215        } else {
216            qnsLog("Current network is: " + currentNetwork.SSID + " ,ID is: "
217                    + currentNetwork.networkId);
218        }
219
220        //if current connected network is an ephemeral network,we will consider
221        // there is no current network
222        if (currentNetwork.ephemeral) {
223            qnsLog("Current is ephemeral. Start reselect");
224            return false;
225        }
226
227        //if current network is open network, not qualified
228        if (mWifiConfigManager.isOpenNetwork(currentNetwork)) {
229            qnsLog("Current network is open network");
230            return false;
231        }
232
233        // Current network band must match with user preference selection
234        if (mWifiInfo.is24GHz() && (mUserPreferedBand != WifiManager.WIFI_FREQUENCY_BAND_2GHZ)) {
235            qnsLog("Current band dose not match user preference. Start Qualified Network"
236                    + " Selection Current band = " + (mWifiInfo.is24GHz() ? "2.4GHz band"
237                    : "5GHz band") + "UserPreference band = " + mUserPreferedBand);
238            return false;
239        }
240
241        int currentRssi = mWifiInfo.getRssi();
242        if ((mWifiInfo.is24GHz()
243                        && currentRssi < mWifiConfigManager.mThresholdQualifiedRssi24.get())
244                || (mWifiInfo.is5GHz()
245                        && currentRssi < mWifiConfigManager.mThresholdQualifiedRssi5.get())) {
246            qnsLog("Current band = " + (mWifiInfo.is24GHz() ? "2.4GHz band" : "5GHz band")
247                    + "current RSSI is: " + currentRssi);
248            return false;
249        }
250
251        return true;
252    }
253
254    /**
255     * check whether QualifiedNetworkSelection is needed or not
256     *
257     * @param isLinkDebouncing true -- Link layer is under debouncing
258     *                         false -- Link layer is not under debouncing
259     * @param isConnected true -- device is connected to an AP currently
260     *                    false -- device is not connected to an AP currently
261     * @param isDisconnected true -- WifiStateMachine is at disconnected state
262     *                       false -- WifiStateMachine is not at disconnected state
263     * @param isSupplicantTransientState true -- supplicant is in a transient state now
264     *                                   false -- supplicant is not in a transient state now
265     * @return true -- need a Qualified Network Selection procedure
266     *         false -- do not need a QualifiedNetworkSelection procedure
267     */
268    private boolean needQualifiedNetworkSelection(boolean isLinkDebouncing, boolean isConnected,
269            boolean isDisconnected, boolean isSupplicantTransientState) {
270        if (mScanDetails.size() == 0) {
271            qnsLog("empty scan result");
272            return false;
273        }
274
275        // Do not trigger Qualified Network Selection during L2 link debouncing procedure
276        if (isLinkDebouncing) {
277            qnsLog("Need not Qualified Network Selection during L2 debouncing");
278            return false;
279        }
280
281        if (isConnected) {
282            //already connected. Just try to find better candidate
283            //if switch network is not allowed in connected mode, do not trigger Qualified Network
284            //Selection
285            if (!mWifiConfigManager.getEnableAutoJoinWhenAssociated()) {
286                qnsLog("Switch network under connection is not allowed");
287                return false;
288            }
289
290            //Do not select again if last selection is within
291            //MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL
292            if (mLastQualifiedNetworkSelectionTimeStamp != INVALID_TIME_STAMP) {
293                long gap = mClock.currentTimeMillis() - mLastQualifiedNetworkSelectionTimeStamp;
294                if (gap < MINIMUM_QUALIFIED_NETWORK_SELECTION_INTERVAL) {
295                    qnsLog("Too short to last successful Qualified Network Selection Gap is:" + gap
296                            + " ms!");
297                    return false;
298                }
299            }
300
301            WifiConfiguration currentNetwork =
302                    mWifiConfigManager.getWifiConfiguration(mWifiInfo.getNetworkId());
303            if (currentNetwork == null) {
304                // WifiStateMachine in connected state but WifiInfo is not. It means there is a race
305                // condition happened. Do not make QNS until WifiStateMachine goes into
306                // disconnected state
307                return false;
308            }
309
310            if (mCurrentConnectedNetwork != null
311                    && mCurrentConnectedNetwork.networkId != currentNetwork.networkId) {
312                //If this happens, supplicant switch the connection silently. This is a bug
313                // FIXME: 11/10/15
314                qnsLoge("supplicant switched the network silently" + " last Qualified Network"
315                        + " Selection:" + getNetworkString(mCurrentConnectedNetwork)
316                        + " current network:" + getNetworkString(currentNetwork));
317                mCurrentConnectedNetwork = currentNetwork;
318                mCurrentBssid = mWifiInfo.getBSSID();
319                //We do not believe lower layer choice
320                return true;
321            }
322
323            String bssid = mWifiInfo.getBSSID();
324            if (mCurrentBssid != null && !mCurrentBssid.equals(bssid)) {
325                //If this happens, supplicant roamed silently. This is a bug
326                // FIXME: 11/10/15
327                qnsLoge("supplicant roamed silently. Last selected BSSID:" + mCurrentBssid
328                        + " current BSSID:" + bssid);
329                mCurrentBssid = mWifiInfo.getBSSID();
330                //We do not believe lower layer choice
331                return true;
332            }
333
334            if (!isNetworkQualified(mCurrentConnectedNetwork)) {
335                //need not trigger Qualified Network Selection if current network is qualified
336                qnsLog("Current network is not qualified");
337                return true;
338            } else {
339                return false;
340            }
341        } else if (isDisconnected) {
342            mCurrentConnectedNetwork = null;
343            mCurrentBssid = null;
344            //Do not start Qualified Network Selection if current state is a transient state
345            if (isSupplicantTransientState) {
346                return false;
347            }
348        } else {
349            //Do not allow new network selection in other state
350            qnsLog("WifiStateMachine is not on connected or disconnected state");
351            return false;
352        }
353
354        return true;
355    }
356
357    int calculateBssidScore(ScanResult scanResult, WifiConfiguration network,
358            WifiConfiguration currentNetwork, boolean sameBssid, boolean sameSelect,
359            StringBuffer sbuf) {
360
361        int score = 0;
362        //calculate the RSSI score
363        int rssi = scanResult.level <= mWifiConfigManager.mThresholdSaturatedRssi24.get()
364                ? scanResult.level : mWifiConfigManager.mThresholdSaturatedRssi24.get();
365        score += (rssi + mRssiScoreOffset) * mRssiScoreSlope;
366        sbuf.append(" RSSI score: " +  score);
367        if (scanResult.is5GHz()) {
368            //5GHz band
369            score += mWifiConfigManager.mBandAward5Ghz.get();
370            sbuf.append(" 5GHz bonus: " + mWifiConfigManager.mBandAward5Ghz.get());
371        }
372
373        //last user selection award
374        if (sameSelect) {
375            long timeDifference = mClock.currentTimeMillis()
376                    - mWifiConfigManager.getLastSelectedTimeStamp();
377
378            if (timeDifference > 0) {
379                int bonus = mLastSelectionAward - (int) (timeDifference / 1000 / 60);
380                score += bonus > 0 ? bonus : 0;
381                sbuf.append(" User selected it last time " + (timeDifference / 1000 / 60)
382                        + " minutes ago, bonus:" + bonus);
383            }
384        }
385
386        //same network award
387        if (network == currentNetwork || network.isLinked(currentNetwork)) {
388            score += mWifiConfigManager.mCurrentNetworkBoost.get();
389            sbuf.append(" Same network with current associated. Bonus: "
390                    + mWifiConfigManager.mCurrentNetworkBoost.get());
391        }
392
393        //same BSSID award
394        if (sameBssid) {
395            score += mSameBssidAward;
396            sbuf.append(" Same BSSID with current association. Bonus: " + mSameBssidAward);
397        }
398
399        //security award
400        if (network.isPasspoint()) {
401            score += mPasspointSecurityAward;
402            sbuf.append(" Passpoint Bonus:" + mPasspointSecurityAward);
403        } else if (!mWifiConfigManager.isOpenNetwork(network)) {
404            score += mSecurityAward;
405            sbuf.append(" Secure network Bonus:" + mSecurityAward);
406        }
407
408        //Penalty for no internet network. Make sure if there is any network with Internet,
409        //however, if there is no any other network with internet, this network can be chosen
410        if (network.numNoInternetAccessReports > 0 && !network.validatedInternetAccess) {
411            score -= mNoIntnetPenalty;
412            sbuf.append(" No internet Penalty:-" + mNoIntnetPenalty);
413        }
414
415
416        sbuf.append(" Score for scanResult: " + scanResult +  " and Network ID: "
417                + network.networkId + " final score:" + score + "\n\n");
418
419        return score;
420    }
421
422    /**
423     * This API try to update all the saved networks' network selection status
424     */
425    private void updateSavedNetworkSelectionStatus() {
426        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
427        if (savedNetworks.size() == 0) {
428            qnsLog("no saved network");
429            return;
430        }
431
432        StringBuffer sbuf = new StringBuffer("Saved Network List\n");
433        for (WifiConfiguration network : savedNetworks) {
434            WifiConfiguration config = mWifiConfigManager.getWifiConfiguration(network.networkId);
435            WifiConfiguration.NetworkSelectionStatus status =
436                    config.getNetworkSelectionStatus();
437
438            //If the configuration is temporarily disabled, try to re-enable it
439            if (status.isNetworkTemporaryDisabled()) {
440                mWifiConfigManager.tryEnableQualifiedNetwork(network.networkId);
441            }
442
443            //clean the cached candidate, score and seen
444            status.setCandidate(null);
445            status.setCandidateScore(Integer.MIN_VALUE);
446            status.setSeenInLastQualifiedNetworkSelection(false);
447
448            //print the debug messages
449            sbuf.append("    " + getNetworkString(network) + " " + " User Preferred BSSID:"
450                    + network.BSSID + " FQDN:" + network.FQDN + " "
451                    + status.getNetworkStatusString() + " Disable account: ");
452            for (int index = status.NETWORK_SELECTION_ENABLE;
453                    index < status.NETWORK_SELECTION_DISABLED_MAX; index++) {
454                sbuf.append(status.getDisableReasonCounter(index) + " ");
455            }
456            sbuf.append("Connect Choice:" + status.getConnectChoice() + " set time:"
457                    + status.getConnectChoiceTimestamp());
458            sbuf.append("\n");
459        }
460        qnsLog(sbuf.toString());
461    }
462
463    /**
464     * This API is called when user explicitly select a network. Currently, it is used in following
465     * cases:
466     * (1) User explicitly choose to connect to a saved network
467     * (2) User save a network after add a new network
468     * (3) User save a network after modify a saved network
469     * Following actions will be triggered:
470     * 1. if this network is disabled, we need re-enable it again
471     * 2. we considered user prefer this network over all the networks visible in latest network
472     *    selection procedure
473     *
474     * @param netId new network ID for either the network the user choose or add
475     * @param persist whether user has the authority to overwrite current connect choice
476     * @return true -- There is change made to connection choice of any saved network
477     *         false -- There is no change made to connection choice of any saved network
478     */
479    public boolean userSelectNetwork(int netId, boolean persist) {
480        WifiConfiguration selected = mWifiConfigManager.getWifiConfiguration(netId);
481        qnsLog("userSelectNetwork:" + netId + " persist:" + persist);
482        if (selected == null || selected.SSID == null) {
483            qnsLoge("userSelectNetwork: Bad configuration with nid=" + netId);
484            return false;
485        }
486
487
488        if (!selected.getNetworkSelectionStatus().isNetworkEnabled()) {
489            mWifiConfigManager.updateNetworkSelectionStatus(netId,
490                    WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE);
491        }
492
493        if (!persist) {
494            qnsLog("User has no privilege to overwrite the current priority");
495            return false;
496        }
497
498        boolean change = false;
499        String key = selected.configKey();
500        long currentTime = mClock.currentTimeMillis();
501        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
502
503        for (WifiConfiguration network : savedNetworks) {
504            WifiConfiguration config = mWifiConfigManager.getWifiConfiguration(network.networkId);
505            WifiConfiguration.NetworkSelectionStatus status = config.getNetworkSelectionStatus();
506            if (config.networkId == selected.networkId) {
507                if (status.getConnectChoice() != null) {
508                    qnsLog("Remove user selection preference of " + status.getConnectChoice()
509                            + " Set Time: " + status.getConnectChoiceTimestamp() + " from "
510                            + config.SSID + " : " + config.networkId);
511                    status.setConnectChoice(null);
512                    status.setConnectChoiceTimestamp(WifiConfiguration.NetworkSelectionStatus
513                            .INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP);
514                    change = true;
515                }
516                continue;
517            }
518
519            if (status.getSeenInLastQualifiedNetworkSelection()
520                    && (status.getConnectChoice() == null
521                    || !status.getConnectChoice().equals(key))) {
522                qnsLog("Add key:" + key + " Set Time: " + currentTime + " to "
523                        + getNetworkString(config));
524                status.setConnectChoice(key);
525                status.setConnectChoiceTimestamp(currentTime);
526                change = true;
527            }
528        }
529        //Write this change to file
530        if (change) {
531            mWifiConfigManager.writeKnownNetworkHistory();
532            return true;
533        }
534
535        return false;
536    }
537
538    /**
539     * enable/disable a BSSID for Quality Network Selection
540     * When an association rejection event is obtained, Quality Network Selector will disable this
541     * BSSID but supplicant still can try to connect to this bssid. If supplicant connect to it
542     * successfully later, this bssid can be re-enabled.
543     *
544     * @param bssid the bssid to be enabled / disabled
545     * @param enable -- true enable a bssid if it has been disabled
546     *               -- false disable a bssid
547     */
548    public boolean enableBssidForQualityNetworkSelection(String bssid, boolean enable) {
549        if (enable) {
550            return (mBssidBlacklist.remove(bssid) != null);
551        } else {
552            if (bssid != null) {
553                BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
554                if (status == null) {
555                    //first time
556                    BssidBlacklistStatus newStatus = new BssidBlacklistStatus();
557                    newStatus.mCounter++;
558                    mBssidBlacklist.put(bssid, newStatus);
559                } else if (!status.mIsBlacklisted) {
560                    status.mCounter++;
561                    if (status.mCounter >= BSSID_BLACKLIST_THRESHOLD) {
562                        status.mIsBlacklisted = true;
563                        status.mBlacklistedTimeStamp = mClock.currentTimeMillis();
564                        return true;
565                    }
566                }
567            }
568        }
569        return false;
570    }
571
572    /**
573     * update the buffered BSSID blacklist
574     *
575     * Go through the whole buffered BSSIDs blacklist and check when the BSSIDs is blocked. If they
576     * were blacked before BSSID_BLACKLIST_EXPIRE_TIME, re-enable it again.
577     */
578    private void updateBssidBlacklist() {
579        Iterator<BssidBlacklistStatus> iter = mBssidBlacklist.values().iterator();
580        while (iter.hasNext()) {
581            BssidBlacklistStatus status = iter.next();
582            if (status != null && status.mIsBlacklisted) {
583                if (mClock.currentTimeMillis() - status.mBlacklistedTimeStamp
584                            >= BSSID_BLACKLIST_EXPIRE_TIME) {
585                    iter.remove();
586                }
587            }
588        }
589    }
590
591    /**
592     * Check whether a bssid is disabled
593     * @param bssid -- the bssid to check
594     * @return true -- bssid is disabled
595     *         false -- bssid is not disabled
596     */
597    public boolean isBssidDisabled(String bssid) {
598        BssidBlacklistStatus status = mBssidBlacklist.get(bssid);
599        return status == null ? false : status.mIsBlacklisted;
600    }
601
602    /**
603     * ToDo: This should be called in Connectivity Manager when it gets new scan result
604     * check whether a network slection is needed. If need, check all the new scan results and
605     * select a new qualified network/BSSID to connect to
606     *
607     * @param forceSelectNetwork true -- start a qualified network selection anyway,no matter
608     *                           current network is already qualified or not.
609     *                           false -- if current network is already qualified, do not do new
610     *                           selection
611     * @param isUntrustedConnectionsAllowed true -- user allow to connect to untrusted network
612     *                                      false -- user do not allow to connect to untrusted
613     *                                      network
614     * @param scanDetails latest scan result obtained (should be connectivity scan only)
615     * @param isLinkDebouncing true -- Link layer is under debouncing
616     *                         false -- Link layer is not under debouncing
617     * @param isConnected true -- device is connected to an AP currently
618     *                    false -- device is not connected to an AP currently
619     * @param isDisconnected true -- WifiStateMachine is at disconnected state
620     *                       false -- WifiStateMachine is not at disconnected state
621     * @param isSupplicantTransient true -- supplicant is in a transient state
622     *                              false -- supplicant is not in a transient state
623     * @return the qualified network candidate found. If no available candidate, return null
624     */
625    public WifiConfiguration selectQualifiedNetwork(boolean forceSelectNetwork ,
626            boolean isUntrustedConnectionsAllowed, List<ScanDetail>  scanDetails,
627            boolean isLinkDebouncing, boolean isConnected, boolean isDisconnected,
628            boolean isSupplicantTransient) {
629        qnsLog("==========start qualified Network Selection==========");
630        mScanDetails = scanDetails;
631        List<ScanDetail>  filteredScanDetails = new ArrayList<>();
632        if (mCurrentConnectedNetwork == null) {
633            mCurrentConnectedNetwork =
634                    mWifiConfigManager.getWifiConfiguration(mWifiInfo.getNetworkId());
635        }
636
637        if (mCurrentBssid == null) {
638            mCurrentBssid = mWifiInfo.getBSSID();
639        }
640
641        if (!forceSelectNetwork && !needQualifiedNetworkSelection(isLinkDebouncing, isConnected,
642                isDisconnected, isSupplicantTransient)) {
643            qnsLog("Quit qualified Network Selection since it is not forced and current network is"
644                    + " qualified already");
645            mFilteredScanDetails = filteredScanDetails;
646            return null;
647        }
648
649        int currentHighestScore = Integer.MIN_VALUE;
650        ScanResult scanResultCandidate = null;
651        WifiConfiguration networkCandidate = null;
652        int unTrustedHighestScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
653        ScanResult untrustedScanResultCandidate = null;
654        WifiConfiguration unTrustedNetworkCandidate = null;
655        String lastUserSelectedNetWorkKey = mWifiConfigManager.getLastSelectedConfiguration();
656        WifiConfiguration lastUserSelectedNetwork =
657                mWifiConfigManager.getWifiConfiguration(lastUserSelectedNetWorkKey);
658        if (lastUserSelectedNetwork != null) {
659            qnsLog("Last selection is " + lastUserSelectedNetwork.SSID + " Time to now: "
660                    + ((mClock.currentTimeMillis() - mWifiConfigManager.getLastSelectedTimeStamp())
661                            / 1000 / 60 + " minutes"));
662        }
663
664        updateSavedNetworkSelectionStatus();
665        updateBssidBlacklist();
666
667        StringBuffer lowSignalScan = new StringBuffer();
668        StringBuffer notSavedScan = new StringBuffer();
669        StringBuffer noValidSsid = new StringBuffer();
670        StringBuffer scoreHistory =  new StringBuffer();
671        ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
672
673        //iterate all scan results and find the best candidate with the highest score
674        for (ScanDetail scanDetail : mScanDetails) {
675            ScanResult scanResult = scanDetail.getScanResult();
676            //skip bad scan result
677            if (scanResult.SSID == null || TextUtils.isEmpty(scanResult.SSID)) {
678                if (mDbg) {
679                    //We should not see this in ePNO
680                    noValidSsid.append(scanResult.BSSID + " / ");
681                }
682                continue;
683            }
684
685            String scanId = scanResult.SSID + ":" + scanResult.BSSID;
686            //check whether this BSSID is blocked or not
687            if (mWifiConfigManager.isBssidBlacklisted(scanResult.BSSID)
688                    || isBssidDisabled(scanResult.BSSID)) {
689                //We should not see this in ePNO
690                Log.e(TAG, scanId + " is in blacklist.");
691                continue;
692            }
693
694            //skip scan result with too weak signals
695            if ((scanResult.is24GHz() && scanResult.level
696                    < mWifiConfigManager.mThresholdMinimumRssi24.get())
697                    || (scanResult.is5GHz() && scanResult.level
698                    < mWifiConfigManager.mThresholdMinimumRssi5.get())) {
699                if (mDbg) {
700                    lowSignalScan.append(scanId + "(" + (scanResult.is24GHz() ? "2.4GHz" : "5GHz")
701                            + ")" + scanResult.level + " / ");
702                }
703                continue;
704            }
705
706            //check if there is already a score for this network
707            if (mNetworkScoreCache != null && !mNetworkScoreCache.isScoredNetwork(scanResult)) {
708                //no score for this network yet.
709                WifiKey wifiKey;
710
711                try {
712                    wifiKey = new WifiKey("\"" + scanResult.SSID + "\"", scanResult.BSSID);
713                    NetworkKey ntwkKey = new NetworkKey(wifiKey);
714                    //add to the unscoredNetworks list so we can request score later
715                    unscoredNetworks.add(ntwkKey);
716                } catch (IllegalArgumentException e) {
717                    Log.w(TAG, "Invalid SSID=" + scanResult.SSID + " BSSID=" + scanResult.BSSID
718                            + " for network score. Skip.");
719                }
720            }
721
722            //check whether this scan result belong to a saved network
723            boolean potentiallyEphemeral = false;
724            List<WifiConfiguration> associatedWifiConfigurations =
725                    mWifiConfigManager.updateSavedNetworkWithNewScanDetail(scanDetail);
726            if (associatedWifiConfigurations == null) {
727                potentiallyEphemeral =  true;
728                if (mDbg) {
729                    notSavedScan.append(scanId + " / ");
730                }
731            } else if (associatedWifiConfigurations.size() == 1) {
732                //if there are more than 1 associated network, it must be a passpoint network
733                WifiConfiguration network = associatedWifiConfigurations.get(0);
734                if (network.ephemeral) {
735                    potentiallyEphemeral =  true;
736                }
737            }
738
739            if (potentiallyEphemeral) {
740                if (isUntrustedConnectionsAllowed && mNetworkScoreCache != null) {
741                    int netScore = mNetworkScoreCache.getNetworkScore(scanResult, false);
742                    //get network score (Determine if this is an 'Ephemeral' network)
743                    if (netScore != WifiNetworkScoreCache.INVALID_NETWORK_SCORE) {
744                        qnsLog(scanId + "has score: " + netScore);
745                        if (netScore > unTrustedHighestScore) {
746                            unTrustedHighestScore = netScore;
747                            untrustedScanResultCandidate = scanResult;
748                            qnsLog(scanId + " become the new untrusted candidate");
749                        }
750                        // scanDetail is for available ephemeral network
751                        filteredScanDetails.add(scanDetail);
752                    }
753                }
754                continue;
755            } else {
756                // scanDetail is for available saved network
757                filteredScanDetails.add(scanDetail);
758            }
759
760            // calculate the core of each scanresult whose associated network is not ephemeral. Due
761            // to one scane result can associated with more than 1 network, we need calcualte all
762            // the scores and use the highest one as the scanresults score
763            int highestScore = Integer.MIN_VALUE;
764            int score;
765            WifiConfiguration configurationCandidateForThisScan = null;
766
767            for (WifiConfiguration network : associatedWifiConfigurations) {
768                WifiConfiguration.NetworkSelectionStatus status =
769                        network.getNetworkSelectionStatus();
770                status.setSeenInLastQualifiedNetworkSelection(true);
771                if (!status.isNetworkEnabled()) {
772                    continue;
773                } else if (network.BSSID != null && !network.BSSID.equals("any")
774                        && !network.BSSID.equals(scanResult.BSSID)) {
775                    //in such scenario, user (APP) has specified the only BSSID to connect for this
776                    // configuration. So only the matched scan result can be candidate
777                    qnsLog("Network: " + getNetworkString(network) + " has specified" + "BSSID:"
778                            + network.BSSID + ". Skip " + scanResult.BSSID);
779                    continue;
780                }
781                score = calculateBssidScore(scanResult, network, mCurrentConnectedNetwork,
782                        (mCurrentBssid == null ? false : mCurrentBssid.equals(scanResult.BSSID)),
783                        (lastUserSelectedNetwork == null ? false : lastUserSelectedNetwork.networkId
784                         == network.networkId), scoreHistory);
785                if (score > highestScore) {
786                    highestScore = score;
787                    configurationCandidateForThisScan = network;
788                }
789                //update the cached candidate
790                if (score > status.getCandidateScore()) {
791                    status.setCandidate(scanResult);
792                    status.setCandidateScore(score);
793                }
794            }
795
796            if (highestScore > currentHighestScore || (highestScore == currentHighestScore
797                    && scanResultCandidate != null
798                    && scanResult.level > scanResultCandidate.level)) {
799                currentHighestScore = highestScore;
800                scanResultCandidate = scanResult;
801                networkCandidate = configurationCandidateForThisScan;
802            }
803        }
804
805        mFilteredScanDetails = filteredScanDetails;
806
807        //kick the score manager if there is any unscored network
808        if (mScoreManager != null && unscoredNetworks.size() != 0) {
809            NetworkKey[] unscoredNetworkKeys =
810                    unscoredNetworks.toArray(new NetworkKey[unscoredNetworks.size()]);
811            mScoreManager.requestScores(unscoredNetworkKeys);
812        }
813
814        if (mDbg) {
815            qnsLog(lowSignalScan + " skipped due to low signal\n");
816            qnsLog(notSavedScan + " skipped due to not saved\n ");
817            qnsLog(noValidSsid + " skipped due to not valid SSID\n");
818            qnsLog(scoreHistory.toString());
819        }
820
821        //we need traverse the whole user preference to choose the one user like most now
822        if (scanResultCandidate != null) {
823            WifiConfiguration tempConfig = networkCandidate;
824
825            while (tempConfig.getNetworkSelectionStatus().getConnectChoice() != null) {
826                String key = tempConfig.getNetworkSelectionStatus().getConnectChoice();
827                tempConfig = mWifiConfigManager.getWifiConfiguration(key);
828
829                if (tempConfig != null) {
830                    WifiConfiguration.NetworkSelectionStatus tempStatus =
831                            tempConfig.getNetworkSelectionStatus();
832                    if (tempStatus.getCandidate() != null && tempStatus.isNetworkEnabled()) {
833                        scanResultCandidate = tempStatus.getCandidate();
834                        networkCandidate = tempConfig;
835                    }
836                } else {
837                    //we should not come here in theory
838                    qnsLoge("Connect choice: " + key + " has no corresponding saved config");
839                    break;
840                }
841            }
842            qnsLog("After user choice adjust, the final candidate is:"
843                    + getNetworkString(networkCandidate) + " : " + scanResultCandidate.BSSID);
844        }
845
846        // if we can not find scanCadidate in saved network
847        if (scanResultCandidate == null && isUntrustedConnectionsAllowed) {
848
849            if (untrustedScanResultCandidate == null) {
850                qnsLog("Can not find any candidate");
851                return null;
852            }
853
854            if (unTrustedNetworkCandidate == null) {
855                unTrustedNetworkCandidate =
856                        mWifiConfigManager.wifiConfigurationFromScanResult(
857                                untrustedScanResultCandidate);
858
859                unTrustedNetworkCandidate.ephemeral = true;
860                if (mNetworkScoreCache != null) {
861                    boolean meteredHint =
862                            mNetworkScoreCache.getMeteredHint(untrustedScanResultCandidate);
863                    unTrustedNetworkCandidate.meteredHint = meteredHint;
864                }
865                mWifiConfigManager.saveNetwork(unTrustedNetworkCandidate,
866                        WifiConfiguration.UNKNOWN_UID);
867
868
869                qnsLog(String.format("new ephemeral candidate %s:%s network ID:%d, meteredHint=%b",
870                        untrustedScanResultCandidate.SSID, untrustedScanResultCandidate.BSSID,
871                        unTrustedNetworkCandidate.networkId,
872                        unTrustedNetworkCandidate.meteredHint));
873
874            } else {
875                qnsLog(String.format("choose existing ephemeral candidate %s:%s network ID:%d, "
876                                + "meteredHint=%b",
877                        untrustedScanResultCandidate.SSID, untrustedScanResultCandidate.BSSID,
878                        unTrustedNetworkCandidate.networkId,
879                        unTrustedNetworkCandidate.meteredHint));
880            }
881            unTrustedNetworkCandidate.getNetworkSelectionStatus()
882                    .setCandidate(untrustedScanResultCandidate);
883            scanResultCandidate = untrustedScanResultCandidate;
884            networkCandidate = unTrustedNetworkCandidate;
885        }
886
887        if (scanResultCandidate == null) {
888            qnsLog("Can not find any suitable candidates");
889            return null;
890        }
891
892        String currentAssociationId = mCurrentConnectedNetwork == null ? "Disconnected" :
893                getNetworkString(mCurrentConnectedNetwork);
894        String targetAssociationId = getNetworkString(networkCandidate);
895        //In passpoint, saved configuration has garbage SSID. We need update it with the SSID of
896        //the scan result.
897        if (networkCandidate.isPasspoint()) {
898            // This will updateb the passpoint configuration in WifiConfigManager
899            networkCandidate.SSID = "\"" + scanResultCandidate.SSID + "\"";
900        }
901
902        //For debug purpose only
903        if (scanResultCandidate.BSSID.equals(mCurrentBssid)) {
904            qnsLog(currentAssociationId + " is already the best choice!");
905        } else if (mCurrentConnectedNetwork != null
906                && (mCurrentConnectedNetwork.networkId == networkCandidate.networkId
907                || mCurrentConnectedNetwork.isLinked(networkCandidate))) {
908            qnsLog("Roaming from " + currentAssociationId + " to " + targetAssociationId);
909        } else {
910            qnsLog("reconnect from " + currentAssociationId + " to " + targetAssociationId);
911        }
912
913        mCurrentBssid = scanResultCandidate.BSSID;
914        mCurrentConnectedNetwork = networkCandidate;
915        mLastQualifiedNetworkSelectionTimeStamp = mClock.currentTimeMillis();
916        return networkCandidate;
917    }
918
919    //Dump the logs
920    void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
921        pw.println("Dump of Quality Network Selection");
922        pw.println(" - Log Begin ----");
923        mLocalLog.dump(fd, pw, args);
924        pw.println(" - Log End ----");
925    }
926}
927