Store configKey in wpa_supplicant.conf

With per-user networks, the data normally present in wpa_supplicant.conf
will no longer be sufficient to reconstruct a network's configKey. The
configKey must thus explicitly be stored with each wpa_supplicant.conf
entry.

The wpa_supplicant.conf format provides a single variable for keeping
such metadata, "id_str". Since that variable is already used to store the
FQDN for Passpoint networks, it must be extended to hold both the FQDN
and the configKey. This is done by changing the variable's data format to
a serialized JSON dictionary. For future reference, the configuration's
creator UID is also added to the dictionary.

BUG=25600871

Change-Id: I01518fee6237f4cf60efa4be92c3c7a1aff32704
diff --git a/service/java/com/android/server/wifi/ConfigurationMap.java b/service/java/com/android/server/wifi/ConfigurationMap.java
index 0fb56ea..837c579 100644
--- a/service/java/com/android/server/wifi/ConfigurationMap.java
+++ b/service/java/com/android/server/wifi/ConfigurationMap.java
@@ -1,15 +1,10 @@
 package com.android.server.wifi;
 
 import android.net.wifi.WifiConfiguration;
-import android.util.Log;
-
-import com.android.server.wifi.hotspot2.Utils;
-import com.android.server.wifi.hotspot2.pps.HomeSP;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -29,41 +24,6 @@
         return current;
     }
 
