Merge "WifiConfigStore: Remove legacy modules" into oc-mr1-dev
diff --git a/service/java/com/android/server/wifi/OpenNetworkNotifier.java b/service/java/com/android/server/wifi/OpenNetworkNotifier.java
index fc144c1..692c8e2 100644
--- a/service/java/com/android/server/wifi/OpenNetworkNotifier.java
+++ b/service/java/com/android/server/wifi/OpenNetworkNotifier.java
@@ -29,6 +29,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -36,18 +38,25 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Takes care of handling the "open wi-fi network available" notification
+ *
+ * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
  * @hide
  */
 public class OpenNetworkNotifier {
 
+    private static final String TAG = "OpenNetworkNotifier";
+
     static final String ACTION_USER_DISMISSED_NOTIFICATION =
             "com.android.server.wifi.OpenNetworkNotifier.USER_DISMISSED_NOTIFICATION";
     static final String ACTION_USER_TAPPED_CONTENT =
             "com.android.server.wifi.OpenNetworkNotifier.USER_TAPPED_CONTENT";
 
+    /** Identifier of the {@link SsidSetStoreData}. */
+    private static final String STORE_DATA_IDENTIFIER = "OpenNetworkNotifierBlacklist";
     /**
      * The {@link Clock#getWallClockMillis()} must be at least this value for us
      * to show the notification again.
@@ -68,10 +77,14 @@
     /** Whether the screen is on or not. */
     private boolean mScreenOn;
 
+    /** List of SSIDs blacklisted from recommendation. */
+    private final Set<String> mBlacklistedSsids;
+
     private final Context mContext;
     private final Handler mHandler;
     private final FrameworkFacade mFrameworkFacade;
     private final Clock mClock;
+    private final WifiConfigManager mConfigManager;
     private final OpenNetworkRecommender mOpenNetworkRecommender;
     private final OpenNetworkNotificationBuilder mOpenNetworkNotificationBuilder;
 
@@ -82,15 +95,22 @@
             Looper looper,
             FrameworkFacade framework,
             Clock clock,
+            WifiConfigManager wifiConfigManager,
+            WifiConfigStore wifiConfigStore,
             OpenNetworkRecommender openNetworkRecommender) {
         mContext = context;
         mHandler = new Handler(looper);
         mFrameworkFacade = framework;
         mClock = clock;
+        mConfigManager = wifiConfigManager;
         mOpenNetworkRecommender = openNetworkRecommender;
         mOpenNetworkNotificationBuilder = new OpenNetworkNotificationBuilder(context, framework);
         mScreenOn = false;
 
+        mBlacklistedSsids = new ArraySet<>();
+        wifiConfigStore.registerStoreData(new SsidSetStoreData(
+                STORE_DATA_IDENTIFIER, new OpenNetworkNotifierStoreData()));
+
         // Setting is in seconds
         mNotificationRepeatDelay = mFrameworkFacade.getIntegerSetting(context,
                 Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
@@ -165,7 +185,7 @@
         }
 
         mRecommendedNetwork = mOpenNetworkRecommender.recommendNetwork(
-                availableNetworks, mRecommendedNetwork);
+                availableNetworks, new ArraySet<>(mBlacklistedSsids));
 
         postNotification(availableNetworks.size());
     }
@@ -201,8 +221,14 @@
                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
     }
 
-    /** A delay is set before the next shown notification after user dismissal. */
     private void handleUserDismissedAction() {
+        if (mRecommendedNetwork != null) {
+            // blacklist dismissed network
+            mBlacklistedSsids.add(mRecommendedNetwork.SSID);
+            mConfigManager.saveToStore(false /* forceWrite */);
+            Log.d(TAG, "Network is added to the open network notification blacklist: "
+                    + mRecommendedNetwork.SSID);
+        }
         mNotificationShown = false;
     }
 
@@ -213,6 +239,19 @@
         pw.println("currentTime: " + mClock.getWallClockMillis());
         pw.println("mNotificationRepeatTime: " + mNotificationRepeatTime);
         pw.println("mNotificationShown: " + mNotificationShown);
