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