/* * 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 static com.android.server.wifi.hotspot2.Utils.isCarrierEapMethod; import android.annotation.NonNull; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.hotspot2.PasspointConfiguration; import android.os.Process; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.LocalLog; import android.util.Pair; import com.android.server.wifi.CarrierNetworkConfig; import com.android.server.wifi.NetworkUpdateResult; import com.android.server.wifi.ScanDetail; import com.android.server.wifi.WifiConfigManager; import com.android.server.wifi.WifiInjector; import com.android.server.wifi.WifiNetworkSelector; import com.android.server.wifi.util.ScanResultUtil; import com.android.server.wifi.util.TelephonyUtil; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * This class is the WifiNetworkSelector.NetworkEvaluator implementation for * Passpoint networks. */ public class PasspointNetworkEvaluator implements WifiNetworkSelector.NetworkEvaluator { private static final String NAME = "PasspointNetworkEvaluator"; private final PasspointManager mPasspointManager; private final WifiConfigManager mWifiConfigManager; private final LocalLog mLocalLog; private final CarrierNetworkConfig mCarrierNetworkConfig; private final WifiInjector mWifiInjector; private TelephonyManager mTelephonyManager; private SubscriptionManager mSubscriptionManager; /** * 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 PasspointNetworkEvaluator(PasspointManager passpointManager, WifiConfigManager wifiConfigManager, LocalLog localLog, CarrierNetworkConfig carrierNetworkConfig, WifiInjector wifiInjector, SubscriptionManager subscriptionManager) { mPasspointManager = passpointManager; mWifiConfigManager = wifiConfigManager; mLocalLog = localLog; mCarrierNetworkConfig = carrierNetworkConfig; mWifiInjector = wifiInjector; mSubscriptionManager = subscriptionManager; } private TelephonyManager getTelephonyManager() { if (mTelephonyManager == null) { mTelephonyManager = mWifiInjector.makeTelephonyManager(); } return mTelephonyManager; } @Override public @EvaluatorId int getId() { return EVALUATOR_ID_PASSPOINT; } @Override public String getName() { return NAME; } @Override public void update(List scanDetails) {} @Override public WifiConfiguration evaluateNetworks(List scanDetails, WifiConfiguration currentNetwork, String currentBssid, boolean connected, boolean untrustedNetworkAllowed, @NonNull OnConnectableListener onConnectableListener) { // Sweep the ANQP cache to remove any expired ANQP entries. mPasspointManager.sweepCache(); List filteredScanDetails = scanDetails.stream() .filter(s -> s.getNetworkDetail().isInterworking()) .filter(s -> { if (!mWifiConfigManager.wasEphemeralNetworkDeleted( ScanResultUtil.createQuotedSSID(s.getScanResult().SSID))) { return true; } else { // If the user previously disconnects this network, don't select it. mLocalLog.log("Ignoring disabled the SSID of Passpoint AP: " + WifiNetworkSelector.toScanId(s.getScanResult())); return false; } }).collect(Collectors.toList()); createEphemeralProfileForMatchingAp(filteredScanDetails); // Go through each ScanDetail and find the best provider for each ScanDetail. List candidateList = new ArrayList<>(); for (ScanDetail scanDetail : filteredScanDetails) { ScanResult scanResult = scanDetail.getScanResult(); // Find the best provider for this ScanDetail. Pair bestProvider = mPasspointManager.matchProvider(scanResult); if (bestProvider != null) { if (bestProvider.first.isSimCredential() && !TelephonyUtil.isSimPresent(mSubscriptionManager)) { // Skip providers backed by SIM credential when SIM is not present. continue; } candidateList.add(new PasspointNetworkCandidate( bestProvider.first, bestProvider.second, scanDetail)); } } // Done if no candidate is found. if (candidateList.isEmpty()) { localLog("No suitable Passpoint network found"); return null; } // Find the best Passpoint network among all candidates. PasspointNetworkCandidate bestNetwork = findBestNetwork(candidateList, currentNetwork == null ? null : currentNetwork.SSID); // Return the configuration for the current connected network if it is the best network. if (currentNetwork != null && TextUtils.equals(currentNetwork.SSID, ScanResultUtil.createQuotedSSID(bestNetwork.mScanDetail.getSSID()))) { localLog("Staying with current Passpoint network " + currentNetwork.SSID); // Update current network with the latest scan info. TODO - pull into common code mWifiConfigManager.setNetworkCandidateScanResult(currentNetwork.networkId, bestNetwork.mScanDetail.getScanResult(), 0); mWifiConfigManager.updateScanDetailForNetwork(currentNetwork.networkId, bestNetwork.mScanDetail); onConnectableListener.onConnectable(bestNetwork.mScanDetail, currentNetwork, 0); return currentNetwork; } WifiConfiguration config = createWifiConfigForProvider(bestNetwork); if (config != null) { onConnectableListener.onConnectable(bestNetwork.mScanDetail, config, 0); localLog("Passpoint network to connect to: " + config.SSID); } return config; } /** * Creates an ephemeral Passpoint profile if it finds a matching Passpoint AP for MCC/MNC * of the current MNO carrier on the device. */ private void createEphemeralProfileForMatchingAp(List filteredScanDetails) { TelephonyManager telephonyManager = getTelephonyManager(); if (telephonyManager == null) { return; } if (TelephonyUtil.getCarrierType(telephonyManager) != TelephonyUtil.CARRIER_MNO_TYPE) { return; } if (!mCarrierNetworkConfig.isCarrierEncryptionInfoAvailable()) { return; } String mccMnc = telephonyManager .createForSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId()) .getSimOperator(); if (mPasspointManager.hasCarrierProvider(mccMnc)) { return; } int eapMethod = mPasspointManager.findEapMethodFromNAIRealmMatchedWithCarrier(filteredScanDetails); if (!isCarrierEapMethod(eapMethod)) { return; } PasspointConfiguration carrierConfig = mPasspointManager.createEphemeralPasspointConfigForCarrier(eapMethod); if (carrierConfig == null) { return; } mPasspointManager.installEphemeralPasspointConfigForCarrier(carrierConfig); } /** * Create and return a WifiConfiguration for the given ScanDetail and PasspointProvider. * The newly created WifiConfiguration will also be added to WifiConfigManager. * * @param networkInfo Contained information for the Passpoint network to connect to * @return {@link WifiConfiguration} */ private WifiConfiguration createWifiConfigForProvider(PasspointNetworkCandidate networkInfo) { WifiConfiguration config = networkInfo.mProvider.getWifiConfig(); config.SSID = ScanResultUtil.createQuotedSSID(networkInfo.mScanDetail.getSSID()); if (networkInfo.mMatchStatus == PasspointMatch.HomeProvider) { config.isHomeProviderNetwork = true; } WifiConfiguration existingNetwork = mWifiConfigManager.getConfiguredNetwork( config.configKey()); if (existingNetwork != null) { WifiConfiguration.NetworkSelectionStatus status = existingNetwork.getNetworkSelectionStatus(); if (!status.isNetworkEnabled() && !mWifiConfigManager.tryEnableNetwork(existingNetwork.networkId)) { localLog("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. NetworkUpdateResult result = mWifiConfigManager.addOrUpdateNetwork(config, Process.WIFI_UID); if (!result.isSuccess()) { localLog("Failed to add passpoint network"); return existingNetwork; } mWifiConfigManager.enableNetwork(result.getNetworkId(), false, Process.WIFI_UID); mWifiConfigManager.setNetworkCandidateScanResult(result.getNetworkId(), networkInfo.mScanDetail.getScanResult(), 0); mWifiConfigManager.updateScanDetailForNetwork( result.getNetworkId(), networkInfo.mScanDetail); return mWifiConfigManager.getConfiguredNetwork(result.getNetworkId()); } /** * Given a list of Passpoint networks (with both provider and scan info), find and return * the one with highest score. The score is calculated using * {@link PasspointNetworkScore#calculateScore}. * * @param networkList List of Passpoint networks * @param currentNetworkSsid The SSID of the currently connected network, null if not connected * @return {@link PasspointNetworkCandidate} */ private PasspointNetworkCandidate findBestNetwork( List networkList, String currentNetworkSsid) { PasspointNetworkCandidate bestCandidate = null; int bestScore = Integer.MIN_VALUE; for (PasspointNetworkCandidate candidate : networkList) { ScanDetail scanDetail = candidate.mScanDetail; PasspointMatch match = candidate.mMatchStatus; boolean isActiveNetwork = TextUtils.equals(currentNetworkSsid, ScanResultUtil.createQuotedSSID(scanDetail.getSSID())); int score = PasspointNetworkScore.calculateScore(match == PasspointMatch.HomeProvider, scanDetail, mPasspointManager.getANQPElements(scanDetail.getScanResult()), isActiveNetwork); if (score > bestScore) { bestCandidate = candidate; bestScore = score; } } localLog("Best Passpoint network " + bestCandidate.mScanDetail.getSSID() + " provided by " + bestCandidate.mProvider.getConfig().getHomeSp().getFqdn()); return bestCandidate; } private void localLog(String log) { mLocalLog.log(log); } }