+        pw.println("mBlacklistedSsids: " + mBlacklistedSsids.toString());
+    }
+
+    private class OpenNetworkNotifierStoreData implements SsidSetStoreData.DataSource {
+        @Override
+        public Set<String> getSsids() {
+            return new ArraySet<>(mBlacklistedSsids);
+        }
+
+        @Override
+        public void setSsids(Set<String> ssidList) {
+            mBlacklistedSsids.addAll(ssidList);
+        }
     }
 
     private class NotificationEnabledSettingObserver extends ContentObserver {
diff --git a/service/java/com/android/server/wifi/OpenNetworkRecommender.java b/service/java/com/android/server/wifi/OpenNetworkRecommender.java
index cd460e5..5ceeddd 100644
--- a/service/java/com/android/server/wifi/OpenNetworkRecommender.java
+++ b/service/java/com/android/server/wifi/OpenNetworkRecommender.java
@@ -20,9 +20,12 @@
 import android.net.wifi.ScanResult;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * Helps recommend the best available network for {@link OpenNetworkNotifier}.
+ *
+ * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
  * @hide
  */
 public class OpenNetworkRecommender {
@@ -32,31 +35,24 @@
      *
      * @param networks List of scan details to pick a recommendation. This list should not be null
      *                 or empty.
-     * @param currentRecommendation The currently recommended network.
+     * @param blacklistedSsids The list of SSIDs that should not be recommended.
      */
-    public ScanResult recommendNetwork(
-            @NonNull List<ScanDetail> networks, ScanResult currentRecommendation) {
-        ScanResult currentUpdatedRecommendation = null;
+    public ScanResult recommendNetwork(@NonNull List<ScanDetail> networks,
+                                       @NonNull Set<String> blacklistedSsids) {
         ScanResult result = null;
         int highestRssi = Integer.MIN_VALUE;
         for (ScanDetail scanDetail : networks) {
             ScanResult scanResult = scanDetail.getScanResult();
 
-            if (currentRecommendation != null
-                    && currentRecommendation.SSID.equals(scanResult.SSID)) {
-                currentUpdatedRecommendation = scanResult;
-            }
-
             if (scanResult.level > highestRssi) {
                 result = scanResult;
                 highestRssi = scanResult.level;
             }
         }
-        if (currentUpdatedRecommendation != null
-                && currentUpdatedRecommendation.level >= result.level) {
-            return currentUpdatedRecommendation;
-        } else {
-            return result;
+
+        if (result != null && blacklistedSsids.contains(result.SSID)) {
+            result = null;
         }
+        return result;
     }
 }
diff --git a/service/java/com/android/server/wifi/SsidSetStoreData.java b/service/java/com/android/server/wifi/SsidSetStoreData.java
new file mode 100644
index 0000000..daed26a
--- /dev/null
+++ b/service/java/com/android/server/wifi/SsidSetStoreData.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+import android.text.TextUtils;
+
+import com.android.server.wifi.util.XmlUtil;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Store data for network notifiers.
+ *
+ * Below are the current configuration data for each respective store file:
+ *
+ * Share Store (system wide configurations)
+ * - No data
+ *
+ * User Store (user specific configurations)
+ * - Set of blacklisted SSIDs
+ */
+public class SsidSetStoreData implements WifiConfigStore.StoreData {
+    private static final String XML_TAG_SECTION_HEADER_SUFFIX = "ConfigData";
+    private static final String XML_TAG_SSID_SET = "SSIDSet";
+
+    private final String mTagName;
+    private final DataSource mDataSource;
+
+    /**
+     * Interface define the data source for the notifier store data.
+     */
+    public interface DataSource {
+        /**
+         * Retrieve the SSID set from the data source.
+         *
+         * @return Set of SSIDs
+         */
+        Set<String> getSsids();
+
+        /**
+         * Update the SSID set in the data source.
+         *
+         * @param ssidSet The set of SSIDs
+         */
+        void setSsids(Set<String> ssidSet);
+    }
+
+    /**
+     * Creates the SSID Set store data.
+     *
+     * @param name Identifier of the SSID set.
+     * @param dataSource The DataSource that implements the update and retrieval of the SSID set.
+     */
+    SsidSetStoreData(String name, DataSource dataSource) {
+        mTagName = name + XML_TAG_SECTION_HEADER_SUFFIX;
+        mDataSource = dataSource;
+    }
+
+    @Override
+    public void serializeData(XmlSerializer out, boolean shared)
+            throws XmlPullParserException, IOException {
+        if (shared) {
+            throw new XmlPullParserException("Share data not supported");
+        }
+        Set<String> ssidSet = mDataSource.getSsids();
+        if (ssidSet != null && !ssidSet.isEmpty()) {
+            XmlUtil.writeNextValue(out, XML_TAG_SSID_SET, mDataSource.getSsids());
+        }
+    }
+
+    @Override
+    public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
+            throws XmlPullParserException, IOException {
+        if (shared) {
+            throw new XmlPullParserException("Share data not supported");
+        }
+
+        while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
+            String[] valueName = new String[1];
+            Object value = XmlUtil.readCurrentValue(in, valueName);
+            if (TextUtils.isEmpty(valueName[0])) {
+                throw new XmlPullParserException("Missing value name");
+            }
+            switch (valueName[0]) {
+                case XML_TAG_SSID_SET:
+                    mDataSource.setSsids((Set<String>) value);
+                    break;
+                default:
+                    throw new XmlPullParserException("Unknown tag under "
+                            + mTagName + ": " + valueName[0]);
+            }
+        }
+    }
+
+    @Override
+    public void resetData(boolean shared) {
+        if (!shared) {
+            mDataSource.setSsids(new HashSet<>());
+        }
+    }
+
+    @Override
+    public String getName() {
+        return mTagName;
+    }
+
+    @Override
+    public boolean supportShareData() {
+        return false;
+    }
+}
diff --git a/service/java/com/android/server/wifi/WifiConnectivityManager.java b/service/java/com/android/server/wifi/WifiConnectivityManager.java
index 93db5ab..e417fde 100644
--- a/service/java/com/android/server/wifi/WifiConnectivityManager.java
+++ b/service/java/com/android/server/wifi/WifiConnectivityManager.java
@@ -512,6 +512,8 @@
         }
         @Override
         public void onSavedNetworkUpdated(int networkId) {
+            // User might have changed meteredOverride, so update capabilties
+            mStateMachine.updateCapabilities();
             updatePnoScan();
         }
         @Override
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java
index 719bed7..734c28d 100644
--- a/service/java/com/android/server/wifi/WifiInjector.java
+++ b/service/java/com/android/server/wifi/WifiInjector.java
@@ -226,7 +226,7 @@
         mCertManager = new WifiCertManager(mContext);
         mOpenNetworkNotifier = new OpenNetworkNotifier(mContext,
                 mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade, mClock,
-                new OpenNetworkRecommender());
+                mWifiConfigManager, mWifiConfigStore, new OpenNetworkRecommender());
         mLockManager = new WifiLockManager(mContext, BatteryStatsService.getService());
         mWifiController = new WifiController(mContext, mWifiStateMachine, mSettingsStore,
                 mLockManager, mWifiServiceHandlerThread.getLooper(), mFrameworkFacade);
diff --git a/service/java/com/android/server/wifi/WifiMetrics.java b/service/java/com/android/server/wifi/WifiMetrics.java
index 7dfdb86..5db5ee6 100644
--- a/service/java/com/android/server/wifi/WifiMetrics.java
+++ b/service/java/com/android/server/wifi/WifiMetrics.java
@@ -37,6 +37,7 @@
 import com.android.server.wifi.hotspot2.PasspointMatch;
 import com.android.server.wifi.hotspot2.PasspointProvider;
 import com.android.server.wifi.nano.WifiMetricsProto;
+import com.android.server.wifi.nano.WifiMetricsProto.PnoScanMetrics;
 import com.android.server.wifi.nano.WifiMetricsProto.StaEvent;
 import com.android.server.wifi.nano.WifiMetricsProto.StaEvent.ConfigInfo;
 import com.android.server.wifi.util.InformationElementUtil;
@@ -86,6 +87,7 @@
     private boolean mScreenOn;
     private int mWifiState;
     private WifiAwareMetrics mWifiAwareMetrics;
+    private final PnoScanMetrics mPnoScanMetrics = new PnoScanMetrics();
     private Handler mHandler;
     private WifiConfigManager mWifiConfigManager;
     private WifiNetworkSelector mWifiNetworkSelector;
@@ -407,6 +409,51 @@
         mPasspointManager = passpointManager;
     }
 
+    /**
+     * Increment total number of attempts to start a pno scan
+     */
+    public void incrementPnoScanStartAttempCount() {
+        synchronized (mLock) {
+            mPnoScanMetrics.numPnoScanAttempts++;
+        }
+    }
+
+    /**
+     * Increment total number of attempts with pno scan failed
+     */
+    public void incrementPnoScanFailedCount() {
+        synchronized (mLock) {
+            mPnoScanMetrics.numPnoScanFailed++;
+        }
+    }
+
+    /**
+     * Increment number of pno scans started successfully over offload
+     */
+    public void incrementPnoScanStartedOverOffloadCount() {
+        synchronized (mLock) {
+            mPnoScanMetrics.numPnoScanStartedOverOffload++;
+        }
+    }
+
+    /**
+     * Increment number of pno scans failed over offload
+     */
+    public void incrementPnoScanFailedOverOffloadCount() {
+        synchronized (mLock) {
+            mPnoScanMetrics.numPnoScanFailedOverOffload++;
+        }
+    }
+
+    /**
+     * Increment number of times pno scan found a result
+     */
+    public void incrementPnoFoundNetworkEventCount() {
+        synchronized (mLock) {
+            mPnoScanMetrics.numPnoFoundNetworkEvents++;
+        }
+    }
+
     // Values used for indexing SystemStateEntries
     private static final int SCREEN_ON = 1;
     private static final int SCREEN_OFF = 0;
@@ -1388,8 +1435,8 @@
                 pw.println("mWifiLogProto.numWifiOnFailureDueToWificond="
                         + mWifiLogProto.numWifiOnFailureDueToWificond);
                 pw.println("StaEventList:");
