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