• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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