/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wifi; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.database.ContentObserver; import android.net.NetworkKey; import android.net.NetworkScoreManager; import android.net.NetworkScorerAppData; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiNetworkScoreCache; import android.os.Handler; import android.os.Looper; import android.os.Process; import android.provider.Settings; import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; import com.android.server.wifi.WifiNetworkSelector.NetworkEvaluator.OnConnectableListener; import com.android.server.wifi.util.ScanResultUtil; import com.android.server.wifi.util.WifiPermissionsUtil; import java.util.ArrayList; import java.util.List; /** * {@link WifiNetworkSelector.NetworkEvaluator} implementation that uses scores obtained by * {@link NetworkScoreManager#requestScores(NetworkKey[])} to make network connection decisions. */ public class ScoredNetworkEvaluator implements WifiNetworkSelector.NetworkEvaluator { private static final String TAG = "ScoredNetworkEvaluator"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final NetworkScoreManager mNetworkScoreManager; private final WifiConfigManager mWifiConfigManager; private final LocalLog mLocalLog; private final ContentObserver mContentObserver; private final WifiPermissionsUtil mWifiPermissionsUtil; private boolean mNetworkRecommendationsEnabled; private WifiNetworkScoreCache mScoreCache; ScoredNetworkEvaluator(final Context context, Looper looper, final FrameworkFacade frameworkFacade, NetworkScoreManager networkScoreManager, WifiConfigManager wifiConfigManager, LocalLog localLog, WifiNetworkScoreCache wifiNetworkScoreCache, WifiPermissionsUtil wifiPermissionsUtil) { mScoreCache = wifiNetworkScoreCache; mWifiPermissionsUtil = wifiPermissionsUtil; mNetworkScoreManager = networkScoreManager; mWifiConfigManager = wifiConfigManager; mLocalLog = localLog; mContentObserver = new ContentObserver(new Handler(looper)) { @Override public void onChange(boolean selfChange) { mNetworkRecommendationsEnabled = frameworkFacade.getIntegerSetting(context, Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0) == 1; } }; frameworkFacade.registerContentObserver(context, Settings.Global.getUriFor(Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED), false /* notifyForDescendents */, mContentObserver); mContentObserver.onChange(false /* unused */); mLocalLog.log("ScoredNetworkEvaluator constructed. mNetworkRecommendationsEnabled: " + mNetworkRecommendationsEnabled); } @Override public void update(List scanDetails) { if (mNetworkRecommendationsEnabled) { updateNetworkScoreCache(scanDetails); } } private void updateNetworkScoreCache(List scanDetails) { ArrayList unscoredNetworks = new ArrayList(); for (int i = 0; i < scanDetails.size(); i++) { ScanResult scanResult = scanDetails.get(i).getScanResult(); NetworkKey networkKey = NetworkKey.createFromScanResult(scanResult); if (networkKey != null) { // Is there a ScoredNetwork for this ScanResult? If not, request a score. if (mScoreCache.getScoredNetwork(networkKey) == null) { unscoredNetworks.add(networkKey); } } } // Kick the score manager if there are any unscored network. if (!unscoredNetworks.isEmpty() && activeScorerAllowedtoSeeScanResults()) { NetworkKey[] unscoredNetworkKeys = unscoredNetworks.toArray(new NetworkKey[unscoredNetworks.size()]); mNetworkScoreManager.requestScores(unscoredNetworkKeys); } } private boolean activeScorerAllowedtoSeeScanResults() { NetworkScorerAppData networkScorerAppData = mNetworkScoreManager.getActiveScorer(); String packageName = mNetworkScoreManager.getActiveScorerPackage(); if (networkScorerAppData == null || packageName == null) return false; int uid = networkScorerAppData.packageUid; try { mWifiPermissionsUtil.enforceCanAccessScanResults(packageName, uid); return true; } catch (SecurityException e) { return false; } } @Override public WifiConfiguration evaluateNetworks(List scanDetails, WifiConfiguration currentNetwork, String currentBssid, boolean connected, boolean untrustedNetworkAllowed, @NonNull OnConnectableListener onConnectableListener) { if (!mNetworkRecommendationsEnabled) { mLocalLog.log("Skipping evaluateNetworks; Network recommendations disabled."); return null; } final ScoreTracker scoreTracker = new ScoreTracker(); for (int i = 0; i < scanDetails.size(); i++) { ScanDetail scanDetail = scanDetails.get(i); ScanResult scanResult = scanDetail.getScanResult(); if (scanResult == null) continue; if (mWifiConfigManager.wasEphemeralNetworkDeleted( ScanResultUtil.createQuotedSSID(scanResult.SSID))) { debugLog("Ignoring disabled ephemeral SSID: " + scanResult.SSID); continue; } final WifiConfiguration configuredNetwork = mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail); boolean untrustedScanResult = configuredNetwork == null || !configuredNetwork.trusted; if (!untrustedNetworkAllowed && untrustedScanResult) { continue; } // Track scan results for open wifi networks if (configuredNetwork == null) { if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) { scoreTracker.trackUntrustedCandidate(scanDetail); } continue; } // Ignore trusted and non-externally scored networks if (configuredNetwork.trusted && !configuredNetwork.useExternalScores) { continue; } // Ignore externally scored or ephemeral networks that have been disabled for selection if (!configuredNetwork.getNetworkSelectionStatus().isNetworkEnabled()) { debugLog("Ignoring disabled SSID: " + configuredNetwork.SSID); continue; } // TODO(b/37485956): consider applying a boost for networks with only the same SSID boolean isCurrentNetwork = currentNetwork != null && currentNetwork.networkId == configuredNetwork.networkId && TextUtils.equals(currentBssid, scanResult.BSSID); if (!configuredNetwork.trusted) { scoreTracker.trackUntrustedCandidate( scanResult, configuredNetwork, isCurrentNetwork); } else { scoreTracker.trackExternallyScoredCandidate( scanResult, configuredNetwork, isCurrentNetwork); } onConnectableListener.onConnectable(scanDetail, configuredNetwork, 0); } return scoreTracker.getCandidateConfiguration(onConnectableListener); } /** Used to track the network with the highest score. */ class ScoreTracker { private static final int EXTERNAL_SCORED_NONE = 0; private static final int EXTERNAL_SCORED_SAVED_NETWORK = 1; private static final int EXTERNAL_SCORED_UNTRUSTED_NETWORK = 2; private int mBestCandidateType = EXTERNAL_SCORED_NONE; private int mHighScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE; private WifiConfiguration mEphemeralConfig; private WifiConfiguration mSavedConfig; private ScanResult mScanResultCandidate; private ScanDetail mScanDetailCandidate; /** * Returns the available external network score or null if no score is available. * * @param scanResult The scan result of the network to score. * @param isCurrentNetwork Flag which indicates whether this is the current network. * @return A valid external score if one is available or NULL. */ @Nullable private Integer getNetworkScore(ScanResult scanResult, boolean isCurrentNetwork) { if (mScoreCache.isScoredNetwork(scanResult)) { int score = mScoreCache.getNetworkScore(scanResult, isCurrentNetwork); if (DEBUG) { mLocalLog.log(WifiNetworkSelector.toScanId(scanResult) + " has score: " + score + " isCurrentNetwork network: " + isCurrentNetwork); } return score; } return null; } /** Track an untrusted {@link ScanDetail}. */ void trackUntrustedCandidate(ScanDetail scanDetail) { ScanResult scanResult = scanDetail.getScanResult(); Integer score = getNetworkScore(scanResult, false /* isCurrentNetwork */); if (score != null && score > mHighScore) { mHighScore = score; mScanResultCandidate = scanResult; mScanDetailCandidate = scanDetail; mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK; debugLog(WifiNetworkSelector.toScanId(scanResult) + " becomes the new untrusted candidate."); } } /** * Track an untrusted {@link ScanResult} that already has a corresponding * ephemeral {@link WifiConfiguration}. */ void trackUntrustedCandidate( ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) { Integer score = getNetworkScore(scanResult, isCurrentNetwork); if (score != null && score > mHighScore) { mHighScore = score; mScanResultCandidate = scanResult; mScanDetailCandidate = null; mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK; mEphemeralConfig = config; mWifiConfigManager.setNetworkCandidateScanResult(config.networkId, scanResult, 0); debugLog(WifiNetworkSelector.toScanId(scanResult) + " becomes the new untrusted candidate."); } } /** Tracks a saved network that has been marked with useExternalScores */ void trackExternallyScoredCandidate( ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) { // Always take the highest score. If there's a tie and an untrusted network is currently // the best then pick the saved network. Integer score = getNetworkScore(scanResult, isCurrentNetwork); if (score != null && (score > mHighScore || (mBestCandidateType == EXTERNAL_SCORED_UNTRUSTED_NETWORK && score == mHighScore))) { mHighScore = score; mSavedConfig = config; mScanResultCandidate = scanResult; mScanDetailCandidate = null; mBestCandidateType = EXTERNAL_SCORED_SAVED_NETWORK; mWifiConfigManager.setNetworkCandidateScanResult(config.networkId, scanResult, 0); debugLog(WifiNetworkSelector.toScanId(scanResult) + " becomes the new externally scored saved network candidate."); } } /** Returns the best candidate network tracked by this {@link ScoreTracker}. */ @Nullable WifiConfiguration getCandidateConfiguration( @NonNull OnConnectableListener onConnectableListener) { int candidateNetworkId = WifiConfiguration.INVALID_NETWORK_ID; switch (mBestCandidateType) { case ScoreTracker.EXTERNAL_SCORED_UNTRUSTED_NETWORK: if (mEphemeralConfig != null) { candidateNetworkId = mEphemeralConfig.networkId; mLocalLog.log(String.format("existing ephemeral candidate %s network ID:%d" + ", meteredHint=%b", WifiNetworkSelector.toScanId(mScanResultCandidate), candidateNetworkId, mEphemeralConfig.meteredHint)); break; } mEphemeralConfig = ScanResultUtil.createNetworkFromScanResult(mScanResultCandidate); // Mark this config as ephemeral so it isn't persisted. mEphemeralConfig.ephemeral = true; // Mark this network as untrusted. mEphemeralConfig.trusted = false; mEphemeralConfig.meteredHint = mScoreCache.getMeteredHint(mScanResultCandidate); NetworkUpdateResult result = mWifiConfigManager.addOrUpdateNetwork(mEphemeralConfig, Process.WIFI_UID); if (!result.isSuccess()) { mLocalLog.log("Failed to add ephemeral network"); break; } if (!mWifiConfigManager.updateNetworkSelectionStatus(result.getNetworkId(), WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE)) { mLocalLog.log("Failed to make ephemeral network selectable"); break; } candidateNetworkId = result.getNetworkId(); if (mScanDetailCandidate == null) { // This should never happen, but if it does, WNS will log a wtf. // A message here might help with the diagnosis. Log.e(TAG, "mScanDetailCandidate is null!"); } mWifiConfigManager.setNetworkCandidateScanResult(candidateNetworkId, mScanResultCandidate, 0); mLocalLog.log(String.format("new ephemeral candidate %s network ID:%d, " + "meteredHint=%b", WifiNetworkSelector.toScanId(mScanResultCandidate), candidateNetworkId, mEphemeralConfig.meteredHint)); break; case ScoreTracker.EXTERNAL_SCORED_SAVED_NETWORK: candidateNetworkId = mSavedConfig.networkId; mLocalLog.log(String.format("new saved network candidate %s network ID:%d", WifiNetworkSelector.toScanId(mScanResultCandidate), candidateNetworkId)); break; case ScoreTracker.EXTERNAL_SCORED_NONE: default: mLocalLog.log("ScoredNetworkEvaluator did not see any good candidates."); break; } WifiConfiguration ans = mWifiConfigManager.getConfiguredNetwork( candidateNetworkId); if (ans != null && mScanDetailCandidate != null) { // This is a newly created config, so we need to call onConnectable. onConnectableListener.onConnectable(mScanDetailCandidate, ans, 0); } return ans; } } private void debugLog(String msg) { if (DEBUG) { mLocalLog.log(msg); } } @Override public @EvaluatorId int getId() { return EVALUATOR_ID_SCORED; } @Override public String getName() { return TAG; } }