-                for (StaEvent event : mStaEventList) {
-                    pw.println(staEventToString(event));
+                for (StaEventWithTime event : mStaEventList) {
+                    pw.println(event);
                 }
 
                 pw.println("mWifiLogProto.numPasspointProviders="
@@ -1430,6 +1477,17 @@
                         + mWifiLogProto.fullBandAllSingleScanListenerResults);
                 pw.println("mWifiAwareMetrics:");
                 mWifiAwareMetrics.dump(fd, pw, args);
+
+                pw.println("mPnoScanMetrics.numPnoScanAttempts="
+                        + mPnoScanMetrics.numPnoScanAttempts);
+                pw.println("mPnoScanMetrics.numPnoScanFailed="
+                        + mPnoScanMetrics.numPnoScanFailed);
+                pw.println("mPnoScanMetrics.numPnoScanStartedOverOffload="
+                        + mPnoScanMetrics.numPnoScanStartedOverOffload);
+                pw.println("mPnoScanMetrics.numPnoScanFailedOverOffload="
+                        + mPnoScanMetrics.numPnoScanFailedOverOffload);
+                pw.println("mPnoScanMetrics.numPnoFoundNetworkEvents="
+                        + mPnoScanMetrics.numPnoFoundNetworkEvents);
             }
         }
     }
@@ -1605,6 +1663,14 @@
                 mWifiLogProto.softApReturnCode[sapCode].count =
                         mSoftApManagerReturnCodeCounts.valueAt(sapCode);
             }
+
+            /**
+             * Convert StaEventList to array of StaEvents
+             */
+            mWifiLogProto.staEventList = new StaEvent[mStaEventList.size()];
+            for (int i = 0; i < mStaEventList.size(); i++) {
+                mWifiLogProto.staEventList[i] = mStaEventList.get(i).staEvent;
+            }
             mWifiLogProto.totalSsidsInScanHistogram =
                     makeNumConnectableNetworksBucketArray(mTotalSsidsInScanHistogram);
             mWifiLogProto.totalBssidsInScanHistogram =
@@ -1629,8 +1695,9 @@
             mWifiLogProto.availableSavedPasspointProviderBssidsInScanHistogram =
                     makeNumConnectableNetworksBucketArray(
                     mAvailableSavedPasspointProviderBssidsInScanHistogram);
-            mWifiLogProto.staEventList = mStaEventList.toArray(mWifiLogProto.staEventList);
             mWifiLogProto.wifiAwareLog = mWifiAwareMetrics.consolidateProto();
+
+            mWifiLogProto.pnoScanMetrics = mPnoScanMetrics;
         }
     }
 
@@ -1679,6 +1746,7 @@
             mAvailableOpenOrSavedBssidsInScanHistogram.clear();
             mAvailableSavedPasspointProviderProfilesInScanHistogram.clear();
             mAvailableSavedPasspointProviderBssidsInScanHistogram.clear();
+            mPnoScanMetrics.clear();
         }
     }
 
@@ -1826,7 +1894,7 @@
         mLastPollRssi = -127;
         mLastPollFreq = -1;
         mLastPollLinkSpeed = -1;
-        mStaEventList.add(staEvent);
+        mStaEventList.add(new StaEventWithTime(staEvent, mClock.getWallClockMillis()));
         // Prune StaEventList if it gets too long
         if (mStaEventList.size() > MAX_STA_EVENTS) mStaEventList.remove();
     }
