1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wifi.hotspot2; 18 19 import static com.android.server.wifi.hotspot2.PasspointMatch.HomeProvider; 20 21 import android.annotation.NonNull; 22 import android.content.res.Resources; 23 import android.net.wifi.WifiConfiguration; 24 import android.net.wifi.util.ScanResultUtil; 25 import android.util.ArrayMap; 26 import android.util.LocalLog; 27 import android.util.Pair; 28 29 import androidx.annotation.VisibleForTesting; 30 31 import com.android.server.wifi.Clock; 32 import com.android.server.wifi.NetworkUpdateResult; 33 import com.android.server.wifi.ScanDetail; 34 import com.android.server.wifi.WifiCarrierInfoManager; 35 import com.android.server.wifi.WifiConfigManager; 36 import com.android.server.wifi.hotspot2.anqp.ANQPElement; 37 import com.android.server.wifi.hotspot2.anqp.Constants; 38 import com.android.server.wifi.hotspot2.anqp.HSWanMetricsElement; 39 import com.android.wifi.resources.R; 40 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Collections; 44 import java.util.Comparator; 45 import java.util.HashMap; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Optional; 50 import java.util.Set; 51 import java.util.stream.Collectors; 52 53 /** 54 * This class is the WifiNetworkSelector.NetworkNominator implementation for 55 * Passpoint networks. 56 */ 57 public class PasspointNetworkNominateHelper { 58 @NonNull private final PasspointManager mPasspointManager; 59 @NonNull private final WifiConfigManager mWifiConfigManager; 60 @NonNull private final Map<String, ScanDetail> mCachedScanDetails = new ArrayMap<>(); 61 @NonNull private final LocalLog mLocalLog; 62 @NonNull private final WifiCarrierInfoManager mCarrierInfoManager; 63 @NonNull private final Resources mResources; 64 @NonNull private final Clock mClock; 65 66 @VisibleForTesting static final long SCAN_DETAIL_EXPIRATION_MS = 60_000; 67 68 /** 69 * Contained information for a Passpoint network candidate. 70 */ 71 private class PasspointNetworkCandidate { PasspointNetworkCandidate(PasspointProvider provider, PasspointMatch matchStatus, ScanDetail scanDetail)72 PasspointNetworkCandidate(PasspointProvider provider, PasspointMatch matchStatus, 73 ScanDetail scanDetail) { 74 mProvider = provider; 75 mMatchStatus = matchStatus; 76 mScanDetail = scanDetail; 77 } 78 PasspointProvider mProvider; 79 PasspointMatch mMatchStatus; 80 ScanDetail mScanDetail; 81 } 82 PasspointNetworkNominateHelper(@onNull PasspointManager passpointManager, @NonNull WifiConfigManager wifiConfigManager, @NonNull LocalLog localLog, WifiCarrierInfoManager carrierInfoManager, Resources resources, Clock clock)83 public PasspointNetworkNominateHelper(@NonNull PasspointManager passpointManager, 84 @NonNull WifiConfigManager wifiConfigManager, @NonNull LocalLog localLog, 85 WifiCarrierInfoManager carrierInfoManager, Resources resources, Clock clock) { 86 mPasspointManager = passpointManager; 87 mWifiConfigManager = wifiConfigManager; 88 mLocalLog = localLog; 89 mCarrierInfoManager = carrierInfoManager; 90 mResources = resources; 91 mClock = clock; 92 } 93 94 /** 95 * Update the matched passpoint network to the WifiConfigManager. 96 * Should be called each time have new scan details. 97 */ updatePasspointConfig(List<ScanDetail> scanDetails)98 public void updatePasspointConfig(List<ScanDetail> scanDetails) { 99 updateBestMatchScanDetailForProviders(filterAndUpdateScanDetails(scanDetails)); 100 } 101 102 /** 103 * Get best matched available Passpoint network candidates for scanDetails. 104 * @param scanDetails List of ScanDetail. 105 * @param isFromSuggestion True to indicate profile from suggestion, false for user saved. 106 * @return List of pair of scanDetail and WifiConfig from matched available provider. 107 */ getPasspointNetworkCandidates( List<ScanDetail> scanDetails, boolean isFromSuggestion)108 public List<Pair<ScanDetail, WifiConfiguration>> getPasspointNetworkCandidates( 109 List<ScanDetail> scanDetails, boolean isFromSuggestion) { 110 return findBestMatchScanDetailForProviders(isFromSuggestion, 111 filterAndUpdateScanDetails(scanDetails)); 112 } 113 114 /** 115 * Filter out non-passpoint networks 116 */ filterAndUpdateScanDetails(List<ScanDetail> scanDetails)117 @NonNull private List<ScanDetail> filterAndUpdateScanDetails(List<ScanDetail> scanDetails) { 118 // Sweep the ANQP cache to remove any expired ANQP entries. 119 mPasspointManager.sweepCache(); 120 List<ScanDetail> filteredScanDetails = new ArrayList<>(); 121 // Filter out all invalid scanDetail 122 for (ScanDetail scanDetail : scanDetails) { 123 if (scanDetail.getNetworkDetail() == null 124 || !scanDetail.getNetworkDetail().isInterworking() 125 || scanDetail.getNetworkDetail().getHSRelease() == null) { 126 // If scanDetail is not Passpoint network, ignore. 127 continue; 128 } 129 filteredScanDetails.add(scanDetail); 130 } 131 addCachedScanDetails(filteredScanDetails); 132 return filteredScanDetails; 133 } 134 addCachedScanDetails(List<ScanDetail> scanDetails)135 private void addCachedScanDetails(List<ScanDetail> scanDetails) { 136 for (ScanDetail scanDetail : scanDetails) { 137 mCachedScanDetails.put(scanDetail.toKeyString(), scanDetail); 138 } 139 removeExpiredScanDetails(); 140 } 141 updateAndGetCachedScanDetails()142 private List<ScanDetail> updateAndGetCachedScanDetails() { 143 removeExpiredScanDetails(); 144 return new ArrayList<>(mCachedScanDetails.values()); 145 } 146 removeExpiredScanDetails()147 private void removeExpiredScanDetails() { 148 long currentMillis = mClock.getWallClockMillis(); 149 mCachedScanDetails.values().removeIf(detail -> 150 currentMillis >= detail.getSeen() + SCAN_DETAIL_EXPIRATION_MS); 151 } 152 153 /** 154 * Check if ANQP element inside that scanDetail indicate AP WAN port link status is down. 155 * 156 * @param scanDetail contains ANQP element to check. 157 * @return return true is link status is down, otherwise return false. 158 */ isApWanLinkStatusDown(ScanDetail scanDetail)159 private boolean isApWanLinkStatusDown(ScanDetail scanDetail) { 160 Map<Constants.ANQPElementType, ANQPElement> anqpElements = 161 mPasspointManager.getANQPElements(scanDetail.getScanResult()); 162 if (anqpElements == null) { 163 return false; 164 } 165 HSWanMetricsElement wm = (HSWanMetricsElement) anqpElements.get( 166 Constants.ANQPElementType.HSWANMetrics); 167 if (wm == null) { 168 return false; 169 } 170 171 // Check if the WAN Metrics ANQP element is initialized with values other than 0's 172 if (!wm.isElementInitialized()) { 173 // WAN Metrics ANQP element is not initialized in this network. Ignore it. 174 return false; 175 } 176 return wm.getStatus() != HSWanMetricsElement.LINK_STATUS_UP || wm.isAtCapacity(); 177 } 178 179 /** 180 * Use the latest scan details to add/update the matched passpoint to WifiConfigManager. 181 * @param scanDetails 182 */ updateBestMatchScanDetailForProviders(List<ScanDetail> scanDetails)183 public void updateBestMatchScanDetailForProviders(List<ScanDetail> scanDetails) { 184 if (mPasspointManager.isProvidersListEmpty() || !mPasspointManager.isWifiPasspointEnabled() 185 || scanDetails.isEmpty()) { 186 return; 187 } 188 Map<PasspointProvider, List<PasspointNetworkCandidate>> candidatesPerProvider = 189 getMatchedCandidateGroupByProvider(scanDetails, false); 190 // For each provider find the best scanDetail(prefer home, higher RSSI) for it and update 191 // it to the WifiConfigManager. 192 for (List<PasspointNetworkCandidate> candidates : candidatesPerProvider.values()) { 193 List<PasspointNetworkCandidate> bestCandidates = findHomeNetworksIfPossible(candidates); 194 Optional<PasspointNetworkCandidate> highestRssi = bestCandidates.stream().max( 195 Comparator.comparingInt(a -> a.mScanDetail.getScanResult().level)); 196 if (!highestRssi.isEmpty()) { 197 createWifiConfigForProvider(highestRssi.get()); 198 } 199 } 200 } 201 202 /** 203 * Refreshes the Wifi configs for each provider using the cached scans. 204 */ refreshWifiConfigsForProviders()205 public void refreshWifiConfigsForProviders() { 206 updateBestMatchScanDetailForProviders(updateAndGetCachedScanDetails()); 207 } 208 209 /** 210 * Match available providers for each scan detail and add their configs to WifiConfigManager. 211 * Then for each available provider, find the best scan detail for it. 212 * @param isFromSuggestion True to indicate profile from suggestion, false for user saved. 213 * @param scanDetailList Scan details to choose from. 214 * @return List of pair of scanDetail and WifiConfig from matched available provider. 215 */ findBestMatchScanDetailForProviders( boolean isFromSuggestion, List<ScanDetail> scanDetailList)216 private @NonNull List<Pair<ScanDetail, WifiConfiguration>> findBestMatchScanDetailForProviders( 217 boolean isFromSuggestion, List<ScanDetail> scanDetailList) { 218 if (mResources.getBoolean( 219 R.bool.config_wifiPasspointUseApWanLinkStatusAnqpElement)) { 220 scanDetailList = scanDetailList.stream() 221 .filter(a -> !isApWanLinkStatusDown(a)) 222 .collect(Collectors.toList()); 223 } 224 if (mPasspointManager.isProvidersListEmpty() 225 || !mPasspointManager.isWifiPasspointEnabled() || scanDetailList.isEmpty()) { 226 return Collections.emptyList(); 227 } 228 List<Pair<ScanDetail, WifiConfiguration>> results = new ArrayList<>(); 229 Map<PasspointProvider, List<PasspointNetworkCandidate>> candidatesPerProvider = 230 getMatchedCandidateGroupByProvider(scanDetailList, true); 231 // For each provider find the best scanDetails(prefer home) for it and create selection 232 // candidate pair. 233 for (Map.Entry<PasspointProvider, List<PasspointNetworkCandidate>> candidates : 234 candidatesPerProvider.entrySet()) { 235 if (candidates.getKey().isFromSuggestion() != isFromSuggestion) { 236 continue; 237 } 238 List<PasspointNetworkCandidate> bestCandidates = 239 findHomeNetworksIfPossible(candidates.getValue()); 240 for (PasspointNetworkCandidate candidate : bestCandidates) { 241 WifiConfiguration config = createWifiConfigForProvider(candidate); 242 if (config == null) { 243 continue; 244 } 245 246 if (mWifiConfigManager.isNonCarrierMergedNetworkTemporarilyDisabled(config)) { 247 mLocalLog.log("Ignoring non-carrier-merged SSID: " + config.FQDN); 248 continue; 249 } 250 if (mWifiConfigManager.isNetworkTemporarilyDisabledByUser(config.FQDN)) { 251 mLocalLog.log("Ignoring user disabled FQDN: " + config.FQDN); 252 continue; 253 } 254 results.add(Pair.create(candidate.mScanDetail, config)); 255 } 256 } 257 return results; 258 } 259 260 private Map<PasspointProvider, List<PasspointNetworkCandidate>> getMatchedCandidateGroupByProvider(List<ScanDetail> scanDetails, boolean onlyHomeIfAvailable)261 getMatchedCandidateGroupByProvider(List<ScanDetail> scanDetails, 262 boolean onlyHomeIfAvailable) { 263 Map<PasspointProvider, List<PasspointNetworkCandidate>> candidatesPerProvider = 264 new HashMap<>(); 265 Set<String> fqdnSet = new HashSet<>(Arrays.asList(mResources.getStringArray( 266 R.array.config_wifiPasspointUseApWanLinkStatusAnqpElementFqdnAllowlist))); 267 // Match each scanDetail with the best provider (home > roaming), and grouped by provider. 268 for (ScanDetail scanDetail : scanDetails) { 269 List<Pair<PasspointProvider, PasspointMatch>> matchedProviders = 270 mPasspointManager.matchProvider(scanDetail.getScanResult()); 271 if (matchedProviders == null) { 272 continue; 273 } 274 // If wan link status check is disabled, check the FQDN allow list. 275 if (!mResources.getBoolean(R.bool.config_wifiPasspointUseApWanLinkStatusAnqpElement) 276 && !fqdnSet.isEmpty()) { 277 matchedProviders = matchedProviders.stream().filter(a -> 278 !fqdnSet.contains(a.first.getConfig().getHomeSp().getFqdn()) 279 || !isApWanLinkStatusDown(scanDetail)) 280 .collect(Collectors.toList()); 281 } 282 if (onlyHomeIfAvailable) { 283 List<Pair<PasspointProvider, PasspointMatch>> homeProviders = 284 matchedProviders.stream() 285 .filter(a -> a.second == HomeProvider) 286 .collect(Collectors.toList()); 287 if (!homeProviders.isEmpty()) { 288 matchedProviders = homeProviders; 289 } 290 } 291 for (Pair<PasspointProvider, PasspointMatch> matchedProvider : matchedProviders) { 292 List<PasspointNetworkCandidate> candidates = candidatesPerProvider 293 .computeIfAbsent(matchedProvider.first, k -> new ArrayList<>()); 294 candidates.add(new PasspointNetworkCandidate(matchedProvider.first, 295 matchedProvider.second, scanDetail)); 296 } 297 } 298 return candidatesPerProvider; 299 } 300 301 /** 302 * Create and return a WifiConfiguration for the given ScanDetail and PasspointProvider. 303 * The newly created WifiConfiguration will also be added to WifiConfigManager. 304 * 305 * @return {@link WifiConfiguration} 306 */ createWifiConfigForProvider( PasspointNetworkCandidate candidate)307 private WifiConfiguration createWifiConfigForProvider( 308 PasspointNetworkCandidate candidate) { 309 WifiConfiguration config = candidate.mProvider.getWifiConfig(); 310 config.SSID = ScanResultUtil.createQuotedSsid(candidate.mScanDetail.getSSID()); 311 config.isHomeProviderNetwork = candidate.mMatchStatus == HomeProvider; 312 if (candidate.mScanDetail.getNetworkDetail().getAnt() 313 == NetworkDetail.Ant.ChargeablePublic) { 314 config.meteredHint = true; 315 } 316 if (mCarrierInfoManager.shouldDisableMacRandomization(config.SSID, 317 config.carrierId, config.subscriptionId)) { 318 mLocalLog.log("Disabling MAC randomization on " + config.SSID 319 + " due to CarrierConfig override"); 320 config.macRandomizationSetting = WifiConfiguration.RANDOMIZATION_NONE; 321 } 322 WifiConfiguration existingNetwork = mWifiConfigManager.getConfiguredNetwork( 323 config.getProfileKey()); 324 if (existingNetwork != null) { 325 WifiConfiguration.NetworkSelectionStatus status = 326 existingNetwork.getNetworkSelectionStatus(); 327 if (!(status.isNetworkEnabled() 328 || mWifiConfigManager.tryEnableNetwork(existingNetwork.networkId))) { 329 mLocalLog.log("Current configuration for the Passpoint AP " + config.SSID 330 + " is disabled, skip this candidate"); 331 return null; 332 } 333 } 334 335 // Add or update with the newly created WifiConfiguration to WifiConfigManager. 336 // NOTE: if existingNetwork != null, this update is a no-op in most cases if the SSID is the 337 // same (since we update the cached config in PasspointManager#addOrUpdateProvider(). 338 NetworkUpdateResult result = mWifiConfigManager.addOrUpdateNetwork( 339 config, config.creatorUid, config.creatorName, false); 340 341 if (!result.isSuccess()) { 342 mLocalLog.log("Failed to add passpoint network"); 343 return existingNetwork; 344 } 345 mWifiConfigManager.enableNetwork(result.getNetworkId(), false, config.creatorUid, null); 346 mWifiConfigManager.setNetworkCandidateScanResult(result.getNetworkId(), 347 candidate.mScanDetail.getScanResult(), 0, null); 348 mWifiConfigManager.updateScanDetailForNetwork( 349 result.getNetworkId(), candidate.mScanDetail); 350 return mWifiConfigManager.getConfiguredNetwork(result.getNetworkId()); 351 } 352 353 /** 354 * Given a list of Passpoint networks (with both provider and scan info), return all 355 * homeProvider matching networks if there is any, otherwise return all roamingProvider matching 356 * networks. 357 * 358 * @param networkList List of Passpoint networks 359 * @return List of {@link PasspointNetworkCandidate} 360 */ findHomeNetworksIfPossible( @onNull List<PasspointNetworkCandidate> networkList)361 private @NonNull List<PasspointNetworkCandidate> findHomeNetworksIfPossible( 362 @NonNull List<PasspointNetworkCandidate> networkList) { 363 List<PasspointNetworkCandidate> homeProviderCandidates = networkList.stream() 364 .filter(candidate -> candidate.mMatchStatus == HomeProvider) 365 .collect(Collectors.toList()); 366 if (homeProviderCandidates.isEmpty()) { 367 return networkList; 368 } 369 return homeProviderCandidates; 370 } 371 } 372