blob: bf2e460d8757812276c663346d2e1c143819020d [file] [log] [blame]
/*
* Copyright (C) 2016 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.hotspot2;
import android.annotation.NonNull;
import android.net.wifi.WifiConfiguration;
import android.os.Process;
import android.util.LocalLog;
import android.util.Pair;
import com.android.server.wifi.NetworkUpdateResult;
import com.android.server.wifi.ScanDetail;
import com.android.server.wifi.WifiConfigManager;
import com.android.server.wifi.WifiNetworkSelector;
import com.android.server.wifi.hotspot2.anqp.ANQPElement;
import com.android.server.wifi.hotspot2.anqp.Constants;
import com.android.server.wifi.hotspot2.anqp.HSWanMetricsElement;
import com.android.server.wifi.util.ScanResultUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* This class is the WifiNetworkSelector.NetworkNominator implementation for
* Passpoint networks.
*/
public class PasspointNetworkNominateHelper {
private final PasspointManager mPasspointManager;
private final WifiConfigManager mWifiConfigManager;
private final LocalLog mLocalLog;
/**
* Contained information for a Passpoint network candidate.
*/
private class PasspointNetworkCandidate {
PasspointNetworkCandidate(PasspointProvider provider, PasspointMatch matchStatus,
ScanDetail scanDetail) {
mProvider = provider;
mMatchStatus = matchStatus;
mScanDetail = scanDetail;
}
PasspointProvider mProvider;
PasspointMatch mMatchStatus;
ScanDetail mScanDetail;
}
public PasspointNetworkNominateHelper(PasspointManager passpointManager,
WifiConfigManager wifiConfigManager, LocalLog localLog) {
mPasspointManager = passpointManager;
mWifiConfigManager = wifiConfigManager;
mLocalLog = localLog;
}
/**
* Get best matched available Passpoint network candidates for scanDetails.
* @param scanDetails List of ScanDetail.
* @param isFromSuggestion True to indicate profile from suggestion, false for user saved.
* @return List of pair of scanDetail and WifiConfig from matched available provider.
*/
public List<Pair<ScanDetail, WifiConfiguration>> getPasspointNetworkCandidates(
List<ScanDetail> scanDetails, boolean isFromSuggestion) {
// Sweep the ANQP cache to remove any expired ANQP entries.
mPasspointManager.sweepCache();
List<ScanDetail> filteredScanDetails = new ArrayList<>();
// Filter out all invalid scanDetail
for (ScanDetail scanDetail : scanDetails) {
if (scanDetail.getNetworkDetail() == null
|| !scanDetail.getNetworkDetail().isInterworking()
|| scanDetail.getNetworkDetail().getHSRelease() == null) {
// If scanDetail is not Passpoint network, ignore.
continue;
}
if (!scanDetail.getNetworkDetail().isInternet()
|| isApWanLinkStatusDown(scanDetail)) {
// If scanDetail has no internet connection, ignore.
mLocalLog.log("Ignoring no internet connection Passpoint AP: "
+ WifiNetworkSelector.toScanId(scanDetail.getScanResult()));
continue;
}
filteredScanDetails.add(scanDetail);
}
return findBestMatchScanDetailForProviders(filteredScanDetails, isFromSuggestion);
}
/**
* Check if ANQP element inside that scanDetail indicate AP WAN port link status is down.
*
* @param scanDetail contains ANQP element to check.
* @return return true is link status is down, otherwise return false.
*/
private boolean isApWanLinkStatusDown(ScanDetail scanDetail) {
Map<Constants.ANQPElementType, ANQPElement> anqpElements =
mPasspointManager.getANQPElements(scanDetail.getScanResult());
if (anqpElements == null) {
return false;
}
HSWanMetricsElement wm = (HSWanMetricsElement) anqpElements.get(
Constants.ANQPElementType.HSWANMetrics);
if (wm == null) {
return false;
}
return wm.getStatus() != HSWanMetricsElement.LINK_STATUS_UP || wm.isCapped();
}
/**
* Match available providers for each scan detail. Then for each available provider, find the
* best scan detail for it.
* @param scanDetails all details for this scan.
* @param isFromSuggestion True to indicate profile from suggestion, false for user saved.
* @return List of pair of scanDetail and WifiConfig from matched available provider.
*/
private @NonNull List<Pair<ScanDetail, WifiConfiguration>> findBestMatchScanDetailForProviders(
List<ScanDetail> scanDetails, boolean isFromSuggestion) {
if (mPasspointManager.isProvidersListEmpty()) {
return Collections.emptyList();
}
List<Pair<ScanDetail, WifiConfiguration>> results = new ArrayList<>();
Map<PasspointProvider, List<PasspointNetworkCandidate>> candidatesPerProvider =
new HashMap<>();
// Match each scanDetail with best provider (home > roaming), and grouped by FQDN.
for (ScanDetail scanDetail : scanDetails) {
List<Pair<PasspointProvider, PasspointMatch>> matchedProviders =
mPasspointManager.matchProvider(scanDetail.getScanResult());
if (matchedProviders == null) {
continue;
}
for (Pair<PasspointProvider, PasspointMatch> matchedProvider : matchedProviders) {
if (matchedProvider.first.isFromSuggestion() != isFromSuggestion) {
continue;
}
List<PasspointNetworkCandidate> candidates = candidatesPerProvider
.computeIfAbsent(matchedProvider.first, k -> new ArrayList<>());
candidates.add(new PasspointNetworkCandidate(matchedProvider.first,
matchedProvider.second, scanDetail));
}
}
// For each provider find the best scanDetail for it and create selection candidate pair.
for (List<PasspointNetworkCandidate> candidates : candidatesPerProvider.values()) {
List<PasspointNetworkCandidate> bestCandidates = findHomeNetworksIfPossible(candidates);
for (PasspointNetworkCandidate candidate : bestCandidates) {
WifiConfiguration config = createWifiConfigForProvider(candidate);
if (config == null) {
continue;
}
if (mWifiConfigManager.isNetworkTemporarilyDisabledByUser(config.FQDN)) {
mLocalLog.log("Ignoring user disabled FQDN: " + config.FQDN);
continue;
}
results.add(Pair.create(candidate.mScanDetail, config));
}
}
return results;
}
/**
* Create and return a WifiConfiguration for the given ScanDetail and PasspointProvider.
* The newly created WifiConfiguration will also be added to WifiConfigManager.
*
* @return {@link WifiConfiguration}
*/
private WifiConfiguration createWifiConfigForProvider(
PasspointNetworkCandidate candidate) {
WifiConfiguration config = candidate.mProvider.getWifiConfig();
config.SSID = ScanResultUtil.createQuotedSSID(candidate.mScanDetail.getSSID());
config.isHomeProviderNetwork = candidate.mMatchStatus == PasspointMatch.HomeProvider;
if (candidate.mScanDetail.getNetworkDetail().getAnt()
== NetworkDetail.Ant.ChargeablePublic) {
config.meteredHint = true;
}
WifiConfiguration existingNetwork = mWifiConfigManager.getConfiguredNetwork(
config.getKey());
if (existingNetwork != null) {
WifiConfiguration.NetworkSelectionStatus status =
existingNetwork.getNetworkSelectionStatus();
if (!(status.isNetworkEnabled()
|| mWifiConfigManager.tryEnableNetwork(existingNetwork.networkId))) {
mLocalLog.log("Current configuration for the Passpoint AP " + config.SSID
+ " is disabled, skip this candidate");
return null;
}
}
// Add or update with the newly created WifiConfiguration to WifiConfigManager.
// NOTE: if existingNetwork != null, this update is a no-op in most cases if the SSID is the
// same (since we update the cached config in PasspointManager#addOrUpdateProvider().
NetworkUpdateResult result = mWifiConfigManager.addOrUpdateNetwork(
config, config.creatorUid, config.creatorName);
if (!result.isSuccess()) {
mLocalLog.log("Failed to add passpoint network");
return existingNetwork;
}
mWifiConfigManager.allowAutojoin(result.getNetworkId(), config.allowAutojoin);
mWifiConfigManager.enableNetwork(result.getNetworkId(), false, Process.WIFI_UID, null);
mWifiConfigManager.setNetworkCandidateScanResult(result.getNetworkId(),
candidate.mScanDetail.getScanResult(), 0);
mWifiConfigManager.updateScanDetailForNetwork(
result.getNetworkId(), candidate.mScanDetail);
return mWifiConfigManager.getConfiguredNetwork(result.getNetworkId());
}
/**
* Given a list of Passpoint networks (with both provider and scan info), return all
* homeProvider matching networks if there is any, otherwise return all roamingProvider matching
* networks.
*
* @param networkList List of Passpoint networks
* @return List of {@link PasspointNetworkCandidate}
*/
private @NonNull List<PasspointNetworkCandidate> findHomeNetworksIfPossible(
@NonNull List<PasspointNetworkCandidate> networkList) {
List<PasspointNetworkCandidate> homeProviderCandidates = networkList.stream()
.filter(candidate -> candidate.mMatchStatus == PasspointMatch.HomeProvider)
.collect(Collectors.toList());
if (homeProviderCandidates.isEmpty()) {
return networkList;
}
return homeProviderCandidates;
}
}