@@ -1903,7 +1971,7 @@
 
     private static String supplicantStateChangesBitmaskToString(int mask) {
         StringBuilder sb = new StringBuilder();
-        sb.append("SUPPLICANT_STATE_CHANGE_EVENTS: {");
+        sb.append("supplicantStateChangeEvents: {");
         if ((mask & (1 << StaEvent.STATE_DISCONNECTED)) > 0) sb.append(" DISCONNECTED");
         if ((mask & (1 << StaEvent.STATE_INTERFACE_DISABLED)) > 0) sb.append(" INTERFACE_DISABLED");
         if ((mask & (1 << StaEvent.STATE_INACTIVE)) > 0) sb.append(" INACTIVE");
@@ -1928,58 +1996,56 @@
     public static String staEventToString(StaEvent event) {
         if (event == null) return "<NULL>";
         StringBuilder sb = new StringBuilder();
-        Long time = event.startTimeMillis;
-        sb.append(String.format("%9d ", time.longValue())).append(" ");
         switch (event.type) {
             case StaEvent.TYPE_ASSOCIATION_REJECTION_EVENT:
-                sb.append("ASSOCIATION_REJECTION_EVENT:")
+                sb.append("ASSOCIATION_REJECTION_EVENT")
                         .append(" timedOut=").append(event.associationTimedOut)
                         .append(" status=").append(event.status).append(":")
                         .append(ISupplicantStaIfaceCallback.StatusCode.toString(event.status));
                 break;
             case StaEvent.TYPE_AUTHENTICATION_FAILURE_EVENT:
-                sb.append("AUTHENTICATION_FAILURE_EVENT: reason=").append(event.authFailureReason)
+                sb.append("AUTHENTICATION_FAILURE_EVENT reason=").append(event.authFailureReason)
                         .append(":").append(authFailureReasonToString(event.authFailureReason));
                 break;
             case StaEvent.TYPE_NETWORK_CONNECTION_EVENT:
-                sb.append("NETWORK_CONNECTION_EVENT:");
+                sb.append("NETWORK_CONNECTION_EVENT");
                 break;
             case StaEvent.TYPE_NETWORK_DISCONNECTION_EVENT:
-                sb.append("NETWORK_DISCONNECTION_EVENT:")
+                sb.append("NETWORK_DISCONNECTION_EVENT")
                         .append(" local_gen=").append(event.localGen)
                         .append(" reason=").append(event.reason).append(":")
                         .append(ISupplicantStaIfaceCallback.ReasonCode.toString(
                                 (event.reason >= 0 ? event.reason : -1 * event.reason)));
                 break;
             case StaEvent.TYPE_CMD_ASSOCIATED_BSSID:
-                sb.append("CMD_ASSOCIATED_BSSID:");
+                sb.append("CMD_ASSOCIATED_BSSID");
                 break;
             case StaEvent.TYPE_CMD_IP_CONFIGURATION_SUCCESSFUL:
-                sb.append("CMD_IP_CONFIGURATION_SUCCESSFUL:");
+                sb.append("CMD_IP_CONFIGURATION_SUCCESSFUL");
                 break;
             case StaEvent.TYPE_CMD_IP_CONFIGURATION_LOST:
-                sb.append("CMD_IP_CONFIGURATION_LOST:");
+                sb.append("CMD_IP_CONFIGURATION_LOST");
                 break;
             case StaEvent.TYPE_CMD_IP_REACHABILITY_LOST:
-                sb.append("CMD_IP_REACHABILITY_LOST:");
+                sb.append("CMD_IP_REACHABILITY_LOST");
                 break;
             case StaEvent.TYPE_CMD_TARGET_BSSID:
-                sb.append("CMD_TARGET_BSSID:");
+                sb.append("CMD_TARGET_BSSID");
                 break;
             case StaEvent.TYPE_CMD_START_CONNECT:
-                sb.append("CMD_START_CONNECT:");
+                sb.append("CMD_START_CONNECT");
                 break;
             case StaEvent.TYPE_CMD_START_ROAM:
-                sb.append("CMD_START_ROAM:");
+                sb.append("CMD_START_ROAM");
                 break;
             case StaEvent.TYPE_CONNECT_NETWORK:
-                sb.append("CONNECT_NETWORK:");
+                sb.append("CONNECT_NETWORK");
                 break;
             case StaEvent.TYPE_NETWORK_AGENT_VALID_NETWORK:
-                sb.append("NETWORK_AGENT_VALID_NETWORK:");
+                sb.append("NETWORK_AGENT_VALID_NETWORK");
                 break;
             case StaEvent.TYPE_FRAMEWORK_DISCONNECT:
-                sb.append("FRAMEWORK_DISCONNECT:")
+                sb.append("FRAMEWORK_DISCONNECT")
                         .append(" reason=")
                         .append(frameworkDisconnectReasonToString(event.frameworkDisconnectReason));
                 break;
@@ -1991,11 +2057,11 @@
         if (event.lastFreq != -1) sb.append(" lastFreq=").append(event.lastFreq);
         if (event.lastLinkSpeed != -1) sb.append(" lastLinkSpeed=").append(event.lastLinkSpeed);
         if (event.supplicantStateChangesBitmask != 0) {
-            sb.append("\n             ").append(supplicantStateChangesBitmaskToString(
+            sb.append(", ").append(supplicantStateChangesBitmaskToString(
                     event.supplicantStateChangesBitmask));
         }
         if (event.configInfo != null) {
-            sb.append("\n             ").append(configInfoToString(event.configInfo));
+            sb.append(", ").append(configInfoToString(event.configInfo));
         }
 
         return sb.toString();
@@ -2053,7 +2119,7 @@
     }
 
     public static final int MAX_STA_EVENTS = 512;
-    private LinkedList<StaEvent> mStaEventList = new LinkedList<StaEvent>();
+    private LinkedList<StaEventWithTime> mStaEventList = new LinkedList<StaEventWithTime>();
     private int mLastPollRssi = -127;
     private int mLastPollLinkSpeed = -1;
     private int mLastPollFreq = -1;
@@ -2085,4 +2151,27 @@
         int count = sia.get(element);
         sia.put(element, count + 1);
     }
+
+    private static class StaEventWithTime {
+        public StaEvent staEvent;
+        public long wallClockMillis;
+
+        StaEventWithTime(StaEvent event, long wallClockMillis) {
+            staEvent = event;
+            this.wallClockMillis = wallClockMillis;
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            Calendar c = Calendar.getInstance();
+            c.setTimeInMillis(wallClockMillis);
+            if (wallClockMillis != 0) {
+                sb.append(String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
+            } else {
+                sb.append("                  ");
+            }
+            sb.append(" ").append(staEventToString(staEvent));
+            return sb.toString();
+        }
+    }
 }
diff --git a/service/java/com/android/server/wifi/WifiStateMachine.java b/service/java/com/android/server/wifi/WifiStateMachine.java
index 49be0e8..38c767f 100644
--- a/service/java/com/android/server/wifi/WifiStateMachine.java
+++ b/service/java/com/android/server/wifi/WifiStateMachine.java
@@ -1913,9 +1913,13 @@
 
     public List<WifiConfiguration> syncGetConfiguredNetworks(int uuid, AsyncChannel channel) {
         Message resultMsg = channel.sendMessageSynchronously(CMD_GET_CONFIGURED_NETWORKS, uuid);
-        List<WifiConfiguration> result = (List<WifiConfiguration>) resultMsg.obj;
-        resultMsg.recycle();
-        return result;
+        if (resultMsg == null) { // an error has occurred
+            return null;
+        } else {
+            List<WifiConfiguration> result = (List<WifiConfiguration>) resultMsg.obj;
+            resultMsg.recycle();
+            return result;
+        }
     }
 
     public List<WifiConfiguration> syncGetPrivilegedConfiguredNetwork(AsyncChannel channel) {
@@ -3482,14 +3486,14 @@
         final WifiConfiguration config = getCurrentWifiConfiguration();
         if (config != null) {
             mWifiInfo.setEphemeral(config.ephemeral);
-
-            // Set meteredHint if DHCP result says network is metered
-            if (dhcpResults.hasMeteredHint()) {
-                mWifiInfo.setMeteredHint(true);
-            }
         }
 
-        updateCapabilities();
+        // Set meteredHint if DHCP result says network is metered
+        if (dhcpResults.hasMeteredHint()) {
+            mWifiInfo.setMeteredHint(true);
+        }
+
+        updateCapabilities(config);
     }
 
     private void handleSuccessfulIpConfiguration() {
@@ -3501,7 +3505,7 @@
                     WifiConfiguration.NetworkSelectionStatus.DISABLED_DHCP_FAILURE);
 
             // Tell the framework whether the newly connected network is trusted or untrusted.
-            updateCapabilities();
+            updateCapabilities(c);
         }
         if (c != null) {
             ScanResult result = getCurrentScanResult();
@@ -5503,7 +5507,11 @@
         }
     }
 
-    private void updateCapabilities() {
+    public void updateCapabilities() {
+        updateCapabilities(getCurrentWifiConfiguration());
+    }
+
+    private void updateCapabilities(WifiConfiguration config) {
         final NetworkCapabilities result = new NetworkCapabilities(mDfltNetworkCapabilities);
 
         if (mWifiInfo != null && !mWifiInfo.isEphemeral()) {
@@ -5512,7 +5520,7 @@
             result.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED);
         }
 
-        if (mWifiInfo != null && !mWifiInfo.getMeteredHint()) {
+        if (mWifiInfo != null && !WifiConfiguration.isMetered(config, mWifiInfo)) {
             result.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
         } else {
             result.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
@@ -5524,7 +5532,9 @@
             result.setSignalStrength(NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED);
         }
 
-        mNetworkAgent.sendNetworkCapabilities(result);
+        if (mNetworkAgent != null) {
+            mNetworkAgent.sendNetworkCapabilities(result);
+        }
     }
 
     /**
@@ -6139,7 +6149,9 @@
                 if (mVerboseLoggingEnabled) {
                     log("explictlySelected acceptUnvalidated=" + config.noInternetAccessExpected);
                 }
-                mNetworkAgent.explicitlySelected(config.noInternetAccessExpected);
+                if (mNetworkAgent != null) {
+                    mNetworkAgent.explicitlySelected(config.noInternetAccessExpected);
+                }
             }
         }
 
@@ -6511,13 +6523,17 @@
                         dstMac = NativeUtil.macAddressToByteArray(dstMacStr);
                     } catch (NullPointerException | IllegalArgumentException e) {
                         loge("Can't find MAC address for next hop to " + pkt.dstAddress);
-                        mNetworkAgent.onPacketKeepaliveEvent(slot,
-                                ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+                        if (mNetworkAgent != null) {
+                            mNetworkAgent.onPacketKeepaliveEvent(slot,
+                                    ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+                        }
                         break;
                     }
                     pkt.dstMac = dstMac;
                     int result = startWifiIPPacketOffload(slot, pkt, intervalSeconds);
-                    mNetworkAgent.onPacketKeepaliveEvent(slot, result);
+                    if (mNetworkAgent != null) {
+                        mNetworkAgent.onPacketKeepaliveEvent(slot, result);
+                    }
                     break;
                 }
                 default:
diff --git a/service/java/com/android/server/wifi/WificondControl.java b/service/java/com/android/server/wifi/WificondControl.java
index 70ecabc..056777f 100644
--- a/service/java/com/android/server/wifi/WificondControl.java
+++ b/service/java/com/android/server/wifi/WificondControl.java
@@ -99,24 +99,25 @@
         public void OnPnoNetworkFound() {
             Log.d(TAG, "Pno scan result event");
             mWifiMonitor.broadcastPnoScanResultEvent(mClientInterfaceName);
+            mWifiInjector.getWifiMetrics().incrementPnoFoundNetworkEventCount();
         }
 
         @Override
         public void OnPnoScanFailed() {
             Log.d(TAG, "Pno Scan failed event");
-            // Nothing to do for now.
+            mWifiInjector.getWifiMetrics().incrementPnoScanFailedCount();
         }
 
         @Override
         public void OnPnoScanOverOffloadStarted() {
             Log.d(TAG, "Pno scan over offload started");
-            // Update metrics
+            mWifiInjector.getWifiMetrics().incrementPnoScanStartedOverOffloadCount();
         }
 
         @Override
         public void OnPnoScanOverOffloadFailed(int reason) {
             Log.d(TAG, "Pno scan over offload failed");
-            // Update metrics
+            mWifiInjector.getWifiMetrics().incrementPnoScanFailedOverOffloadCount();
         }
     }
 
@@ -475,9 +476,14 @@
         }
 
         try {
-            return mWificondScanner.startPnoScan(settings);
+            boolean success = mWificondScanner.startPnoScan(settings);
+            mWifiInjector.getWifiMetrics().incrementPnoScanStartAttempCount();
+            if (!success) {
+                mWifiInjector.getWifiMetrics().incrementPnoScanFailedCount();
+            }
+            return success;
         } catch (RemoteException e1) {
-            Log.e(TAG, "Failed to stop pno scan due to remote exception");
+            Log.e(TAG, "Failed to start pno scan due to remote exception");
         }
         return false;
     }
diff --git a/service/java/com/android/server/wifi/scanner/WifiScanningServiceImpl.java b/service/java/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
index ab2a5dc..4b8e284 100644
--- a/service/java/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
+++ b/service/java/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
@@ -2129,20 +2129,21 @@
             pw.println();
             pw.println("Latest scan results:");
             List<ScanResult> scanResults = mSingleScanStateMachine.getCachedScanResultsAsList();
-            long nowMs = System.currentTimeMillis();
+            long nowMs = mClock.getElapsedSinceBootMillis();
             if (scanResults != null && scanResults.size() != 0) {
                 pw.println("    BSSID              Frequency  RSSI  Age(sec)   SSID "
                         + "                                Flags");
                 for (ScanResult r : scanResults) {
+                    long timeStampMs = r.timestamp / 1000;
                     String age;
-                    if (r.seen <= 0) {
+                    if (timeStampMs <= 0) {
                         age = "___?___";
-                    } else if (nowMs < r.seen) {
+                    } else if (nowMs < timeStampMs) {
                         age = "  0.000";
-                    } else if (r.seen < nowMs - 1000000) {
+                    } else if (timeStampMs < nowMs - 1000000) {
                         age = ">1000.0";
                     } else {
-                        age = String.format("%3.3f", (nowMs - r.seen) / 1000.0);
+                        age = String.format("%3.3f", (nowMs - timeStampMs) / 1000.0);
                     }
                     String ssid = r.SSID == null ? "" : r.SSID;
                     pw.printf("  %17s  %9d  %5d   %7s    %-32s  %s\n",
diff --git a/tests/wifitests/src/com/android/server/wifi/OpenNetworkNotifierTest.java b/tests/wifitests/src/com/android/server/wifi/OpenNetworkNotifierTest.java
index 29c068d..957fc22 100644
--- a/tests/wifitests/src/com/android/server/wifi/OpenNetworkNotifierTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/OpenNetworkNotifierTest.java
@@ -37,6 +37,7 @@
 import android.os.UserManager;
 import android.os.test.TestLooper;
 import android.provider.Settings;
+import android.util.ArraySet;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -47,6 +48,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Unit tests for {@link OpenNetworkNotifier}.
@@ -60,6 +62,8 @@
     @Mock private Resources mResources;
     @Mock private FrameworkFacade mFrameworkFacade;
     @Mock private Clock mClock;
+    @Mock private WifiConfigStore mWifiConfigStore;
+    @Mock private WifiConfigManager mWifiConfigManager;
     @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Notification.Builder mNotificationBuilder;
     @Mock private NotificationManager mNotificationManager;
     @Mock private OpenNetworkRecommender mOpenNetworkRecommender;
@@ -67,6 +71,8 @@
     private OpenNetworkNotifier mNotificationController;
     private BroadcastReceiver mBroadcastReceiver;
     private ScanResult mDummyNetwork;
+    private List<ScanDetail> mOpenNetworks;
+    private Set<String> mBlacklistedSsids;
 
 
     /** Initialize objects before each test run. */
@@ -90,11 +96,14 @@
         mDummyNetwork.capabilities = "[ESS]";
         mDummyNetwork.level = MIN_RSSI_LEVEL;
         when(mOpenNetworkRecommender.recommendNetwork(any(), any())).thenReturn(mDummyNetwork);
+        mOpenNetworks = new ArrayList<>();
+        mOpenNetworks.add(new ScanDetail(mDummyNetwork, null /* networkDetail */));
+        mBlacklistedSsids = new ArraySet<>();
 
         TestLooper mock_looper = new TestLooper();
         mNotificationController = new OpenNetworkNotifier(
-                mContext, mock_looper.getLooper(), mFrameworkFacade,
-                mClock, mOpenNetworkRecommender);
+                mContext, mock_looper.getLooper(), mFrameworkFacade, mClock, mWifiConfigManager,
+                mWifiConfigStore, mOpenNetworkRecommender);
         ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mContext).registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any());
