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