blob: b2b28c0f576a3b157662b15f9ab25a16fd2d2168 [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
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.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.database.ContentObserver;
24import android.net.NetworkKey;
25import android.net.NetworkScoreManager;
26import android.net.wifi.ScanResult;
27import android.net.wifi.WifiConfiguration;
28import android.os.Handler;
29import android.provider.Settings;
30import android.text.TextUtils;
31import android.util.LocalLog;
32import android.util.Log;
33import android.util.Pair;
34
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.server.wifi.util.ScanResultUtil;
37import com.android.server.wifi.util.WifiPermissionsUtil;
38
39import java.util.ArrayList;
40import java.util.List;
41
42/**
43 * {@link WifiNetworkSelector.NetworkNominator} implementation that uses scores obtained by
44 * {@link NetworkScoreManager#requestScores(NetworkKey[])} to make network connection decisions.
45 */
46public class ScoredNetworkNominator implements WifiNetworkSelector.NetworkNominator {
47 private static final String TAG = "ScoredNetworkNominator";
48 // TODO (b/150977740): Stop using the @hide settings global flag.
49 @VisibleForTesting
50 public static final String SETTINGS_GLOBAL_USE_OPEN_WIFI_PACKAGE =
51 "use_open_wifi_package";
52 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
53
54 private final NetworkScoreManager mNetworkScoreManager;
55 private final PackageManager mPackageManager;
56 private final WifiConfigManager mWifiConfigManager;
57 private final LocalLog mLocalLog;
58 private final ContentObserver mContentObserver;
59 private final WifiPermissionsUtil mWifiPermissionsUtil;
60 private boolean mNetworkRecommendationsEnabled;
61 private WifiNetworkScoreCache mScoreCache;
62
63 ScoredNetworkNominator(final Context context, Handler handler,
64 final FrameworkFacade frameworkFacade, NetworkScoreManager networkScoreManager,
65 PackageManager packageManager,
66 WifiConfigManager wifiConfigManager, LocalLog localLog,
67 WifiNetworkScoreCache wifiNetworkScoreCache,
68 WifiPermissionsUtil wifiPermissionsUtil) {
69 mScoreCache = wifiNetworkScoreCache;
70 mWifiPermissionsUtil = wifiPermissionsUtil;
71 mNetworkScoreManager = networkScoreManager;
72 mPackageManager = packageManager;
73 mWifiConfigManager = wifiConfigManager;
74 mLocalLog = localLog;
75 mContentObserver = new ContentObserver(handler) {
76 @Override
77 public void onChange(boolean selfChange) {
78 mNetworkRecommendationsEnabled = frameworkFacade.getStringSetting(context,
79 SETTINGS_GLOBAL_USE_OPEN_WIFI_PACKAGE) != null;
80 }
81 };
82 frameworkFacade.registerContentObserver(context,
83 Settings.Global.getUriFor(SETTINGS_GLOBAL_USE_OPEN_WIFI_PACKAGE),
84 false /* notifyForDescendents */, mContentObserver);
85 mContentObserver.onChange(false /* unused */);
86 mLocalLog.log("ScoredNetworkNominator constructed. mNetworkRecommendationsEnabled: "
87 + mNetworkRecommendationsEnabled);
88 }
89
90 @Override
91 public void update(List<ScanDetail> scanDetails) {
92 if (mNetworkRecommendationsEnabled) {
93 updateNetworkScoreCache(scanDetails);
94 }
95 }
96
97 private void updateNetworkScoreCache(List<ScanDetail> scanDetails) {
98 ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
99 for (int i = 0; i < scanDetails.size(); i++) {
100 ScanResult scanResult = scanDetails.get(i).getScanResult();
101 NetworkKey networkKey = NetworkKey.createFromScanResult(scanResult);
102 if (networkKey != null) {
103 // Is there a ScoredNetwork for this ScanResult? If not, request a score.
104 if (mScoreCache.getScoredNetwork(networkKey) == null) {
105 unscoredNetworks.add(networkKey);
106 }
107 }
108 }
109
110 // Kick the score manager if there are any unscored network.
111 if (!unscoredNetworks.isEmpty() && activeScorerAllowedtoSeeScanResults()) {
112 mNetworkScoreManager.requestScores(unscoredNetworks);
113 }
114 }
115
116 private Pair<Integer, String> getActiveScorerUidAndPackage() {
117 String packageName = mNetworkScoreManager.getActiveScorerPackage();
118 if (packageName == null) return null;
119 int uid = -1;
120 try {
121 uid = mPackageManager.getApplicationInfo(packageName, 0).uid;
122 } catch (PackageManager.NameNotFoundException e) {
123 Log.e(TAG, "Failed to retrieve package uid", e);
124 return null;
125 }
126 return Pair.create(uid, packageName);
127 }
128
129 private boolean activeScorerAllowedtoSeeScanResults() {
130 Pair<Integer, String> scorerUidAndPackage = getActiveScorerUidAndPackage();
131 if (scorerUidAndPackage == null) return false;
132 try {
133 // TODO moltmann: Can we set a featureID here instead of null?
134 mWifiPermissionsUtil.enforceCanAccessScanResults(
135 scorerUidAndPackage.second, null, scorerUidAndPackage.first, null);
136 return true;
137 } catch (SecurityException e) {
138 return false;
139 }
140 }
141
142 @Override
143 public void nominateNetworks(List<ScanDetail> scanDetails,
144 WifiConfiguration currentNetwork, String currentBssid, boolean connected,
145 boolean untrustedNetworkAllowed,
146 @NonNull OnConnectableListener onConnectableListener) {
147 if (!mNetworkRecommendationsEnabled) {
148 mLocalLog.log("Skipping nominateNetworks; Network recommendations disabled.");
149 return;
150 }
151
152 final ScoreTracker scoreTracker = new ScoreTracker();
153 for (int i = 0; i < scanDetails.size(); i++) {
154 ScanDetail scanDetail = scanDetails.get(i);
155 ScanResult scanResult = scanDetail.getScanResult();
156 if (scanResult == null) continue;
157 if (mWifiConfigManager.isNetworkTemporarilyDisabledByUser(
158 ScanResultUtil.createQuotedSSID(scanResult.SSID))) {
159 debugLog("Ignoring user disabled SSID: " + scanResult.SSID);
160 continue;
161 }
162 final WifiConfiguration configuredNetwork =
163 mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail);
164 boolean untrustedScanResult = configuredNetwork == null || !configuredNetwork.trusted;
165
166 if (!untrustedNetworkAllowed && untrustedScanResult) {
167 continue;
168 }
169
170 // Track scan results for open wifi networks
171 if (configuredNetwork == null) {
172 if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
173 scoreTracker.trackUntrustedCandidate(scanDetail);
174 }
175 continue;
176 }
177
178 // Ignore trusted and non-externally scored networks
179 if (configuredNetwork.trusted && !configuredNetwork.useExternalScores) {
180 continue;
181 }
182
183 // Ignore externally scored or ephemeral networks that have been disabled for selection
184 if (!configuredNetwork.getNetworkSelectionStatus().isNetworkEnabled()) {
185 debugLog("Ignoring disabled SSID: " + configuredNetwork.SSID);
186 continue;
187 }
188
189 // TODO(b/37485956): consider applying a boost for networks with only the same SSID
190 boolean isCurrentNetwork = currentNetwork != null
191 && currentNetwork.networkId == configuredNetwork.networkId
192 && TextUtils.equals(currentBssid, scanResult.BSSID);
193 if (!configuredNetwork.trusted) {
194 scoreTracker.trackUntrustedCandidate(
195 scanResult, configuredNetwork, isCurrentNetwork);
196 } else {
197 scoreTracker.trackExternallyScoredCandidate(
198 scanResult, configuredNetwork, isCurrentNetwork);
199 }
200 onConnectableListener.onConnectable(scanDetail, configuredNetwork);
201 }
202 scoreTracker.getCandidateConfiguration(onConnectableListener);
203 }
204
205 /** Used to track the network with the highest score. */
206 class ScoreTracker {
207 private static final int EXTERNAL_SCORED_NONE = 0;
208 private static final int EXTERNAL_SCORED_SAVED_NETWORK = 1;
209 private static final int EXTERNAL_SCORED_UNTRUSTED_NETWORK = 2;
210
211 private int mBestCandidateType = EXTERNAL_SCORED_NONE;
212 private int mHighScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
213 private WifiConfiguration mEphemeralConfig;
214 private WifiConfiguration mSavedConfig;
215 private ScanResult mScanResultCandidate;
216 private ScanDetail mScanDetailCandidate;
217
218 /**
219 * Returns the available external network score or null if no score is available.
220 *
221 * @param scanResult The scan result of the network to score.
222 * @param isCurrentNetwork Flag which indicates whether this is the current network.
223 * @return A valid external score if one is available or NULL.
224 */
225 @Nullable
226 private Integer getNetworkScore(ScanResult scanResult, boolean isCurrentNetwork) {
227 if (mScoreCache.isScoredNetwork(scanResult)) {
228 int score = mScoreCache.getNetworkScore(scanResult, isCurrentNetwork);
229 if (DEBUG) {
230 mLocalLog.log(WifiNetworkSelector.toScanId(scanResult) + " has score: "
231 + score + " isCurrentNetwork network: " + isCurrentNetwork);
232 }
233 return score;
234 }
235 return null;
236 }
237
238 /** Track an untrusted {@link ScanDetail}. */
239 void trackUntrustedCandidate(ScanDetail scanDetail) {
240 ScanResult scanResult = scanDetail.getScanResult();
241 Integer score = getNetworkScore(scanResult, false /* isCurrentNetwork */);
242 if (score != null && score > mHighScore) {
243 mHighScore = score;
244 mScanResultCandidate = scanResult;
245 mScanDetailCandidate = scanDetail;
246 mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK;
247 debugLog(WifiNetworkSelector.toScanId(scanResult)
248 + " becomes the new untrusted candidate.");
249 }
250 }
251
252 /**
253 * Track an untrusted {@link ScanResult} that already has a corresponding
254 * ephemeral {@link WifiConfiguration}.
255 */
256 void trackUntrustedCandidate(
257 ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) {
258 Integer score = getNetworkScore(scanResult, isCurrentNetwork);
259 if (score != null && score > mHighScore) {
260 mHighScore = score;
261 mScanResultCandidate = scanResult;
262 mScanDetailCandidate = null;
263 mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK;
264 mEphemeralConfig = config;
265 mWifiConfigManager.setNetworkCandidateScanResult(config.networkId, scanResult, 0);
266 debugLog(WifiNetworkSelector.toScanId(scanResult)
267 + " becomes the new untrusted candidate.");
268 }
269 }
270
271 /** Tracks a saved network that has been marked with useExternalScores */
272 void trackExternallyScoredCandidate(
273 ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) {
274 // Always take the highest score. If there's a tie and an untrusted network is currently
275 // the best then pick the saved network.
276 Integer score = getNetworkScore(scanResult, isCurrentNetwork);
277 if (score != null
278 && (score > mHighScore
279 || (mBestCandidateType == EXTERNAL_SCORED_UNTRUSTED_NETWORK
280 && score == mHighScore))) {
281 mHighScore = score;
282 mSavedConfig = config;
283 mScanResultCandidate = scanResult;
284 mScanDetailCandidate = null;
285 mBestCandidateType = EXTERNAL_SCORED_SAVED_NETWORK;
286 mWifiConfigManager.setNetworkCandidateScanResult(config.networkId, scanResult, 0);
287 debugLog(WifiNetworkSelector.toScanId(scanResult)
288 + " becomes the new externally scored saved network candidate.");
289 }
290 }
291
292 /** Returns the best candidate network tracked by this {@link ScoreTracker}. */
293 @Nullable
294 WifiConfiguration getCandidateConfiguration(
295 @NonNull OnConnectableListener onConnectableListener) {
296 int candidateNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
297 switch (mBestCandidateType) {
298 case ScoreTracker.EXTERNAL_SCORED_UNTRUSTED_NETWORK:
299 if (mEphemeralConfig != null) {
300 candidateNetworkId = mEphemeralConfig.networkId;
301 mLocalLog.log(String.format("existing ephemeral candidate %s network ID:%d"
302 + ", meteredHint=%b",
303 WifiNetworkSelector.toScanId(mScanResultCandidate),
304 candidateNetworkId,
305 mEphemeralConfig.meteredHint));
306 break;
307 }
308 Pair<Integer, String> scorerUidAndPackage = getActiveScorerUidAndPackage();
309 if (scorerUidAndPackage == null) {
310 mLocalLog.log("Can't find active scorer uid and package");
311 break;
312 }
313
314 mEphemeralConfig =
315 ScanResultUtil.createNetworkFromScanResult(mScanResultCandidate);
316 // Mark this config as ephemeral so it isn't persisted.
317 mEphemeralConfig.ephemeral = true;
318 // Mark this network as untrusted.
319 mEphemeralConfig.trusted = false;
320 mEphemeralConfig.meteredHint = mScoreCache.getMeteredHint(mScanResultCandidate);
321 NetworkUpdateResult result =
322 mWifiConfigManager.addOrUpdateNetwork(mEphemeralConfig,
323 scorerUidAndPackage.first, scorerUidAndPackage.second);
324 if (!result.isSuccess()) {
325 mLocalLog.log("Failed to add ephemeral network");
326 break;
327 }
328 if (!mWifiConfigManager.updateNetworkSelectionStatus(result.getNetworkId(),
329 WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE)) {
330 mLocalLog.log("Failed to make ephemeral network selectable");
331 break;
332 }
333 candidateNetworkId = result.getNetworkId();
334 if (mScanDetailCandidate == null) {
335 // This should never happen, but if it does, WNS will log a wtf.
336 // A message here might help with the diagnosis.
337 Log.e(TAG, "mScanDetailCandidate is null!");
338 }
339 mWifiConfigManager.setNetworkCandidateScanResult(candidateNetworkId,
340 mScanResultCandidate, 0);
341 mLocalLog.log(String.format("new ephemeral candidate %s network ID:%d, "
342 + "meteredHint=%b",
343 WifiNetworkSelector.toScanId(mScanResultCandidate),
344 candidateNetworkId,
345 mEphemeralConfig.meteredHint));
346 break;
347 case ScoreTracker.EXTERNAL_SCORED_SAVED_NETWORK:
348 candidateNetworkId = mSavedConfig.networkId;
349 mLocalLog.log(String.format("new saved network candidate %s network ID:%d",
350 WifiNetworkSelector.toScanId(mScanResultCandidate),
351 candidateNetworkId));
352 break;
353 case ScoreTracker.EXTERNAL_SCORED_NONE:
354 default:
355 mLocalLog.log("ScoredNetworkNominator did not see any good candidates.");
356 break;
357 }
358 WifiConfiguration ans = mWifiConfigManager.getConfiguredNetwork(
359 candidateNetworkId);
360 if (ans != null && mScanDetailCandidate != null) {
361 // This is a newly created config, so we need to call onConnectable.
362 onConnectableListener.onConnectable(mScanDetailCandidate, ans);
363 }
364 return ans;
365 }
366 }
367
368 private void debugLog(String msg) {
369 if (DEBUG) {
370 mLocalLog.log(msg);
371 }
372 }
373
374 @Override
375 public @NominatorId int getId() {
376 return NOMINATOR_ID_SCORED;
377 }
378
379 @Override
380 public String getName() {
381 return TAG;
382 }
383}