@@ -102,20 +111,14 @@
         mNotificationController.handleScreenStateChanged(true);
     }
 
-    private List<ScanDetail> createOpenScanResults() {
-        List<ScanDetail> scanResults = new ArrayList<>();
-        scanResults.add(new ScanDetail(mDummyNetwork, null /* networkDetail */));
-        return scanResults;
-    }
-
     /**
      * When scan results with open networks are handled, a notification is posted.
      */
     @Test
     public void handleScanResults_hasOpenNetworks_notificationDisplayed() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
     }
 
@@ -136,9 +139,9 @@
      */
     @Test
     public void handleScanResults_notificationShown_emptyList_notificationCleared() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mNotificationController.handleScanResults(new ArrayList<>());
@@ -151,9 +154,9 @@
      */
     @Test
     public void handleScanResults_notificationShown_screenOff_emptyList_notificationCleared() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mNotificationController.handleScreenStateChanged(false);
@@ -167,10 +170,10 @@
      */
     @Test
     public void handleScanResults_notificationShowing_doesNotRepostNotification() {
-        mNotificationController.handleScanResults(createOpenScanResults());
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
     }
 
@@ -180,9 +183,9 @@
      */
     @Test
     public void clearPendingNotification_clearsNotificationIfOneIsShowing() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mNotificationController.clearPendingNotification(true);