-    public void populatePasspointData(Collection<HomeSP> homeSPs, WifiNative wifiNative) {
-        mPerFQDN.clear();
-
-        for (HomeSP homeSp : homeSPs) {
-            String fqdn = homeSp.getFQDN();
-            Log.d(WifiConfigStore.TAG, "Looking for " + fqdn);
-            for (WifiConfiguration config : mPerID.values()) {
-                Log.d(WifiConfigStore.TAG, "Testing " + config.SSID);
-
-                String id_str = Utils.unquote(wifiNative.getNetworkVariable(
-                        config.networkId, WifiConfigStore.idStringVarName));
-                if (id_str != null && id_str.equals(fqdn) && config.enterpriseConfig != null) {
-                    Log.d(WifiConfigStore.TAG, "Matched " + id_str + " with " + config.networkId);
-                    config.FQDN = fqdn;
-                    config.providerFriendlyName = homeSp.getFriendlyName();
-
-                    HashSet<Long> roamingConsortiumIds = homeSp.getRoamingConsortiums();
-                    config.roamingConsortiumIds = new long[roamingConsortiumIds.size()];
-                    int i = 0;
-                    for (long id : roamingConsortiumIds) {
-                        config.roamingConsortiumIds[i] = id;
-                        i++;
-                    }
-                    IMSIParameter imsiParameter = homeSp.getCredential().getImsi();
-                    config.enterpriseConfig.setPlmn(
-                            imsiParameter != null ? imsiParameter.toString() : null);
-                    config.enterpriseConfig.setRealm(homeSp.getCredential().getRealm());
-                    mPerFQDN.put(fqdn, config.networkId);
-                }
-            }
-        }
-
-        Log.d(WifiConfigStore.TAG, "loaded " + mPerFQDN.size() + " passpoint configs");
-    }
-
     public WifiConfiguration remove(int netID) {
         WifiConfiguration config = mPerID.remove(netID);
         if (config == null) {
diff --git a/service/java/com/android/server/wifi/WifiConfigStore.java b/service/java/com/android/server/wifi/WifiConfigStore.java
index b2f5313..35f146e 100644
--- a/service/java/com/android/server/wifi/WifiConfigStore.java
+++ b/service/java/com/android/server/wifi/WifiConfigStore.java
@@ -324,7 +324,14 @@
     private static final String ENABLE_RSSI_POLL_WHILE_ASSOCIATED_KEY
             = "ENABLE_RSSI_POLL_WHILE_ASSOCIATED_KEY";
 
-    public static final String idStringVarName = "id_str";
+    // This is the only variable whose contents will not be interpreted by wpa_supplicant. We use it
+    // to store metadata that allows us to correlate a wpa_supplicant.conf entry with additional
+    // information about the same network stored in other files. The metadata is stored as a
+    // serialized JSON dictionary.
+    public static final String ID_STRING_VAR_NAME = "id_str";
+    public static final String ID_STRING_KEY_FQDN = "fqdn";
+    public static final String ID_STRING_KEY_CREATOR_UID = "creatorUid";
+    public static final String ID_STRING_KEY_CONFIG_KEY = "configKey";
 
     // The Wifi verbose log is provided as a way to persist the verbose logging settings
     // for testing purpose.
@@ -1930,6 +1937,8 @@
 
         mConfiguredNetworks.clear();
 
+        final SparseArray<Map<String, String>> networkExtras = new SparseArray<>();
+
         int last_id = -1;
         boolean done = false;
         while (!done) {
@@ -1972,6 +1981,22 @@
 
                 readNetworkVariables(config);
 
+                // Parse the serialized JSON dictionary in ID_STRING_VAR_NAME once and cache the
+                // result for efficiency.
+                Map<String, String> extras = mWifiNative.getNetworkExtra(config.networkId,
+                        ID_STRING_VAR_NAME);
+                if (extras == null) {
+                    extras = new HashMap<String, String>();
+                    // If ID_STRING_VAR_NAME did not contain a dictionary, assume that it contains
+                    // just a quoted FQDN. This is the legacy format that was used in Marshmallow.
+                    final String fqdn = Utils.unquote(mWifiNative.getNetworkVariable(
+                            config.networkId, ID_STRING_VAR_NAME));
+                    if (fqdn != null) {
+                        extras.put(ID_STRING_KEY_FQDN, fqdn);
+                    }
+                }
+                networkExtras.put(config.networkId, extras);
+
                 Checksum csum = new CRC32();
                 if (config.SSID != null) {
                     csum.update(config.SSID.getBytes(), 0, config.SSID.getBytes().length);
@@ -2019,7 +2044,7 @@
             done = (lines.length == 1);
         }
 
-        readPasspointConfig();
+        readPasspointConfig(networkExtras);
         readIpAndProxyConfigurations();
         readNetworkHistory();
         readAutoJoinConfig();
@@ -2167,7 +2192,7 @@
         return false;
     }
 
-    void readPasspointConfig() {
+    void readPasspointConfig(SparseArray<Map<String, String>> networkExtras) {
 
         List<HomeSP> homeSPs;
         try {
@@ -2177,7 +2202,42 @@
             return;
         }
 
-        mConfiguredNetworks.populatePasspointData(homeSPs, mWifiNative);
+        int matchedConfigs = 0;
+        for (HomeSP homeSp : homeSPs) {
+            String fqdn = homeSp.getFQDN();
+            Log.d(TAG, "Looking for " + fqdn);
+            for (WifiConfiguration config : mConfiguredNetworks.values()) {
+                Log.d(TAG, "Testing " + config.SSID);
+
+                if (config.enterpriseConfig == null) {
+                    continue;
+                }
+                final String configFqdn =
+                        networkExtras.get(config.networkId).get(ID_STRING_KEY_FQDN);
+                if (configFqdn != null && configFqdn.equals(fqdn)) {
+                    Log.d(TAG, "Matched " + configFqdn + " with " + config.networkId);
+                    ++matchedConfigs;
+                    config.FQDN = fqdn;
+                    config.providerFriendlyName = homeSp.getFriendlyName();
+
+                    HashSet<Long> roamingConsortiumIds = homeSp.getRoamingConsortiums();
+                    config.roamingConsortiumIds = new long[roamingConsortiumIds.size()];
+                    int i = 0;
+                    for (long id : roamingConsortiumIds) {
+                        config.roamingConsortiumIds[i] = id;
+                        i++;
+                    }
+                    IMSIParameter imsiParameter = homeSp.getCredential().getImsi();
+                    config.enterpriseConfig.setPlmn(
+                            imsiParameter != null ? imsiParameter.toString() : null);
+                    config.enterpriseConfig.setRealm(homeSp.getCredential().getRealm());
+                    // Allow mConfiguredNetworks to cache the FQDN.
+                    mConfiguredNetworks.put(config.networkId, config);
+                }
+            }
+        }
+
+        Log.d(TAG, "loaded " + matchedConfigs + " passpoint configs");
     }
 
     public void writePasspointConfigs(final String fqdn, final HomeSP homeSP) {
@@ -2804,14 +2864,15 @@
                 break setVariables;
             }
 
+            final Map<String, String> metadata = new HashMap<String, String>();
             if (config.isPasspoint()) {
-                if (!mWifiNative.setNetworkVariable(
-                            netId,
-                            idStringVarName,
-                            '"' + config.FQDN + '"')) {
-                    loge("failed to set id_str: " + config.FQDN);
-                    break setVariables;
-                }
+                metadata.put(ID_STRING_KEY_FQDN, config.FQDN);
+            }
+            metadata.put(ID_STRING_KEY_CONFIG_KEY, config.configKey());
+            metadata.put(ID_STRING_KEY_CREATOR_UID, Integer.toString(config.creatorUid));
+            if (!mWifiNative.setNetworkExtra(netId, ID_STRING_VAR_NAME, metadata)) {
+                loge("failed to set id_str: " + metadata.toString());
+                break setVariables;
             }
 
             if (config.BSSID != null) {
diff --git a/service/java/com/android/server/wifi/WifiNative.java b/service/java/com/android/server/wifi/WifiNative.java
index a1d9379..7fe0bf7 100644
--- a/service/java/com/android/server/wifi/WifiNative.java
+++ b/service/java/com/android/server/wifi/WifiNative.java
@@ -16,15 +16,21 @@
 
 package com.android.server.wifi;
 
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.net.wifi.RttManager;
 import android.net.wifi.ScanResult;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiEnterpriseConfig;
 import android.net.wifi.WifiLinkLayerStats;
-import android.net.wifi.WifiWakeReasonAndCounts;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiScanner;
 import android.net.wifi.WifiSsid;
+import android.net.wifi.WifiWakeReasonAndCounts;
 import android.net.wifi.WpsInfo;
 import android.net.wifi.p2p.WifiP2pConfig;
 import android.net.wifi.p2p.WifiP2pGroup;
@@ -34,12 +40,7 @@
 import android.text.TextUtils;
 import android.util.LocalLog;
 import android.util.Log;
-import android.content.Context;
-import android.content.Intent;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.IntentFilter;
-import android.content.BroadcastReceiver;
+
 import com.android.server.connectivity.KeepalivePacketData;
 import com.android.server.wifi.hotspot2.NetworkDetail;
 import com.android.server.wifi.hotspot2.SupplicantBridge;
@@ -48,16 +49,26 @@
 
 import libcore.util.HexEncoding;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
+
 /**
  * Native calls for bring up/shut down of the supplicant daemon and for
  * sending requests to the supplicant daemon
@@ -370,6 +381,20 @@
         return doIntCommand("ADD_NETWORK");
     }
 
+    public boolean setNetworkExtra(int netId, String name, Map<String, String> values) {
+        final String encoded;
+        try {
+            encoded = URLEncoder.encode(new JSONObject(values).toString(), "UTF-8");
+        } catch (NullPointerException e) {
+            Log.e(TAG, "Unable to serialize networkExtra: " + e.toString());
+            return false;
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "Unable to serialize networkExtra: " + e.toString());
+            return false;
+        }
+        return setNetworkVariable(netId, name, "\"" + encoded + "\"");
+    }
+
     public boolean setNetworkVariable(int netId, String name, String value) {
         if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value)) return false;
         if (name.equals(WifiConfiguration.pskVarName)
@@ -380,6 +405,39 @@
         }
     }
 
+    public Map<String, String> getNetworkExtra(int netId, String name) {
+        final String wrapped = getNetworkVariable(netId, name);
+        if (wrapped == null || !wrapped.startsWith("\"") || !wrapped.endsWith("\"")) {
+            return null;
+        }
+        try {
+            final String encoded = wrapped.substring(1, wrapped.length() - 1);
+            // This method reads a JSON dictionary that was written by setNetworkExtra(). However,
+            // on devices that upgraded from Marshmallow, it may encounter a legacy value instead -
+            // an FQDN stored as a plain string. If such a value is encountered, the JSONObject
+            // constructor will thrown a JSONException and the method will return null.
+            final JSONObject json = new JSONObject(URLDecoder.decode(encoded, "UTF-8"));
+            final Map<String, String> values = new HashMap<String, String>();
+            final Iterator<?> it = json.keys();
+            while (it.hasNext()) {
+                final String key = (String) it.next();
+                final Object value = json.get(key);
+                if (value instanceof String) {
+                    values.put(key, (String) value);
+                }
+            }
+            return values;
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "Unable to serialize networkExtra: " + e.toString());
+            return null;
+        } catch (JSONException e) {
+            // This is not necessarily an error. This exception will also occur if we encounter a
+            // legacy FQDN stored as a plain string. We want to return null in this case as no JSON
+            // dictionary of extras was found.
+            return null;
+        }
+    }
+
     public String getNetworkVariable(int netId, String name) {
         if (TextUtils.isEmpty(name)) return null;
 
diff --git a/service/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java b/service/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java
new file mode 100644
index 0000000..0a9af5e
--- /dev/null
+++ b/service/tests/wifitests/src/com/android/server/wifi/WifiConfigStoreTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.hamcrest.CoreMatchers.not;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyObject;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.intThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.test.AndroidTestCase;
+
+import com.android.server.wifi.hotspot2.omadm.MOManager;
+import com.android.server.wifi.hotspot2.pps.Credential;
+import com.android.server.wifi.hotspot2.pps.HomeSP;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link com.android.server.wifi.WifiConfigStore}.
+ */
+public class WifiConfigStoreTest extends AndroidTestCase {
+    private static final int UID = 10;
+    private static final String[] SSIDS = {"\"red\"", "\"green\"", "\"blue\""};
+    private static final String[] ENCODED_SSIDS = {"726564", "677265656e", "626c7565"};
+    private static final String[] FQDNS = {null, "example.com", "example.org"};
+    private static final String[] PROVIDER_FRIENDLY_NAMES = {null, "Green", "Blue"};
+    private static final String[] CONFIG_KEYS = {"\"red\"NONE", "example.comWPA_EAP",
+            "example.orgWPA_EAP"};
+
+    @Mock private Context mContext;
+    @Mock private WifiStateMachine mWifiStateMachine;
+    @Mock private WifiNative mWifiNative;
+    @Mock private FrameworkFacade mFrameworkFacade;
+    @Mock private MOManager mMOManager;
+    private WifiConfigStore mConfigStore;
+
+    @Override
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        final Context realContext = getContext();
+        when(mContext.getPackageName()).thenReturn(realContext.getPackageName());
+        when(mContext.getResources()).thenReturn(realContext.getResources());
+
+        mConfigStore = new WifiConfigStore(mContext, mWifiStateMachine, mWifiNative,
+                mFrameworkFacade);
+
+        when(mMOManager.isEnabled()).thenReturn(true);
+        final Field moManagerField = WifiConfigStore.class.getDeclaredField("mMOManager");
+        moManagerField.setAccessible(true);
+        moManagerField.set(mConfigStore, mMOManager);
+    }
+
+    private WifiConfiguration generateWifiConfig(int network) {
+        final WifiConfiguration config = new WifiConfiguration();
+        config.SSID = SSIDS[network];
+        config.creatorUid = UID;
+        if (FQDNS[network] != null) {
+            config.FQDN = FQDNS[network];
+            config.providerFriendlyName = PROVIDER_FRIENDLY_NAMES[network];
+            config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.SIM);
+        }
+        return config;
+    }
+
+    /**
+     * Verifies that saveNetwork() correctly stores a network configuration in wpa_supplicant
+     * variables.
+     * TODO: Test all variables. Currently, only "ssid" and "id_str" are tested.
+     */
+    public void verifySaveNetwork(int network) {
+        // Set up wpa_supplicant.
+        when(mWifiNative.addNetwork()).thenReturn(0);
+        when(mWifiNative.setNetworkVariable(eq(0), anyString(), anyString())).thenReturn(true);
+        when(mWifiNative.setNetworkExtra(eq(0), anyString(), (Map<String, String>) anyObject()))
+                .thenReturn(true);
+        when(mWifiNative.getNetworkVariable(0, WifiConfiguration.ssidVarName))
+                .thenReturn(ENCODED_SSIDS[network]);
+
+        // Store a network configuration.
+        mConfigStore.saveNetwork(generateWifiConfig(network), UID);
+
+        // Verify that wpa_supplicant variables were written correctly for the network
+        // configuration.
+        final Map<String, String> metadata = new HashMap<String, String>();
+        if (FQDNS[network] != null) {
+            metadata.put(WifiConfigStore.ID_STRING_KEY_FQDN, FQDNS[network]);
+        }
+        metadata.put(WifiConfigStore.ID_STRING_KEY_CONFIG_KEY, CONFIG_KEYS[network]);
+        metadata.put(WifiConfigStore.ID_STRING_KEY_CREATOR_UID, Integer.toString(UID));
+        verify(mWifiNative).setNetworkExtra(0, WifiConfigStore.ID_STRING_VAR_NAME,
+                metadata);
+
+        // Verify that no wpa_supplicant variables were read or written for any other network
+        // configurations.
+        verify(mWifiNative, never()).setNetworkExtra(intThat(not(0)), anyString(),
+                (Map<String, String>) anyObject());
+        verify(mWifiNative, never()).setNetworkVariable(intThat(not(0)), anyString(), anyString());
+        verify(mWifiNative, never()).getNetworkVariable(intThat(not(0)), anyString());
+    }
+
+    /**
+     * Verifies that saveNetwork() correctly stores a regular network configuration.
+     */
+    public void testSaveNetworkRegular() {
+        verifySaveNetwork(0);
+    }
+
+    /**
+     * Verifies that saveNetwork() correctly stores a HotSpot 2.0 network configuration.
+     */
+    public void testSaveNetworkHotspot20() {
+        verifySaveNetwork(1);
+    }
+
+    /**
+     * Verifies that loadConfiguredNetworks() correctly reads data from the wpa_supplicant and
+     * the MOManager, correlating the two sources based on the FQDN for HotSpot 2.0 networks.
+     * TODO: Test all variables. Currently, only "ssid" and "id_str" are tested.
+     */
+    public void testLoadConfiguredNetworks() throws Exception {
+        // Set up list of networks returned by wpa_supplicant.
+        final String header = "network id / ssid / bssid / flags";
+        String networks = header;
+        for (int i = 0; i < SSIDS.length; ++i) {
+            networks += "\n" + Integer.toString(i) + "\t" + SSIDS[i] + "\tany";
+        }
+        when(mWifiNative.listNetworks(anyInt())).thenReturn(header);
+        when(mWifiNative.listNetworks(-1)).thenReturn(networks);
+
+        // Set up variables returned by wpa_supplicant for the individual networks.
+        for (int i = 0; i < SSIDS.length; ++i) {
+            when(mWifiNative.getNetworkVariable(i, WifiConfiguration.ssidVarName))
+                .thenReturn(ENCODED_SSIDS[i]);
+        }
+        // Legacy regular network configuration: No "id_str".
+        when(mWifiNative.getNetworkExtra(0, WifiConfigStore.ID_STRING_VAR_NAME))
+            .thenReturn(null);
+        // Legacy Hotspot 2.0 network configuration: Quoted FQDN in "id_str".
+        when(mWifiNative.getNetworkExtra(1, WifiConfigStore.ID_STRING_VAR_NAME))
+            .thenReturn(null);
+        when(mWifiNative.getNetworkVariable(1, WifiConfigStore.ID_STRING_VAR_NAME))
+            .thenReturn('"' + FQDNS[1] + '"');
+        // Up-to-date configuration: Metadata in "id_str".
+        final Map<String, String> metadata = new HashMap<String, String>();
+        metadata.put(WifiConfigStore.ID_STRING_KEY_CONFIG_KEY, CONFIG_KEYS[2]);
+        metadata.put(WifiConfigStore.ID_STRING_KEY_CREATOR_UID, Integer.toString(UID));
+        metadata.put(WifiConfigStore.ID_STRING_KEY_FQDN, FQDNS[2]);
+        when(mWifiNative.getNetworkExtra(2, WifiConfigStore.ID_STRING_VAR_NAME))
+            .thenReturn(metadata);
+
+        // Set up list of home service providers returned by MOManager.
+        final List<HomeSP> homeSPs = new ArrayList<HomeSP>();
+        for (int i : new int[] {1, 2}) {
+            homeSPs.add(new HomeSP(null, FQDNS[i], new HashSet<Long>(), new HashSet<String>(),
+                    new HashSet<Long>(), new ArrayList<Long>(), PROVIDER_FRIENDLY_NAMES[i], null,
+                    new Credential(0, 0, null, false, null, null), null, 0, null, null, null));
+        }
+        when(mMOManager.loadAllSPs()).thenReturn(homeSPs);
+
+        // Load network configurations.
+        mConfigStore.loadConfiguredNetworks();
+
+        // Verify that network configurations were loaded. For HotSpot 2.0 networks, this also
+        // verifies that the data read from the wpa_supplicant was correlated with the data read
+        // from the MOManager based on the FQDN stored in the wpa_supplicant's "id_str" variable.
+        final List<WifiConfiguration> configs = mConfigStore.getConfiguredNetworks();
+        assertEquals(SSIDS.length, configs.size());
+        for (int i = 0; i < SSIDS.length; ++i) {
+            WifiConfiguration config = null;
+            // Find the network configuration to test (getConfiguredNetworks() returns them in
+            // undefined order).
+            for (final WifiConfiguration candidate : configs) {
+                if (candidate.networkId == i) {
+                    config = candidate;
+                    break;
+                }
+            }
+            assertNotNull(config);
+            assertEquals(SSIDS[i], config.SSID);
+            assertEquals(FQDNS[i], config.FQDN);
+            assertEquals(PROVIDER_FRIENDLY_NAMES[i], config.providerFriendlyName);
+            assertEquals(CONFIG_KEYS[i], config.configKey(false));
+        }
+    }
+}
diff --git a/service/tests/wifitests/src/com/android/server/wifi/WifiNativeTest.java b/service/tests/wifitests/src/com/android/server/wifi/WifiNativeTest.java
new file mode 100644
index 0000000..73e2eff
--- /dev/null
+++ b/service/tests/wifitests/src/com/android/server/wifi/WifiNativeTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link com.android.server.wifi.WifiNative}.
+ */
+@SmallTest
+public class WifiNativeTest {
+    private static final int NETWORK_ID = 0;
+    private static final String NETWORK_EXTRAS_VARIABLE = "test";
+    private static final Map<String, String> NETWORK_EXTRAS_VALUES = new HashMap<>();
+    static {
+        NETWORK_EXTRAS_VALUES.put("key1", "value1");
+        NETWORK_EXTRAS_VALUES.put("key2", "value2");
+    }
+    private static final String NETWORK_EXTRAS_SERIALIZED =
+            "\"%7B%22key2%22%3A%22value2%22%2C%22key1%22%3A%22value1%22%7D\"";
+
+    private WifiNative mWifiNative;
+
+    @Before
+    public void setUp() throws Exception {
+        final Constructor<WifiNative> wifiNativeConstructor =
+                WifiNative.class.getDeclaredConstructor(String.class);
+        wifiNativeConstructor.setAccessible(true);
+        mWifiNative = spy(wifiNativeConstructor.newInstance("test"));
+    }
+
+    /**
+     * Verifies that setNetworkExtra() correctly writes a serialized and URL-encoded JSON object.
+     */
+    @Test
+    public void testSetNetworkExtra() {
+        when(mWifiNative.setNetworkVariable(anyInt(), anyString(), anyString())).thenReturn(true);
+        assertTrue(mWifiNative.setNetworkExtra(NETWORK_ID, NETWORK_EXTRAS_VARIABLE,
+                NETWORK_EXTRAS_VALUES));
+        verify(mWifiNative).setNetworkVariable(NETWORK_ID, NETWORK_EXTRAS_VARIABLE,
+                NETWORK_EXTRAS_SERIALIZED);
+    }
+
+    /**
+     * Verifies that getNetworkExtra() correctly reads a serialized and URL-encoded JSON object.
+     */
+    @Test
+    public void testGetNetworkExtra() {
+        when(mWifiNative.getNetworkVariable(NETWORK_ID, NETWORK_EXTRAS_VARIABLE))
+                .thenReturn(NETWORK_EXTRAS_SERIALIZED);
+        final Map<String, String> actualValues =
+                mWifiNative.getNetworkExtra(NETWORK_ID, NETWORK_EXTRAS_VARIABLE);
+        assertEquals(NETWORK_EXTRAS_VALUES, actualValues);
+    }
+}
diff --git a/service/tests/wifitests/src/com/android/server/wifi/WifiStateMachineTest.java b/service/tests/wifitests/src/com/android/server/wifi/WifiStateMachineTest.java
index 3837710..a34da87 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/WifiStateMachineTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/WifiStateMachineTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyObject;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -69,7 +70,6 @@
 import com.android.server.wifi.p2p.WifiP2pServiceImpl;
 
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -86,6 +86,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Example Unit Test File
@@ -456,6 +457,23 @@
                         return true;
                     }
                 });
+        when(mWifiNative.setNetworkExtra(anyInt(), anyString(), (Map<String, String>) anyObject()))
+                .then(new Answer<Boolean>() {
+                        @Override
+                        public Boolean answer(InvocationOnMock invocationOnMock)
+                                throws Throwable {
+                            Object args[] = invocationOnMock.getArguments();
+                            Integer netId = (Integer) args[0];
+                            String name = (String) args[1];
+                            if (netId != 0) {
+                                Log.d(TAG, "Can't set extra " + name + " for " + netId);
+                                return false;
+                            }
+
+                            Log.d(TAG, "Setting extra for " + netId);
+                            return true;
+                        }
+                    });
 
         when(mWifiNative.getNetworkVariable(anyInt(), anyString())).then(
                 new Answer<String>() {