blob: 964fceeb6e9f7cee90b5aa037a95cde288631baa [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wifi.hotspot2;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_BSSID;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_DATA;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_FILE;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_BSSID;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_DELAY;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_ESS;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_METHOD;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_URL;
import static android.net.wifi.WifiManager.PASSPOINT_ICON_RECEIVED_ACTION;
import static android.net.wifi.WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
import com.android.server.wifi.Clock;
import com.android.server.wifi.IMSIParameter;
import com.android.server.wifi.SIMAccessor;
import com.android.server.wifi.ScanDetail;
import com.android.server.wifi.WifiKeyStore;
import com.android.server.wifi.WifiNative;
import com.android.server.wifi.hotspot2.anqp.ANQPElement;
import com.android.server.wifi.hotspot2.anqp.Constants;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class provides the APIs to manage Passpoint provider configurations.
* It deals with the following:
* - Maintaining a list of configured Passpoint providers for provider matching.
* - Persisting the providers configurations to store when required.
* - matching Passpoint providers based on the scan results
* - Supporting WifiManager Public API calls:
* > addOrUpdatePasspointConfiguration()
* > removePasspointConfiguration()
* > getPasspointConfigurations()
*
* The provider matching requires obtaining additional information from the AP (ANQP elements).
* The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests.
*
* NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
*/
public class PasspointManager {
private static final String TAG = "PasspointManager";
private final PasspointEventHandler mHandler;
private final SIMAccessor mSimAccessor;
private final WifiKeyStore mKeyStore;
private final PasspointObjectFactory mObjectFactory;
private final Map<String, PasspointProvider> mProviders;
private final AnqpCache mAnqpCache;
private final ANQPRequestManager mAnqpRequestManager;
// Counter used for assigning unique identifier to a provider.
private long mProviderID;
private class CallbackHandler implements PasspointEventHandler.Callbacks {
private final Context mContext;
CallbackHandler(Context context) {
mContext = context;
}
@Override
public void onANQPResponse(long bssid,
Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
// Notify request manager for the completion of a request.
ScanDetail scanDetail =
mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null);
if (anqpElements == null || scanDetail == null) {
// Query failed or the request wasn't originated from us (not tracked by the
// request manager). Nothing to be done.
return;
}
// Add new entry to the cache.
NetworkDetail networkDetail = scanDetail.getNetworkDetail();
ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(networkDetail.getSSID(),
networkDetail.getBSSID(), networkDetail.getHESSID(),
networkDetail.getAnqpDomainID());
mAnqpCache.addEntry(anqpKey, anqpElements);
// Update ANQP elements in the ScanDetail.
scanDetail.propagateANQPInfo(anqpElements);
}
@Override
public void onIconResponse(long bssid, String fileName, byte[] data) {
Intent intent = new Intent(PASSPOINT_ICON_RECEIVED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(EXTRA_PASSPOINT_ICON_BSSID, bssid);
intent.putExtra(EXTRA_PASSPOINT_ICON_FILE, fileName);
if (data != null) {
intent.putExtra(EXTRA_PASSPOINT_ICON_DATA, data);
}
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
@Override
public void onWnmFrameReceived(WnmData event) {
// %012x HS20-SUBSCRIPTION-REMEDIATION "%u %s", osu_method, url
// %012x HS20-DEAUTH-IMMINENT-NOTICE "%u %u %s", code, reauth_delay, url
Intent intent = new Intent(PASSPOINT_WNM_FRAME_RECEIVED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(EXTRA_PASSPOINT_WNM_BSSID, event.getBssid());
intent.putExtra(EXTRA_PASSPOINT_WNM_URL, event.getUrl());
if (event.isDeauthEvent()) {
intent.putExtra(EXTRA_PASSPOINT_WNM_ESS, event.isEss());
intent.putExtra(EXTRA_PASSPOINT_WNM_DELAY, event.getDelay());
} else {
intent.putExtra(EXTRA_PASSPOINT_WNM_METHOD, event.getMethod());
// TODO(zqiu): set the passpoint matching status with the respect to the
// current connected network (e.g. HomeProvider, RoamingProvider, None,
// Declined).
}
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
}
public PasspointManager(Context context, WifiNative wifiNative, WifiKeyStore keyStore,
Clock clock, SIMAccessor simAccessor, PasspointObjectFactory objectFactory) {
mHandler = objectFactory.makePasspointEventHandler(wifiNative,
new CallbackHandler(context));
mKeyStore = keyStore;
mSimAccessor = simAccessor;
mObjectFactory = objectFactory;
mProviders = new HashMap<>();
mAnqpCache = objectFactory.makeAnqpCache(clock);
mAnqpRequestManager = objectFactory.makeANQPRequestManager(mHandler, clock);
mProviderID = 0;
// TODO(zqiu): load providers from the persistent storage.
}
/**
* Add or update a Passpoint provider with the given configuration.
*
* Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name).
* In the case when there is an existing configuration with the same FQDN
* a provider with the new configuration will replace the existing provider.
*
* @param config Configuration of the Passpoint provider to be added
* @return true if provider is added, false otherwise
*/
public boolean addOrUpdateProvider(PasspointConfiguration config) {
if (config == null) {
Log.e(TAG, "Configuration not provided");
return false;
}
if (!config.validate()) {
Log.e(TAG, "Invalid configuration");
return false;
}
// Verify IMSI against the IMSI of the installed SIM cards for SIM credential.
if (config.getCredential().getSimCredential() != null) {
if (mSimAccessor.getMatchingImsis(IMSIParameter.build(
config.getCredential().getSimCredential().getImsi())) == null) {
Log.e(TAG, "IMSI does not match any SIM card");
return false;
}
}
// Create a provider and install the necessary certificates and keys.
PasspointProvider newProvider = mObjectFactory.makePasspointProvider(
config, mKeyStore, mSimAccessor, mProviderID++);
if (!newProvider.installCertsAndKeys()) {
Log.e(TAG, "Failed to install certificates and keys to keystore");
return false;
}
// Remove existing provider with the same FQDN.
if (mProviders.containsKey(config.getHomeSp().getFqdn())) {
Log.d(TAG, "Replacing configuration for " + config.getHomeSp().getFqdn());
removeProvider(config.getHomeSp().getFqdn());
}
mProviders.put(config.getHomeSp().getFqdn(), newProvider);
// TODO(b/31065385): Persist updated providers configuration to the persistent storage.
return true;
}
/**
* Remove a Passpoint provider identified by the given FQDN.
*
* @param fqdn The FQDN of the provider to remove
* @return true if a provider is removed, false otherwise
*/
public boolean removeProvider(String fqdn) {
if (!mProviders.containsKey(fqdn)) {
Log.e(TAG, "Config doesn't exist");
return false;
}
mProviders.get(fqdn).uninstallCertsAndKeys();
mProviders.remove(fqdn);
return true;
}
/**
* Return the installed Passpoint provider configurations.
*
* An empty list will be returned when no provider is installed.
*
* @return A list of {@link PasspointConfiguration}
*/
public List<PasspointConfiguration> getProviderConfigs() {
List<PasspointConfiguration> configs = new ArrayList<>();
for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
configs.add(entry.getValue().getConfig());
}
return configs;
}
/**
* Find the providers that can provide service through the given AP, which means the
* providers contained credential to authenticate with the given AP.
*
* An empty list will returned in the case when no match is found.
*
* @param scanDetail The detail information of the AP
* @return List of {@link PasspointProvider}
*/
public List<Pair<PasspointProvider, PasspointMatch>> matchProvider(ScanDetail scanDetail) {
// Nothing to be done if no Passpoint provider is installed.
if (mProviders.isEmpty()) {
return new ArrayList<Pair<PasspointProvider, PasspointMatch>>();
}
// Lookup ANQP data in the cache.
NetworkDetail networkDetail = scanDetail.getNetworkDetail();
ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(networkDetail.getSSID(),
networkDetail.getBSSID(), networkDetail.getHESSID(),
networkDetail.getAnqpDomainID());
ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey);
if (anqpEntry == null) {
mAnqpRequestManager.requestANQPElements(networkDetail.getBSSID(), scanDetail,
networkDetail.getAnqpOICount() > 0,
networkDetail.getHSRelease() == NetworkDetail.HSRelease.R2);
return new ArrayList<Pair<PasspointProvider, PasspointMatch>>();
}
List<Pair<PasspointProvider, PasspointMatch>> results = new ArrayList<>();
for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
PasspointProvider provider = entry.getValue();
PasspointMatch matchStatus = provider.match(anqpEntry.getElements());
if (matchStatus == PasspointMatch.HomeProvider
|| matchStatus == PasspointMatch.RoamingProvider) {
results.add(new Pair<PasspointProvider, PasspointMatch>(provider, matchStatus));
}
}
return results;
}
/**
* Sweep the ANQP cache to remove expired entries.
*/
public void sweepCache() {
mAnqpCache.sweep();
}
/**
* Notify the completion of an ANQP request.
* TODO(zqiu): currently the notification is done through WifiMonitor,
* will no longer be the case once we switch over to use wificond.
*/
public void notifyANQPDone(long bssid, boolean success) {
mHandler.notifyANQPDone(bssid, success);
}
/**
* Notify the completion of an icon request.
* TODO(zqiu): currently the notification is done through WifiMonitor,
* will no longer be the case once we switch over to use wificond.
*/
public void notifyIconDone(long bssid, IconEvent iconEvent) {
mHandler.notifyIconDone(bssid, iconEvent);
}
/**
* Notify the reception of a Wireless Network Management (WNM) frame.
* TODO(zqiu): currently the notification is done through WifiMonitor,
* will no longer be the case once we switch over to use wificond.
*/
public void receivedWnmFrame(WnmData data) {
mHandler.notifyWnmFrameReceived(data);
}
/**
* Request the specified icon file |fileName| from the specified AP |bssid|.
* @return true if the request is sent successfully, false otherwise
*/
public boolean queryPasspointIcon(long bssid, String fileName) {
return mHandler.requestIcon(bssid, fileName);
}
}