/*
|
* 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;
|
}
|
}
|
|
}
|