/*
|
* 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<ScanDetail> scanDetails) {
|
if (mNetworkRecommendationsEnabled) {
|
updateNetworkScoreCache(scanDetails);
|
}
|
}
|
|
private void updateNetworkScoreCache(List<ScanDetail> scanDetails) {
|
ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
|
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<ScanDetail> 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;
|
}
|
}
|