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