@@ -208,7 +211,7 @@
     @Test
     public void screenOff_handleScanResults_notificationNotDisplayed() {
         mNotificationController.handleScreenStateChanged(false);
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
         verify(mOpenNetworkRecommender, never()).recommendNetwork(any(), any());
         verify(mNotificationManager, never()).notify(anyInt(), any());
@@ -220,17 +223,18 @@
      */
     @Test
     public void postNotification_clearNotificationWithoutDelayReset_shouldNotPostNotification() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mNotificationController.clearPendingNotification(false);
 
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
         // Recommendation made twice but no new notification posted.
-        verify(mOpenNetworkRecommender, times(2)).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender, times(2)).recommendNetwork(
+                mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
         verify(mNotificationManager).cancel(anyInt());
     }
@@ -241,16 +245,17 @@
      */
     @Test
     public void postNotification_clearNotificationWithDelayReset_shouldPostNotification() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mNotificationController.clearPendingNotification(true);
 
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender, times(2)).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender, times(2)).recommendNetwork(
+                mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager, times(2)).notify(anyInt(), any());
     }
 
@@ -259,9 +264,9 @@
      */
     @Test
     public void notificationTap_opensWifiSettings() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mBroadcastReceiver.onReceive(
@@ -271,14 +276,37 @@
     }
 
     /**
+     * When user dismissed notification and there is a recommended network, network ssid should be
+     * blacklisted.
+     */
+    @Test
+    public void userDismissedNotification_shouldBlacklistNetwork() {
+        mNotificationController.handleScanResults(mOpenNetworks);
+
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
+        verify(mNotificationManager).notify(anyInt(), any());
+
+        mBroadcastReceiver.onReceive(
+                mContext, new Intent(OpenNetworkNotifier.ACTION_USER_DISMISSED_NOTIFICATION));
+
+        verify(mWifiConfigManager).saveToStore(false /* forceWrite */);
+
+        mNotificationController.handleScanResults(mOpenNetworks);
+
+        Set<String> expectedBlacklist = new ArraySet<>();
+        expectedBlacklist.add(mDummyNetwork.SSID);
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, expectedBlacklist);
+    }
+
+    /**
      * When a notification is posted and cleared without reseting delay, after the delay has passed
      * the next scan with open networks should post a notification.
      */
     @Test
     public void delaySet_delayPassed_shouldPostNotification() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         mNotificationController.clearPendingNotification(false);
@@ -286,9 +314,10 @@
         // twice the delay time passed
         when(mClock.getWallClockMillis()).thenReturn(DEFAULT_REPEAT_DELAY_SEC * 1000L * 2);
 
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender, times(2)).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender, times(2)).recommendNetwork(
+                mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager, times(2)).notify(anyInt(), any());
     }
 
@@ -298,7 +327,7 @@
         when(mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, UserHandle.CURRENT))
                 .thenReturn(true);
 
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
         verify(mOpenNetworkRecommender, never()).recommendNetwork(any(), any());
         verify(mNotificationManager, never()).notify(anyInt(), any());
@@ -307,15 +336,15 @@
     /** Verifies that {@link UserManager#DISALLOW_CONFIG_WIFI} clears the showing notification. */
     @Test
     public void userHasDisallowConfigWifiRestriction_showingNotificationIsCleared() {
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
-        verify(mOpenNetworkRecommender).recommendNetwork(any(), any());
+        verify(mOpenNetworkRecommender).recommendNetwork(mOpenNetworks, mBlacklistedSsids);
         verify(mNotificationManager).notify(anyInt(), any());
 
         when(mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, UserHandle.CURRENT))
                 .thenReturn(true);
 
-        mNotificationController.handleScanResults(createOpenScanResults());
+        mNotificationController.handleScanResults(mOpenNetworks);
 
         verify(mNotificationManager).cancel(anyInt());
     }
diff --git a/tests/wifitests/src/com/android/server/wifi/OpenNetworkRecommenderTest.java b/tests/wifitests/src/com/android/server/wifi/OpenNetworkRecommenderTest.java
index becc1d2..720ec37 100644
--- a/tests/wifitests/src/com/android/server/wifi/OpenNetworkRecommenderTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/OpenNetworkRecommenderTest.java
@@ -17,14 +17,18 @@
 package com.android.server.wifi;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import android.net.wifi.ScanResult;
+import android.util.ArraySet;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Tests for {@link OpenNetworkRecommender}.
@@ -36,10 +40,13 @@
     private static final int MIN_RSSI_LEVEL = -127;
 
     private OpenNetworkRecommender mOpenNetworkRecommender;
+    private Set<String> mBlacklistedSsids;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mOpenNetworkRecommender = new OpenNetworkRecommender();
+        mBlacklistedSsids = new ArraySet<>();
     }
 
     private List<ScanDetail> createOpenScanResults(String... ssids) {
@@ -59,7 +66,8 @@
         List<ScanDetail> scanResults = createOpenScanResults(TEST_SSID_1);
         scanResults.get(0).getScanResult().level = MIN_RSSI_LEVEL;
 
-        ScanResult actual = mOpenNetworkRecommender.recommendNetwork(scanResults, null);
+        ScanResult actual = mOpenNetworkRecommender.recommendNetwork(
+                scanResults, mBlacklistedSsids);
         ScanResult expected = scanResults.get(0).getScanResult();
         assertEquals(expected, actual);
     }
@@ -71,28 +79,24 @@
         scanResults.get(0).getScanResult().level = MIN_RSSI_LEVEL;
         scanResults.get(1).getScanResult().level = MIN_RSSI_LEVEL + 1;
 
-        ScanResult actual = mOpenNetworkRecommender.recommendNetwork(scanResults, null);
+        ScanResult actual = mOpenNetworkRecommender.recommendNetwork(
+                scanResults, mBlacklistedSsids);
         ScanResult expected = scanResults.get(1).getScanResult();
         assertEquals(expected, actual);
     }
 
     /**
-     * If the current recommended network is present in the list for the next recommendation and has
-     * an equal RSSI, the recommendation should not change.
+     * If the best available open network is blacklisted, no networks should be recommended.
      */
     @Test
-    public void currentRecommendationHasEquallyHighRssi_shouldNotChangeRecommendation() {
+    public void blacklistBestNetworkSsid_shouldNeverRecommendNetwork() {
         List<ScanDetail> scanResults = createOpenScanResults(TEST_SSID_1, TEST_SSID_2);
         scanResults.get(0).getScanResult().level = MIN_RSSI_LEVEL + 1;
-        scanResults.get(1).getScanResult().level = MIN_RSSI_LEVEL + 1;
+        scanResults.get(1).getScanResult().level = MIN_RSSI_LEVEL;
+        mBlacklistedSsids.add(TEST_SSID_1);
 
-        ScanResult currentRecommendation = new ScanResult(scanResults.get(1).getScanResult());
-        // next recommendation does not depend on the rssi of the input recommendation.
-        currentRecommendation.level = MIN_RSSI_LEVEL;
-
-        ScanResult expected = scanResults.get(1).getScanResult();
         ScanResult actual = mOpenNetworkRecommender.recommendNetwork(
-                scanResults, currentRecommendation);
-        assertEquals(expected, actual);
+                scanResults, mBlacklistedSsids);
+        assertNull(actual);
     }
 }
