ScoredNetworkEvaluator.java revision f14e4b8fdd949b4ba0feb63dded4d9b233859300
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
17package com.android.server.wifi;
18
19import android.annotation.Nullable;
20import android.content.Context;
21import android.database.ContentObserver;
22import android.net.NetworkKey;
23import android.net.NetworkScoreManager;
24import android.net.wifi.ScanResult;
25import android.net.wifi.WifiConfiguration;
26import android.net.wifi.WifiNetworkScoreCache;
27import android.os.Handler;
28import android.os.Looper;
29import android.os.Process;
30import android.provider.Settings;
31import android.text.TextUtils;
32import android.util.LocalLog;
33import android.util.Log;
34import android.util.Pair;
35
36import com.android.server.wifi.util.ScanResultUtil;
37
38import java.util.ArrayList;
39import 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 */
45public 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