Home | History | Annotate | Download | only in wifi
      1 /*
      2  * Copyright (C) 2017 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 
     17 package com.android.server.wifi;
     18 
     19 import android.annotation.Nullable;
     20 import android.content.Context;
     21 import android.database.ContentObserver;
     22 import android.net.NetworkKey;
     23 import android.net.NetworkScoreManager;
     24 import android.net.wifi.ScanResult;
     25 import android.net.wifi.WifiConfiguration;
     26 import android.net.wifi.WifiNetworkScoreCache;
     27 import android.os.Handler;
     28 import android.os.Looper;
     29 import android.os.Process;
     30 import android.provider.Settings;
     31 import android.text.TextUtils;
     32 import android.util.LocalLog;
     33 import android.util.Log;
     34 import android.util.Pair;
     35 
     36 import com.android.server.wifi.util.ScanResultUtil;
     37 
     38 import java.util.ArrayList;
     39 import java.util.List;
     40 
     41 /**
     42  * {@link WifiNetworkSelector.NetworkEvaluator} implementation that uses scores obtained by
     43  * {@link NetworkScoreManager#requestScores(NetworkKey[])} to make network connection decisions.
     44  */
     45 public class ScoredNetworkEvaluator implements WifiNetworkSelector.NetworkEvaluator {
     46     private static final String TAG = "ScoredNetworkEvaluator";
     47     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     48 
     49     private final NetworkScoreManager mNetworkScoreManager;
     50     private final WifiConfigManager mWifiConfigManager;
     51     private final LocalLog mLocalLog;
     52     private final ContentObserver mContentObserver;
     53     private boolean mNetworkRecommendationsEnabled;
     54     private WifiNetworkScoreCache mScoreCache;
     55 
     56     ScoredNetworkEvaluator(final Context context, Looper looper,
     57             final FrameworkFacade frameworkFacade, NetworkScoreManager networkScoreManager,
     58             WifiConfigManager wifiConfigManager, LocalLog localLog,
     59             WifiNetworkScoreCache wifiNetworkScoreCache) {
     60         mScoreCache = wifiNetworkScoreCache;
     61         mNetworkScoreManager = networkScoreManager;
     62         mWifiConfigManager = wifiConfigManager;
     63         mLocalLog = localLog;
     64         mContentObserver = new ContentObserver(new Handler(looper)) {
     65             @Override
     66             public void onChange(boolean selfChange) {
     67                 mNetworkRecommendationsEnabled = frameworkFacade.getIntegerSetting(context,
     68                         Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0) == 1;
     69             }
     70         };
     71         frameworkFacade.registerContentObserver(context,
     72                 Settings.Global.getUriFor(Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED),
     73                 false /* notifyForDescendents */, mContentObserver);
     74         mContentObserver.onChange(false /* unused */);
     75         mLocalLog.log("ScoredNetworkEvaluator constructed. mNetworkRecommendationsEnabled: "
     76                 + mNetworkRecommendationsEnabled);
     77     }
     78 
     79     @Override
     80     public void update(List<ScanDetail> scanDetails) {
     81         if (mNetworkRecommendationsEnabled) {
     82             updateNetworkScoreCache(scanDetails);
     83         }
     84     }
     85 
     86     private void updateNetworkScoreCache(List<ScanDetail> scanDetails) {
     87         ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
     88         for (int i = 0; i < scanDetails.size(); i++) {
     89             ScanResult scanResult = scanDetails.get(i).getScanResult();
     90             NetworkKey networkKey = NetworkKey.createFromScanResult(scanResult);
     91             if (networkKey != null) {
     92                 // Is there a ScoredNetwork for this ScanResult? If not, request a score.
     93                 if (mScoreCache.getScoredNetwork(networkKey) == null) {
     94                     unscoredNetworks.add(networkKey);
     95                 }
     96             }
     97         }
     98 
     99         // Kick the score manager if there are any unscored network.
    100         if (!unscoredNetworks.isEmpty()) {
    101             NetworkKey[] unscoredNetworkKeys =
    102                     unscoredNetworks.toArray(new NetworkKey[unscoredNetworks.size()]);
    103             mNetworkScoreManager.requestScores(unscoredNetworkKeys);
    104         }
    105     }
    106 
    107     @Override
    108     public WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
    109             WifiConfiguration currentNetwork, String currentBssid, boolean connected,
    110             boolean untrustedNetworkAllowed,
    111             List<Pair<ScanDetail, WifiConfiguration>> connectableNetworks) {
    112         if (!mNetworkRecommendationsEnabled) {
    113             mLocalLog.log("Skipping evaluateNetworks; Network recommendations disabled.");
    114             return null;
    115         }
    116 
    117         final ScoreTracker scoreTracker = new ScoreTracker();
    118         for (int i = 0; i < scanDetails.size(); i++) {
    119             ScanDetail scanDetail = scanDetails.get(i);
    120             ScanResult scanResult = scanDetail.getScanResult();
    121             if (scanResult == null) continue;
    122             if (mWifiConfigManager.wasEphemeralNetworkDeleted(
    123                     ScanResultUtil.createQuotedSSID(scanResult.SSID))) {
    124                 debugLog("Ignoring disabled ephemeral SSID: " + scanResult.SSID);
    125                 continue;
    126             }
    127             final WifiConfiguration configuredNetwork =
    128                     mWifiConfigManager.getSavedNetworkForScanDetailAndCache(scanDetail);
    129             boolean untrustedScanResult = configuredNetwork == null || configuredNetwork.ephemeral;
    130 
    131             if (!untrustedNetworkAllowed && untrustedScanResult) {
    132                 continue;
    133             }
    134 
    135             // Track scan results for open wifi networks
    136             if (configuredNetwork == null) {
    137                 if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
    138                     scoreTracker.trackUntrustedCandidate(scanResult);
    139                 }
    140                 continue;
    141             }
    142 
    143             // Ignore non-ephemeral and non-externally scored networks
    144             if (!configuredNetwork.ephemeral && !configuredNetwork.useExternalScores) {
    145                 continue;
    146             }
    147 
    148             // Ignore externally scored or ephemeral networks that have been disabled for selection
    149             if (!configuredNetwork.getNetworkSelectionStatus().isNetworkEnabled()) {
    150                 debugLog("Ignoring disabled SSID: " + configuredNetwork.SSID);
    151                 continue;
    152             }
    153 
    154             // TODO(b/37485956): consider applying a boost for networks with only the same SSID
    155             boolean isCurrentNetwork = currentNetwork != null
    156                     && currentNetwork.networkId == configuredNetwork.networkId
    157                     && TextUtils.equals(currentBssid, scanResult.BSSID);
    158             if (configuredNetwork.ephemeral) {
    159                 scoreTracker.trackUntrustedCandidate(
    160                         scanResult, configuredNetwork, isCurrentNetwork);
    161             } else {
    162                 scoreTracker.trackExternallyScoredCandidate(
    163                         scanResult, configuredNetwork, isCurrentNetwork);
    164             }
    165             if (connectableNetworks != null) {
    166                 connectableNetworks.add(Pair.create(scanDetail, configuredNetwork));
    167             }
    168         }
    169 
    170         return scoreTracker.getCandidateConfiguration();
    171     }
    172 
    173     /** Used to track the network with the highest score. */
    174     class ScoreTracker {
    175         private static final int EXTERNAL_SCORED_NONE = 0;
    176         private static final int EXTERNAL_SCORED_SAVED_NETWORK = 1;
    177         private static final int EXTERNAL_SCORED_UNTRUSTED_NETWORK = 2;
    178 
    179         private int mBestCandidateType = EXTERNAL_SCORED_NONE;
    180         private int mHighScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
    181         private WifiConfiguration mEphemeralConfig;
    182         private WifiConfiguration mSavedConfig;
    183         private ScanResult mScanResultCandidate;
    184 
    185         /**
    186          * Returns the available external network score or null if no score is available.
    187          *
    188          * @param scanResult The scan result of the network to score.
    189          * @param isCurrentNetwork Flag which indicates whether this is the current network.
    190          * @return A valid external score if one is available or NULL.
    191          */
    192         @Nullable
    193         private Integer getNetworkScore(ScanResult scanResult, boolean isCurrentNetwork) {
    194             if (mScoreCache.isScoredNetwork(scanResult)) {
    195                 int score = mScoreCache.getNetworkScore(scanResult, isCurrentNetwork);
    196                 if (DEBUG) {
    197                     mLocalLog.log(WifiNetworkSelector.toScanId(scanResult) + " has score: "
    198                             + score + " isCurrentNetwork network: " + isCurrentNetwork);
    199                 }
    200                 return score;
    201             }
    202             return null;
    203         }
    204 
    205         /** Track an untrusted {@link ScanResult}. */
    206         void trackUntrustedCandidate(ScanResult scanResult) {
    207             Integer score = getNetworkScore(scanResult, false /* isCurrentNetwork */);
    208             if (score != null && score > mHighScore) {
    209                 mHighScore = score;
    210                 mScanResultCandidate = scanResult;
    211                 mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK;
    212                 debugLog(WifiNetworkSelector.toScanId(scanResult)
    213                         + " becomes the new untrusted candidate.");
    214             }
    215         }
    216 
    217         /**
    218          * Track an untrusted {@link ScanResult} that already has a corresponding
    219          * ephemeral {@link WifiConfiguration}.
    220          */
    221         void trackUntrustedCandidate(
    222                 ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) {
    223             Integer score = getNetworkScore(scanResult, isCurrentNetwork);
    224             if (score != null && score > mHighScore) {
    225                 mHighScore = score;
    226                 mScanResultCandidate = scanResult;
    227                 mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK;
    228                 mEphemeralConfig = config;
    229                 mWifiConfigManager.setNetworkCandidateScanResult(config.networkId, scanResult, 0);
    230                 debugLog(WifiNetworkSelector.toScanId(scanResult)
    231                         + " becomes the new untrusted candidate.");
    232             }
    233         }
    234 
    235         /** Tracks a saved network that has been marked with useExternalScores */
    236         void trackExternallyScoredCandidate(
    237                 ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) {
    238             // Always take the highest score. If there's a tie and an untrusted network is currently
    239             // the best then pick the saved network.
    240             Integer score = getNetworkScore(scanResult, isCurrentNetwork);
    241             if (score != null
    242                     && (score > mHighScore
    243                     || (mBestCandidateType == EXTERNAL_SCORED_UNTRUSTED_NETWORK
    244                     && score == mHighScore))) {
    245                 mHighScore = score;
    246                 mSavedConfig = config;
    247                 mScanResultCandidate = scanResult;
    248                 mBestCandidateType = EXTERNAL_SCORED_SAVED_NETWORK;
    249                 mWifiConfigManager.setNetworkCandidateScanResult(config.networkId, scanResult, 0);
    250                 debugLog(WifiNetworkSelector.toScanId(scanResult)
    251                         + " becomes the new externally scored saved network candidate.");
    252             }
    253         }
    254 
    255         /** Returns the best candidate network tracked by this {@link ScoreTracker}. */
    256         @Nullable
    257         WifiConfiguration getCandidateConfiguration() {
    258             int candidateNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
    259             switch (mBestCandidateType) {
    260                 case ScoreTracker.EXTERNAL_SCORED_UNTRUSTED_NETWORK:
    261                     if (mEphemeralConfig != null) {
    262                         candidateNetworkId = mEphemeralConfig.networkId;
    263                         mLocalLog.log(String.format("existing ephemeral candidate %s network ID:%d"
    264                                         + ", meteredHint=%b",
    265                                 WifiNetworkSelector.toScanId(mScanResultCandidate),
    266                                 candidateNetworkId,
    267                                 mEphemeralConfig.meteredHint));
    268                         break;
    269                     }
    270 
    271                     mEphemeralConfig =
    272                             ScanResultUtil.createNetworkFromScanResult(mScanResultCandidate);
    273                     // Mark this config as ephemeral so it isn't persisted.
    274                     mEphemeralConfig.ephemeral = true;
    275                     mEphemeralConfig.meteredHint = mScoreCache.getMeteredHint(mScanResultCandidate);
    276                     NetworkUpdateResult result =
    277                             mWifiConfigManager.addOrUpdateNetwork(mEphemeralConfig,
    278                                     Process.WIFI_UID);
    279                     if (!result.isSuccess()) {
    280                         mLocalLog.log("Failed to add ephemeral network");
    281                         break;
    282                     }
    283                     if (!mWifiConfigManager.updateNetworkSelectionStatus(result.getNetworkId(),
    284                             WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE)) {
    285                         mLocalLog.log("Failed to make ephemeral network selectable");
    286                         break;
    287                     }
    288                     candidateNetworkId = result.getNetworkId();
    289                     mWifiConfigManager.setNetworkCandidateScanResult(candidateNetworkId,
    290                             mScanResultCandidate, 0);
    291                     mLocalLog.log(String.format("new ephemeral candidate %s network ID:%d, "
    292                                                 + "meteredHint=%b",
    293                                         WifiNetworkSelector.toScanId(mScanResultCandidate),
    294                                         candidateNetworkId,
    295                                         mEphemeralConfig.meteredHint));
    296                     break;
    297                 case ScoreTracker.EXTERNAL_SCORED_SAVED_NETWORK:
    298                     candidateNetworkId = mSavedConfig.networkId;
    299                     mLocalLog.log(String.format("new saved network candidate %s network ID:%d",
    300                                         WifiNetworkSelector.toScanId(mScanResultCandidate),
    301                                         candidateNetworkId));
    302                     break;
    303                 case ScoreTracker.EXTERNAL_SCORED_NONE:
    304                 default:
    305                     mLocalLog.log("ScoredNetworkEvaluator did not see any good candidates.");
    306                     break;
    307             }
    308             return mWifiConfigManager.getConfiguredNetwork(candidateNetworkId);
    309         }
    310     }
    311 
    312     private void debugLog(String msg) {
    313         if (DEBUG) {
    314             mLocalLog.log(msg);
    315         }
    316     }
    317 
    318     @Override
    319     public String getName() {
    320         return TAG;
    321     }
    322 }
    323