diff --git a/tests/wifitests/src/com/android/server/wifi/SsidSetStoreDataTest.java b/tests/wifitests/src/com/android/server/wifi/SsidSetStoreDataTest.java
new file mode 100644
index 0000000..606b825
--- /dev/null
+++ b/tests/wifitests/src/com/android/server/wifi/SsidSetStoreDataTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.util.Xml;
+
+import com.android.internal.util.FastXmlSerializer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Unit tests for {@link com.android.server.wifi.SsidSetStoreData}.
+ */
+public class SsidSetStoreDataTest {
+    private static final String TEST_NOTIFIER_NAME = "TestNetwork";
+    private static final String TEST_SSID1 = "SSID 1";
+    private static final String TEST_SSID2 = "SSID 2";
+    private static final String TEST_SSID_SET_XML_STRING =
+            "<set name=\"SSIDSet\">\n"
+                    + "<string>" + TEST_SSID1 + "</string>\n"
+                    + "<string>" + TEST_SSID2 + "</string>\n"
+                    + "</set>\n";
+    private static final byte[] TEST_SSID_SET_XML_BYTES =
+            TEST_SSID_SET_XML_STRING.getBytes(StandardCharsets.UTF_8);
+
+    @Mock SsidSetStoreData.DataSource mDataSource;
+    SsidSetStoreData mSsidSetStoreData;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mSsidSetStoreData = new SsidSetStoreData(TEST_NOTIFIER_NAME, mDataSource);
+    }
+
+    /**
+     * Helper function for serializing configuration data to a XML block.
+     *
+     * @param shared Flag indicating serializing shared or user configurations
+     * @return byte[] of the XML data
+     * @throws Exception
+     */
+    private byte[] serializeData(boolean shared) throws Exception {
+        final XmlSerializer out = new FastXmlSerializer();
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        out.setOutput(outputStream, StandardCharsets.UTF_8.name());
+        mSsidSetStoreData.serializeData(out, shared);
+        out.flush();
+        return outputStream.toByteArray();
+    }
+
+    /**
+     * Helper function for parsing configuration data from a XML block.
+     *
+     * @param data XML data to parse from
+     * @param shared Flag indicating parsing of shared or user configurations
+     * @throws Exception
+     */
+    private void deserializeData(byte[] data, boolean shared) throws Exception {
+        final XmlPullParser in = Xml.newPullParser();
+        final ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
+        in.setInput(inputStream, StandardCharsets.UTF_8.name());
+        mSsidSetStoreData.deserializeData(in, in.getDepth(), shared);
+    }
+
+    /**
+     * Verify that a XmlPullParserException will be thrown when attempting to serialize data
+     * to the share store.
+     *
+     * @throws Exception
+     */
+    @Test(expected = XmlPullParserException.class)
+    public void serializeShareData() throws Exception {
+        serializeData(true /* shared */);
+    }
+
+    /**
+     * Verify that a XmlPullParserException will be thrown when attempting to deserialize
+     * data from the share store.
+     *
+     * @throws Exception
+     */
+    @Test(expected = XmlPullParserException.class)
+    public void deserializeShareData() throws Exception {
+        deserializeData(new byte[0], true /* shared */);
+    }
+
+    /**
+     * Verify that serializing the user store data without any configuration doesn't cause any
+     * crash and no data should be serialized.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void serializeEmptyConfigs() throws Exception {
+        when(mDataSource.getSsids()).thenReturn(new HashSet<String>());
+        assertEquals(0, serializeData(false /* shared */).length);
+    }
+
+    /**
+     * Verify that parsing an empty data doesn't cause any crash and no configuration should
+     * be deserialized.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void deserializeEmptyStoreData() throws Exception {
+        deserializeData(new byte[0], false /* shared */);
+        verify(mDataSource, never()).setSsids(any(Set.class));
+    }
+
+    /**
+     * Verify that {@link SsidSetStoreData} does not support share data.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void supportShareData() throws Exception {
+        assertFalse(mSsidSetStoreData.supportShareData());
+    }
+
+    /**
+     * Verify that the store data is serialized correctly, matches the predefined test XML data.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void serializeSsidSet() throws Exception {
+        Set<String> ssidSet = new HashSet<>();
+        ssidSet.add(TEST_SSID1);
+        ssidSet.add(TEST_SSID2);
+        when(mDataSource.getSsids()).thenReturn(ssidSet);
+        byte[] actualData = serializeData(false /* shared */);
+        assertTrue(Arrays.equals(TEST_SSID_SET_XML_BYTES, actualData));
+    }
+
+    /**
+     * Verify that the store data is deserialized correctly using the predefined test XML data.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void deserializeSsidSet() throws Exception {
+        Set<String> ssidSet = new HashSet<>();
+        ssidSet.add(TEST_SSID1);
+        ssidSet.add(TEST_SSID2);
+        deserializeData(TEST_SSID_SET_XML_BYTES, false /* shared */);
+        verify(mDataSource).setSsids(eq(ssidSet));
+    }
+
+    /**
+     * Verify that a XmlPullParserException will be thrown when parsing a SSIDSet set with an
+     * unknown tag.
+     *
+     * @throws Exception
+     */
+    @Test(expected = XmlPullParserException.class)
+    public void parseSetWithUnknownTag() throws Exception {
+        String ssidSet =
+                "<set name=\"SSIDSet\">\n"
+                        + "<string>" + TEST_SSID1 + "</string>\n"
+                        + "<string>" + TEST_SSID2 + "</string>\n"
+                        + "<Unknown>" + "badInput" + "</Unknown>" // Unknown tag.
+                        + "</set>\n";
+        deserializeData(ssidSet.getBytes(StandardCharsets.UTF_8), false /* shared */);
+    }
+}
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java b/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java
index 5a13928..10ad3c6 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiMetricsTest.java
@@ -16,6 +16,7 @@
 package com.android.server.wifi;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.*;
 
@@ -37,6 +38,7 @@
 import com.android.server.wifi.hotspot2.PasspointMatch;
 import com.android.server.wifi.hotspot2.PasspointProvider;
 import com.android.server.wifi.nano.WifiMetricsProto;
+import com.android.server.wifi.nano.WifiMetricsProto.PnoScanMetrics;
 import com.android.server.wifi.nano.WifiMetricsProto.StaEvent;
 
 import org.junit.Before;
@@ -252,6 +254,11 @@
     private static final int NUM_PASSPOINT_PROVIDER_UNINSTALL_SUCCESS = 2;
     private static final int NUM_PASSPOINT_PROVIDERS_SUCCESSFULLY_CONNECTED = 1;
     private static final int NUM_PARTIAL_SCAN_RESULTS = 73;
