blob: ddc0b71a7e0a62d327c66a0e427b8cf73c80196b [file] [log] [blame]
/*
* Copyright (C) 2018 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.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiNetworkSuggestion;
import android.util.LocalLog;
import android.util.Log;
import com.android.server.wifi.util.ScanResultUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Evaluator to pick the best network to connect to from the list of active network suggestions
* provided by apps.
* Note:
* <li> This class is not thread safe and meant to be used only from {@link WifiNetworkSelector}.
* </li>
*
* This is a non-optimal implementation which picks any network suggestion which matches
* the scan result with the highest RSSI.
* TODO: More advanced implementation will follow!
* Params to consider for evaluating network suggestions:
* - Regular network evaluator params like security, band, RSSI, etc.
* - Priority of suggestions provided by a single app.
* - Whether the network suggestions requires user/app interaction or if it is metered.
* - Historical quality of suggestions provided by the corresponding app.
*/
@NotThreadSafe
public class NetworkSuggestionEvaluator implements WifiNetworkSelector.NetworkEvaluator {
private static final String TAG = "NetworkSuggestionEvaluator";
private final WifiNetworkSuggestionsManager mWifiNetworkSuggestionsManager;
private final WifiConfigManager mWifiConfigManager;
private final LocalLog mLocalLog;
NetworkSuggestionEvaluator(WifiNetworkSuggestionsManager networkSuggestionsManager,
WifiConfigManager wifiConfigManager, LocalLog localLog) {
mWifiNetworkSuggestionsManager = networkSuggestionsManager;
mWifiConfigManager = wifiConfigManager;
mLocalLog = localLog;
}
@Override
public void update(List<ScanDetail> scanDetails) {
// TODO(b/115504887): This could be used to re-evaluate any temporary blacklists.
}
@Override
public WifiConfiguration evaluateNetworks(List<ScanDetail> scanDetails,
WifiConfiguration currentNetwork, String currentBssid, boolean connected,
boolean untrustedNetworkAllowed,
@NonNull OnConnectableListener onConnectableListener) {
MatchMetaInfo matchMetaInfo = new MatchMetaInfo();
for (int i = 0; i < scanDetails.size(); i++) {
ScanDetail scanDetail = scanDetails.get(i);
ScanResult scanResult = scanDetail.getScanResult();
// If the user previously forgot this network, don't select it.
if (mWifiConfigManager.wasEphemeralNetworkDeleted(
ScanResultUtil.createQuotedSSID(scanResult.SSID))) {
mLocalLog.log("Ignoring disabled ephemeral SSID: "
+ WifiNetworkSelector.toScanId(scanResult));
continue;
}
Set<WifiNetworkSuggestion> matchingNetworkSuggestions =
mWifiNetworkSuggestionsManager.getNetworkSuggestionsForScanDetail(scanDetail);
if (matchingNetworkSuggestions == null || matchingNetworkSuggestions.isEmpty()) {
continue;
}
// All matching suggestions have the same network credentials type. So, use any one of
// them to lookup/add the credentials to WifiConfigManager.
// Note: Apps could provide different credentials (password, ceritificate) for the same
// network, need to handle that in the future.
WifiNetworkSuggestion matchingNetworkSuggestion =
matchingNetworkSuggestions.stream().findAny().get();
// Check if we already have a network with the same credentials in WifiConfigManager
// database. If yes, we should check if the network is currently blacklisted.
WifiConfiguration wCmConfiguredNetwork =
mWifiConfigManager.getConfiguredNetwork(
matchingNetworkSuggestion.wifiConfiguration.configKey());
if (wCmConfiguredNetwork != null) {
if (!wCmConfiguredNetwork.getNetworkSelectionStatus().isNetworkEnabled()
&& !mWifiConfigManager.tryEnableNetwork(wCmConfiguredNetwork.networkId)) {
mLocalLog.log("Ignoring blacklisted network: "
+ WifiNetworkSelector.toNetworkString(wCmConfiguredNetwork));
continue;
}
}
matchMetaInfo.putAll(matchingNetworkSuggestions, wCmConfiguredNetwork, scanDetail);
}
// Return early on no match.
if (matchMetaInfo.isEmpty()) {
mLocalLog.log("did not see any matching network suggestions.");
return null;
}
// Note: These matched sets should be very small & hence these additional manipulations that
// follow should not be very expensive.
PerNetworkSuggestionMatchMetaInfo candidate =
matchMetaInfo.findConnectableNetworksAndPickBest(onConnectableListener);
if (candidate == null) { // should never happen.
Log.wtf(TAG, "Unexepectedly got null");
return null;
}
return candidate.wCmConfiguredNetwork;
}
// Add and enable this network to the central database (i.e WifiConfigManager).
// Returns the copy of WifiConfiguration with the allocated network ID filled in.
private WifiConfiguration addCandidateToWifiConfigManager(
@NonNull WifiConfiguration wifiConfiguration, int uid, @NonNull String packageName) {
// Mark the network ephemeral because we don't want these persisted by WifiConfigManager.
wifiConfiguration.ephemeral = true;
wifiConfiguration.fromWifiNetworkSuggestion = true;
NetworkUpdateResult result =
mWifiConfigManager.addOrUpdateNetwork(wifiConfiguration, uid, packageName);
if (!result.isSuccess()) {
mLocalLog.log("Failed to add network suggestion");
return null;
}
if (!mWifiConfigManager.updateNetworkSelectionStatus(result.getNetworkId(),
WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE)) {
mLocalLog.log("Failed to make network suggestion selectable");
return null;
}
int candidateNetworkId = result.getNetworkId();
return mWifiConfigManager.getConfiguredNetwork(candidateNetworkId);
}
@Override
public @EvaluatorId int getId() {
return EVALUATOR_ID_SUGGESTION;
}
@Override
public String getName() {
return TAG;
}
// Container classes to handle book-keeping while we're iterating through the scan list.
private class PerNetworkSuggestionMatchMetaInfo {
public final WifiNetworkSuggestion wifiNetworkSuggestion;
public final ScanDetail matchingScanDetail;
public WifiConfiguration wCmConfiguredNetwork; // Added to WifiConfigManager.
PerNetworkSuggestionMatchMetaInfo(@NonNull WifiNetworkSuggestion wifiNetworkSuggestion,
@Nullable WifiConfiguration wCmConfiguredNetwork,
@NonNull ScanDetail matchingScanDetail) {
this.wifiNetworkSuggestion = wifiNetworkSuggestion;
this.wCmConfiguredNetwork = wCmConfiguredNetwork;
this.matchingScanDetail = matchingScanDetail;
}
}
private class PerAppMatchMetaInfo {
public final List<PerNetworkSuggestionMatchMetaInfo> networkInfos = new ArrayList<>();
/**
* Add the network suggestion & associated info to this package meta info.
*/
public void put(WifiNetworkSuggestion wifiNetworkSuggestion,
WifiConfiguration matchingWifiConfiguration,
ScanDetail matchingScanDetail) {
networkInfos.add(new PerNetworkSuggestionMatchMetaInfo(
wifiNetworkSuggestion, matchingWifiConfiguration, matchingScanDetail));
}
/**
* Pick the highest priority networks among the current match info candidates for this
* app.
*/
public List<PerNetworkSuggestionMatchMetaInfo> getHighestPriorityNetworks() {
// Partition the list to a map of network suggestions keyed in by the priorities.
// There can be multiple networks with the same priority, hence a list in the value.
Map<Integer, List<PerNetworkSuggestionMatchMetaInfo>> matchedNetworkInfosPerPriority =
networkInfos.stream()
.collect(Collectors.toMap(
e -> e.wifiNetworkSuggestion.wifiConfiguration.priority,
e -> Arrays.asList(e),
(v1, v2) -> { // concatenate networks with the same priority.
List<PerNetworkSuggestionMatchMetaInfo> concatList =
new ArrayList<>(v1);
concatList.addAll(v2);
return concatList;
}));
if (matchedNetworkInfosPerPriority.isEmpty()) { // should never happen.
Log.wtf(TAG, "Unexepectedly got empty");
return Collections.EMPTY_LIST;
}
// Return the list associated with the highest priority value.
return matchedNetworkInfosPerPriority.get(Collections.max(
matchedNetworkInfosPerPriority.keySet()));
}
}
private class MatchMetaInfo {
private Map<String, PerAppMatchMetaInfo> mAppInfos = new HashMap<>();
/**
* Add all the network suggestion & associated info.
*/
public void putAll(Set<WifiNetworkSuggestion> wifiNetworkSuggestions,
WifiConfiguration wCmConfiguredNetwork,
ScanDetail matchingScanDetail) {
// Separate the suggestions into buckets for each app to allow sorting based on
// priorities set by app.
for (WifiNetworkSuggestion wifiNetworkSuggestion : wifiNetworkSuggestions) {
PerAppMatchMetaInfo appInfo = mAppInfos.computeIfAbsent(
wifiNetworkSuggestion.suggestorPackageName, k -> new PerAppMatchMetaInfo());
appInfo.put(wifiNetworkSuggestion, wCmConfiguredNetwork, matchingScanDetail);
}
}
/**
* Are there any matched candidates?
*/
public boolean isEmpty() {
return mAppInfos.isEmpty();
}
/**
* Find all the connectable networks and pick the best network among the current match info
* candidates.
*
* Among the highest priority suggestions from different packages, choose the suggestion
* with the highest RSSI.
* Note: This should need to be replaced by a more sophisticated algorithm.
*/
public PerNetworkSuggestionMatchMetaInfo findConnectableNetworksAndPickBest(
@NonNull OnConnectableListener onConnectableListener) {
List<PerNetworkSuggestionMatchMetaInfo> allMatchedNetworkInfos = new ArrayList<>();
for (PerAppMatchMetaInfo appInfo : mAppInfos.values()) {
List<PerNetworkSuggestionMatchMetaInfo> matchedNetworkInfos =
appInfo.getHighestPriorityNetworks();
for (PerNetworkSuggestionMatchMetaInfo matchedNetworkInfo : matchedNetworkInfos) {
// if the network does not already exist in WifiConfigManager, add now.
if (matchedNetworkInfo.wCmConfiguredNetwork == null) {
matchedNetworkInfo.wCmConfiguredNetwork = addCandidateToWifiConfigManager(
matchedNetworkInfo.wifiNetworkSuggestion.wifiConfiguration,
matchedNetworkInfo.wifiNetworkSuggestion.suggestorUid,
matchedNetworkInfo.wifiNetworkSuggestion.suggestorPackageName);
if (matchedNetworkInfo.wCmConfiguredNetwork == null) continue;
mLocalLog.log(String.format("network suggestion candidate %s (new)",
WifiNetworkSelector.toNetworkString(
matchedNetworkInfo.wCmConfiguredNetwork)));
} else {
mLocalLog.log(String.format("network suggestion candidate %s (existing)",
WifiNetworkSelector.toNetworkString(
matchedNetworkInfo.wCmConfiguredNetwork)));
}
allMatchedNetworkInfos.add(matchedNetworkInfo);
// Invoke onConnectable for the best networks from each app.
onConnectableListener.onConnectable(
matchedNetworkInfo.matchingScanDetail,
matchedNetworkInfo.wCmConfiguredNetwork,
0);
}
}
PerNetworkSuggestionMatchMetaInfo networkInfo = allMatchedNetworkInfos
.stream()
.max(Comparator.comparing(e -> e.matchingScanDetail.getScanResult().level))
.orElse(null);
if (networkInfo == null) { // should never happen.
Log.wtf(TAG, "Unexepectedly got null");
return null;
}
return networkInfo;
}
}
}