summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRajiv Ranjan <rrajiv@codeaurora.org>2017-12-19 20:15:03 +0530
committerMichael Bestas <mkbestas@lineageos.org>2018-04-30 23:37:37 +0300
commit0c28b97966c3b3379c7c5b59de98032971437378 (patch)
tree881d46894a787ed27bfe1ca394114fb7cb6d39e6
parente0f48b975501e131179f17763b2bbe153f726380 (diff)
downloadandroid_frameworks_opt_net_wifi-0c28b97966c3b3379c7c5b59de98032971437378.tar.gz
android_frameworks_opt_net_wifi-0c28b97966c3b3379c7c5b59de98032971437378.tar.bz2
android_frameworks_opt_net_wifi-0c28b97966c3b3379c7c5b59de98032971437378.zip
Revert "WifiConfigStore: Remove legacy modules"
This reverts commit a83bd15861ae7175b1f62bc2dc8de63e0dbe808e and eventually support loading saved network creds from wpa_supplicant.conf to WifiConfigStore.xml file. CRs-Fixed: 2161641 Change-Id: Ic8222be386bc5064656b9df3f2f1a6af28de73b0
-rw-r--r--service/java/com/android/server/wifi/WifiConfigManager.java50
-rw-r--r--service/java/com/android/server/wifi/WifiConfigStoreLegacy.java354
-rw-r--r--service/java/com/android/server/wifi/WifiInjector.java9
-rw-r--r--service/java/com/android/server/wifi/WifiNetworkHistory.java630
-rw-r--r--service/java/com/android/server/wifi/WifiStateMachine.java4
-rw-r--r--service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java513
-rw-r--r--tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java76
-rw-r--r--tests/wifitests/src/com/android/server/wifi/WifiConfigStoreLegacyTest.java273
-rw-r--r--tests/wifitests/src/com/android/server/wifi/hotspot2/LegacyPasspointConfigParserTest.java204
9 files changed, 2109 insertions, 4 deletions
diff --git a/service/java/com/android/server/wifi/WifiConfigManager.java b/service/java/com/android/server/wifi/WifiConfigManager.java
index 02f8302f9..11ae10b0d 100644
--- a/service/java/com/android/server/wifi/WifiConfigManager.java
+++ b/service/java/com/android/server/wifi/WifiConfigManager.java
@@ -47,6 +47,7 @@ import android.util.Log;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
+import com.android.server.wifi.WifiConfigStoreLegacy.WifiConfigStoreDataLegacy;
import com.android.server.wifi.hotspot2.PasspointManager;
import com.android.server.wifi.util.TelephonyUtil;
import com.android.server.wifi.util.WifiPermissionsUtil;
@@ -247,6 +248,7 @@ public class WifiConfigManager {
private final TelephonyManager mTelephonyManager;
private final WifiKeyStore mWifiKeyStore;
private final WifiConfigStore mWifiConfigStore;
+ private final WifiConfigStoreLegacy mWifiConfigStoreLegacy;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private final WifiPermissionsWrapper mWifiPermissionsWrapper;
/**
@@ -340,7 +342,7 @@ public class WifiConfigManager {
WifiConfigManager(
Context context, Clock clock, UserManager userManager,
TelephonyManager telephonyManager, WifiKeyStore wifiKeyStore,
- WifiConfigStore wifiConfigStore,
+ WifiConfigStore wifiConfigStore, WifiConfigStoreLegacy wifiConfigStoreLegacy,
WifiPermissionsUtil wifiPermissionsUtil,
WifiPermissionsWrapper wifiPermissionsWrapper,
NetworkListStoreData networkListStoreData,
@@ -352,6 +354,7 @@ public class WifiConfigManager {
mTelephonyManager = telephonyManager;
mWifiKeyStore = wifiKeyStore;
mWifiConfigStore = wifiConfigStore;
+ mWifiConfigStoreLegacy = wifiConfigStoreLegacy;
mWifiPermissionsUtil = wifiPermissionsUtil;
mWifiPermissionsWrapper = wifiPermissionsWrapper;
@@ -2702,6 +2705,46 @@ public class WifiConfigManager {
}
/**
+ * Migrate data from legacy store files. The function performs the following operations:
+ * 1. Check if the legacy store files are present and the new store files are absent on device.
+ * 2. Read all the data from the store files.
+ * 3. Save it to the new store files.
+ * 4. Delete the legacy store file.
+ *
+ * @return true if migration was successful or not needed (fresh install), false if it failed.
+ */
+ public boolean migrateFromLegacyStore() {
+ if (!mWifiConfigStoreLegacy.areStoresPresent()) {
+ Log.d(TAG, "Legacy store files not found. No migration needed!");
+ return true;
+ }
+ if (mWifiConfigStore.areStoresPresent()) {
+ Log.d(TAG, "New store files found. No migration needed!"
+ + " Remove legacy store files");
+ mWifiConfigStoreLegacy.removeStores();
+ return true;
+ }
+ WifiConfigStoreDataLegacy storeData = mWifiConfigStoreLegacy.read();
+ Log.d(TAG, "Reading from legacy store completed");
+ loadInternalData(storeData.getConfigurations(), new ArrayList<WifiConfiguration>(),
+ storeData.getDeletedEphemeralSSIDs());
+
+ // Setup user store for the current user in case it have not setup yet, so that data
+ // owned by the current user will be backed to the user store.
+ if (mDeferredUserUnlockRead) {
+ mWifiConfigStore.setUserStore(WifiConfigStore.createUserFile(mCurrentUserId));
+ mDeferredUserUnlockRead = false;
+ }
+
+ if (!saveToStore(true)) {
+ return false;
+ }
+ mWifiConfigStoreLegacy.removeStores();
+ Log.d(TAG, "Migration from legacy store completed");
+ return true;
+ }
+
+ /**
* Read the config store and load the in-memory lists from the store data retrieved and sends
* out the networks changed broadcast.
*
@@ -2715,7 +2758,10 @@ public class WifiConfigManager {
public boolean loadFromStore() {
if (!mWifiConfigStore.areStoresPresent()) {
Log.d(TAG, "New store files not found. No saved networks loaded!");
- mPendingStoreRead = false;
+ if (!mWifiConfigStoreLegacy.areStoresPresent()) {
+ // No legacy store files either, so reset the pending store read flag.
+ mPendingStoreRead = false;
+ }
return true;
}
// If the user unlock comes in before we load from store, which means the user store have
diff --git a/service/java/com/android/server/wifi/WifiConfigStoreLegacy.java b/service/java/com/android/server/wifi/WifiConfigStoreLegacy.java
new file mode 100644
index 000000000..39e48a5cb
--- /dev/null
+++ b/service/java/com/android/server/wifi/WifiConfigStoreLegacy.java
@@ -0,0 +1,354 @@
+/*
+ * 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;
+
+import android.net.IpConfiguration;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.os.Environment;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.server.net.IpConfigStore;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfig;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class provides the API's to load network configurations from legacy store
+ * mechanism (Pre O release).
+ * This class loads network configurations from:
+ * 1. /data/misc/wifi/networkHistory.txt
+ * 2. /data/misc/wifi/wpa_supplicant.conf
+ * 3. /data/misc/wifi/ipconfig.txt
+ * 4. /data/misc/wifi/PerProviderSubscription.conf
+ *
+ * The order of invocation of the public methods during migration is the following:
+ * 1. Check if legacy stores are present using {@link #areStoresPresent()}.
+ * 2. Load all the store data using {@link #read()}
+ * 3. Write the store data to the new store.
+ * 4. Remove all the legacy stores using {@link #removeStores()}
+ *
+ * NOTE: This class should only be used from WifiConfigManager and is not thread-safe!
+ *
+ * TODO(b/31065385): Passpoint config store data migration & deletion.
+ */
+public class WifiConfigStoreLegacy {
+ /**
+ * Log tag.
+ */
+ private static final String TAG = "WifiConfigStoreLegacy";
+ /**
+ * NetworkHistory config store file path.
+ */
+ private static final File NETWORK_HISTORY_FILE =
+ new File(WifiNetworkHistory.NETWORK_HISTORY_CONFIG_FILE);
+ /**
+ * Passpoint config store file path.
+ */
+ private static final File PPS_FILE =
+ new File(Environment.getDataMiscDirectory(), "wifi/PerProviderSubscription.conf");
+ /**
+ * IpConfig config store file path.
+ */
+ private static final File IP_CONFIG_FILE =
+ new File(Environment.getDataMiscDirectory(), "wifi/ipconfig.txt");
+ /**
+ * List of external dependencies for WifiConfigManager.
+ */
+ private final WifiNetworkHistory mWifiNetworkHistory;
+ private final WifiNative mWifiNative;
+ private final IpConfigStore mIpconfigStore;
+
+ private final LegacyPasspointConfigParser mPasspointConfigParser;
+
+ WifiConfigStoreLegacy(WifiNetworkHistory wifiNetworkHistory,
+ WifiNative wifiNative, IpConfigStore ipConfigStore,
+ LegacyPasspointConfigParser passpointConfigParser) {
+ mWifiNetworkHistory = wifiNetworkHistory;
+ mWifiNative = wifiNative;
+ mIpconfigStore = ipConfigStore;
+ mPasspointConfigParser = passpointConfigParser;
+ }
+
+ /**
+ * Helper function to lookup the WifiConfiguration object from configKey to WifiConfiguration
+ * object map using the hashcode of the configKey.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param hashCode hash code of the configKey to match.
+ * @return
+ */
+ private static WifiConfiguration lookupWifiConfigurationUsingConfigKeyHash(
+ Map<String, WifiConfiguration> configurationMap, int hashCode) {
+ for (Map.Entry<String, WifiConfiguration> entry : configurationMap.entrySet()) {
+ if (entry.getKey().hashCode() == hashCode) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper function to load {@link IpConfiguration} data from the ip config store file and
+ * populate the provided configuration map.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ */
+ private void loadFromIpConfigStore(Map<String, WifiConfiguration> configurationMap) {
+ // This is a map of the hash code of the network's configKey to the corresponding
+ // IpConfiguration.
+ SparseArray<IpConfiguration> ipConfigurations =
+ mIpconfigStore.readIpAndProxyConfigurations(IP_CONFIG_FILE.getAbsolutePath());
+ if (ipConfigurations == null || ipConfigurations.size() == 0) {
+ Log.w(TAG, "No ip configurations found in ipconfig store");
+ return;
+ }
+ for (int i = 0; i < ipConfigurations.size(); i++) {
+ int id = ipConfigurations.keyAt(i);
+ WifiConfiguration config =
+ lookupWifiConfigurationUsingConfigKeyHash(configurationMap, id);
+ // This is the only place the map is looked up through a (dangerous) hash-value!
+ if (config == null || config.ephemeral) {
+ Log.w(TAG, "configuration found for missing network, nid=" + id
+ + ", ignored, networks.size=" + Integer.toString(ipConfigurations.size()));
+ } else {
+ config.setIpConfiguration(ipConfigurations.valueAt(i));
+ }
+ }
+ }
+
+ /**
+ * Helper function to load {@link WifiConfiguration} data from networkHistory file and populate
+ * the provided configuration map and deleted ephemeral ssid list.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param deletedEphemeralSSIDs Map of configKey to WifiConfiguration object.
+ */
+ private void loadFromNetworkHistory(
+ Map<String, WifiConfiguration> configurationMap, Set<String> deletedEphemeralSSIDs) {
+ // TODO: Need to revisit the scan detail cache persistance. We're not doing it in the new
+ // config store, so ignore it here as well.
+ Map<Integer, ScanDetailCache> scanDetailCaches = new HashMap<>();
+ mWifiNetworkHistory.readNetworkHistory(
+ configurationMap, scanDetailCaches, deletedEphemeralSSIDs);
+ }
+
+ /**
+ * Helper function to load {@link WifiConfiguration} data from wpa_supplicant and populate
+ * the provided configuration map and network extras.
+ *
+ * This method needs to manually parse the wpa_supplicant.conf file to retrieve some of the
+ * password fields like psk, wep_keys. password, etc.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param networkExtras Map of network extras parsed from wpa_supplicant.
+ */
+ private void loadFromWpaSupplicant(
+ Map<String, WifiConfiguration> configurationMap,
+ SparseArray<Map<String, String>> networkExtras) {
+ if (!mWifiNative.migrateNetworksFromSupplicant(configurationMap, networkExtras)) {
+ Log.wtf(TAG, "Failed to load wifi configurations from wpa_supplicant");
+ return;
+ }
+ if (configurationMap.isEmpty()) {
+ Log.w(TAG, "No wifi configurations found in wpa_supplicant");
+ return;
+ }
+ }
+
+ /**
+ * Helper function to update {@link WifiConfiguration} that represents a Passpoint
+ * configuration.
+ *
+ * This method will manually parse PerProviderSubscription.conf file to retrieve missing
+ * fields: provider friendly name, roaming consortium OIs, realm, IMSI.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param networkExtras Map of network extras parsed from wpa_supplicant.
+ */
+ private void loadFromPasspointConfigStore(
+ Map<String, WifiConfiguration> configurationMap,
+ SparseArray<Map<String, String>> networkExtras) {
+ Map<String, LegacyPasspointConfig> passpointConfigMap = null;
+ try {
+ passpointConfigMap = mPasspointConfigParser.parseConfig(PPS_FILE.getAbsolutePath());
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read/parse Passpoint config file: " + e.getMessage());
+ }
+
+ List<String> entriesToBeRemoved = new ArrayList<>();
+ for (Map.Entry<String, WifiConfiguration> entry : configurationMap.entrySet()) {
+ WifiConfiguration wifiConfig = entry.getValue();
+ // Ignore non-Enterprise network since enterprise configuration is required for
+ // Passpoint.
+ if (wifiConfig.enterpriseConfig == null || wifiConfig.enterpriseConfig.getEapMethod()
+ == WifiEnterpriseConfig.Eap.NONE) {
+ continue;
+ }
+ // Ignore configuration without FQDN.
+ Map<String, String> extras = networkExtras.get(wifiConfig.networkId);
+ if (extras == null || !extras.containsKey(SupplicantStaNetworkHal.ID_STRING_KEY_FQDN)) {
+ continue;
+ }
+ String fqdn = networkExtras.get(wifiConfig.networkId).get(
+ SupplicantStaNetworkHal.ID_STRING_KEY_FQDN);
+
+ // Remove the configuration if failed to find the matching configuration in the
+ // Passpoint configuration file.
+ if (passpointConfigMap == null || !passpointConfigMap.containsKey(fqdn)) {
+ entriesToBeRemoved.add(entry.getKey());
+ continue;
+ }
+
+ // Update the missing Passpoint configuration fields to this WifiConfiguration.
+ LegacyPasspointConfig passpointConfig = passpointConfigMap.get(fqdn);
+ wifiConfig.isLegacyPasspointConfig = true;
+ wifiConfig.FQDN = fqdn;
+ wifiConfig.providerFriendlyName = passpointConfig.mFriendlyName;
+ if (passpointConfig.mRoamingConsortiumOis != null) {
+ wifiConfig.roamingConsortiumIds = Arrays.copyOf(
+ passpointConfig.mRoamingConsortiumOis,
+ passpointConfig.mRoamingConsortiumOis.length);
+ }
+ if (passpointConfig.mImsi != null) {
+ wifiConfig.enterpriseConfig.setPlmn(passpointConfig.mImsi);
+ }
+ if (passpointConfig.mRealm != null) {
+ wifiConfig.enterpriseConfig.setRealm(passpointConfig.mRealm);
+ }
+ }
+
+ // Remove any incomplete Passpoint configurations. Should never happen, in case it does
+ // remove them to avoid maintaining any invalid Passpoint configurations.
+ for (String key : entriesToBeRemoved) {
+ Log.w(TAG, "Remove incomplete Passpoint configuration: " + key);
+ configurationMap.remove(key);
+ }
+ }
+
+ /**
+ * Helper function to load from the different legacy stores:
+ * 1. Read the network configurations from wpa_supplicant using {@link WifiNative}.
+ * 2. Read the network configurations from networkHistory.txt using {@link WifiNetworkHistory}.
+ * 3. Read the Ip configurations from ipconfig.txt using {@link IpConfigStore}.
+ * 4. Read all the passpoint info from PerProviderSubscription.conf using
+ * {@link LegacyPasspointConfigParser}.
+ */
+ public WifiConfigStoreDataLegacy read() {
+ final Map<String, WifiConfiguration> configurationMap = new HashMap<>();
+ final SparseArray<Map<String, String>> networkExtras = new SparseArray<>();
+ final Set<String> deletedEphemeralSSIDs = new HashSet<>();
+
+ loadFromWpaSupplicant(configurationMap, networkExtras);
+ loadFromNetworkHistory(configurationMap, deletedEphemeralSSIDs);
+ loadFromIpConfigStore(configurationMap);
+ loadFromPasspointConfigStore(configurationMap, networkExtras);
+
+ // Now create config store data instance to be returned.
+ return new WifiConfigStoreDataLegacy(
+ new ArrayList<>(configurationMap.values()), deletedEphemeralSSIDs);
+ }
+
+ /**
+ * Function to check if the legacy store files are present and hence load from those stores and
+ * then delete them.
+ *
+ * @return true if legacy store files are present, false otherwise.
+ */
+ public boolean areStoresPresent() {
+ // We may have to keep the wpa_supplicant.conf file around. So, just use networkhistory.txt
+ // as a check to see if we have not yet migrated or not. This should be the last file
+ // that is deleted after migration.
+ File file = new File(WifiNetworkHistory.NETWORK_HISTORY_CONFIG_FILE);
+ return file.exists();
+ }
+
+ /**
+ * Method to remove all the legacy store files. This should only be invoked once all
+ * the data has been migrated to the new store file.
+ * 1. Removes all networks from wpa_supplicant and saves it to wpa_supplicant.conf
+ * 2. Deletes ipconfig.txt
+ * 3. Deletes networkHistory.txt
+ *
+ * @return true if all the store files were deleted successfully, false otherwise.
+ */
+ public boolean removeStores() {
+ // TODO(b/29352330): Delete wpa_supplicant.conf file instead.
+ // First remove all networks from wpa_supplicant and save configuration.
+ if (!mWifiNative.removeAllNetworks()) {
+ Log.e(TAG, "Removing networks from wpa_supplicant failed");
+ }
+
+ // Now remove the ipconfig.txt file.
+ if (!IP_CONFIG_FILE.delete()) {
+ Log.e(TAG, "Removing ipconfig.txt failed");
+ }
+
+ // Now finally remove network history.txt
+ if (!NETWORK_HISTORY_FILE.delete()) {
+ Log.e(TAG, "Removing networkHistory.txt failed");
+ }
+
+ if (!PPS_FILE.delete()) {
+ Log.e(TAG, "Removing PerProviderSubscription.conf failed");
+ }
+
+ Log.i(TAG, "All legacy stores removed!");
+ return true;
+ }
+
+ /**
+ * Interface used to set a masked value in the provided configuration. The masked value is
+ * retrieved by parsing the wpa_supplicant.conf file.
+ */
+ private interface MaskedWpaSupplicantFieldSetter {
+ void setValue(WifiConfiguration config, String value);
+ }
+
+ /**
+ * Class used to encapsulate all the store data retrieved from the legacy (Pre O) store files.
+ */
+ public static class WifiConfigStoreDataLegacy {
+ private List<WifiConfiguration> mConfigurations;
+ private Set<String> mDeletedEphemeralSSIDs;
+ // private List<HomeSP> mHomeSps;
+
+ WifiConfigStoreDataLegacy(List<WifiConfiguration> configurations,
+ Set<String> deletedEphemeralSSIDs) {
+ mConfigurations = configurations;
+ mDeletedEphemeralSSIDs = deletedEphemeralSSIDs;
+ }
+
+ public List<WifiConfiguration> getConfigurations() {
+ return mConfigurations;
+ }
+
+ public Set<String> getDeletedEphemeralSSIDs() {
+ return mDeletedEphemeralSSIDs;
+ }
+ }
+}
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java
index 4b8e6829e..b752e347f 100644
--- a/service/java/com/android/server/wifi/WifiInjector.java
+++ b/service/java/com/android/server/wifi/WifiInjector.java
@@ -45,6 +45,7 @@ import com.android.server.am.BatteryStatsService;
import com.android.server.net.DelayedDiskWrite;
import com.android.server.net.IpConfigStore;
import com.android.server.wifi.aware.WifiAwareMetrics;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser;
import com.android.server.wifi.hotspot2.PasspointManager;
import com.android.server.wifi.hotspot2.PasspointNetworkEvaluator;
import com.android.server.wifi.hotspot2.PasspointObjectFactory;
@@ -100,7 +101,9 @@ public class WifiInjector {
private final WifiMulticastLockManager mWifiMulticastLockManager;
private final WifiConfigStore mWifiConfigStore;
private final WifiKeyStore mWifiKeyStore;
+ private final WifiNetworkHistory mWifiNetworkHistory;
private final IpConfigStore mIpConfigStore;
+ private final WifiConfigStoreLegacy mWifiConfigStoreLegacy;
private final WifiConfigManager mWifiConfigManager;
private final WifiConnectivityHelper mWifiConnectivityHelper;
private final LocalLog mConnectivityLocalLog;
@@ -192,11 +195,15 @@ public class WifiInjector {
WifiConfigStore.createSharedFile());
// Legacy config store
DelayedDiskWrite writer = new DelayedDiskWrite();
+ mWifiNetworkHistory = new WifiNetworkHistory(mContext, writer);
mIpConfigStore = new IpConfigStore(writer);
+ mWifiConfigStoreLegacy = new WifiConfigStoreLegacy(
+ mWifiNetworkHistory, mWifiNative, mIpConfigStore,
+ new LegacyPasspointConfigParser());
// Config Manager
mWifiConfigManager = new WifiConfigManager(mContext, mClock,
UserManager.get(mContext), TelephonyManager.from(mContext),
- mWifiKeyStore, mWifiConfigStore, mWifiPermissionsUtil,
+ mWifiKeyStore, mWifiConfigStore, mWifiConfigStoreLegacy, mWifiPermissionsUtil,
mWifiPermissionsWrapper, new NetworkListStoreData(mContext),
new DeletedEphemeralSsidsStoreData());
mWifiMetrics.setWifiConfigManager(mWifiConfigManager);
diff --git a/service/java/com/android/server/wifi/WifiNetworkHistory.java b/service/java/com/android/server/wifi/WifiNetworkHistory.java
new file mode 100644
index 000000000..282f6057d
--- /dev/null
+++ b/service/java/com/android/server/wifi/WifiNetworkHistory.java
@@ -0,0 +1,630 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
+import android.net.wifi.WifiSsid;
+import android.os.Environment;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.net.DelayedDiskWrite;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Provides an API to read and write the network history from WifiConfigurations to file
+ * This is largely separate and extra to the supplicant config file.
+ */
+public class WifiNetworkHistory {
+ public static final String TAG = "WifiNetworkHistory";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = true;
+ static final String NETWORK_HISTORY_CONFIG_FILE = Environment.getDataDirectory()
+ + "/misc/wifi/networkHistory.txt";
+ /* Network History Keys */
+ private static final String SSID_KEY = "SSID";
+ static final String CONFIG_KEY = "CONFIG";
+ private static final String CONFIG_BSSID_KEY = "CONFIG_BSSID";
+ private static final String CHOICE_KEY = "CHOICE";
+ private static final String CHOICE_TIME_KEY = "CHOICE_TIME";
+ private static final String LINK_KEY = "LINK";
+ private static final String BSSID_KEY = "BSSID";
+ private static final String BSSID_KEY_END = "/BSSID";
+ private static final String RSSI_KEY = "RSSI";
+ private static final String FREQ_KEY = "FREQ";
+ private static final String DATE_KEY = "DATE";
+ private static final String MILLI_KEY = "MILLI";
+ private static final String NETWORK_ID_KEY = "ID";
+ private static final String PRIORITY_KEY = "PRIORITY";
+ private static final String DEFAULT_GW_KEY = "DEFAULT_GW";
+ private static final String AUTH_KEY = "AUTH";
+ private static final String BSSID_STATUS_KEY = "BSSID_STATUS";
+ private static final String SELF_ADDED_KEY = "SELF_ADDED";
+ private static final String DID_SELF_ADD_KEY = "DID_SELF_ADD";
+ private static final String PEER_CONFIGURATION_KEY = "PEER_CONFIGURATION";
+ static final String CREATOR_UID_KEY = "CREATOR_UID_KEY";
+ private static final String CONNECT_UID_KEY = "CONNECT_UID_KEY";
+ private static final String UPDATE_UID_KEY = "UPDATE_UID";
+ private static final String FQDN_KEY = "FQDN";
+ private static final String SCORER_OVERRIDE_KEY = "SCORER_OVERRIDE";
+ private static final String SCORER_OVERRIDE_AND_SWITCH_KEY = "SCORER_OVERRIDE_AND_SWITCH";
+ private static final String VALIDATED_INTERNET_ACCESS_KEY = "VALIDATED_INTERNET_ACCESS";
+ private static final String NO_INTERNET_ACCESS_REPORTS_KEY = "NO_INTERNET_ACCESS_REPORTS";
+ private static final String NO_INTERNET_ACCESS_EXPECTED_KEY = "NO_INTERNET_ACCESS_EXPECTED";
+ private static final String EPHEMERAL_KEY = "EPHEMERAL";
+ private static final String USE_EXTERNAL_SCORES_KEY = "USE_EXTERNAL_SCORES";
+ private static final String METERED_HINT_KEY = "METERED_HINT";
+ private static final String METERED_OVERRIDE_KEY = "METERED_OVERRIDE";
+ private static final String NUM_ASSOCIATION_KEY = "NUM_ASSOCIATION";
+ private static final String DELETED_EPHEMERAL_KEY = "DELETED_EPHEMERAL";
+ private static final String CREATOR_NAME_KEY = "CREATOR_NAME";
+ private static final String UPDATE_NAME_KEY = "UPDATE_NAME";
+ private static final String USER_APPROVED_KEY = "USER_APPROVED";
+ private static final String CREATION_TIME_KEY = "CREATION_TIME";
+ private static final String UPDATE_TIME_KEY = "UPDATE_TIME";
+ static final String SHARED_KEY = "SHARED";
+ private static final String NETWORK_SELECTION_STATUS_KEY = "NETWORK_SELECTION_STATUS";
+ private static final String NETWORK_SELECTION_DISABLE_REASON_KEY =
+ "NETWORK_SELECTION_DISABLE_REASON";
+ private static final String HAS_EVER_CONNECTED_KEY = "HAS_EVER_CONNECTED";
+
+ private static final String SEPARATOR = ": ";
+ private static final String NL = "\n";
+
+ protected final DelayedDiskWrite mWriter;
+ Context mContext;
+ /*
+ * Lost config list, whenever we read a config from networkHistory.txt that was not in
+ * wpa_supplicant.conf
+ */
+ HashSet<String> mLostConfigsDbg = new HashSet<String>();
+
+ public WifiNetworkHistory(Context c, DelayedDiskWrite writer) {
+ mContext = c;
+ mWriter = writer;
+ }
+
+ /**
+ * Write network history to file, for configured networks
+ *
+ * @param networks List of ConfiguredNetworks to write to NetworkHistory
+ */
+ public void writeKnownNetworkHistory(final List<WifiConfiguration> networks,
+ final ConcurrentHashMap<Integer, ScanDetailCache> scanDetailCaches,
+ final Set<String> deletedEphemeralSSIDs) {
+
+ /* Make a copy */
+ //final List<WifiConfiguration> networks = new ArrayList<WifiConfiguration>();
+
+ //for (WifiConfiguration config : mConfiguredNetworks.valuesForAllUsers()) {
+ // networks.add(new WifiConfiguration(config));
+ //}
+
+ mWriter.write(NETWORK_HISTORY_CONFIG_FILE, new DelayedDiskWrite.Writer() {
+ public void onWriteCalled(DataOutputStream out) throws IOException {
+ for (WifiConfiguration config : networks) {
+ //loge("onWriteCalled write SSID: " + config.SSID);
+ /* if (config.getLinkProperties() != null)
+ loge(" lp " + config.getLinkProperties().toString());
+ else
+ loge("attempt config w/o lp");
+ */
+ NetworkSelectionStatus status = config.getNetworkSelectionStatus();
+ if (VDBG) {
+ int numlink = 0;
+ if (config.linkedConfigurations != null) {
+ numlink = config.linkedConfigurations.size();
+ }
+ String disableTime;
+ if (config.getNetworkSelectionStatus().isNetworkEnabled()) {
+ disableTime = "";
+ } else {
+ disableTime = "Disable time: " + DateFormat.getInstance().format(
+ config.getNetworkSelectionStatus().getDisableTime());
+ }
+ logd("saving network history: " + config.configKey() + " gw: "
+ + config.defaultGwMacAddress + " Network Selection-status: "
+ + status.getNetworkStatusString()
+ + disableTime + " ephemeral=" + config.ephemeral
+ + " choice:" + status.getConnectChoice()
+ + " link:" + numlink
+ + " status:" + config.status
+ + " nid:" + config.networkId
+ + " hasEverConnected: " + status.getHasEverConnected());
+ }
+
+ if (!isValid(config)) {
+ continue;
+ }
+
+ if (config.SSID == null) {
+ if (VDBG) {
+ logv("writeKnownNetworkHistory trying to write config with null SSID");
+ }
+ continue;
+ }
+ if (VDBG) {
+ logv("writeKnownNetworkHistory write config " + config.configKey());
+ }
+ out.writeUTF(CONFIG_KEY + SEPARATOR + config.configKey() + NL);
+
+ if (config.SSID != null) {
+ out.writeUTF(SSID_KEY + SEPARATOR + config.SSID + NL);
+ }
+ if (config.BSSID != null) {
+ out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + config.BSSID + NL);
+ } else {
+ out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + "null" + NL);
+ }
+ if (config.FQDN != null) {
+ out.writeUTF(FQDN_KEY + SEPARATOR + config.FQDN + NL);
+ }
+
+ out.writeUTF(PRIORITY_KEY + SEPARATOR + Integer.toString(config.priority) + NL);
+ out.writeUTF(NETWORK_ID_KEY + SEPARATOR
+ + Integer.toString(config.networkId) + NL);
+ out.writeUTF(SELF_ADDED_KEY + SEPARATOR
+ + Boolean.toString(config.selfAdded) + NL);
+ out.writeUTF(DID_SELF_ADD_KEY + SEPARATOR
+ + Boolean.toString(config.didSelfAdd) + NL);
+ out.writeUTF(NO_INTERNET_ACCESS_REPORTS_KEY + SEPARATOR
+ + Integer.toString(config.numNoInternetAccessReports) + NL);
+ out.writeUTF(VALIDATED_INTERNET_ACCESS_KEY + SEPARATOR
+ + Boolean.toString(config.validatedInternetAccess) + NL);
+ out.writeUTF(NO_INTERNET_ACCESS_EXPECTED_KEY + SEPARATOR +
+ Boolean.toString(config.noInternetAccessExpected) + NL);
+ out.writeUTF(EPHEMERAL_KEY + SEPARATOR
+ + Boolean.toString(config.ephemeral) + NL);
+ out.writeUTF(METERED_HINT_KEY + SEPARATOR
+ + Boolean.toString(config.meteredHint) + NL);
+ out.writeUTF(METERED_OVERRIDE_KEY + SEPARATOR
+ + Integer.toString(config.meteredOverride) + NL);
+ out.writeUTF(USE_EXTERNAL_SCORES_KEY + SEPARATOR
+ + Boolean.toString(config.useExternalScores) + NL);
+ if (config.creationTime != null) {
+ out.writeUTF(CREATION_TIME_KEY + SEPARATOR + config.creationTime + NL);
+ }
+ if (config.updateTime != null) {
+ out.writeUTF(UPDATE_TIME_KEY + SEPARATOR + config.updateTime + NL);
+ }
+ if (config.peerWifiConfiguration != null) {
+ out.writeUTF(PEER_CONFIGURATION_KEY + SEPARATOR
+ + config.peerWifiConfiguration + NL);
+ }
+ out.writeUTF(SCORER_OVERRIDE_KEY + SEPARATOR
+ + Integer.toString(config.numScorerOverride) + NL);
+ out.writeUTF(SCORER_OVERRIDE_AND_SWITCH_KEY + SEPARATOR
+ + Integer.toString(config.numScorerOverrideAndSwitchedNetwork) + NL);
+ out.writeUTF(NUM_ASSOCIATION_KEY + SEPARATOR
+ + Integer.toString(config.numAssociation) + NL);
+ out.writeUTF(CREATOR_UID_KEY + SEPARATOR
+ + Integer.toString(config.creatorUid) + NL);
+ out.writeUTF(CONNECT_UID_KEY + SEPARATOR
+ + Integer.toString(config.lastConnectUid) + NL);
+ out.writeUTF(UPDATE_UID_KEY + SEPARATOR
+ + Integer.toString(config.lastUpdateUid) + NL);
+ out.writeUTF(CREATOR_NAME_KEY + SEPARATOR
+ + config.creatorName + NL);
+ out.writeUTF(UPDATE_NAME_KEY + SEPARATOR
+ + config.lastUpdateName + NL);
+ out.writeUTF(USER_APPROVED_KEY + SEPARATOR
+ + Integer.toString(config.userApproved) + NL);
+ out.writeUTF(SHARED_KEY + SEPARATOR + Boolean.toString(config.shared) + NL);
+ String allowedKeyManagementString =
+ makeString(config.allowedKeyManagement,
+ WifiConfiguration.KeyMgmt.strings);
+ out.writeUTF(AUTH_KEY + SEPARATOR
+ + allowedKeyManagementString + NL);
+ out.writeUTF(NETWORK_SELECTION_STATUS_KEY + SEPARATOR
+ + status.getNetworkSelectionStatus() + NL);
+ out.writeUTF(NETWORK_SELECTION_DISABLE_REASON_KEY + SEPARATOR
+ + status.getNetworkSelectionDisableReason() + NL);
+
+ if (status.getConnectChoice() != null) {
+ out.writeUTF(CHOICE_KEY + SEPARATOR + status.getConnectChoice() + NL);
+ out.writeUTF(CHOICE_TIME_KEY + SEPARATOR
+ + status.getConnectChoiceTimestamp() + NL);
+ }
+
+ if (config.linkedConfigurations != null) {
+ log("writeKnownNetworkHistory write linked "
+ + config.linkedConfigurations.size());
+
+ for (String key : config.linkedConfigurations.keySet()) {
+ out.writeUTF(LINK_KEY + SEPARATOR + key + NL);
+ }
+ }
+
+ String macAddress = config.defaultGwMacAddress;
+ if (macAddress != null) {
+ out.writeUTF(DEFAULT_GW_KEY + SEPARATOR + macAddress + NL);
+ }
+
+ if (getScanDetailCache(config, scanDetailCaches) != null) {
+ for (ScanDetail scanDetail : getScanDetailCache(config,
+ scanDetailCaches).values()) {
+ ScanResult result = scanDetail.getScanResult();
+ out.writeUTF(BSSID_KEY + SEPARATOR
+ + result.BSSID + NL);
+ out.writeUTF(FREQ_KEY + SEPARATOR
+ + Integer.toString(result.frequency) + NL);
+
+ out.writeUTF(RSSI_KEY + SEPARATOR
+ + Integer.toString(result.level) + NL);
+
+ out.writeUTF(BSSID_KEY_END + NL);
+ }
+ }
+ out.writeUTF(HAS_EVER_CONNECTED_KEY + SEPARATOR
+ + Boolean.toString(status.getHasEverConnected()) + NL);
+ out.writeUTF(NL);
+ // Add extra blank lines for clarity
+ out.writeUTF(NL);
+ out.writeUTF(NL);
+ }
+ if (deletedEphemeralSSIDs != null && deletedEphemeralSSIDs.size() > 0) {
+ for (String ssid : deletedEphemeralSSIDs) {
+ out.writeUTF(DELETED_EPHEMERAL_KEY);
+ out.writeUTF(ssid);
+ out.writeUTF(NL);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Adds information stored in networkHistory.txt to the given configs. The configs are provided
+ * as a mapping from configKey to WifiConfiguration, because the WifiConfigurations themselves
+ * do not contain sufficient information to compute their configKeys until after the information
+ * that is stored in networkHistory.txt has been added to them.
+ *
+ * @param configs mapping from configKey to a WifiConfiguration that contains the information
+ * information read from wpa_supplicant.conf
+ */
+ public void readNetworkHistory(Map<String, WifiConfiguration> configs,
+ Map<Integer, ScanDetailCache> scanDetailCaches,
+ Set<String> deletedEphemeralSSIDs) {
+
+ try (DataInputStream in =
+ new DataInputStream(new BufferedInputStream(
+ new FileInputStream(NETWORK_HISTORY_CONFIG_FILE)))) {
+
+ String bssid = null;
+ String ssid = null;
+
+ int freq = 0;
+ int status = 0;
+ long seen = 0;
+ int rssi = WifiConfiguration.INVALID_RSSI;
+ String caps = null;
+
+ WifiConfiguration config = null;
+ while (true) {
+ String line = in.readUTF();
+ if (line == null) {
+ break;
+ }
+ int colon = line.indexOf(':');
+ if (colon < 0) {
+ continue;
+ }
+
+ String key = line.substring(0, colon).trim();
+ String value = line.substring(colon + 1).trim();
+
+ if (key.equals(CONFIG_KEY)) {
+ config = configs.get(value);
+
+ // skip reading that configuration data
+ // since we don't have a corresponding network ID
+ if (config == null) {
+ Log.e(TAG, "readNetworkHistory didnt find netid for hash="
+ + Integer.toString(value.hashCode())
+ + " key: " + value);
+ mLostConfigsDbg.add(value);
+ continue;
+ } else {
+ // After an upgrade count old connections as owned by system
+ if (config.creatorName == null || config.lastUpdateName == null) {
+ config.creatorName =
+ mContext.getPackageManager().getNameForUid(Process.SYSTEM_UID);
+ config.lastUpdateName = config.creatorName;
+
+ if (DBG) {
+ Log.w(TAG, "Upgrading network " + config.networkId
+ + " to " + config.creatorName);
+ }
+ }
+ }
+ } else if (config != null) {
+ NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
+ switch (key) {
+ case SSID_KEY:
+ if (config.isPasspoint()) {
+ break;
+ }
+ ssid = value;
+ if (config.SSID != null && !config.SSID.equals(ssid)) {
+ loge("Error parsing network history file, mismatched SSIDs");
+ config = null; //error
+ ssid = null;
+ } else {
+ config.SSID = ssid;
+ }
+ break;
+ case CONFIG_BSSID_KEY:
+ config.BSSID = value.equals("null") ? null : value;
+ break;
+ case FQDN_KEY:
+ // Check for literal 'null' to be backwards compatible.
+ config.FQDN = value.equals("null") ? null : value;
+ break;
+ case DEFAULT_GW_KEY:
+ config.defaultGwMacAddress = value;
+ break;
+ case SELF_ADDED_KEY:
+ config.selfAdded = Boolean.parseBoolean(value);
+ break;
+ case DID_SELF_ADD_KEY:
+ config.didSelfAdd = Boolean.parseBoolean(value);
+ break;
+ case NO_INTERNET_ACCESS_REPORTS_KEY:
+ config.numNoInternetAccessReports = Integer.parseInt(value);
+ break;
+ case VALIDATED_INTERNET_ACCESS_KEY:
+ config.validatedInternetAccess = Boolean.parseBoolean(value);
+ break;
+ case NO_INTERNET_ACCESS_EXPECTED_KEY:
+ config.noInternetAccessExpected = Boolean.parseBoolean(value);
+ break;
+ case CREATION_TIME_KEY:
+ config.creationTime = value;
+ break;
+ case UPDATE_TIME_KEY:
+ config.updateTime = value;
+ break;
+ case EPHEMERAL_KEY:
+ config.ephemeral = Boolean.parseBoolean(value);
+ break;
+ case METERED_HINT_KEY:
+ config.meteredHint = Boolean.parseBoolean(value);
+ break;
+ case METERED_OVERRIDE_KEY:
+ config.meteredOverride = Integer.parseInt(value);
+ break;
+ case USE_EXTERNAL_SCORES_KEY:
+ config.useExternalScores = Boolean.parseBoolean(value);
+ break;
+ case CREATOR_UID_KEY:
+ config.creatorUid = Integer.parseInt(value);
+ break;
+ case SCORER_OVERRIDE_KEY:
+ config.numScorerOverride = Integer.parseInt(value);
+ break;
+ case SCORER_OVERRIDE_AND_SWITCH_KEY:
+ config.numScorerOverrideAndSwitchedNetwork = Integer.parseInt(value);
+ break;
+ case NUM_ASSOCIATION_KEY:
+ config.numAssociation = Integer.parseInt(value);
+ break;
+ case CONNECT_UID_KEY:
+ config.lastConnectUid = Integer.parseInt(value);
+ break;
+ case UPDATE_UID_KEY:
+ config.lastUpdateUid = Integer.parseInt(value);
+ break;
+ case PEER_CONFIGURATION_KEY:
+ config.peerWifiConfiguration = value;
+ break;
+ case NETWORK_SELECTION_STATUS_KEY:
+ int networkStatusValue = Integer.parseInt(value);
+ // Reset temporarily disabled network status
+ if (networkStatusValue ==
+ NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED) {
+ networkStatusValue =
+ NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
+ }
+ networkStatus.setNetworkSelectionStatus(networkStatusValue);
+ break;
+ case NETWORK_SELECTION_DISABLE_REASON_KEY:
+ networkStatus.setNetworkSelectionDisableReason(Integer.parseInt(value));
+ break;
+ case CHOICE_KEY:
+ networkStatus.setConnectChoice(value);
+ break;
+ case CHOICE_TIME_KEY:
+ networkStatus.setConnectChoiceTimestamp(Long.parseLong(value));
+ break;
+ case LINK_KEY:
+ if (config.linkedConfigurations == null) {
+ config.linkedConfigurations = new HashMap<>();
+ } else {
+ config.linkedConfigurations.put(value, -1);
+ }
+ break;
+ case BSSID_KEY:
+ status = 0;
+ ssid = null;
+ bssid = null;
+ freq = 0;
+ seen = 0;
+ rssi = WifiConfiguration.INVALID_RSSI;
+ caps = "";
+ break;
+ case RSSI_KEY:
+ rssi = Integer.parseInt(value);
+ break;
+ case FREQ_KEY:
+ freq = Integer.parseInt(value);
+ break;
+ case DATE_KEY:
+ /*
+ * when reading the configuration from file we don't update the date
+ * so as to avoid reading back stale or non-sensical data that would
+ * depend on network time.
+ * The date of a WifiConfiguration should only come from actual scan
+ * result.
+ *
+ String s = key.replace(FREQ_KEY, "");
+ seen = Integer.getInteger(s);
+ */
+ break;
+ case BSSID_KEY_END:
+ if ((bssid != null) && (ssid != null)) {
+ if (getScanDetailCache(config, scanDetailCaches) != null) {
+ WifiSsid wssid = WifiSsid.createFromAsciiEncoded(ssid);
+ ScanDetail scanDetail = new ScanDetail(wssid, bssid,
+ caps, rssi, freq, (long) 0, seen);
+ getScanDetailCache(config, scanDetailCaches).put(scanDetail);
+ }
+ }
+ break;
+ case DELETED_EPHEMERAL_KEY:
+ if (!TextUtils.isEmpty(value)) {
+ deletedEphemeralSSIDs.add(value);
+ }
+ break;
+ case CREATOR_NAME_KEY:
+ config.creatorName = value;
+ break;
+ case UPDATE_NAME_KEY:
+ config.lastUpdateName = value;
+ break;
+ case USER_APPROVED_KEY:
+ config.userApproved = Integer.parseInt(value);
+ break;
+ case SHARED_KEY:
+ config.shared = Boolean.parseBoolean(value);
+ break;
+ case HAS_EVER_CONNECTED_KEY:
+ networkStatus.setHasEverConnected(Boolean.parseBoolean(value));
+ break;
+ }
+ }
+ }
+ } catch (EOFException e) {
+ // do nothing
+ } catch (FileNotFoundException e) {
+ Log.i(TAG, "readNetworkHistory: no config file, " + e);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "readNetworkHistory: failed to parse, " + e, e);
+ } catch (IOException e) {
+ Log.e(TAG, "readNetworkHistory: failed to read, " + e, e);
+ }
+ }
+
+ /**
+ * Ported this out of WifiServiceImpl, I have no idea what it's doing
+ * <TODO> figure out what/why this is doing
+ * <TODO> Port it into WifiConfiguration, then remove all the silly business from ServiceImpl
+ */
+ public boolean isValid(WifiConfiguration config) {
+ if (config.allowedKeyManagement == null) {
+ return false;
+ }
+ if (config.allowedKeyManagement.cardinality() > 1) {
+ if (config.allowedKeyManagement.cardinality() != 2) {
+ return false;
+ }
+ if (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) {
+ return false;
+ }
+ if ((!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.IEEE8021X))
+ && (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static String makeString(BitSet set, String[] strings) {
+ StringBuffer buf = new StringBuffer();
+ int nextSetBit = -1;
+
+ /* Make sure all set bits are in [0, strings.length) to avoid
+ * going out of bounds on strings. (Shouldn't happen, but...) */
+ set = set.get(0, strings.length);
+
+ while ((nextSetBit = set.nextSetBit(nextSetBit + 1)) != -1) {
+ buf.append(strings[nextSetBit].replace('_', '-')).append(' ');
+ }
+
+ // remove trailing space
+ if (set.cardinality() > 0) {
+ buf.setLength(buf.length() - 1);
+ }
+
+ return buf.toString();
+ }
+
+ protected void logv(String s) {
+ Log.v(TAG, s);
+ }
+ protected void logd(String s) {
+ Log.d(TAG, s);
+ }
+ protected void log(String s) {
+ Log.d(TAG, s);
+ }
+ protected void loge(String s) {
+ loge(s, false);
+ }
+ protected void loge(String s, boolean stack) {
+ if (stack) {
+ Log.e(TAG, s + " stack:" + Thread.currentThread().getStackTrace()[2].getMethodName()
+ + " - " + Thread.currentThread().getStackTrace()[3].getMethodName()
+ + " - " + Thread.currentThread().getStackTrace()[4].getMethodName()
+ + " - " + Thread.currentThread().getStackTrace()[5].getMethodName());
+ } else {
+ Log.e(TAG, s);
+ }
+ }
+
+ private ScanDetailCache getScanDetailCache(WifiConfiguration config,
+ Map<Integer, ScanDetailCache> scanDetailCaches) {
+ if (config == null || scanDetailCaches == null) return null;
+ ScanDetailCache cache = scanDetailCaches.get(config.networkId);
+ if (cache == null && config.networkId != WifiConfiguration.INVALID_NETWORK_ID) {
+ cache =
+ new ScanDetailCache(
+ config, WifiConfigManager.SCAN_CACHE_ENTRIES_MAX_SIZE,
+ WifiConfigManager.SCAN_CACHE_ENTRIES_TRIM_SIZE);
+ scanDetailCaches.put(config.networkId, cache);
+ }
+ return cache;
+ }
+}
diff --git a/service/java/com/android/server/wifi/WifiStateMachine.java b/service/java/com/android/server/wifi/WifiStateMachine.java
index ac7d748bb..44d6339c6 100644
--- a/service/java/com/android/server/wifi/WifiStateMachine.java
+++ b/service/java/com/android/server/wifi/WifiStateMachine.java
@@ -4278,6 +4278,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mLastSignalLevel = -1;
mWifiInfo.setMacAddress(mWifiNative.getMacAddress());
+ // Attempt to migrate data out of legacy store.
+ if (!mWifiConfigManager.migrateFromLegacyStore()) {
+ Log.e(TAG, "Failed to migrate from legacy config store");
+ }
initializeWpsDetails();
sendSupplicantConnectionChangedBroadcast(true);
transitionTo(mSupplicantStartedState);
diff --git a/service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java b/service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java
new file mode 100644
index 000000000..31795f126
--- /dev/null
+++ b/service/java/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2017 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 android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class for parsing legacy (N and older) Passpoint configuration file content
+ * (/data/misc/wifi/PerProviderSubscription.conf). In N and older, only Release 1 is supported.
+ *
+ * This class only retrieve the relevant Release 1 configuration fields that are not backed
+ * elsewhere. Below are relevant fields:
+ * - FQDN (used for linking with configuration data stored elsewhere)
+ * - Friendly Name
+ * - Roaming Consortium
+ * - Realm
+ * - IMSI (for SIM credential)
+ *
+ * Below is an example content of a Passpoint configuration file:
+ *
+ * tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)
+ * 8:MgmtTree+
+ * 17:PerProviderSubscription+
+ * 4:r1i1+
+ * 6:HomeSP+
+ * c:FriendlyName=d:Test Provider
+ * 4:FQDN=8:test.net
+ * 13:RoamingConsortiumOI=9:1234,5678
+ * .
+ * a:Credential+
+ * 10:UsernamePassword+
+ * 8:Username=4:user
+ * 8:Password=4:pass
+ *
+ * 9:EAPMethod+
+ * 7:EAPType=2:21
+ * b:InnerMethod=3:PAP
+ * .
+ * .
+ * 5:Realm=a:boingo.com
+ * .
+ * .
+ * .
+ * .
+ *
+ * Each string is prefixed with a "|StringBytesInHex|:".
+ * '+' indicates start of a new internal node.
+ * '.' indicates end of the current internal node.
+ * '=' indicates "value" of a leaf node.
+ *
+ */
+public class LegacyPasspointConfigParser {
+ private static final String TAG = "LegacyPasspointConfigParser";
+
+ private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
+ private static final String TAG_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";
+ private static final String TAG_HOMESP = "HomeSP";
+ private static final String TAG_FQDN = "FQDN";
+ private static final String TAG_FRIENDLY_NAME = "FriendlyName";
+ private static final String TAG_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
+ private static final String TAG_CREDENTIAL = "Credential";
+ private static final String TAG_REALM = "Realm";
+ private static final String TAG_SIM = "SIM";
+ private static final String TAG_IMSI = "IMSI";
+
+ private static final String LONG_ARRAY_SEPARATOR = ",";
+ private static final String END_OF_INTERNAL_NODE_INDICATOR = ".";
+ private static final char START_OF_INTERNAL_NODE_INDICATOR = '+';
+ private static final char STRING_PREFIX_INDICATOR = ':';
+ private static final char STRING_VALUE_INDICATOR = '=';
+
+ /**
+ * An abstraction for a node within a tree. A node can be an internal node (contained
+ * children nodes) or a leaf node (contained a String value).
+ */
+ private abstract static class Node {
+ private final String mName;
+ Node(String name) {
+ mName = name;
+ }
+
+ /**
+ * @return the name of the node
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Applies for internal node only.
+ *
+ * @return the list of children nodes.
+ */
+ public abstract List<Node> getChildren();
+
+ /**
+ * Applies for leaf node only.
+ *
+ * @return the string value of the node
+ */
+ public abstract String getValue();
+ }
+
+ /**
+ * Class representing an internal node of a tree. It contained a list of child nodes.
+ */
+ private static class InternalNode extends Node {
+ private final List<Node> mChildren;
+ InternalNode(String name, List<Node> children) {
+ super(name);
+ mChildren = children;
+ }
+
+ @Override
+ public List<Node> getChildren() {
+ return mChildren;
+ }
+
+ @Override
+ public String getValue() {
+ return null;
+ }
+ }
+
+ /**
+ * Class representing a leaf node of a tree. It contained a String type value.
+ */
+ private static class LeafNode extends Node {
+ private final String mValue;
+ LeafNode(String name, String value) {
+ super(name);
+ mValue = value;
+ }
+
+ @Override
+ public List<Node> getChildren() {
+ return null;
+ }
+
+ @Override
+ public String getValue() {
+ return mValue;
+ }
+ }
+
+ public LegacyPasspointConfigParser() {}
+
+ /**
+ * Parse the legacy Passpoint configuration file content, only retrieve the relevant
+ * configurations that are not saved elsewhere.
+ *
+ * For both N and M, only Release 1 is supported. Most of the configurations are saved
+ * elsewhere as part of the {@link android.net.wifi.WifiConfiguration} data.
+ * The configurations needed from the legacy Passpoint configuration file are:
+ *
+ * - FQDN - needed to be able to link to the associated {@link WifiConfiguration} data
+ * - Friendly Name
+ * - Roaming Consortium OIs
+ * - Realm
+ * - IMSI (for SIM credential)
+ *
+ * Make this function non-static so that it can be mocked during unit test.
+ *
+ * @param fileName The file name of the configuration file
+ * @return Map of FQDN to {@link LegacyPasspointConfig}
+ * @throws IOException
+ */
+ public Map<String, LegacyPasspointConfig> parseConfig(String fileName)
+ throws IOException {
+ Map<String, LegacyPasspointConfig> configs = new HashMap<>();
+ BufferedReader in = new BufferedReader(new FileReader(fileName));
+ in.readLine(); // Ignore the first line which contained the header.
+
+ // Convert the configuration data to a management tree represented by a root {@link Node}.
+ Node root = buildNode(in);
+
+ if (root == null || root.getChildren() == null) {
+ Log.d(TAG, "Empty configuration data");
+ return configs;
+ }
+
+ // Verify root node name.
+ if (!TextUtils.equals(TAG_MANAGEMENT_TREE, root.getName())) {
+ throw new IOException("Unexpected root node: " + root.getName());
+ }
+
+ // Process and retrieve the configuration from each PPS (PerProviderSubscription) node.
+ List<Node> ppsNodes = root.getChildren();
+ for (Node ppsNode : ppsNodes) {
+ LegacyPasspointConfig config = processPpsNode(ppsNode);
+ configs.put(config.mFqdn, config);
+ }
+ return configs;
+ }
+
+ /**
+ * Build a {@link Node} from the current line in the buffer. A node can be an internal
+ * node (ends with '+') or a leaf node.
+ *
+ * @param in Input buffer to read data from
+ * @return {@link Node} representing the current line
+ * @throws IOException
+ */
+ private static Node buildNode(BufferedReader in) throws IOException {
+ // Read until non-empty line.
+ String currentLine = null;
+ while ((currentLine = in.readLine()) != null) {
+ if (!currentLine.isEmpty()) {
+ break;
+ }
+ }
+
+ // Return null if EOF is reached.
+ if (currentLine == null) {
+ return null;
+ }
+
+ // Remove the leading and the trailing whitespaces.
+ currentLine = currentLine.trim();
+
+ // Check for the internal node terminator.
+ if (TextUtils.equals(END_OF_INTERNAL_NODE_INDICATOR, currentLine)) {
+ return null;
+ }
+
+ // Parse the name-value of the current line. The value will be null if the current line
+ // is not a leaf node (e.g. line ends with a '+').
+ // Each line is encoded in UTF-8.
+ Pair<String, String> nameValuePair =
+ parseLine(currentLine.getBytes(StandardCharsets.UTF_8));
+ if (nameValuePair.second != null) {
+ return new LeafNode(nameValuePair.first, nameValuePair.second);
+ }
+
+ // Parse the children contained under this internal node.
+ List<Node> children = new ArrayList<>();
+ Node child = null;
+ while ((child = buildNode(in)) != null) {
+ children.add(child);
+ }
+ return new InternalNode(nameValuePair.first, children);
+ }
+
+ /**
+ * Process a PPS (PerProviderSubscription) node to retrieve Passpoint configuration data.
+ *
+ * @param ppsNode The PPS node to process
+ * @return {@link LegacyPasspointConfig}
+ * @throws IOException
+ */
+ private static LegacyPasspointConfig processPpsNode(Node ppsNode) throws IOException {
+ if (ppsNode.getChildren() == null || ppsNode.getChildren().size() != 1) {
+ throw new IOException("PerProviderSubscription node should contain "
+ + "one instance node");
+ }
+
+ if (!TextUtils.equals(TAG_PER_PROVIDER_SUBSCRIPTION, ppsNode.getName())) {
+ throw new IOException("Unexpected name for PPS node: " + ppsNode.getName());
+ }
+
+ // Retrieve the PPS instance node.
+ Node instanceNode = ppsNode.getChildren().get(0);
+ if (instanceNode.getChildren() == null) {
+ throw new IOException("PPS instance node doesn't contained any children");
+ }
+
+ // Process and retrieve the relevant configurations under the PPS instance node.
+ LegacyPasspointConfig config = new LegacyPasspointConfig();
+ for (Node node : instanceNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_HOMESP:
+ processHomeSPNode(node, config);
+ break;
+ case TAG_CREDENTIAL:
+ processCredentialNode(node, config);
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under PPS instance: " + node.getName());
+ break;
+ }
+ }
+ if (config.mFqdn == null) {
+ throw new IOException("PPS instance missing FQDN");
+ }
+ return config;
+ }
+
+ /**
+ * Process a HomeSP node to retrieve configuration data into the given |config|.
+ *
+ * @param homeSpNode The HomeSP node to process
+ * @param config The config object to fill in the data
+ * @throws IOException
+ */
+ private static void processHomeSPNode(Node homeSpNode, LegacyPasspointConfig config)
+ throws IOException {
+ if (homeSpNode.getChildren() == null) {
+ throw new IOException("HomeSP node should contain at least one child node");
+ }
+
+ for (Node node : homeSpNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_FQDN:
+ config.mFqdn = getValue(node);
+ break;
+ case TAG_FRIENDLY_NAME:
+ config.mFriendlyName = getValue(node);
+ break;
+ case TAG_ROAMING_CONSORTIUM_OI:
+ config.mRoamingConsortiumOis = parseLongArray(getValue(node));
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under HomeSP: " + node.getName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Process a Credential node to retrieve configuration data into the given |config|.
+ *
+ * @param credentialNode The Credential node to process
+ * @param config The config object to fill in the data
+ * @throws IOException
+ */
+ private static void processCredentialNode(Node credentialNode,
+ LegacyPasspointConfig config)
+ throws IOException {
+ if (credentialNode.getChildren() == null) {
+ throw new IOException("Credential node should contain at least one child node");
+ }
+
+ for (Node node : credentialNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_REALM:
+ config.mRealm = getValue(node);
+ break;
+ case TAG_SIM:
+ processSimNode(node, config);
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under Credential: " + node.getName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Process a SIM node to retrieve configuration data into the given |config|.
+ *
+ * @param simNode The SIM node to process
+ * @param config The config object to fill in the data
+ * @throws IOException
+ */
+ private static void processSimNode(Node simNode, LegacyPasspointConfig config)
+ throws IOException {
+ if (simNode.getChildren() == null) {
+ throw new IOException("SIM node should contain at least one child node");
+ }
+
+ for (Node node : simNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_IMSI:
+ config.mImsi = getValue(node);
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under SIM: " + node.getName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parse the given line in the legacy Passpoint configuration file.
+ * A line can be in the following formats:
+ * 2:ab+ // internal node
+ * 2:ab=2:bc // leaf node
+ * . // end of internal node
+ *
+ * @param line The line to parse
+ * @return name-value pair, a value of null indicates internal node
+ * @throws IOException
+ */
+ private static Pair<String, String> parseLine(byte[] lineBytes) throws IOException {
+ Pair<String, Integer> nameIndexPair = parseString(lineBytes, 0);
+ int currentIndex = nameIndexPair.second;
+ try {
+ if (lineBytes[currentIndex] == START_OF_INTERNAL_NODE_INDICATOR) {
+ return Pair.create(nameIndexPair.first, null);
+ }
+
+ if (lineBytes[currentIndex] != STRING_VALUE_INDICATOR) {
+ throw new IOException("Invalid line - missing both node and value indicator: "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("Invalid line - " + e.getMessage() + ": "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+ Pair<String, Integer> valueIndexPair = parseString(lineBytes, currentIndex + 1);
+ return Pair.create(nameIndexPair.first, valueIndexPair.first);
+ }
+
+ /**
+ * Parse a string value in the given line from the given start index.
+ * A string value is in the following format:
+ * |HexByteLength|:|String|
+ *
+ * The length value indicates the number of UTF-8 bytes in hex for the given string.
+ *
+ * For example: 3:abc
+ *
+ * @param lineBytes The UTF-8 bytes of the line to parse
+ * @param startIndex The start index from the given line to parse from
+ * @return Pair of a string value and an index pointed to character after the string value
+ * @throws IOException
+ */
+ private static Pair<String, Integer> parseString(byte[] lineBytes, int startIndex)
+ throws IOException {
+ // Locate the index that separate length and the string value.
+ int prefixIndex = -1;
+ for (int i = startIndex; i < lineBytes.length; i++) {
+ if (lineBytes[i] == STRING_PREFIX_INDICATOR) {
+ prefixIndex = i;
+ break;
+ }
+ }
+ if (prefixIndex == -1) {
+ throw new IOException("Invalid line - missing string prefix: "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+
+ try {
+ String lengthStr = new String(lineBytes, startIndex, prefixIndex - startIndex,
+ StandardCharsets.UTF_8);
+ int length = Integer.parseInt(lengthStr, 16);
+ int strStartIndex = prefixIndex + 1;
+ // The length might account for bytes for the whitespaces, since the whitespaces are
+ // already trimmed, ignore them.
+ if ((strStartIndex + length) > lineBytes.length) {
+ length = lineBytes.length - strStartIndex;
+ }
+ return Pair.create(
+ new String(lineBytes, strStartIndex, length, StandardCharsets.UTF_8),
+ strStartIndex + length);
+ } catch (NumberFormatException | IndexOutOfBoundsException e) {
+ throw new IOException("Invalid line - " + e.getMessage() + ": "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+ }
+
+ /**
+ * Parse a long array from the given string.
+ *
+ * @param str The string to parse
+ * @return long[]
+ * @throws IOException
+ */
+ private static long[] parseLongArray(String str)
+ throws IOException {
+ String[] strArray = str.split(LONG_ARRAY_SEPARATOR);
+ long[] longArray = new long[strArray.length];
+ for (int i = 0; i < longArray.length; i++) {
+ try {
+ longArray[i] = Long.parseLong(strArray[i], 16);
+ } catch (NumberFormatException e) {
+ throw new IOException("Invalid long integer value: " + strArray[i]);
+ }
+ }
+ return longArray;
+ }
+
+ /**
+ * Get the String value of the given node. An IOException will be thrown if the given
+ * node doesn't contain a String value (internal node).
+ *
+ * @param node The node to get the value from
+ * @return String
+ * @throws IOException
+ */
+ private static String getValue(Node node) throws IOException {
+ if (node.getValue() == null) {
+ throw new IOException("Attempt to retreive value from non-leaf node: "
+ + node.getName());
+ }
+ return node.getValue();
+ }
+}
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java
index 0fa660081..d1e05529b 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiConfigManagerTest.java
@@ -43,6 +43,7 @@ import android.text.TextUtils;
import android.util.Pair;
import com.android.internal.R;
+import com.android.server.wifi.WifiConfigStoreLegacy.WifiConfigStoreDataLegacy;
import com.android.server.wifi.util.WifiPermissionsUtil;
import com.android.server.wifi.util.WifiPermissionsWrapper;
@@ -100,6 +101,7 @@ public class WifiConfigManagerTest {
@Mock private TelephonyManager mTelephonyManager;
@Mock private WifiKeyStore mWifiKeyStore;
@Mock private WifiConfigStore mWifiConfigStore;
+ @Mock private WifiConfigStoreLegacy mWifiConfigStoreLegacy;
@Mock private PackageManager mPackageManager;
@Mock private DevicePolicyManagerInternal mDevicePolicyManagerInternal;
@Mock private WifiPermissionsUtil mWifiPermissionsUtil;
@@ -2619,6 +2621,74 @@ public class WifiConfigManagerTest {
}
/**
+ * Verifies the loading of networks using {@link WifiConfigManager#migrateFromLegacyStore()} ()}
+ * attempts to migrate data from legacy stores when the legacy store files are present.
+ */
+ @Test
+ public void testMigrationFromLegacyStore() throws Exception {
+ // Create the store data to be returned from legacy stores.
+ List<WifiConfiguration> networks = new ArrayList<>();
+ networks.add(WifiConfigurationTestUtil.createPskNetwork());
+ networks.add(WifiConfigurationTestUtil.createEapNetwork());
+ networks.add(WifiConfigurationTestUtil.createWepNetwork());
+ String deletedEphemeralSSID = "EphemeralSSID";
+ Set<String> deletedEphermalSSIDs = new HashSet<>(Arrays.asList(deletedEphemeralSSID));
+ WifiConfigStoreDataLegacy storeData =
+ new WifiConfigStoreDataLegacy(networks, deletedEphermalSSIDs);
+
+ when(mWifiConfigStoreLegacy.areStoresPresent()).thenReturn(true);
+ when(mWifiConfigStore.areStoresPresent()).thenReturn(false);
+ when(mWifiConfigStoreLegacy.read()).thenReturn(storeData);
+
+ // Now trigger the migration from legacy store. This should populate the in memory list with
+ // all the networks above from the legacy store.
+ assertTrue(mWifiConfigManager.migrateFromLegacyStore());
+
+ verify(mWifiConfigStoreLegacy).read();
+ verify(mWifiConfigStoreLegacy).removeStores();
+
+ List<WifiConfiguration> retrievedNetworks =
+ mWifiConfigManager.getConfiguredNetworksWithPasswords();
+ WifiConfigurationTestUtil.assertConfigurationsEqualForConfigManagerAddOrUpdate(
+ networks, retrievedNetworks);
+ assertTrue(mWifiConfigManager.wasEphemeralNetworkDeleted(deletedEphemeralSSID));
+ }
+
+ /**
+ * Verifies the loading of networks using {@link WifiConfigManager#migrateFromLegacyStore()} ()}
+ * does not attempt to migrate data from legacy stores when the legacy store files are absent
+ * (i.e migration was already done once).
+ */
+ @Test
+ public void testNoDuplicateMigrationFromLegacyStore() throws Exception {
+ when(mWifiConfigStoreLegacy.areStoresPresent()).thenReturn(false);
+
+ // Now trigger a migration from legacy store.
+ assertTrue(mWifiConfigManager.migrateFromLegacyStore());
+
+ verify(mWifiConfigStoreLegacy, never()).read();
+ verify(mWifiConfigStoreLegacy, never()).removeStores();
+ }
+
+ /**
+ * Verifies the loading of networks using {@link WifiConfigManager#migrateFromLegacyStore()} ()}
+ * does not attempt to migrate data from legacy stores when the new store files are present
+ * (i.e migration was already done once).
+ */
+ @Test
+ public void testNewStoreFilesPresentNoMigrationFromLegacyStore() throws Exception {
+ when(mWifiConfigStore.areStoresPresent()).thenReturn(true);
+ when(mWifiConfigStoreLegacy.areStoresPresent()).thenReturn(true);
+
+ // Now trigger a migration from legacy store.
+ assertTrue(mWifiConfigManager.migrateFromLegacyStore());
+
+ verify(mWifiConfigStoreLegacy, never()).read();
+ // Verify that we went ahead and deleted the old store files.
+ verify(mWifiConfigStoreLegacy).removeStores();
+ }
+
+ /**
* Verifies the loading of networks using {@link WifiConfigManager#loadFromStore()} does
* not attempt to read from any of the stores (new or legacy) when the store files are
* not present.
@@ -2626,10 +2696,12 @@ public class WifiConfigManagerTest {
@Test
public void testFreshInstallDoesNotLoadFromStore() throws Exception {
when(mWifiConfigStore.areStoresPresent()).thenReturn(false);
+ when(mWifiConfigStoreLegacy.areStoresPresent()).thenReturn(false);
assertTrue(mWifiConfigManager.loadFromStore());
verify(mWifiConfigStore, never()).read();
+ verify(mWifiConfigStoreLegacy, never()).read();
assertTrue(mWifiConfigManager.getConfiguredNetworksWithPasswords().isEmpty());
}
@@ -2642,9 +2714,11 @@ public class WifiConfigManagerTest {
public void testHandleUserSwitchAfterFreshInstall() throws Exception {
int user2 = TEST_DEFAULT_USER + 1;
when(mWifiConfigStore.areStoresPresent()).thenReturn(false);
+ when(mWifiConfigStoreLegacy.areStoresPresent()).thenReturn(false);
assertTrue(mWifiConfigManager.loadFromStore());
verify(mWifiConfigStore, never()).read();
+ verify(mWifiConfigStoreLegacy, never()).read();
setupStoreDataForUserRead(new ArrayList<WifiConfiguration>(), new HashSet<String>());
// Now switch the user to user 2.
@@ -3403,7 +3477,7 @@ public class WifiConfigManagerTest {
mWifiConfigManager =
new WifiConfigManager(
mContext, mClock, mUserManager, mTelephonyManager,
- mWifiKeyStore, mWifiConfigStore,
+ mWifiKeyStore, mWifiConfigStore, mWifiConfigStoreLegacy,
mWifiPermissionsUtil, mWifiPermissionsWrapper, mNetworkListStoreData,
mDeletedEphemeralSsidsStoreData);
mWifiConfigManager.enableVerboseLogging(1);
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreLegacyTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreLegacyTest.java
new file mode 100644
index 000000000..4b4e875a1
--- /dev/null
+++ b/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreLegacyTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.net.IpConfiguration;
+import android.net.wifi.WifiConfiguration;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.server.net.IpConfigStore;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfig;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link com.android.server.wifi.WifiConfigStoreLegacy}.
+ */
+@SmallTest
+public class WifiConfigStoreLegacyTest {
+ private static final String MASKED_FIELD_VALUE = "*";
+
+ // Test mocks
+ @Mock private WifiNative mWifiNative;
+ @Mock private WifiNetworkHistory mWifiNetworkHistory;
+ @Mock private IpConfigStore mIpconfigStore;
+ @Mock private LegacyPasspointConfigParser mPasspointConfigParser;
+
+ /**
+ * Test instance of WifiConfigStore.
+ */
+ private WifiConfigStoreLegacy mWifiConfigStore;
+
+
+ /**
+ * Setup the test environment.
+ */
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mWifiConfigStore = new WifiConfigStoreLegacy(mWifiNetworkHistory, mWifiNative,
+ mIpconfigStore, mPasspointConfigParser);
+ }
+
+ /**
+ * Called after each test
+ */
+ @After
+ public void cleanup() {
+ validateMockitoUsage();
+ }
+
+ /**
+ * Verify loading of network configurations from legacy stores. This is verifying the population
+ * of the masked wpa_supplicant fields using wpa_supplicant.conf file.
+ */
+ @Test
+ public void testLoadFromStores() throws Exception {
+ WifiConfiguration pskNetwork = WifiConfigurationTestUtil.createPskNetwork();
+ WifiConfiguration wepNetwork = WifiConfigurationTestUtil.createWepNetwork();
+ WifiConfiguration eapNetwork = WifiConfigurationTestUtil.createEapNetwork();
+ WifiConfiguration passpointNetwork = WifiConfigurationTestUtil.createPasspointNetwork();
+ eapNetwork.enterpriseConfig.setPassword("EapPassword");
+
+ // Initialize Passpoint configuration data.
+ int passpointNetworkId = 1234;
+ String fqdn = passpointNetwork.FQDN;
+ String providerFriendlyName = passpointNetwork.providerFriendlyName;
+ long[] roamingConsortiumIds = new long[] {0x1234, 0x5678};
+ String realm = "test.com";
+ String imsi = "214321";
+
+ // Update Passpoint network.
+ // Network ID is used for lookup network extras, so use an unique ID for passpoint network.
+ passpointNetwork.networkId = passpointNetworkId;
+ passpointNetwork.enterpriseConfig.setPassword("PaspointPassword");
+ // Reset FQDN and provider friendly name so that the derived network from #read will
+ // obtained these information from networkExtras and {@link LegacyPasspointConfigParser}.
+ passpointNetwork.FQDN = null;
+ passpointNetwork.providerFriendlyName = null;
+
+ final List<WifiConfiguration> networks = new ArrayList<>();
+ networks.add(pskNetwork);
+ networks.add(wepNetwork);
+ networks.add(eapNetwork);
+ networks.add(passpointNetwork);
+
+ // Setup legacy Passpoint configuration data.
+ Map<String, LegacyPasspointConfig> passpointConfigs = new HashMap<>();
+ LegacyPasspointConfig passpointConfig = new LegacyPasspointConfig();
+ passpointConfig.mFqdn = fqdn;
+ passpointConfig.mFriendlyName = providerFriendlyName;
+ passpointConfig.mRoamingConsortiumOis = roamingConsortiumIds;
+ passpointConfig.mRealm = realm;
+ passpointConfig.mImsi = imsi;
+ passpointConfigs.put(fqdn, passpointConfig);
+
+ // Return the config data with passwords masked from wpa_supplicant control interface.
+ doAnswer(new AnswerWithArguments() {
+ public boolean answer(Map<String, WifiConfiguration> configs,
+ SparseArray<Map<String, String>> networkExtras) {
+ for (Map.Entry<String, WifiConfiguration> entry:
+ createWpaSupplicantLoadData(networks).entrySet()) {
+ configs.put(entry.getKey(), entry.getValue());
+ }
+ // Setup networkExtras for Passpoint configuration.
+ networkExtras.put(passpointNetworkId, createNetworkExtrasForPasspointConfig(fqdn));
+ return true;
+ }
+ }).when(mWifiNative).migrateNetworksFromSupplicant(any(Map.class), any(SparseArray.class));
+
+ when(mPasspointConfigParser.parseConfig(anyString())).thenReturn(passpointConfigs);
+ WifiConfigStoreLegacy.WifiConfigStoreDataLegacy storeData = mWifiConfigStore.read();
+
+ // Update the expected configuration for Passpoint network.
+ passpointNetwork.isLegacyPasspointConfig = true;
+ passpointNetwork.FQDN = fqdn;
+ passpointNetwork.providerFriendlyName = providerFriendlyName;
+ passpointNetwork.roamingConsortiumIds = roamingConsortiumIds;
+ passpointNetwork.enterpriseConfig.setRealm(realm);
+ passpointNetwork.enterpriseConfig.setPlmn(imsi);
+
+ WifiConfigurationTestUtil.assertConfigurationsEqualForConfigStore(
+ networks, storeData.getConfigurations());
+ }
+
+ private SparseArray<IpConfiguration> createIpConfigStoreLoadData(
+ List<WifiConfiguration> configurations) {
+ SparseArray<IpConfiguration> newIpConfigurations = new SparseArray<>();
+ for (WifiConfiguration config : configurations) {
+ newIpConfigurations.put(
+ config.configKey().hashCode(),
+ new IpConfiguration(config.getIpConfiguration()));
+ }
+ return newIpConfigurations;
+ }
+
+ private Map<String, String> createPskMap(List<WifiConfiguration> configurations) {
+ Map<String, String> pskMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ if (!TextUtils.isEmpty(config.preSharedKey)) {
+ pskMap.put(config.configKey(), config.preSharedKey);
+ }
+ }
+ return pskMap;
+ }
+
+ private Map<String, String> createWepKey0Map(List<WifiConfiguration> configurations) {
+ Map<String, String> wepKeyMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ if (!TextUtils.isEmpty(config.wepKeys[0])) {
+ wepKeyMap.put(config.configKey(), config.wepKeys[0]);
+ }
+ }
+ return wepKeyMap;
+ }
+
+ private Map<String, String> createWepKey1Map(List<WifiConfiguration> configurations) {
+ Map<String, String> wepKeyMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ if (!TextUtils.isEmpty(config.wepKeys[1])) {
+ wepKeyMap.put(config.configKey(), config.wepKeys[1]);
+ }
+ }
+ return wepKeyMap;
+ }
+
+ private Map<String, String> createWepKey2Map(List<WifiConfiguration> configurations) {
+ Map<String, String> wepKeyMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ if (!TextUtils.isEmpty(config.wepKeys[2])) {
+ wepKeyMap.put(config.configKey(), config.wepKeys[2]);
+ }
+ }
+ return wepKeyMap;
+ }
+
+ private Map<String, String> createWepKey3Map(List<WifiConfiguration> configurations) {
+ Map<String, String> wepKeyMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ if (!TextUtils.isEmpty(config.wepKeys[3])) {
+ wepKeyMap.put(config.configKey(), config.wepKeys[3]);
+ }
+ }
+ return wepKeyMap;
+ }
+
+ private Map<String, String> createEapPasswordMap(List<WifiConfiguration> configurations) {
+ Map<String, String> eapPasswordMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ if (!TextUtils.isEmpty(config.enterpriseConfig.getPassword())) {
+ eapPasswordMap.put(config.configKey(), config.enterpriseConfig.getPassword());
+ }
+ }
+ return eapPasswordMap;
+ }
+
+ private Map<String, WifiConfiguration> createWpaSupplicantLoadData(
+ List<WifiConfiguration> configurations) {
+ Map<String, WifiConfiguration> configurationMap = new HashMap<>();
+ for (WifiConfiguration config : configurations) {
+ configurationMap.put(config.configKey(true), config);
+ }
+ return configurationMap;
+ }
+
+ private List<WifiConfiguration> createMaskedWifiConfigurations(
+ List<WifiConfiguration> configurations) {
+ List<WifiConfiguration> newConfigurations = new ArrayList<>();
+ for (WifiConfiguration config : configurations) {
+ newConfigurations.add(createMaskedWifiConfiguration(config));
+ }
+ return newConfigurations;
+ }
+
+ private WifiConfiguration createMaskedWifiConfiguration(WifiConfiguration configuration) {
+ WifiConfiguration newConfig = new WifiConfiguration(configuration);
+ if (!TextUtils.isEmpty(configuration.preSharedKey)) {
+ newConfig.preSharedKey = MASKED_FIELD_VALUE;
+ }
+ if (!TextUtils.isEmpty(configuration.wepKeys[0])) {
+ newConfig.wepKeys[0] = MASKED_FIELD_VALUE;
+ }
+ if (!TextUtils.isEmpty(configuration.wepKeys[1])) {
+ newConfig.wepKeys[1] = MASKED_FIELD_VALUE;
+ }
+ if (!TextUtils.isEmpty(configuration.wepKeys[2])) {
+ newConfig.wepKeys[2] = MASKED_FIELD_VALUE;
+ }
+ if (!TextUtils.isEmpty(configuration.wepKeys[3])) {
+ newConfig.wepKeys[3] = MASKED_FIELD_VALUE;
+ }
+ if (!TextUtils.isEmpty(configuration.enterpriseConfig.getPassword())) {
+ newConfig.enterpriseConfig.setPassword(MASKED_FIELD_VALUE);
+ }
+ return newConfig;
+ }
+
+ private Map<String, String> createNetworkExtrasForPasspointConfig(String fqdn) {
+ Map<String, String> extras = new HashMap<>();
+ extras.put(SupplicantStaNetworkHal.ID_STRING_KEY_FQDN, fqdn);
+ return extras;
+ }
+}
diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/LegacyPasspointConfigParserTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/LegacyPasspointConfigParserTest.java
new file mode 100644
index 000000000..10ebceb22
--- /dev/null
+++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/LegacyPasspointConfigParserTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2017 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 org.junit.Assert.*;
+
+import android.os.FileUtils;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link com.android.server.wifi.hotspot2.LegacyPasspointConfigParser}.
+ */
+@SmallTest
+public class LegacyPasspointConfigParserTest {
+ private static final String TEST_CONFIG =
+ "tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)\n"
+ + "8:MgmtTree+\n"
+ + "17:PerProviderSubscription+\n"
+ + "4:r1i1+\n"
+ + "6:HomeSP+\n"
+ + "c:FriendlyName=12:Test Provider 1™\n"
+ + "4:FQDN=9:test1.net\n"
+ + "13:RoamingConsortiumOI=9:1234,5678\n"
+ + ".\n"
+ + "a:Credential+\n"
+ + "10:UsernamePassword+\n"
+ + "8:Username=5:user1\n"
+ + "8:Password=5:pass1\n"
+ + "\n"
+ + "9:EAPMethod+\n"
+ + "7:EAPType=2:21\n"
+ + "b:InnerMethod=3:PAP\n"
+ + ".\n"
+ + ".\n"
+ + "5:Realm=9:test1.com\n"
+ + ".\n"
+ + ".\n"
+ + ".\n"
+ + "17:PerProviderSubscription+\n"
+ + "4:r1i2+\n"
+ + "6:HomeSP+\n"
+ + "c:FriendlyName=f:Test Provider 2\n"
+ + "4:FQDN=9:test2.net\n"
+ + ".\n"
+ + "a:Credential+\n"
+ + "3:SIM+\n"
+ + "4:IMSI=4:1234\n"
+ + "7:EAPType=2:18\n"
+ + ".\n"
+ + "5:Realm=9:test2.com\n"
+ + ".\n"
+ + ".\n"
+ + ".\n"
+ + ".\n";
+
+ /**
+ * Helper function for generating {@link LegacyPasspointConfig} objects based on the predefined
+ * test configuration string {@link #TEST_CONFIG}
+ *
+ * @return Map of FQDN to {@link LegacyPasspointConfig}
+ */
+ private Map<String, LegacyPasspointConfig> generateTestConfig() {
+ Map<String, LegacyPasspointConfig> configs = new HashMap<>();
+
+ LegacyPasspointConfig config1 = new LegacyPasspointConfig();
+ config1.mFqdn = "test1.net";
+ config1.mFriendlyName = "Test Provider 1™";
+ config1.mRoamingConsortiumOis = new long[] {0x1234, 0x5678};
+ config1.mRealm = "test1.com";
+ configs.put("test1.net", config1);
+
+ LegacyPasspointConfig config2 = new LegacyPasspointConfig();
+ config2.mFqdn = "test2.net";
+ config2.mFriendlyName = "Test Provider 2";
+ config2.mRealm = "test2.com";
+ config2.mImsi = "1234";
+ configs.put("test2.net", config2);
+
+ return configs;
+ }
+
+ /**
+ * Helper function for parsing configuration data.
+ *
+ * @param data The configuration data to parse
+ * @return Map of FQDN to {@link LegacyPasspointConfig}
+ * @throws Exception
+ */
+ private Map<String, LegacyPasspointConfig> parseConfig(String data) throws Exception {
+ // Write configuration data to file.
+ File configFile = File.createTempFile("LegacyPasspointConfig", "");
+ FileUtils.stringToFile(configFile, data);
+
+ // Parse the configuration file.
+ LegacyPasspointConfigParser parser = new LegacyPasspointConfigParser();
+ Map<String, LegacyPasspointConfig> configMap =
+ parser.parseConfig(configFile.getAbsolutePath());
+
+ configFile.delete();
+ return configMap;
+ }
+
+ /**
+ * Verify that the expected {@link LegacyPasspointConfig} objects are return when parsing
+ * predefined test configuration data {@link #TEST_CONFIG}.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void parseTestConfig() throws Exception {
+ Map<String, LegacyPasspointConfig> parsedConfig = parseConfig(TEST_CONFIG);
+ assertEquals(generateTestConfig(), parsedConfig);
+ }
+
+ /**
+ * Verify that an empty map is return when parsing a configuration containing an empty
+ * configuration (MgmtTree).
+ *
+ * @throws Exception
+ */
+ @Test
+ public void parseEmptyConfig() throws Exception {
+ String emptyConfig = "tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)\n"
+ + "8:MgmtTree+\n"
+ + ".\n";
+ Map<String, LegacyPasspointConfig> parsedConfig = parseConfig(emptyConfig);
+ assertTrue(parsedConfig.isEmpty());
+ }
+
+ /**
+ * Verify that an empty map is return when parsing an empty configuration data.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void parseEmptyData() throws Exception {
+ Map<String, LegacyPasspointConfig> parsedConfig = parseConfig("");
+ assertTrue(parsedConfig.isEmpty());
+ }
+
+ /**
+ * Verify that an IOException is thrown when parsing a configuration containing an unknown
+ * root name. The expected root name is "MgmtTree".
+ *
+ * @throws Exception
+ */
+ @Test(expected = IOException.class)
+ public void parseConfigWithUnknownRootName() throws Exception {
+ String config = "tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)\n"
+ + "8:TestTest+\n"
+ + ".\n";
+ parseConfig(config);
+ }
+
+ /**
+ * Verify that an IOException is thrown when parsing a configuration containing a line with
+ * mismatched string length for the name.
+ *
+ * @throws Exception
+ */
+ @Test(expected = IOException.class)
+ public void parseConfigWithMismatchedStringLengthInName() throws Exception {
+ String config = "tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)\n"
+ + "9:MgmtTree+\n"
+ + ".\n";
+ parseConfig(config);
+ }
+
+ /**
+ * Verify that an IOException is thrown when parsing a configuration containing a line with
+ * mismatched string length for the value.
+ *
+ * @throws Exception
+ */
+ @Test(expected = IOException.class)
+ public void parseConfigWithMismatchedStringLengthInValue() throws Exception {
+ String config = "tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)\n"
+ + "8:MgmtTree+\n"
+ + "4:test=5:test\n"
+ + ".\n";
+ parseConfig(config);
+ }
+}