/* * 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: *
  • This class is not thread safe and meant to be used only from {@link WifiNetworkSelector}. *
  • * * 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 scanDetails) { // TODO(b/115504887): This could be used to re-evaluate any temporary blacklists. } @Override public WifiConfiguration evaluateNetworks(List 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 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 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 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> matchedNetworkInfosPerPriority = networkInfos.stream() .collect(Collectors.toMap( e -> e.wifiNetworkSuggestion.wifiConfiguration.priority, e -> Arrays.asList(e), (v1, v2) -> { // concatenate networks with the same priority. List 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 mAppInfos = new HashMap<>(); /** * Add all the network suggestion & associated info. */ public void putAll(Set 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 allMatchedNetworkInfos = new ArrayList<>(); for (PerAppMatchMetaInfo appInfo : mAppInfos.values()) { List 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; } } }