+    private static final int NUM_PNO_SCAN_ATTEMPTS = 20;
+    private static final int NUM_PNO_SCAN_FAILED = 5;
+    private static final int NUM_PNO_SCAN_STARTED_OVER_OFFLOAD = 17;
+    private static final int NUM_PNO_SCAN_FAILED_OVER_OFFLOAD = 8;
+    private static final int NUM_PNO_FOUND_NETWORK_EVENTS = 10;
 
     private ScanDetail buildMockScanDetail(boolean hidden, NetworkDetail.HSRelease hSRelease,
             String capabilities) {
@@ -474,6 +481,23 @@
         for (int i = 0; i < NUM_PASSPOINT_PROVIDER_UNINSTALL_SUCCESS; i++) {
             mWifiMetrics.incrementNumPasspointProviderUninstallSuccess();
         }
+
+        // increment pno scan metrics
+        for (int i = 0; i < NUM_PNO_SCAN_ATTEMPTS; i++) {
+            mWifiMetrics.incrementPnoScanStartAttempCount();
+        }
+        for (int i = 0; i < NUM_PNO_SCAN_FAILED; i++) {
+            mWifiMetrics.incrementPnoScanFailedCount();
+        }
+        for (int i = 0; i < NUM_PNO_SCAN_STARTED_OVER_OFFLOAD; i++) {
+            mWifiMetrics.incrementPnoScanStartedOverOffloadCount();
+        }
+        for (int i = 0; i < NUM_PNO_SCAN_FAILED_OVER_OFFLOAD; i++) {
+            mWifiMetrics.incrementPnoScanFailedOverOffloadCount();
+        }
+        for (int i = 0; i < NUM_PNO_FOUND_NETWORK_EVENTS; i++) {
+            mWifiMetrics.incrementPnoFoundNetworkEventCount();
+        }
     }
 
     /**
@@ -618,6 +642,14 @@
                 mDecodedProto.numPasspointProviderUninstallSuccess);
         assertEquals(NUM_PASSPOINT_PROVIDERS_SUCCESSFULLY_CONNECTED,
                 mDecodedProto.numPasspointProvidersSuccessfullyConnected);
+
+        PnoScanMetrics pno_metrics = mDecodedProto.pnoScanMetrics;
+        assertNotNull(pno_metrics);
+        assertEquals(NUM_PNO_SCAN_ATTEMPTS, pno_metrics.numPnoScanAttempts);
+        assertEquals(NUM_PNO_SCAN_FAILED, pno_metrics.numPnoScanFailed);
+        assertEquals(NUM_PNO_SCAN_STARTED_OVER_OFFLOAD, pno_metrics.numPnoScanStartedOverOffload);
+        assertEquals(NUM_PNO_SCAN_FAILED_OVER_OFFLOAD, pno_metrics.numPnoScanFailedOverOffload);
+        assertEquals(NUM_PNO_FOUND_NETWORK_EVENTS, pno_metrics.numPnoFoundNetworkEvents);
     }
 
     /**
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiScoreReportTest.java b/tests/wifitests/src/com/android/server/wifi/WifiScoreReportTest.java
index 24d3afa..6f01c8e 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiScoreReportTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiScoreReportTest.java
@@ -48,6 +48,18 @@
 
     private static final int CELLULAR_THRESHOLD_SCORE = 50;
 
+    class FakeClock extends Clock {
+        long mWallClockMillis = 1500000000000L;
+        int mStepMillis = 1001;
+
+        @Override
+        public long getWallClockMillis() {
+            mWallClockMillis += mStepMillis;
+            return mWallClockMillis;
+        }
+    }
+
+    FakeClock mClock;
     WifiConfiguration mWifiConfiguration;
     WifiScoreReport mWifiScoreReport;
     ScanDetailCache mScanDetailCache;
@@ -122,7 +134,8 @@
         when(mWifiConfigManager.getScanDetailCacheForNetwork(anyInt()))
                 .thenReturn(mScanDetailCache);
         when(mContext.getResources()).thenReturn(mResources);
-        mWifiScoreReport = new WifiScoreReport(mContext, mWifiConfigManager, new Clock());
+        mClock = new FakeClock();
+        mWifiScoreReport = new WifiScoreReport(mContext, mWifiConfigManager, mClock);
     }
 
     /**
diff --git a/tests/wifitests/src/com/android/server/wifi/WificondControlTest.java b/tests/wifitests/src/com/android/server/wifi/WificondControlTest.java
index 164f759..cca2045 100644
--- a/tests/wifitests/src/com/android/server/wifi/WificondControlTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WificondControlTest.java
@@ -66,6 +66,7 @@
 public class WificondControlTest {
     private WifiInjector mWifiInjector;
     private WifiMonitor mWifiMonitor;
+    private WifiMetrics mWifiMetrics;
     private CarrierNetworkConfig mCarrierNetworkConfig;
     private WificondControl mWificondControl;
     private static final String TEST_INTERFACE_NAME = "test_wlan_if";
@@ -145,6 +146,8 @@
     public void setUp() throws Exception {
         mWifiInjector = mock(WifiInjector.class);
         mWifiMonitor = mock(WifiMonitor.class);
+        mWifiMetrics = mock(WifiMetrics.class);
+        when(mWifiInjector.getWifiMetrics()).thenReturn(mWifiMetrics);
         mCarrierNetworkConfig = mock(CarrierNetworkConfig.class);
         mWificondControl = new WificondControl(mWifiInjector, mWifiMonitor, mCarrierNetworkConfig);
     }
@@ -660,7 +663,7 @@
 
     /**
      * Verifies that WificondControl can invoke WifiMonitor broadcast methods upon pno scan
-     * reuslt event.
+     * result event.
      */
     @Test
     public void testPnoScanResultEvent() throws Exception {
@@ -671,11 +674,48 @@
         IPnoScanEvent pnoScanEvent = messageCaptor.getValue();
         assertNotNull(pnoScanEvent);
         pnoScanEvent.OnPnoNetworkFound();
-
         verify(mWifiMonitor).broadcastPnoScanResultEvent(any(String.class));
     }
 
     /**
+     * Verifies that WificondControl can invoke WifiMetrics pno scan count methods upon pno event.
+     */
+    @Test
+    public void testPnoScanEventsForMetrics() throws Exception {
+        IWifiScannerImpl scanner = setupClientInterfaceAndCreateMockWificondScanner();
+
+        ArgumentCaptor<IPnoScanEvent> messageCaptor = ArgumentCaptor.forClass(IPnoScanEvent.class);
+        verify(scanner).subscribePnoScanEvents(messageCaptor.capture());
+        IPnoScanEvent pnoScanEvent = messageCaptor.getValue();
+        assertNotNull(pnoScanEvent);
+
+        pnoScanEvent.OnPnoNetworkFound();
+        verify(mWifiMetrics).incrementPnoFoundNetworkEventCount();
+
+        pnoScanEvent.OnPnoScanFailed();
+        verify(mWifiMetrics).incrementPnoScanFailedCount();
+
+        pnoScanEvent.OnPnoScanOverOffloadStarted();
+        verify(mWifiMetrics).incrementPnoScanStartedOverOffloadCount();
+
+        pnoScanEvent.OnPnoScanOverOffloadFailed(0);
+        verify(mWifiMetrics).incrementPnoScanFailedOverOffloadCount();
+    }
+
+    /**
+     * Verifies that startPnoScan() can invoke WifiMetrics pno scan count methods correctly.
+     */
+    @Test
+    public void testStartPnoScanForMetrics() throws Exception {
+        IWifiScannerImpl scanner = setupClientInterfaceAndCreateMockWificondScanner();
+
+        when(scanner.startPnoScan(any(PnoSettings.class))).thenReturn(false);
+        assertFalse(mWificondControl.startPnoScan(TEST_PNO_SETTINGS));
+        verify(mWifiMetrics).incrementPnoScanStartAttempCount();
+        verify(mWifiMetrics).incrementPnoScanFailedCount();
+    }
+
+    /**
      * Verifies that abortScan() calls underlying wificond.
      */
     @Test