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