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
17package com.android.server.wifi.hotspot2;
18
19import static android.net.wifi.WifiManager.ACTION_PASSPOINT_DEAUTH_IMMINENT;
20import static android.net.wifi.WifiManager.ACTION_PASSPOINT_ICON;
21import static android.net.wifi.WifiManager.ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION;
22import static android.net.wifi.WifiManager.EXTRA_BSSID_LONG;
23import static android.net.wifi.WifiManager.EXTRA_DELAY;
24import static android.net.wifi.WifiManager.EXTRA_ESS;
25import static android.net.wifi.WifiManager.EXTRA_FILENAME;
26import static android.net.wifi.WifiManager.EXTRA_ICON;
27import static android.net.wifi.WifiManager.EXTRA_SUBSCRIPTION_REMEDIATION_METHOD;
28import static android.net.wifi.WifiManager.EXTRA_URL;
29
30import android.content.Context;
31import android.content.Intent;
32import android.graphics.drawable.Icon;
33import android.net.wifi.ScanResult;
34import android.net.wifi.WifiConfiguration;
35import android.net.wifi.WifiEnterpriseConfig;
36import android.net.wifi.hotspot2.IProvisioningCallback;
37import android.net.wifi.hotspot2.OsuProvider;
38import android.net.wifi.hotspot2.PasspointConfiguration;
39import android.os.Looper;
40import android.os.UserHandle;
41import android.text.TextUtils;
42import android.util.Log;
43import android.util.Pair;
44
45import com.android.server.wifi.Clock;
46import com.android.server.wifi.SIMAccessor;
47import com.android.server.wifi.WifiConfigManager;
48import com.android.server.wifi.WifiConfigStore;
49import com.android.server.wifi.WifiKeyStore;
50import com.android.server.wifi.WifiMetrics;
51import com.android.server.wifi.WifiNative;
52import com.android.server.wifi.hotspot2.anqp.ANQPElement;
53import com.android.server.wifi.hotspot2.anqp.Constants;
54import com.android.server.wifi.hotspot2.anqp.HSOsuProvidersElement;
55import com.android.server.wifi.hotspot2.anqp.OsuProviderInfo;
56import com.android.server.wifi.util.InformationElementUtil;
57import com.android.server.wifi.util.ScanResultUtil;
58
59import java.io.PrintWriter;
60import java.util.ArrayList;
61import java.util.HashMap;
62import java.util.List;
63import java.util.Map;
64
65/**
66 * This class provides the APIs to manage Passpoint provider configurations.
67 * It deals with the following:
68 * - Maintaining a list of configured Passpoint providers for provider matching.
69 * - Persisting the providers configurations to store when required.
70 * - matching Passpoint providers based on the scan results
71 * - Supporting WifiManager Public API calls:
72 *   > addOrUpdatePasspointConfiguration()
73 *   > removePasspointConfiguration()
74 *   > getPasspointConfigurations()
75 *
76 * The provider matching requires obtaining additional information from the AP (ANQP elements).
77 * The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests.
78 *
79 * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
80 */
81public class PasspointManager {
82    private static final String TAG = "PasspointManager";
83
84    /**
85     * Handle for the current {@link PasspointManager} instance.  This is needed to avoid
86     * circular dependency with the WifiConfigManger, it will be used for adding the
87     * legacy Passpoint configurations.
88     *
89     * This can be eliminated once we can remove the dependency for WifiConfigManager (for
90     * triggering config store write) from this class.
91     */
92    private static PasspointManager sPasspointManager;
93
94    private final PasspointEventHandler mHandler;
95    private final SIMAccessor mSimAccessor;
96    private final WifiKeyStore mKeyStore;
97    private final PasspointObjectFactory mObjectFactory;
98    private final Map<String, PasspointProvider> mProviders;
99    private final AnqpCache mAnqpCache;
100    private final ANQPRequestManager mAnqpRequestManager;
101    private final WifiConfigManager mWifiConfigManager;
102    private final CertificateVerifier mCertVerifier;
103    private final WifiMetrics mWifiMetrics;
104    private final PasspointProvisioner mPasspointProvisioner;
105
106    // Counter used for assigning unique identifier to each provider.
107    private long mProviderIndex;
108
109    private class CallbackHandler implements PasspointEventHandler.Callbacks {
110        private final Context mContext;
111        CallbackHandler(Context context) {
112            mContext = context;
113        }
114
115        @Override
116        public void onANQPResponse(long bssid,
117                Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
118            // Notify request manager for the completion of a request.
119            ANQPNetworkKey anqpKey =
120                    mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null);
121            if (anqpElements == null || anqpKey == null) {
122                // Query failed or the request wasn't originated from us (not tracked by the
123                // request manager). Nothing to be done.
124                return;
125            }
126
127            // Add new entry to the cache.
128            mAnqpCache.addEntry(anqpKey, anqpElements);
129        }
130
131        @Override
132        public void onIconResponse(long bssid, String fileName, byte[] data) {
133            Intent intent = new Intent(ACTION_PASSPOINT_ICON);
134            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
135            intent.putExtra(EXTRA_BSSID_LONG, bssid);
136            intent.putExtra(EXTRA_FILENAME, fileName);
137            if (data != null) {
138                intent.putExtra(EXTRA_ICON, Icon.createWithData(data, 0, data.length));
139            }
140            mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
141                    android.Manifest.permission.ACCESS_WIFI_STATE);
142        }
143
144        @Override
145        public void onWnmFrameReceived(WnmData event) {
146            // %012x HS20-SUBSCRIPTION-REMEDIATION "%u %s", osu_method, url
147            // %012x HS20-DEAUTH-IMMINENT-NOTICE "%u %u %s", code, reauth_delay, url
148            Intent intent;
149            if (event.isDeauthEvent()) {
150                intent = new Intent(ACTION_PASSPOINT_DEAUTH_IMMINENT);
151                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
152                intent.putExtra(EXTRA_BSSID_LONG, event.getBssid());
153                intent.putExtra(EXTRA_URL, event.getUrl());
154                intent.putExtra(EXTRA_ESS, event.isEss());
155                intent.putExtra(EXTRA_DELAY, event.getDelay());
156            } else {
157                intent = new Intent(ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION);
158                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
159                intent.putExtra(EXTRA_BSSID_LONG, event.getBssid());
160                intent.putExtra(EXTRA_SUBSCRIPTION_REMEDIATION_METHOD, event.getMethod());
161                intent.putExtra(EXTRA_URL, event.getUrl());
162            }
163            mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
164                    android.Manifest.permission.ACCESS_WIFI_STATE);
165        }
166    }
167
168    /**
169     * Data provider for the Passpoint configuration store data {@link PasspointConfigStoreData}.
170     */
171    private class DataSourceHandler implements PasspointConfigStoreData.DataSource {
172        @Override
173        public List<PasspointProvider> getProviders() {
174            List<PasspointProvider> providers = new ArrayList<>();
175            for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
176                providers.add(entry.getValue());
177            }
178            return providers;
179        }
180
181        @Override
182        public void setProviders(List<PasspointProvider> providers) {
183            mProviders.clear();
184            for (PasspointProvider provider : providers) {
185                mProviders.put(provider.getConfig().getHomeSp().getFqdn(), provider);
186            }
187        }
188
189        @Override
190        public long getProviderIndex() {
191            return mProviderIndex;
192        }
193
194        @Override
195        public void setProviderIndex(long providerIndex) {
196            mProviderIndex = providerIndex;
197        }
198    }
199
200    public PasspointManager(Context context, WifiNative wifiNative, WifiKeyStore keyStore,
201            Clock clock, SIMAccessor simAccessor, PasspointObjectFactory objectFactory,
202            WifiConfigManager wifiConfigManager, WifiConfigStore wifiConfigStore,
203            WifiMetrics wifiMetrics) {
204        mHandler = objectFactory.makePasspointEventHandler(wifiNative,
205                new CallbackHandler(context));
206        mKeyStore = keyStore;
207        mSimAccessor = simAccessor;
208        mObjectFactory = objectFactory;
209        mProviders = new HashMap<>();
210        mAnqpCache = objectFactory.makeAnqpCache(clock);
211        mAnqpRequestManager = objectFactory.makeANQPRequestManager(mHandler, clock);
212        mCertVerifier = objectFactory.makeCertificateVerifier();
213        mWifiConfigManager = wifiConfigManager;
214        mWifiMetrics = wifiMetrics;
215        mProviderIndex = 0;
216        wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigStoreData(
217                mKeyStore, mSimAccessor, new DataSourceHandler()));
218        mPasspointProvisioner = objectFactory.makePasspointProvisioner(context);
219        sPasspointManager = this;
220    }
221
222    /**
223     * Initializes the provisioning flow with a looper
224     */
225    public void initializeProvisioner(Looper looper) {
226        mPasspointProvisioner.init(looper);
227    }
228
229    /**
230     * Enable verbose logging
231     * @param verbose more than 0 enables verbose logging
232     */
233    public void enableVerboseLogging(int verbose) {
234        mPasspointProvisioner.enableVerboseLogging(verbose);
235    }
236
237    /**
238     * Add or update a Passpoint provider with the given configuration.
239     *
240     * Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name).
241     * In the case when there is an existing configuration with the same FQDN
242     * a provider with the new configuration will replace the existing provider.
243     *
244     * @param config Configuration of the Passpoint provider to be added
245     * @return true if provider is added, false otherwise
246     */
247    public boolean addOrUpdateProvider(PasspointConfiguration config, int uid) {
248        mWifiMetrics.incrementNumPasspointProviderInstallation();
249        if (config == null) {
250            Log.e(TAG, "Configuration not provided");
251            return false;
252        }
253        if (!config.validate()) {
254            Log.e(TAG, "Invalid configuration");
255            return false;
256        }
257
258        // For Hotspot 2.0 Release 1, the CA Certificate must be trusted by one of the pre-loaded
259        // public CAs in the system key store on the device.  Since the provisioning method
260        // for Release 1 is not standardized nor trusted,  this is a reasonable restriction
261        // to improve security.  The presence of UpdateIdentifier is used to differentiate
262        // between R1 and R2 configuration.
263        if (config.getUpdateIdentifier() == Integer.MIN_VALUE
264                && config.getCredential().getCaCertificate() != null) {
265            try {
266                mCertVerifier.verifyCaCert(config.getCredential().getCaCertificate());
267            } catch (Exception e) {
268                Log.e(TAG, "Failed to verify CA certificate: " + e.getMessage());
269                return false;
270            }
271        }
272
273        // Create a provider and install the necessary certificates and keys.
274        PasspointProvider newProvider = mObjectFactory.makePasspointProvider(
275                config, mKeyStore, mSimAccessor, mProviderIndex++, uid);
276
277        if (!newProvider.installCertsAndKeys()) {
278            Log.e(TAG, "Failed to install certificates and keys to keystore");
279            return false;
280        }
281
282        // Remove existing provider with the same FQDN.
283        if (mProviders.containsKey(config.getHomeSp().getFqdn())) {
284            Log.d(TAG, "Replacing configuration for " + config.getHomeSp().getFqdn());
285            mProviders.get(config.getHomeSp().getFqdn()).uninstallCertsAndKeys();
286            mProviders.remove(config.getHomeSp().getFqdn());
287        }
288
289        mProviders.put(config.getHomeSp().getFqdn(), newProvider);
290        mWifiConfigManager.saveToStore(true /* forceWrite */);
291        Log.d(TAG, "Added/updated Passpoint configuration: " + config.getHomeSp().getFqdn()
292                + " by " + uid);
293        mWifiMetrics.incrementNumPasspointProviderInstallSuccess();
294        return true;
295    }
296
297    /**
298     * Remove a Passpoint provider identified by the given FQDN.
299     *
300     * @param fqdn The FQDN of the provider to remove
301     * @return true if a provider is removed, false otherwise
302     */
303    public boolean removeProvider(String fqdn) {
304        mWifiMetrics.incrementNumPasspointProviderUninstallation();
305        if (!mProviders.containsKey(fqdn)) {
306            Log.e(TAG, "Config doesn't exist");
307            return false;
308        }
309
310        mProviders.get(fqdn).uninstallCertsAndKeys();
311        mProviders.remove(fqdn);
312        mWifiConfigManager.saveToStore(true /* forceWrite */);
313        Log.d(TAG, "Removed Passpoint configuration: " + fqdn);
314        mWifiMetrics.incrementNumPasspointProviderUninstallSuccess();
315        return true;
316    }
317
318    /**
319     * Return the installed Passpoint provider configurations.
320     *
321     * An empty list will be returned when no provider is installed.
322     *
323     * @return A list of {@link PasspointConfiguration}
324     */
325    public List<PasspointConfiguration> getProviderConfigs() {
326        List<PasspointConfiguration> configs = new ArrayList<>();
327        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
328            configs.add(entry.getValue().getConfig());
329        }
330        return configs;
331    }
332
333    /**
334     * Find the best provider that can provide service through the given AP, which means the
335     * provider contained credential to authenticate with the given AP.
336     *
337     * Here is the current precedence of the matching rule in descending order:
338     * 1. Home Provider
339     * 2. Roaming Provider
340     *
341     * A {code null} will be returned if no matching is found.
342     *
343     * @param scanResult The scan result associated with the AP
344     * @return A pair of {@link PasspointProvider} and match status.
345     */
346    public Pair<PasspointProvider, PasspointMatch> matchProvider(ScanResult scanResult) {
347        List<Pair<PasspointProvider, PasspointMatch>> allMatches = getAllMatchedProviders(
348                scanResult);
349        if (allMatches == null) {
350            return null;
351        }
352
353        Pair<PasspointProvider, PasspointMatch> bestMatch = null;
354        for (Pair<PasspointProvider, PasspointMatch> match : allMatches) {
355            if (match.second == PasspointMatch.HomeProvider) {
356                bestMatch = match;
357                break;
358            }
359            if (match.second == PasspointMatch.RoamingProvider && bestMatch == null) {
360                bestMatch = match;
361            }
362        }
363
364        if (bestMatch != null) {
365            Log.d(TAG, String.format("Matched %s to %s as %s", scanResult.SSID,
366                    bestMatch.first.getConfig().getHomeSp().getFqdn(),
367                    bestMatch.second == PasspointMatch.HomeProvider ? "Home Provider"
368                            : "Roaming Provider"));
369        } else {
370            Log.d(TAG, "Match not found for " + scanResult.SSID);
371        }
372        return bestMatch;
373    }
374
375    /**
376     * Return a list of all providers that can provide service through the given AP.
377     *
378     * @param scanResult The scan result associated with the AP
379     * @return a list of pairs of {@link PasspointProvider} and match status.
380     */
381    public List<Pair<PasspointProvider, PasspointMatch>> getAllMatchedProviders(
382            ScanResult scanResult) {
383        List<Pair<PasspointProvider, PasspointMatch>> allMatches = new ArrayList<>();
384
385        // Retrieve the relevant information elements, mainly Roaming Consortium IE and Hotspot 2.0
386        // Vendor Specific IE.
387        InformationElementUtil.RoamingConsortium roamingConsortium =
388                InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements);
389        InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE(
390                scanResult.informationElements);
391
392        // Lookup ANQP data in the cache.
393        long bssid;
394        try {
395            bssid = Utils.parseMac(scanResult.BSSID);
396        } catch (IllegalArgumentException e) {
397            Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID);
398            return allMatches;
399        }
400        ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid,
401                vsa.anqpDomainID);
402        ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey);
403
404        if (anqpEntry == null) {
405            mAnqpRequestManager.requestANQPElements(bssid, anqpKey,
406                    roamingConsortium.anqpOICount > 0,
407                    vsa.hsRelease  == NetworkDetail.HSRelease.R2);
408            Log.d(TAG, "ANQP entry not found for: " + anqpKey);
409            return allMatches;
410        }
411
412        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
413            PasspointProvider provider = entry.getValue();
414            PasspointMatch matchStatus = provider.match(anqpEntry.getElements(), roamingConsortium);
415            if (matchStatus == PasspointMatch.HomeProvider
416                    || matchStatus == PasspointMatch.RoamingProvider) {
417                allMatches.add(Pair.create(provider, matchStatus));
418            }
419        }
420
421        if (allMatches.size() != 0) {
422            for (Pair<PasspointProvider, PasspointMatch> match : allMatches) {
423                Log.d(TAG, String.format("Matched %s to %s as %s", scanResult.SSID,
424                        match.first.getConfig().getHomeSp().getFqdn(),
425                        match.second == PasspointMatch.HomeProvider ? "Home Provider"
426                                : "Roaming Provider"));
427            }
428        } else {
429            Log.d(TAG, "No matches not found for " + scanResult.SSID);
430        }
431
432        return allMatches;
433    }
434
435    /**
436     * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration} to the
437     * current {@link PasspointManager}.
438     *
439     * This will not trigger a config store write, since this will be invoked as part of the
440     * configuration migration, the caller will be responsible for triggering store write
441     * after the migration is completed.
442     *
443     * @param config {@link WifiConfiguration} representation of the Passpoint configuration
444     * @return true on success
445     */
446    public static boolean addLegacyPasspointConfig(WifiConfiguration config) {
447        if (sPasspointManager == null) {
448            Log.e(TAG, "PasspointManager have not been initialized yet");
449            return false;
450        }
451        Log.d(TAG, "Installing legacy Passpoint configuration: " + config.FQDN);
452        return sPasspointManager.addWifiConfig(config);
453    }
454
455    /**
456     * Sweep the ANQP cache to remove expired entries.
457     */
458    public void sweepCache() {
459        mAnqpCache.sweep();
460    }
461
462    /**
463     * Notify the completion of an ANQP request.
464     * TODO(zqiu): currently the notification is done through WifiMonitor,
465     * will no longer be the case once we switch over to use wificond.
466     */
467    public void notifyANQPDone(AnqpEvent anqpEvent) {
468        mHandler.notifyANQPDone(anqpEvent);
469    }
470
471    /**
472     * Notify the completion of an icon request.
473     * TODO(zqiu): currently the notification is done through WifiMonitor,
474     * will no longer be the case once we switch over to use wificond.
475     */
476    public void notifyIconDone(IconEvent iconEvent) {
477        mHandler.notifyIconDone(iconEvent);
478    }
479
480    /**
481     * Notify the reception of a Wireless Network Management (WNM) frame.
482     * TODO(zqiu): currently the notification is done through WifiMonitor,
483     * will no longer be the case once we switch over to use wificond.
484     */
485    public void receivedWnmFrame(WnmData data) {
486        mHandler.notifyWnmFrameReceived(data);
487    }
488
489    /**
490     * Request the specified icon file |fileName| from the specified AP |bssid|.
491     * @return true if the request is sent successfully, false otherwise
492     */
493    public boolean queryPasspointIcon(long bssid, String fileName) {
494        return mHandler.requestIcon(bssid, fileName);
495    }
496
497    /**
498     * Lookup the ANQP elements associated with the given AP from the cache. An empty map
499     * will be returned if no match found in the cache.
500     *
501     * @param scanResult The scan result associated with the AP
502     * @return Map of ANQP elements
503     */
504    public Map<Constants.ANQPElementType, ANQPElement> getANQPElements(ScanResult scanResult) {
505        // Retrieve the Hotspot 2.0 Vendor Specific IE.
506        InformationElementUtil.Vsa vsa =
507                InformationElementUtil.getHS2VendorSpecificIE(scanResult.informationElements);
508
509        // Lookup ANQP data in the cache.
510        long bssid;
511        try {
512            bssid = Utils.parseMac(scanResult.BSSID);
513        } catch (IllegalArgumentException e) {
514            Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID);
515            return new HashMap<Constants.ANQPElementType, ANQPElement>();
516        }
517        ANQPData anqpEntry = mAnqpCache.getEntry(ANQPNetworkKey.buildKey(
518                scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID));
519        if (anqpEntry != null) {
520            return anqpEntry.getElements();
521        }
522        return new HashMap<Constants.ANQPElementType, ANQPElement>();
523    }
524
525    /**
526     * Match the given WiFi AP to an installed Passpoint provider.  A {@link WifiConfiguration}
527     * will be generated and returned if a match is found.  The returned {@link WifiConfiguration}
528     * will contained all the necessary credentials for connecting to the given WiFi AP.
529     *
530     * A {code null} will be returned if no matching provider is found.
531     *
532     * @param scanResult The scan result of the given AP
533     * @return {@link WifiConfiguration}
534     */
535    public WifiConfiguration getMatchingWifiConfig(ScanResult scanResult) {
536        if (scanResult == null) {
537            Log.e(TAG, "Attempt to get matching config for a null ScanResult");
538            return null;
539        }
540        if (!scanResult.isPasspointNetwork()) {
541            Log.e(TAG, "Attempt to get matching config for a non-Passpoint AP");
542            return null;
543        }
544        Pair<PasspointProvider, PasspointMatch> matchedProvider = matchProvider(scanResult);
545        if (matchedProvider == null) {
546            return null;
547        }
548        WifiConfiguration config = matchedProvider.first.getWifiConfig();
549        config.SSID = ScanResultUtil.createQuotedSSID(scanResult.SSID);
550        if (matchedProvider.second == PasspointMatch.HomeProvider) {
551            config.isHomeProviderNetwork = true;
552        }
553        return config;
554    }
555
556    /**
557     * Match the given WiFi AP to all installed Passpoint configurations. Return the list of all
558     * matching configurations (or an empty list if none).
559     *
560     * @param scanResult The scan result of the given AP
561     * @return List of {@link WifiConfiguration}
562     */
563    public List<WifiConfiguration> getAllMatchingWifiConfigs(ScanResult scanResult) {
564        if (scanResult == null) {
565            Log.e(TAG, "Attempt to get matching config for a null ScanResult");
566            return new ArrayList<WifiConfiguration>();
567        }
568        if (!scanResult.isPasspointNetwork()) {
569            Log.e(TAG, "Attempt to get matching config for a non-Passpoint AP");
570            return new ArrayList<WifiConfiguration>();
571        }
572
573        List<Pair<PasspointProvider, PasspointMatch>> matchedProviders = getAllMatchedProviders(
574                scanResult);
575        List<WifiConfiguration> configs = new ArrayList<>();
576        for (Pair<PasspointProvider, PasspointMatch> matchedProvider : matchedProviders) {
577            WifiConfiguration config = matchedProvider.first.getWifiConfig();
578            config.SSID = ScanResultUtil.createQuotedSSID(scanResult.SSID);
579            if (matchedProvider.second == PasspointMatch.HomeProvider) {
580                config.isHomeProviderNetwork = true;
581            }
582            configs.add(config);
583        }
584
585        return configs;
586    }
587
588    /**
589     * Return the list of Hosspot 2.0 OSU (Online Sign-Up) providers associated with the given
590     * AP.
591     *
592     * An empty list will be returned when an invalid scan result is provided or no match is found.
593     *
594     * @param scanResult The scan result of the AP
595     * @return List of {@link OsuProvider}
596     */
597    public List<OsuProvider> getMatchingOsuProviders(ScanResult scanResult) {
598        if (scanResult == null) {
599            Log.e(TAG, "Attempt to retrieve OSU providers for a null ScanResult");
600            return new ArrayList<OsuProvider>();
601        }
602        if (!scanResult.isPasspointNetwork()) {
603            Log.e(TAG, "Attempt to retrieve OSU providers for a non-Passpoint AP");
604            return new ArrayList<OsuProvider>();
605        }
606
607        // Lookup OSU Providers ANQP element.
608        Map<Constants.ANQPElementType, ANQPElement> anqpElements = getANQPElements(scanResult);
609        if (!anqpElements.containsKey(Constants.ANQPElementType.HSOSUProviders)) {
610            return new ArrayList<OsuProvider>();
611        }
612
613        HSOsuProvidersElement element =
614                (HSOsuProvidersElement) anqpElements.get(Constants.ANQPElementType.HSOSUProviders);
615        List<OsuProvider> providers = new ArrayList<>();
616        for (OsuProviderInfo info : element.getProviders()) {
617            // TODO(b/62256482): include icon data once the icon file retrieval and management
618            // support is added.
619            OsuProvider provider = new OsuProvider(element.getOsuSsid(), info.getFriendlyName(),
620                    info.getServiceDescription(), info.getServerUri(),
621                    info.getNetworkAccessIdentifier(), info.getMethodList(), null);
622            providers.add(provider);
623        }
624        return providers;
625    }
626
627    /**
628     * Invoked when a Passpoint network was successfully connected based on the credentials
629     * provided by the given Passpoint provider (specified by its FQDN).
630     *
631     * @param fqdn The FQDN of the Passpoint provider
632     */
633    public void onPasspointNetworkConnected(String fqdn) {
634        PasspointProvider provider = mProviders.get(fqdn);
635        if (provider == null) {
636            Log.e(TAG, "Passpoint network connected without provider: " + fqdn);
637            return;
638        }
639
640        if (!provider.getHasEverConnected()) {
641            // First successful connection using this provider.
642            provider.setHasEverConnected(true);
643        }
644    }
645
646    /**
647     * Update metrics related to installed Passpoint providers, this includes the number of
648     * installed providers and the number of those providers that results in a successful network
649     * connection.
650     */
651    public void updateMetrics() {
652        int numProviders = mProviders.size();
653        int numConnectedProviders = 0;
654        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
655            if (entry.getValue().getHasEverConnected()) {
656                numConnectedProviders++;
657            }
658        }
659        mWifiMetrics.updateSavedPasspointProfiles(numProviders, numConnectedProviders);
660    }
661
662    /**
663     * Dump the current state of PasspointManager to the provided output stream.
664     *
665     * @param pw The output stream to write to
666     */
667    public void dump(PrintWriter pw) {
668        pw.println("Dump of PasspointManager");
669        pw.println("PasspointManager - Providers Begin ---");
670        for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
671            pw.println(entry.getValue());
672        }
673        pw.println("PasspointManager - Providers End ---");
674        pw.println("PasspointManager - Next provider ID to be assigned " + mProviderIndex);
675        mAnqpCache.dump(pw);
676    }
677
678    /**
679     * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration}.
680     *
681     * @param wifiConfig {@link WifiConfiguration} representation of the Passpoint configuration
682     * @return true on success
683     */
684    private boolean addWifiConfig(WifiConfiguration wifiConfig) {
685        if (wifiConfig == null) {
686            return false;
687        }
688
689        // Convert to PasspointConfiguration
690        PasspointConfiguration passpointConfig =
691                PasspointProvider.convertFromWifiConfig(wifiConfig);
692        if (passpointConfig == null) {
693            return false;
694        }
695
696        // Setup aliases for enterprise certificates and key.
697        WifiEnterpriseConfig enterpriseConfig = wifiConfig.enterpriseConfig;
698        String caCertificateAliasSuffix = enterpriseConfig.getCaCertificateAlias();
699        String clientCertAndKeyAliasSuffix = enterpriseConfig.getClientCertificateAlias();
700        if (passpointConfig.getCredential().getUserCredential() != null
701                && TextUtils.isEmpty(caCertificateAliasSuffix)) {
702            Log.e(TAG, "Missing CA Certificate for user credential");
703            return false;
704        }
705        if (passpointConfig.getCredential().getCertCredential() != null) {
706            if (TextUtils.isEmpty(caCertificateAliasSuffix)) {
707                Log.e(TAG, "Missing CA certificate for Certificate credential");
708                return false;
709            }
710            if (TextUtils.isEmpty(clientCertAndKeyAliasSuffix)) {
711                Log.e(TAG, "Missing client certificate and key for certificate credential");
712                return false;
713            }
714        }
715
716        // Note that for legacy configuration, the alias for client private key is the same as the
717        // alias for the client certificate.
718        PasspointProvider provider = new PasspointProvider(passpointConfig, mKeyStore,
719                mSimAccessor, mProviderIndex++, wifiConfig.creatorUid,
720                enterpriseConfig.getCaCertificateAlias(),
721                enterpriseConfig.getClientCertificateAlias(),
722                enterpriseConfig.getClientCertificateAlias(), false, false);
723        mProviders.put(passpointConfig.getHomeSp().getFqdn(), provider);
724        return true;
725    }
726
727    /**
728     * Start the subscription provisioning flow with a provider.
729     * @param callingUid integer indicating the uid of the caller
730     * @param provider {@link OsuProvider} the provider to subscribe to
731     * @param callback {@link IProvisioningCallback} callback to update status to the caller
732     * @return boolean return value from the provisioning method
733     */
734    public boolean startSubscriptionProvisioning(int callingUid, OsuProvider provider,
735            IProvisioningCallback callback) {
736        return mPasspointProvisioner.startSubscriptionProvisioning(callingUid, provider, callback);
737    }
738}
739