Merge "WifiBackupRestore: Ignore unkown bitset values from backup" into pi-dev
diff --git a/service/java/com/android/server/wifi/CarrierNetworkConfig.java b/service/java/com/android/server/wifi/CarrierNetworkConfig.java
index efe695b..db107bf 100644
--- a/service/java/com/android/server/wifi/CarrierNetworkConfig.java
+++ b/service/java/com/android/server/wifi/CarrierNetworkConfig.java
@@ -47,6 +47,7 @@
     private static final int CONFIG_ELEMENT_SIZE = 2;
 
     private final Map<String, NetworkInfo> mCarrierNetworkMap;
+    private boolean mIsCarrierImsiEncryptionInfoAvailable = false;
 
     public CarrierNetworkConfig(Context context) {
         mCarrierNetworkMap = new HashMap<>();
@@ -89,6 +90,40 @@
     }
 
     /**
+     * @return True if carrier IMSI encryption info is available, False otherwise.
+     */
+    public boolean isCarrierEncryptionInfoAvailable() {
+        return mIsCarrierImsiEncryptionInfoAvailable;
+    }
+
+    /**
+     * Verify whether carrier IMSI encryption info is available.
+     *
+     * @param context Current application context
+     *
+     * @return True if carrier IMSI encryption info is available, False otherwise.
+     */
+    private boolean verifyCarrierImsiEncryptionInfoIsAvailable(Context context) {
+        TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        if (telephonyManager == null) {
+            return false;
+        }
+        try {
+            ImsiEncryptionInfo imsiEncryptionInfo = telephonyManager
+                    .getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN);
+            if (imsiEncryptionInfo == null) {
+                return false;
+            }
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Failed to get imsi encryption info: " + e.getMessage());
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
      * Utility class for storing carrier network information.
      */
     private static class NetworkInfo {
@@ -108,6 +143,8 @@
      * @param context Current application context
      */
     private void updateNetworkConfig(Context context) {
+        mIsCarrierImsiEncryptionInfoAvailable = verifyCarrierImsiEncryptionInfoIsAvailable(context);
+
         // Reset network map.
         mCarrierNetworkMap.clear();
 
@@ -127,22 +164,6 @@
             return;
         }
 
-        TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(
-                Context.TELEPHONY_SERVICE);
-        if (telephonyManager == null) {
-            return;
-        }
-        try {
-            ImsiEncryptionInfo imsiEncryptionInfo = telephonyManager
-                    .getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN);
-            if (imsiEncryptionInfo == null) {
-                return;
-            }
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Failed to get imsi encryption info: " + e.getMessage());
-            return;
-        }
-
         // Process the carrier config for each active subscription.
         for (SubscriptionInfo subInfo : subInfoList) {
             processNetworkConfig(
diff --git a/service/java/com/android/server/wifi/ClientModeManager.java b/service/java/com/android/server/wifi/ClientModeManager.java
index 7ab33dd..14d55de 100644
--- a/service/java/com/android/server/wifi/ClientModeManager.java
+++ b/service/java/com/android/server/wifi/ClientModeManager.java
@@ -16,27 +16,268 @@
 
 package com.android.server.wifi;
 
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.server.wifi.WifiNative.InterfaceCallback;
+
 /**
  * Manager WiFi in Client Mode where we connect to configured networks.
  */
 public class ClientModeManager implements ActiveModeManager {
+    private static final String TAG = "WifiClientModeManager";
 
-    private static final String TAG = "ClientModeManager";
+    private final ClientModeStateMachine mStateMachine;
 
-    ClientModeManager() {
+    private final Context mContext;
+    private final WifiNative mWifiNative;
+
+    private final WifiMetrics mWifiMetrics;
+    private final Listener mListener;
+    private final ScanRequestProxy mScanRequestProxy;
+
+    private String mClientInterfaceName;
+
+
+    ClientModeManager(Context context, @NonNull Looper looper, WifiNative wifiNative,
+            Listener listener, WifiMetrics wifiMetrics, ScanRequestProxy scanRequestProxy) {
+        mContext = context;
+        mWifiNative = wifiNative;
+        mListener = listener;
+        mWifiMetrics = wifiMetrics;
+        mScanRequestProxy = scanRequestProxy;
+        mStateMachine = new ClientModeStateMachine(looper);
     }
 
     /**
      * Start client mode.
      */
     public void start() {
-
+        mStateMachine.sendMessage(ClientModeStateMachine.CMD_START);
     }
 
     /**
      * Disconnect from any currently connected networks and stop client mode.
      */
     public void stop() {
+        mStateMachine.sendMessage(ClientModeStateMachine.CMD_STOP);
+    }
 
+    /**
+     * Listener for ClientMode state changes.
+     */
+    public interface Listener {
+        /**
+         * Invoke when wifi state changes.
+         * @param state new wifi state
+         */
+        void onStateChanged(int state);
+    }
+
+    /**
+     * Update Wifi state and send the broadcast.
+     * @param newState new Wifi state
+     * @param currentState current wifi state
+     */
+    private void updateWifiState(int newState, int currentState) {
+        mListener.onStateChanged(newState);
+
+        if (newState == WifiManager.WIFI_STATE_UNKNOWN) {
+            // do not need to broadcast failure to system
+            return;
+        }
+
+        final Intent intent = new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        intent.putExtra(WifiManager.EXTRA_WIFI_STATE, newState);
+        intent.putExtra(WifiManager.EXTRA_PREVIOUS_WIFI_STATE, currentState);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private class ClientModeStateMachine extends StateMachine {
+        // Commands for the state machine.
+        public static final int CMD_START = 0;
+        public static final int CMD_STOP = 1;
+        public static final int CMD_WIFINATIVE_FAILURE = 2;
+        public static final int CMD_INTERFACE_STATUS_CHANGED = 3;
+        public static final int CMD_INTERFACE_DESTROYED = 4;
+        private final State mIdleState = new IdleState();
+        private final State mStartedState = new StartedState();
+        private WifiNative.StatusListener mWifiNativeStatusListener = (boolean isReady) -> {
+            if (!isReady) {
+                sendMessage(CMD_WIFINATIVE_FAILURE);
+            }
+        };
+
+        private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
+            @Override
+            public void onDestroyed(String ifaceName) {
+                sendMessage(CMD_INTERFACE_DESTROYED);
+            }
+
+            @Override
+            public void onUp(String ifaceName) {
+                sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1);
+            }
+
+            @Override
+            public void onDown(String ifaceName) {
+                sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
+            }
+        };
+
+        private boolean mIfaceIsUp = false;
+
+        ClientModeStateMachine(Looper looper) {
+            super(TAG, looper);
+
+            addState(mIdleState);
+            addState(mStartedState);
+
+            setInitialState(mIdleState);
+            start();
+        }
+
+        private class IdleState extends State {
+
+            @Override
+            public void enter() {
+                Log.d(TAG, "entering IdleState");
+                mWifiNative.registerStatusListener(mWifiNativeStatusListener);
+                mClientInterfaceName = null;
+            }
+
+            @Override
+            public boolean processMessage(Message message) {
+                switch (message.what) {
+                    case CMD_START:
+                        updateWifiState(WifiManager.WIFI_STATE_ENABLING,
+                                        WifiManager.WIFI_STATE_DISABLED);
+
+                        mClientInterfaceName = mWifiNative.setupInterfaceForClientMode(
+                                false /* not low priority */, mWifiNativeInterfaceCallback);
+                        if (TextUtils.isEmpty(mClientInterfaceName)) {
+                            Log.e(TAG, "Failed to create ClientInterface. Sit in Idle");
+                            sendScanAvailableBroadcast(false);
+                            updateWifiState(WifiManager.WIFI_STATE_UNKNOWN,
+                                            WifiManager.WIFI_STATE_ENABLING);
+                            break;
+                        }
+                        transitionTo(mStartedState);
+                        break;
+                    case CMD_STOP:
+                        // This should be safe to ignore.
+                        Log.d(TAG, "received CMD_STOP when idle, ignoring");
+                        break;
+                    default:
+                        Log.d(TAG, "received an invalid message: " + message);
+                        return NOT_HANDLED;
+                }
+                return HANDLED;
+            }
+        }
+
+        private class StartedState extends State {
+
+            private void onUpChanged(boolean isUp) {
+                if (isUp == mIfaceIsUp) {
+                    return;  // no change
+                }
+                mIfaceIsUp = isUp;
+                if (isUp) {
+                    Log.d(TAG, "Wifi is ready to use for client mode");
+                    sendScanAvailableBroadcast(true);
+                    updateWifiState(WifiManager.WIFI_STATE_ENABLED,
+                                    WifiManager.WIFI_STATE_ENABLING);
+                } else {
+                    // if the interface goes down we should exit and go back to idle state.
+                    Log.d(TAG, "interface down!  may need to restart ClientMode");
+                    updateWifiState(WifiManager.WIFI_STATE_UNKNOWN,
+                                    WifiManager.WIFI_STATE_UNKNOWN);
+                    mStateMachine.sendMessage(CMD_STOP);
+                }
+            }
+
+            @Override
+            public void enter() {
+                Log.d(TAG, "entering StartedState");
+                mIfaceIsUp = false;
+                onUpChanged(mWifiNative.isInterfaceUp(mClientInterfaceName));
+                mScanRequestProxy.enableScanningForHiddenNetworks(true);
+            }
+
+            @Override
+            public boolean processMessage(Message message) {
+                switch(message.what) {
+                    case CMD_START:
+                        // Already started, ignore this command.
+                        break;
+                    case CMD_STOP:
+                        Log.d(TAG, "Stopping client mode.");
+                        updateWifiState(WifiManager.WIFI_STATE_DISABLING,
+                                        WifiManager.WIFI_STATE_ENABLED);
+                        mWifiNative.teardownInterface(mClientInterfaceName);
+                        transitionTo(mIdleState);
+                        break;
+                    case CMD_INTERFACE_STATUS_CHANGED:
+                        boolean isUp = message.arg1 == 1;
+                        onUpChanged(isUp);
+                        break;
+                    case CMD_WIFINATIVE_FAILURE:
+                        Log.d(TAG, "WifiNative failure - may need to restart ClientMode!");
+                        updateWifiState(WifiManager.WIFI_STATE_UNKNOWN,
+                                        WifiManager.WIFI_STATE_UNKNOWN);
+                        updateWifiState(WifiManager.WIFI_STATE_DISABLING,
+                                        WifiManager.WIFI_STATE_ENABLED);
+                        transitionTo(mIdleState);
+                        break;
+                    case CMD_INTERFACE_DESTROYED:
+                        Log.d(TAG, "interface destroyed - client mode stopping");
+
+                        updateWifiState(WifiManager.WIFI_STATE_DISABLING,
+                                        WifiManager.WIFI_STATE_ENABLED);
+                        transitionTo(mIdleState);
+                        break;
+                    default:
+                        return NOT_HANDLED;
+                }
+                return HANDLED;
+            }
+
+            /**
+             * Clean up state, unregister listeners and send broadcast to tell WifiScanner
+             * that wifi is disabled.
+             */
+            @Override
+            public void exit() {
+                // let WifiScanner know that wifi is down.
+                sendScanAvailableBroadcast(false);
+                updateWifiState(WifiManager.WIFI_STATE_DISABLED,
+                                WifiManager.WIFI_STATE_DISABLING);
+                mScanRequestProxy.enableScanningForHiddenNetworks(false);
+                mScanRequestProxy.clearScanResults();
+            }
+        }
+
+        private void sendScanAvailableBroadcast(boolean available) {
+            Log.d(TAG, "sending scan available broadcast: " + available);
+            final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            if (available) {
+                intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_ENABLED);
+            } else {
+                intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_DISABLED);
+            }
+            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        }
     }
 }
diff --git a/service/java/com/android/server/wifi/ScanDetailCache.java b/service/java/com/android/server/wifi/ScanDetailCache.java
index b7f5411..f3cbc95 100644
--- a/service/java/com/android/server/wifi/ScanDetailCache.java
+++ b/service/java/com/android/server/wifi/ScanDetailCache.java
@@ -19,8 +19,6 @@
 import android.annotation.NonNull;
 import android.net.wifi.ScanResult;
 import android.net.wifi.WifiConfiguration;
-import android.os.SystemClock;
-import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -169,75 +167,6 @@
         return list;
     }
 
-    /**
-     * Method to get cached scan results that are less than 'age' old.
-     *
-     * @param age long Time window of desired results.
-     * @return WifiConfiguration.Visibility matches in the given visibility
-     */
-    public WifiConfiguration.Visibility getVisibilityByRssi(long age) {
-        WifiConfiguration.Visibility status = new WifiConfiguration.Visibility();
-
-        long now_ms = System.currentTimeMillis();
-        long now_elapsed_ms = SystemClock.elapsedRealtime();
-        for (ScanDetail scanDetail : values()) {
-            ScanResult result = scanDetail.getScanResult();
-            if (scanDetail.getSeen() == 0) {
-                continue;
-            }
-
-            if (result.is5GHz()) {
-                //strictly speaking: [4915, 5825]
-                //number of known BSSID on 5GHz band
-                status.num5 = status.num5 + 1;
-            } else if (result.is24GHz()) {
-                //strictly speaking: [2412, 2482]
-                //number of known BSSID on 2.4Ghz band
-                status.num24 = status.num24 + 1;
-            }
-
-            if (result.timestamp != 0) {
-                if (DBG) {
-                    Log.e("getVisibilityByRssi", " considering " + result.SSID + " " + result.BSSID
-                            + " elapsed=" + now_elapsed_ms + " timestamp=" + result.timestamp
-                            + " age = " + age);
-                }
-                if ((now_elapsed_ms - (result.timestamp / 1000)) > age) continue;
-            } else {
-                // This checks the time at which we have received the scan result from supplicant
-                if ((now_ms - result.seen) > age) continue;
-            }
-
-            if (result.is5GHz()) {
-                if (result.level > status.rssi5) {
-                    status.rssi5 = result.level;
-                    status.age5 = result.seen;
-                    status.BSSID5 = result.BSSID;
-                }
-            } else if (result.is24GHz()) {
-                if (result.level > status.rssi24) {
-                    status.rssi24 = result.level;
-                    status.age24 = result.seen;
-                    status.BSSID24 = result.BSSID;
-                }
-            }
-        }
-
-        return status;
-    }
-
-    /**
-     * Method to get scan matches for the desired time window.  Returns matches by passpoint time if
-     * the WifiConfiguration is passpoint.
-     *
-     * @param age long desired time for matches.
-     * @return WifiConfiguration.Visibility matches in the given visibility
-     */
-    public WifiConfiguration.Visibility getVisibility(long age) {
-        return getVisibilityByRssi(age);
-    }
-
-
     @Override
     public String toString() {
         StringBuilder sbuf = new StringBuilder();
diff --git a/service/java/com/android/server/wifi/ScanRequestProxy.java b/service/java/com/android/server/wifi/ScanRequestProxy.java
index 588ea6c..8c4c09b 100644
--- a/service/java/com/android/server/wifi/ScanRequestProxy.java
+++ b/service/java/com/android/server/wifi/ScanRequestProxy.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wifi;
 
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.net.wifi.ScanResult;
@@ -24,8 +26,10 @@
 import android.os.Binder;
 import android.os.UserHandle;
 import android.os.WorkSource;
+import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.wifi.util.WifiPermissionsUtil;
 
 import java.util.ArrayList;
@@ -46,18 +50,26 @@
  * {@link WifiManager#getScanResults()} is invoked.
  * c) Will send out the {@link WifiManager#SCAN_RESULTS_AVAILABLE_ACTION} broadcast when new
  * scan results are available.
+ * d) Throttle scan requests from non-setting apps:
+ *    d.1) For foreground apps, throttle to a max of 1 scan per app every 30 seconds.
+ *    d.2) For background apps, throttle to a max of 1 scan from any app every 30 minutes.
  * Note: This class is not thread-safe. It needs to be invoked from WifiStateMachine thread only.
- * TODO (b/68987915): Port over scan throttling logic from WifiService for all apps.
- * TODO: Port over idle mode handling from WifiService.
  */
 @NotThreadSafe
 public class ScanRequestProxy {
     private static final String TAG = "WifiScanRequestProxy";
+    @VisibleForTesting
+    public static final int SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS = 30 * 1000;
+    @VisibleForTesting
+    public static final int SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS = 30 * 60 * 1000;
 
     private final Context mContext;
+    private final AppOpsManager mAppOps;
+    private final ActivityManager mActivityManager;
     private final WifiInjector mWifiInjector;
     private final WifiConfigManager mWifiConfigManager;
     private final WifiPermissionsUtil mWifiPermissionsUtil;
+    private final Clock mClock;
     private WifiScanner mWifiScanner;
 
     // Verbose logging flag.
@@ -66,6 +78,10 @@
     private boolean mScanningForHiddenNetworksEnabled = false;
     // Flag to indicate that we're waiting for scan results from an existing request.
     private boolean mIsScanProcessingComplete = true;
+    // Timestamps for the last scan requested by any background app.
+    private long mLastScanTimestampForBgApps = 0;
+    // Timestamps for the last scan requested by each foreground app.
+    private final ArrayMap<String, Long> mLastScanTimestampsForFgApps = new ArrayMap();
     // Scan results cached from the last full single scan request.
     private final List<ScanResult> mLastScanResults = new ArrayList<>();
     // Common scan listener for scan requests.
@@ -117,12 +133,16 @@
         }
     };
 
-    ScanRequestProxy(Context context, WifiInjector wifiInjector, WifiConfigManager configManager,
-            WifiPermissionsUtil wifiPermissionUtil) {
+    ScanRequestProxy(Context context, AppOpsManager appOpsManager, ActivityManager activityManager,
+                     WifiInjector wifiInjector, WifiConfigManager configManager,
+            WifiPermissionsUtil wifiPermissionUtil, Clock clock) {
         mContext = context;
+        mAppOps = appOpsManager;
+        mActivityManager = activityManager;
         mWifiInjector = wifiInjector;
         mWifiConfigManager = configManager;
         mWifiPermissionsUtil = wifiPermissionUtil;
+        mClock = clock;
     }
 
     /**
@@ -184,15 +204,114 @@
     }
 
     /**
+     * Helper method to send the scan request failure broadcast to specified package.
+     */
+    private void sendScanResultFailureBroadcastToPackage(String packageName) {
+        // clear calling identity to send broadcast
+        long callingIdentity = Binder.clearCallingIdentity();
+        try {
+            Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
+            intent.setPackage(packageName);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            // restore calling identity
+            Binder.restoreCallingIdentity(callingIdentity);
+        }
+    }
+
+    /**
+     * Checks if the scan request from the app (specified by packageName) needs
+     * to be throttled.
+     */
+    private boolean shouldScanRequestBeThrottledForForegroundApp(String packageName) {
+        long lastScanMs = mLastScanTimestampsForFgApps.getOrDefault(packageName, 0L);
+        long elapsedRealtime = mClock.getElapsedSinceBootMillis();
+        if (lastScanMs != 0
+                && (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS) {
+            return true;
+        }
+        // Proceed with the scan request and record the time.
+        mLastScanTimestampsForFgApps.put(packageName, elapsedRealtime);
+        return false;
+    }
+
+    /**
+     * Checks if the scan request from a background app needs to be throttled.
+     */
+    private boolean shouldScanRequestBeThrottledForBackgroundApp() {
+        long lastScanMs = mLastScanTimestampForBgApps;
+        long elapsedRealtime = mClock.getElapsedSinceBootMillis();
+        if (lastScanMs != 0
+                && (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS) {
+            return true;
+        }
+        // Proceed with the scan request and record the time.
+        mLastScanTimestampForBgApps = elapsedRealtime;
+        return false;
+    }
+
+    /**
+     * Check if the request comes from background app.
+     */
+    private boolean isRequestFromBackground(int callingUid, String packageName) {
+        mAppOps.checkPackage(callingUid, packageName);
+        // getPackageImportance requires PACKAGE_USAGE_STATS permission, so clearing the incoming
+        // identity so the permission check can be done on system process where wifi runs in.
+        long callingIdentity = Binder.clearCallingIdentity();
+        // TODO(b/74970282): This try/catch block may not be necessary (here & above) because all
+        // of these calls are already in WSM thread context (offloaded from app's binder thread).
+        try {
+            return mActivityManager.getPackageImportance(packageName)
+                    > ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+        } finally {
+            Binder.restoreCallingIdentity(callingIdentity);
+        }
+    }
+
+    /**
+     * Checks if the scan request from the app (specified by callingUid & packageName) needs
+     * to be throttled.
+     *
+     * a) Each foreground app can request 1 scan every
+     * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS}.
+     * b) Background apps combined can request 1 scan every
+     * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
+     */
+    private boolean shouldScanRequestBeThrottledForApp(int callingUid, String packageName) {
+        boolean isThrottled;
+        if (isRequestFromBackground(callingUid, packageName)) {
+            isThrottled = shouldScanRequestBeThrottledForBackgroundApp();
+            if (isThrottled && mVerboseLoggingEnabled) {
+                Log.v(TAG, "Background scan app request [" + callingUid + ", " + packageName + "]");
+            }
+        } else {
+            isThrottled = shouldScanRequestBeThrottledForForegroundApp(packageName);
+            if (isThrottled && mVerboseLoggingEnabled) {
+                Log.v(TAG, "Foreground scan app request [" + callingUid + ", " + packageName + "]");
+            }
+        }
+        return isThrottled;
+    }
+
+    /**
      * Initiate a wifi scan.
      *
      * @param callingUid The uid initiating the wifi scan. Blame will be given to this uid.
      * @return true if the scan request was placed or a scan is already ongoing, false otherwise.
      */
-    public boolean startScan(int callingUid) {
+    public boolean startScan(int callingUid, String packageName) {
         if (!retrieveWifiScannerIfNecessary()) {
             Log.e(TAG, "Failed to retrieve wifiscanner");
-            sendScanResultBroadcast(false);
+            sendScanResultFailureBroadcastToPackage(packageName);
+            return false;
+        }
+        boolean fromSettings = mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid);
+        // Check and throttle scan request from apps without NETWORK_SETTINGS permission.
+        if (!fromSettings && shouldScanRequestBeThrottledForApp(callingUid, packageName)) {
+            Log.i(TAG, "Scan request from " + packageName + " throttled");
+            sendScanResultFailureBroadcastToPackage(packageName);
             return false;
         }
         // Create a worksource using the caller's UID.
@@ -201,7 +320,7 @@
         // Create the scan settings.
         WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
         // Scan requests from apps with network settings will be of high accuracy type.
-        if (mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)) {
+        if (fromSettings) {
             settings.type = WifiScanner.TYPE_HIGH_ACCURACY;
         }
         // always do full scans
@@ -234,5 +353,7 @@
      */
     public void clearScanResults() {
         mLastScanResults.clear();
+        mLastScanTimestampForBgApps = 0;
+        mLastScanTimestampsForFgApps.clear();
     }
 }
diff --git a/service/java/com/android/server/wifi/WifiConnectivityManager.java b/service/java/com/android/server/wifi/WifiConnectivityManager.java
index 7eeab4d..6594945 100644
--- a/service/java/com/android/server/wifi/WifiConnectivityManager.java
+++ b/service/java/com/android/server/wifi/WifiConnectivityManager.java
@@ -277,8 +277,11 @@
             if (mWifiState == WIFI_STATE_DISCONNECTED) {
                 mOpenNetworkNotifier.handleScanResults(
                         mNetworkSelector.getFilteredScanDetailsForOpenUnsavedNetworks());
-                mCarrierNetworkNotifier.handleScanResults(mNetworkSelector
-                        .getFilteredScanDetailsForCarrierUnsavedNetworks(mCarrierNetworkConfig));
+                if (mCarrierNetworkConfig.isCarrierEncryptionInfoAvailable()) {
+                    mCarrierNetworkNotifier.handleScanResults(
+                            mNetworkSelector.getFilteredScanDetailsForCarrierUnsavedNetworks(
+                                    mCarrierNetworkConfig));
+                }
             }
             return false;
         }
diff --git a/service/java/com/android/server/wifi/WifiInjector.java b/service/java/com/android/server/wifi/WifiInjector.java
index 28e3616..b6beb68 100644
--- a/service/java/com/android/server/wifi/WifiInjector.java
+++ b/service/java/com/android/server/wifi/WifiInjector.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.net.NetworkKey;
 import android.net.NetworkScoreManager;
@@ -237,8 +238,11 @@
         mPasspointNetworkEvaluator = new PasspointNetworkEvaluator(
                 mPasspointManager, mWifiConfigManager, mConnectivityLocalLog);
         mWifiMetrics.setPasspointManager(mPasspointManager);
-        mScanRequestProxy = new ScanRequestProxy(mContext, this, mWifiConfigManager,
-                mWifiPermissionsUtil);
+        mScanRequestProxy = new ScanRequestProxy(mContext,
+                (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE),
+                (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE),
+                this, mWifiConfigManager,
+                mWifiPermissionsUtil, mClock);
         // mWifiStateMachine has an implicit dependency on mJavaRuntime due to WifiDiagnostics.
         mJavaRuntime = Runtime.getRuntime();
         mWifiStateMachine = new WifiStateMachine(mContext, mFrameworkFacade,
@@ -450,6 +454,17 @@
     }
 
     /**
+     * Create a ClientModeManager
+     *
+     * @param listener listener for ClientModeManager state changes
+     * @return a new instance of ClientModeManager
+     */
+    public ClientModeManager makeClientModeManager(ClientModeManager.Listener listener) {
+        return new ClientModeManager(mContext, mWifiStateMachineHandlerThread.getLooper(),
+                mWifiNative, listener, mWifiMetrics, mScanRequestProxy);
+    }
+
+    /**
      * Create a WifiLog instance.
      * @param tag module name to include in all log messages
      */
diff --git a/service/java/com/android/server/wifi/WifiNative.java b/service/java/com/android/server/wifi/WifiNative.java
index d89d6e8..99ae01f 100644
--- a/service/java/com/android/server/wifi/WifiNative.java
+++ b/service/java/com/android/server/wifi/WifiNative.java
@@ -452,15 +452,15 @@
         }
     }
 
-    /** Helper method invoked to cleanup state after one of the native daemon's death. */
+    /**
+     * Helper method invoked to trigger the status changed callback after one of the native
+     * daemon's death.
+     */
     private void onNativeDaemonDeath() {
         synchronized (mLock) {
-            Log.i(TAG, "One of the daemons died. Tearing down everything");
-            teardownAllInterfaces();
             for (StatusListener listener : mStatusListeners) {
                 listener.onStatusChanged(false);
             }
-            // TODO(70572148): Do we need to wait to mark the system ready again?
             for (StatusListener listener : mStatusListeners) {
                 listener.onStatusChanged(true);
             }
diff --git a/service/java/com/android/server/wifi/WifiServiceImpl.java b/service/java/com/android/server/wifi/WifiServiceImpl.java
index 9d292e0..a88b20d 100644
--- a/service/java/com/android/server/wifi/WifiServiceImpl.java
+++ b/service/java/com/android/server/wifi/WifiServiceImpl.java
@@ -92,8 +92,6 @@
 import android.os.UserManager;
 import android.os.WorkSource;
 import android.provider.Settings;
-import android.util.ArrayMap;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.MutableInt;
 import android.util.Slog;
@@ -159,14 +157,12 @@
     private final Context mContext;
     private final FrameworkFacade mFacade;
     private final Clock mClock;
-    private final ArraySet<String> mBackgroundThrottlePackageWhitelist = new ArraySet<>();
 
     private final PowerManager mPowerManager;
     private final AppOpsManager mAppOps;
     private final UserManager mUserManager;
     private final ActivityManager mActivityManager;
     private final WifiCountryCode mCountryCode;
-    private long mBackgroundThrottleInterval;
     // Debug counter tracking scan requests sent by WifiManager
     private int scanRequestCounter = 0;
 
@@ -181,9 +177,6 @@
     /* Backup/Restore Module */
     private final WifiBackupRestore mWifiBackupRestore;
 
-    // Map of package name of background scan apps and last scan timestamp.
-    private final ArrayMap<String, Long> mLastScanTimestamps;
-
     private WifiLog mLog;
 
     /**
@@ -482,9 +475,6 @@
         mWifiPermissionsUtil = mWifiInjector.getWifiPermissionsUtil();
         mLog = mWifiInjector.makeLog(TAG);
         mFrameworkFacade = wifiInjector.getFrameworkFacade();
-        mLastScanTimestamps = new ArrayMap<>();
-        updateBackgroundThrottleInterval();
-        updateBackgroundThrottlingWhitelist();
         mIfaceIpModes = new ConcurrentHashMap<>();
         mLocalOnlyHotspotRequests = new HashMap<>();
         enableVerboseLoggingInternal(getVerboseLoggingLevel());
@@ -524,7 +514,6 @@
                 (wifiEnabled ? "enabled" : "disabled"));
 
         registerForScanModeChange();
-        registerForBackgroundThrottleChanges();
         mContext.registerReceiver(
                 new BroadcastReceiver() {
                     @Override
@@ -629,18 +618,6 @@
 
         int callingUid = Binder.getCallingUid();
         mLog.info("startScan uid=%").c(callingUid).flush();
-        // Check and throttle background apps for wifi scan.
-        if (isRequestFromBackground(packageName)) {
-            long lastScanMs = mLastScanTimestamps.getOrDefault(packageName, 0L);
-            long elapsedRealtime = mClock.getElapsedSinceBootMillis();
-
-            if (lastScanMs != 0 && (elapsedRealtime - lastScanMs) < mBackgroundThrottleInterval) {
-                sendFailedScanBroadcast();
-                return;
-            }
-            // Proceed with the scan request and record the time.
-            mLastScanTimestamps.put(packageName, elapsedRealtime);
-        }
         synchronized (this) {
             if (mInIdleMode) {
                 // Need to send an immediate scan result broadcast in case the
@@ -656,7 +633,7 @@
             }
         }
         boolean success = mWifiInjector.getWifiStateMachineHandler().runWithScissors(() -> {
-            if (!mScanRequestProxy.startScan(callingUid)) {
+            if (!mScanRequestProxy.startScan(callingUid, packageName)) {
                 Log.e(TAG, "Failed to start scan");
             }
         }, RUN_WITH_SCISSORS_TIMEOUT_MILLIS);
@@ -683,29 +660,6 @@
 
     }
 
-    // Check if the request comes from background.
-    private boolean isRequestFromBackground(String packageName) {
-        // Requests from system or wifi are not background.
-        if (Binder.getCallingUid() == Process.SYSTEM_UID
-                || Binder.getCallingUid() == Process.WIFI_UID) {
-            return false;
-        }
-        mAppOps.checkPackage(Binder.getCallingUid(), packageName);
-        if (mBackgroundThrottlePackageWhitelist.contains(packageName)) {
-            return false;
-        }
-
-        // getPackageImportance requires PACKAGE_USAGE_STATS permission, so clearing the incoming
-        // identify so the permission check can be done on system process where wifi runs in.
-        long callingIdentity = Binder.clearCallingIdentity();
-        try {
-            return mActivityManager.getPackageImportance(packageName)
-                    > BACKGROUND_IMPORTANCE_CUTOFF;
-        } finally {
-            Binder.restoreCallingIdentity(callingIdentity);
-        }
-    }
-
     /**
      * WPS support in Client mode is deprecated.  Return null.
      */
@@ -2414,51 +2368,6 @@
 
     }
 
-    // Monitors settings changes related to background wifi scan throttling.
-    private void registerForBackgroundThrottleChanges() {
-        mFrameworkFacade.registerContentObserver(
-                mContext,
-                Settings.Global.getUriFor(
-                        Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_INTERVAL_MS),
-                false,
-                new ContentObserver(null) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateBackgroundThrottleInterval();
-                    }
-                }
-        );
-        mFrameworkFacade.registerContentObserver(
-                mContext,
-                Settings.Global.getUriFor(
-                        Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_PACKAGE_WHITELIST),
-                false,
-                new ContentObserver(null) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateBackgroundThrottlingWhitelist();
-                    }
-                }
-        );
-    }
-
-    private void updateBackgroundThrottleInterval() {
-        mBackgroundThrottleInterval = mFrameworkFacade.getLongSetting(
-                mContext,
-                Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_INTERVAL_MS,
-                DEFAULT_SCAN_BACKGROUND_THROTTLE_INTERVAL_MS);
-    }
-
-    private void updateBackgroundThrottlingWhitelist() {
-        String setting = mFrameworkFacade.getStringSetting(
-                mContext,
-                Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_PACKAGE_WHITELIST);
-        mBackgroundThrottlePackageWhitelist.clear();
-        if (setting != null) {
-            mBackgroundThrottlePackageWhitelist.addAll(Arrays.asList(setting.split(",")));
-        }
-    }
-
     private void registerForBroadcasts() {
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_USER_PRESENT);
diff --git a/service/java/com/android/server/wifi/WifiStateMachine.java b/service/java/com/android/server/wifi/WifiStateMachine.java
index e690ef9..bdc1a60 100644
--- a/service/java/com/android/server/wifi/WifiStateMachine.java
+++ b/service/java/com/android/server/wifi/WifiStateMachine.java
@@ -2226,9 +2226,6 @@
                 config = mWifiConfigManager.getConfiguredNetwork(msg.arg1);
                 if (config != null) {
                     sb.append(" ").append(config.configKey());
-                    if (config.visibility != null) {
-                        sb.append(" ").append(config.visibility.toString());
-                    }
                 }
                 if (mTargetRoamBSSID != null) {
                     sb.append(" ").append(mTargetRoamBSSID);
@@ -2237,9 +2234,6 @@
                 config = getCurrentWifiConfiguration();
                 if (config != null) {
                     sb.append(config.configKey());
-                    if (config.visibility != null) {
-                        sb.append(" ").append(config.visibility.toString());
-                    }
                 }
                 break;
             case CMD_START_ROAM:
diff --git a/service/java/com/android/server/wifi/WifiStateMachinePrime.java b/service/java/com/android/server/wifi/WifiStateMachinePrime.java
index 6baf5ad..8eb521c 100644
--- a/service/java/com/android/server/wifi/WifiStateMachinePrime.java
+++ b/service/java/com/android/server/wifi/WifiStateMachinePrime.java
@@ -80,6 +80,17 @@
     // ScanOnly mode failed
     static final int CMD_SCAN_ONLY_MODE_FAILED                          = BASE + 204;
 
+    // Start Client mode
+    static final int CMD_START_CLIENT_MODE                              = BASE + 300;
+    // Indicates that start client mode failed
+    static final int CMD_START_CLIENT_MODE_FAILURE                      = BASE + 301;
+    // Indicates that client mode stopped
+    static final int CMD_STOP_CLIENT_MODE                               = BASE + 302;
+    // Client mode teardown is complete
+    static final int CMD_CLIENT_MODE_STOPPED                            = BASE + 303;
+    // Client mode failed
+    static final int CMD_CLIENT_MODE_FAILED                             = BASE + 304;
+
     private WifiManager.SoftApCallback mSoftApCallback;
 
     /**
@@ -259,10 +270,30 @@
         }
 
         class ClientModeActiveState extends ModeActiveState {
+            private class ClientListener implements ClientModeManager.Listener {
+                @Override
+                public void onStateChanged(int state) {
+                    Log.d(TAG, "State changed from client mode.");
+                    if (state == WifiManager.WIFI_STATE_UNKNOWN) {
+                        // error while setting up client mode or an unexpected failure.
+                        mModeStateMachine.sendMessage(CMD_CLIENT_MODE_FAILED);
+                    } else if (state == WifiManager.WIFI_STATE_DISABLED) {
+                        // client mode stopped
+                        mModeStateMachine.sendMessage(CMD_CLIENT_MODE_STOPPED);
+                    } else if (state == WifiManager.WIFI_STATE_ENABLED) {
+                        // client mode is ready to go
+                        Log.d(TAG, "client mode active");
+                    } else {
+                        // only care if client mode stopped or started, dropping
+                    }
+                }
+            }
+
             @Override
             public void enter() {
                 Log.d(TAG, "Entering ClientModeActiveState");
-                mManager = new ClientModeManager();
+
+                mManager = mWifiInjector.makeClientModeManager(new ClientListener());
                 // DO NOT CALL START YET
                 // mActiveModemanager.start();
                 mActiveModeManagers.add(mManager);
diff --git a/tests/wifitests/src/com/android/server/wifi/CarrierNetworkConfigTest.java b/tests/wifitests/src/com/android/server/wifi/CarrierNetworkConfigTest.java
index b06c8a4..e8eb6ce 100644
--- a/tests/wifitests/src/com/android/server/wifi/CarrierNetworkConfigTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/CarrierNetworkConfigTest.java
@@ -124,21 +124,32 @@
     }
 
     /**
-     * Verify that {@link CarrierNetworkConfig#isCarrierNetwork} will return false when the given
-     * SSID is associated with a carrier network, but IMSI encryption info is not available.
+     * Verify that {@link CarrierNetworkConfig#isCarrierEncryptionInfoAvailable} will return true
+     * when the carrier IMSI encryption info is available.
      *
      * @throws Exception
      */
     @Test
-    public void getExistingCarrierNetworkInfoWhenEncryptionInfoNotAvailable() throws Exception {
-        when(mCarrierConfigManager.getConfigForSubId(TEST_SUBSCRIPTION_ID))
-                .thenReturn(generateTestConfig(TEST_SSID, TEST_STANDARD_EAP_TYPE));
+    public void verifyIsCarrierEncryptionInfoAvailableReturnsTrueWhenEncryptionInfoIsAvailable()
+            throws Exception {
+        assertTrue(mCarrierNetworkConfig.isCarrierEncryptionInfoAvailable());
+    }
+
+    /**
+     * Verify that {@link CarrierNetworkConfig#isCarrierEncryptionInfoAvailable} will return false
+     * when the carrier IMSI encryption info is not available.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void verifyIsCarrierEncryptionInfoAvailableReturnsFalseWhenEncryptionInfoNotAvailable()
+            throws Exception {
         when(mTelephonyManager.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN))
                 .thenReturn(null);
         mBroadcastReceiver.onReceive(mContext,
                 new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
 
-        assertFalse(mCarrierNetworkConfig.isCarrierNetwork(TEST_SSID));
+        assertFalse(mCarrierNetworkConfig.isCarrierEncryptionInfoAvailable());
     }
 
     /**
diff --git a/tests/wifitests/src/com/android/server/wifi/ClientModeManagerTest.java b/tests/wifitests/src/com/android/server/wifi/ClientModeManagerTest.java
new file mode 100644
index 0000000..942ff1c
--- /dev/null
+++ b/tests/wifitests/src/com/android/server/wifi/ClientModeManagerTest.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2018 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 android.net.wifi.WifiManager.EXTRA_PREVIOUS_WIFI_STATE;
+import static android.net.wifi.WifiManager.EXTRA_SCAN_AVAILABLE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
+import static android.net.wifi.WifiManager.WIFI_SCAN_AVAILABLE;
+import static android.net.wifi.WifiManager.WIFI_STATE_CHANGED_ACTION;
+import static android.net.wifi.WifiManager.WIFI_STATE_DISABLED;
+import static android.net.wifi.WifiManager.WIFI_STATE_DISABLING;
+import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
+import static android.net.wifi.WifiManager.WIFI_STATE_ENABLING;
+import static android.net.wifi.WifiManager.WIFI_STATE_UNKNOWN;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.os.test.TestLooper;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link ClientModeManager}.
+ */
+@SmallTest
+public class ClientModeManagerTest {
+    private static final String TAG = "ClientModeManagerTest";
+    private static final String TEST_INTERFACE_NAME = "testif0";
+    private static final String OTHER_INTERFACE_NAME = "notTestIf";
+
+    TestLooper mLooper;
+
+    ClientModeManager mClientModeManager;
+
+    @Mock Context mContext;
+    @Mock WifiMetrics mWifiMetrics;
+    @Mock WifiNative mWifiNative;
+    @Mock ClientModeManager.Listener mListener;
+    @Mock WifiMonitor mWifiMonitor;
+    @Mock ScanRequestProxy mScanRequestProxy;
+
+    final ArgumentCaptor<WifiNative.StatusListener> mStatusListenerCaptor =
+            ArgumentCaptor.forClass(WifiNative.StatusListener.class);
+    final ArgumentCaptor<WifiNative.InterfaceCallback> mInterfaceCallbackCaptor =
+            ArgumentCaptor.forClass(WifiNative.InterfaceCallback.class);
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mLooper = new TestLooper();
+
+        mClientModeManager = createClientModeManager();
+        mLooper.dispatchAll();
+    }
+
+    private ClientModeManager createClientModeManager() {
+        return new ClientModeManager(mContext, mLooper.getLooper(), mWifiNative, mListener,
+                mWifiMetrics, mScanRequestProxy);
+    }
+
+    private void startClientModeAndVerifyEnabled() throws Exception {
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        when(mWifiNative.setupInterfaceForClientMode(eq(false), any()))
+                .thenReturn(TEST_INTERFACE_NAME);
+        mClientModeManager.start();
+        mLooper.dispatchAll();
+
+        verify(mWifiNative).registerStatusListener(mStatusListenerCaptor.capture());
+        verify(mWifiNative).setupInterfaceForClientMode(
+                eq(false), mInterfaceCallbackCaptor.capture());
+
+        // now mark the interface as up
+        mInterfaceCallbackCaptor.getValue().onUp(TEST_INTERFACE_NAME);
+        mLooper.dispatchAll();
+
+        verify(mContext, atLeastOnce()).sendStickyBroadcastAsUser(intentCaptor.capture(),
+                eq(UserHandle.ALL));
+
+        List<Intent> intents = intentCaptor.getAllValues();
+        assertEquals(3, intents.size());
+        Log.d(TAG, "captured intents: " + intents);
+        checkWifiStateChangedBroadcast(intents.get(0), WIFI_STATE_ENABLING, WIFI_STATE_DISABLED);
+        checkWifiScanStateChangedBroadcast(intents.get(1), WIFI_STATE_ENABLED);
+        checkWifiStateChangedBroadcast(intents.get(2), WIFI_STATE_ENABLED, WIFI_STATE_ENABLING);
+
+        checkWifiStateChangeListenerUpdate(WIFI_STATE_ENABLED);
+        verify(mScanRequestProxy, atLeastOnce()).enableScanningForHiddenNetworks(true);
+    }
+
+    private void checkWifiScanStateChangedBroadcast(Intent intent, int expectedCurrentState) {
+        String action = intent.getAction();
+        assertEquals(WIFI_SCAN_AVAILABLE, action);
+        int currentState = intent.getIntExtra(EXTRA_SCAN_AVAILABLE, WIFI_STATE_UNKNOWN);
+        assertEquals(expectedCurrentState, currentState);
+    }
+
+    private void checkWifiStateChangedBroadcast(
+            Intent intent, int expectedCurrentState, int expectedPrevState) {
+        String action = intent.getAction();
+        assertEquals(WIFI_STATE_CHANGED_ACTION, action);
+        int currentState = intent.getIntExtra(EXTRA_WIFI_STATE, WIFI_STATE_UNKNOWN);
+        assertEquals(expectedCurrentState, currentState);
+        int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_STATE, WIFI_STATE_UNKNOWN);
+        assertEquals(expectedPrevState, prevState);
+    }
+
+
+    private void checkWifiStateChangeListenerUpdate(int expectedCurrentState) {
+        verify(mListener).onStateChanged(eq(expectedCurrentState));
+    }
+
+    private void verifyNotificationsForCleanShutdown(int fromState) {
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, atLeastOnce())
+                .sendStickyBroadcastAsUser(intentCaptor.capture(), eq(UserHandle.ALL));
+
+        List<Intent> intents = intentCaptor.getAllValues();
+        assertEquals(3, intents.size());
+        checkWifiStateChangedBroadcast(intents.get(0), WIFI_STATE_DISABLING, fromState);
+        checkWifiScanStateChangedBroadcast(intents.get(1), WIFI_STATE_DISABLED);
+        checkWifiStateChangedBroadcast(intents.get(2), WIFI_STATE_DISABLED, WIFI_STATE_DISABLING);
+        verify(mScanRequestProxy).enableScanningForHiddenNetworks(false);
+        verify(mScanRequestProxy).clearScanResults();
+    }
+
+    private void verifyNotificationsForFailure() {
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, atLeastOnce())
+                .sendStickyBroadcastAsUser(intentCaptor.capture(), eq(UserHandle.ALL));
+
+        List<Intent> intents = intentCaptor.getAllValues();
+        assertEquals(3, intents.size());
+        checkWifiStateChangedBroadcast(intents.get(0), WIFI_STATE_DISABLING, WIFI_STATE_ENABLED);
+        checkWifiScanStateChangedBroadcast(intents.get(1), WIFI_STATE_DISABLED);
+        checkWifiStateChangedBroadcast(intents.get(2), WIFI_STATE_DISABLED, WIFI_STATE_DISABLING);
+        checkWifiStateChangeListenerUpdate(WIFI_STATE_DISABLED);
+        verify(mScanRequestProxy).enableScanningForHiddenNetworks(false);
+        verify(mScanRequestProxy).clearScanResults();
+    }
+
+    /**
+     * ClientMode start sets up an interface in ClientMode.
+     */
+    @Test
+    public void clientModeStartCreatesClientInterface() throws Exception {
+        startClientModeAndVerifyEnabled();
+    }
+
+    /**
+     * ClientMode increments failure metrics when failing to setup client mode.
+     */
+    @Test
+    public void detectAndReportErrorWhenSetupForClientWifiNativeFailure() throws Exception {
+        when(mWifiNative.setupInterfaceForClientMode(eq(false), any())).thenReturn(null);
+        mClientModeManager.start();
+        mLooper.dispatchAll();
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, atLeastOnce()).sendStickyBroadcastAsUser(intentCaptor.capture(),
+                eq(UserHandle.ALL));
+        checkWifiScanStateChangedBroadcast(intentCaptor.getValue(), WIFI_STATE_DISABLED);
+        checkWifiStateChangeListenerUpdate(WIFI_STATE_UNKNOWN);
+    }
+
+    /**
+     * ClientMode start does not indicate scanning is available when the interface name is empty.
+     */
+    @Test
+    public void clientModeStartDoesNotSendScanningActiveWhenClientInterfaceNameIsEmpty()
+            throws Exception {
+        when(mWifiNative.setupInterfaceForClientMode(eq(false), any())).thenReturn("");
+        mClientModeManager.start();
+        mLooper.dispatchAll();
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, atLeastOnce()).sendStickyBroadcastAsUser(intentCaptor.capture(),
+                eq(UserHandle.ALL));
+
+        List<Intent> intents = intentCaptor.getAllValues();
+        assertEquals(2, intents.size());
+        checkWifiStateChangedBroadcast(intents.get(0), WIFI_STATE_ENABLING, WIFI_STATE_DISABLED);
+        checkWifiScanStateChangedBroadcast(intents.get(1), WIFI_STATE_DISABLED);
+        checkWifiStateChangeListenerUpdate(WIFI_STATE_UNKNOWN);
+    }
+
+    /**
+     * Calling ClientModeManager.start twice does not crash or restart client mode.
+     */
+    @Test
+    public void clientModeStartCalledTwice() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mWifiNative, mContext);
+        mClientModeManager.start();
+        mLooper.dispatchAll();
+        verifyNoMoreInteractions(mWifiNative, mContext);
+    }
+
+    /**
+     * ClientMode stop properly cleans up state
+     */
+    @Test
+    public void clientModeStopCleansUpState() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mContext);
+        mClientModeManager.stop();
+        mLooper.dispatchAll();
+
+        verifyNotificationsForCleanShutdown(WIFI_STATE_ENABLED);
+    }
+
+    /**
+     * Calling stop when ClientMode is not started should not send scan state updates
+     */
+    @Test
+    public void clientModeStopWhenNotStartedDoesNotUpdateScanStateUpdates() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mContext);
+        mClientModeManager.stop();
+        mLooper.dispatchAll();
+        verifyNotificationsForCleanShutdown(WIFI_STATE_ENABLED);
+
+        reset(mContext, mListener);
+        // now call stop again
+        mClientModeManager.stop();
+        mLooper.dispatchAll();
+        verify(mContext, never()).sendStickyBroadcastAsUser(any(), any());
+        verify(mListener, never()).onStateChanged(anyInt());
+    }
+
+    /**
+     * Triggering interface down when ClientMode is active properly exits the active state.
+     */
+    @Test
+    public void clientModeStartedStopsWhenInterfaceDown() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mContext, mScanRequestProxy);
+        mInterfaceCallbackCaptor.getValue().onDown(TEST_INTERFACE_NAME);
+        mLooper.dispatchAll();
+        verifyNotificationsForFailure();
+    }
+
+    /**
+     * Testing the handling of a wifinative failure status change notification.
+     */
+    @Test
+    public void clientModeStartedStopsOnNativeFailure() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mContext, mScanRequestProxy, mListener);
+        mStatusListenerCaptor.getValue().onStatusChanged(false);
+        mLooper.dispatchNext();
+
+        checkWifiStateChangeListenerUpdate(WIFI_STATE_UNKNOWN);
+
+        mLooper.dispatchAll();
+
+        verifyNotificationsForFailure();
+    }
+
+    /**
+     * Testing that handling of a wifinative callback that is not a failuer does not stop client
+     * mode.
+     */
+    @Test
+    public void clientModeStartedAndStaysUpOnNativeNonFailureCallback() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mContext, mScanRequestProxy, mListener);
+        mStatusListenerCaptor.getValue().onStatusChanged(true);
+        mLooper.dispatchAll();
+
+        verify(mListener, never()).onStateChanged(eq(WIFI_STATE_UNKNOWN));
+        verify(mListener, never()).onStateChanged(eq(WIFI_STATE_DISABLING));
+        verify(mListener, never()).onStateChanged(eq(WIFI_STATE_DISABLED));
+    }
+
+
+    /**
+     * Testing the handling of an interface destroyed notification.
+     */
+    @Test
+    public void clientModeStartedStopsOnInterfaceDestroyed() throws Exception {
+        startClientModeAndVerifyEnabled();
+        reset(mContext, mScanRequestProxy, mListener);
+
+        mInterfaceCallbackCaptor.getValue().onDestroyed(TEST_INTERFACE_NAME);
+        mLooper.dispatchAll();
+        verifyNotificationsForCleanShutdown(WIFI_STATE_ENABLED);
+    }
+}
diff --git a/tests/wifitests/src/com/android/server/wifi/ScanRequestProxyTest.java b/tests/wifitests/src/com/android/server/wifi/ScanRequestProxyTest.java
index 759fbe2..7f684bc 100644
--- a/tests/wifitests/src/com/android/server/wifi/ScanRequestProxyTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/ScanRequestProxyTest.java
@@ -16,9 +16,13 @@
 
 package com.android.server.wifi;
 
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+
 import static org.junit.Assert.*;
 import static org.mockito.Mockito.*;
 
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.Intent;
 import android.net.wifi.ScanResult;
@@ -47,6 +51,8 @@
 @SmallTest
 public class ScanRequestProxyTest {
     private static final int TEST_UID = 5;
+    private static final String TEST_PACKAGE_NAME_1 = "com.test.1";
+    private static final String TEST_PACKAGE_NAME_2 = "com.test.2";
     private static final List<WifiScanner.ScanSettings.HiddenNetwork> TEST_HIDDEN_NETWORKS_LIST =
             new ArrayList<WifiScanner.ScanSettings.HiddenNetwork>() {{
                 add(new WifiScanner.ScanSettings.HiddenNetwork("test_ssid_1"));
@@ -55,10 +61,13 @@
             }};
 
     @Mock private Context mContext;
+    @Mock private AppOpsManager mAppOps;
+    @Mock private ActivityManager mActivityManager;
     @Mock private WifiInjector mWifiInjector;
     @Mock private WifiConfigManager mWifiConfigManager;
     @Mock private WifiScanner mWifiScanner;
     @Mock private WifiPermissionsUtil mWifiPermissionsUtil;
+    @Mock private Clock mClock;
     private ArgumentCaptor<WorkSource> mWorkSourceArgumentCaptor =
             ArgumentCaptor.forClass(WorkSource.class);
     private ArgumentCaptor<WifiScanner.ScanSettings> mScanSettingsArgumentCaptor =
@@ -87,7 +96,8 @@
         mTestScanDatas2 = ScanTestUtil.createScanDatas(new int[][]{ { 2412, 2422, 5200, 5210 } });
 
         mScanRequestProxy =
-            new ScanRequestProxy(mContext, mWifiInjector, mWifiConfigManager, mWifiPermissionsUtil);
+            new ScanRequestProxy(mContext, mAppOps, mActivityManager, mWifiInjector,
+                    mWifiConfigManager, mWifiPermissionsUtil, mClock);
     }
 
     @After
@@ -101,8 +111,8 @@
     @Test
     public void testStartScanFailWithoutScanner() {
         when(mWifiInjector.getWifiScanner()).thenReturn(null);
-        assertFalse(mScanRequestProxy.startScan(TEST_UID));
-        validateScanResultsAvailableBroadcastSent(false);
+        assertFalse(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        validateScanResultsFailureBroadcastSent(TEST_PACKAGE_NAME_1);
 
         verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
     }
@@ -112,7 +122,7 @@
      */
     @Test
     public void testStartScanSuccess() {
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
 
         assertTrue(mWorkSourceArgumentCaptor.getValue().equals(new WorkSource(TEST_UID)));
@@ -127,7 +137,7 @@
     @Test
     public void testStartScanSuccessFromAppWithNetworkSettings() {
         when(mWifiPermissionsUtil.checkNetworkSettingsPermission(TEST_UID)).thenReturn(true);
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
 
         assertTrue(mWorkSourceArgumentCaptor.getValue().equals(new WorkSource(TEST_UID)));
@@ -143,7 +153,7 @@
     @Test
     public void testStartScanWithHiddenNetworkScanningDisabled() {
         mScanRequestProxy.enableScanningForHiddenNetworks(false);
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiConfigManager, never()).retrieveHiddenNetworkList();
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
 
@@ -159,7 +169,7 @@
     @Test
     public void testStartScanWithHiddenNetworkScanningEnabled() {
         mScanRequestProxy.enableScanningForHiddenNetworks(true);
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiConfigManager).retrieveHiddenNetworkList();
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
 
@@ -213,7 +223,7 @@
     @Test
     public void testScanSuccessOverwritesPreviousResults() {
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Verify the scan results processing for request 1.
         mScanListenerArgumentCaptor.getValue().onResults(mTestScanDatas1);
@@ -224,7 +234,7 @@
                 mScanRequestProxy.getScanResults().stream().toArray(ScanResult[]::new));
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Verify the scan results processing for request 2.
         mScanListenerArgumentCaptor.getValue().onResults(mTestScanDatas2);
@@ -243,7 +253,7 @@
     @Test
     public void testScanFailureDoesNotOverwritePreviousResults() {
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Verify the scan results processing for request 1.
         mScanListenerArgumentCaptor.getValue().onResults(mTestScanDatas1);
@@ -254,7 +264,7 @@
                 mScanRequestProxy.getScanResults().stream().toArray(ScanResult[]::new));
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Verify the scan failure processing.
         mScanListenerArgumentCaptor.getValue().onFailure(0, "failed");
@@ -277,12 +287,12 @@
         WifiScanner.ScanListener listener1;
         WifiScanner.ScanListener listener2;
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         listener1 = mScanListenerArgumentCaptor.getValue();
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         // Ensure that we did send a second scan request to scanner.
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         listener2 = mScanListenerArgumentCaptor.getValue();
@@ -314,7 +324,7 @@
     @Test
     public void testNewScanRequestAfterPreviousScanSucceeds() {
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Now send the scan results for request 1.
         mScanListenerArgumentCaptor.getValue().onResults(mTestScanDatas1);
@@ -325,7 +335,7 @@
                 mScanRequestProxy.getScanResults().stream().toArray(ScanResult[]::new));
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         // Ensure that we did send a second scan request to scanner.
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Now send the scan results for request 2.
@@ -347,7 +357,7 @@
     @Test
     public void testNewScanRequestAfterPreviousScanSucceedsWithInvalidScanDatas() {
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
 
         // Now send scan success for request 1, but with invalid scan datas.
@@ -358,7 +368,7 @@
         assertTrue(mScanRequestProxy.getScanResults().isEmpty());
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         // Ensure that we did send a second scan request to scanner.
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Now send the scan results for request 2.
@@ -380,7 +390,7 @@
     @Test
     public void testNewScanRequestAfterPreviousScanFailure() {
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
 
         // Now send scan failure for request 1.
@@ -390,7 +400,7 @@
         assertTrue(mScanRequestProxy.getScanResults().isEmpty());
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         // Ensure that we did send a second scan request to scanner.
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Now send the scan results for request 2.
@@ -410,7 +420,7 @@
     @Test
     public void testClearScanResults() {
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         // Verify the scan results processing for request 1.
         mScanListenerArgumentCaptor.getValue().onResults(mTestScanDatas1);
@@ -433,12 +443,12 @@
         WifiScanner.ScanListener listener1;
         WifiScanner.ScanListener listener2;
         // Make scan request 1.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         listener1 = mScanListenerArgumentCaptor.getValue();
 
         // Make scan request 2.
-        assertTrue(mScanRequestProxy.startScan(TEST_UID));
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
         // Ensure that we did send a second scan request to scanner.
         mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
         listener2 = mScanListenerArgumentCaptor.getValue();
@@ -448,6 +458,147 @@
         verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
     }
 
+    /**
+     * Ensure new scan requests from the same app are rejected if it's before
+     * {@link ScanRequestProxy#SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS}
+     */
+    @Test
+    public void testSuccessiveScanRequestFromSameAppThrottled() {
+        long firstRequestMs = 782;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(firstRequestMs);
+        // Make scan request 1.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        long secondRequestMs =
+                firstRequestMs + ScanRequestProxy.SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS - 1;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(secondRequestMs);
+        // Make scan request 2 from the same package name & ensure that it is throttled.
+        assertFalse(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        validateScanResultsFailureBroadcastSent(TEST_PACKAGE_NAME_1);
+
+        verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
+    }
+
+    /**
+     * Ensure new scan requests from the same app are not rejected if it's after
+     * {@link ScanRequestProxy#SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS}
+     */
+    @Test
+    public void testSuccessiveScanRequestFromSameAppNotThrottled() {
+        long firstRequestMs = 782;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(firstRequestMs);
+        // Make scan request 1.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        long secondRequestMs =
+                firstRequestMs + ScanRequestProxy.SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS + 1;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(secondRequestMs);
+        // Make scan request 2 from the same package name & ensure that it is not throttled.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
+    }
+
+    /**
+     * Ensure new scan requests from the same app with NETWORK_SETTINGS permission are not
+     * throttled.
+     */
+    @Test
+    public void testSuccessiveScanRequestFromSameAppWithNetworkSettingsPermissionNotThrottled() {
+        when(mWifiPermissionsUtil.checkNetworkSettingsPermission(TEST_UID)).thenReturn(true);
+
+        long firstRequestMs = 782;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(firstRequestMs);
+        // Make scan request 1.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        long secondRequestMs =
+                firstRequestMs + ScanRequestProxy.SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS - 1;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(secondRequestMs);
+        // Make scan request 2 from the same package name & ensure that it is not throttled.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
+    }
+
+    /**
+     * Ensure new scan requests from different apps are not throttled.
+     */
+    @Test
+    public void testSuccessiveScanRequestFromDifferentAppsNotThrottled() {
+        long firstRequestMs = 782;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(firstRequestMs);
+        // Make scan request 1.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        // Make scan request 2 from the same package name & ensure that it is throttled.
+        assertFalse(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        validateScanResultsFailureBroadcastSent(TEST_PACKAGE_NAME_1);
+
+        // Make scan request 3 from a different package name & ensure that it is not throttled.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
+    }
+
+    /**
+     * Ensure scan requests from different background apps are throttled if it's before
+     * {@link ScanRequestProxy#SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
+     */
+    @Test
+    public void testSuccessiveScanRequestFromBgAppsThrottled() {
+        when(mActivityManager.getPackageImportance(TEST_PACKAGE_NAME_1))
+                .thenReturn(IMPORTANCE_FOREGROUND_SERVICE + 1);
+        when(mActivityManager.getPackageImportance(TEST_PACKAGE_NAME_2))
+                .thenReturn(IMPORTANCE_FOREGROUND_SERVICE + 1);
+
+        long firstRequestMs = 782;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(firstRequestMs);
+        // Make scan request 1.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        // Make scan request 2 from the different package name & ensure that it is throttled.
+        assertFalse(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
+        validateScanResultsFailureBroadcastSent(TEST_PACKAGE_NAME_2);
+
+        verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
+    }
+
+    /**
+     * Ensure scan requests from different background apps are not throttled if it's after
+     * {@link ScanRequestProxy#SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
+     */
+    @Test
+    public void testSuccessiveScanRequestFromBgAppsNotThrottled() {
+        when(mActivityManager.getPackageImportance(TEST_PACKAGE_NAME_1))
+                .thenReturn(ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + 1);
+        when(mActivityManager.getPackageImportance(TEST_PACKAGE_NAME_2))
+                .thenReturn(ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + 1);
+
+        long firstRequestMs = 782;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(firstRequestMs);
+        // Make scan request 1.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_1));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        long secondRequestMs =
+                firstRequestMs + ScanRequestProxy.SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS + 1;
+        when(mClock.getElapsedSinceBootMillis()).thenReturn(secondRequestMs);
+        // Make scan request 2 from the different package name & ensure that it is throttled.
+        assertTrue(mScanRequestProxy.startScan(TEST_UID, TEST_PACKAGE_NAME_2));
+        mInOrder.verify(mWifiScanner).startScan(any(), any(), any());
+
+        verifyNoMoreInteractions(mWifiScanner, mWifiConfigManager, mContext);
+    }
+
     private void validateScanSettings(WifiScanner.ScanSettings scanSettings,
                                       boolean expectHiddenNetworks) {
         validateScanSettings(scanSettings, expectHiddenNetworks, false);
@@ -502,4 +653,21 @@
         boolean scanSucceeded = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
         assertEquals(expectScanSuceeded, scanSucceeded);
     }
+
+    private void validateScanResultsFailureBroadcastSent(String expectedPackageName) {
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        ArgumentCaptor<UserHandle> userHandleCaptor = ArgumentCaptor.forClass(UserHandle.class);
+        mInOrder.verify(mContext).sendBroadcastAsUser(
+                intentCaptor.capture(), userHandleCaptor.capture());
+
+        assertEquals(userHandleCaptor.getValue(), UserHandle.ALL);
+
+        Intent intent = intentCaptor.getValue();
+        assertEquals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, intent.getAction());
+        assertEquals(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT, intent.getFlags());
+        boolean scanSucceeded = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
+        assertFalse(scanSucceeded);
+        String packageName = intent.getPackage();
+        assertEquals(expectedPackageName, packageName);
+    }
 }
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiConnectivityManagerTest.java b/tests/wifitests/src/com/android/server/wifi/WifiConnectivityManagerTest.java
index 490d34b..8b6c2fb 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiConnectivityManagerTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiConnectivityManagerTest.java
@@ -97,6 +97,7 @@
                 R.integer.config_wifi_framework_max_tx_rate_for_full_scan);
         mFullScanMaxRxPacketRate = mResource.getInteger(
                 R.integer.config_wifi_framework_max_rx_rate_for_full_scan);
+        when(mCarrierNetworkConfig.isCarrierEncryptionInfoAvailable()).thenReturn(true);
     }
 
     /**
@@ -764,6 +765,36 @@
     }
 
     /**
+     * {@link CarrierNetworkNotifier} does not handle scan results on network selection if carrier
+     * encryption info is not available.
+     *
+     * Expected behavior: CarrierNetworkNotifier does not handle scan results
+     */
+    @Test
+    public void whenNoEncryptionInfoAvailable_CarrierNetworkNotifierDoesNotHandleScanResults() {
+        // no connection candidate selected
+        when(mWifiNS.selectNetwork(anyObject(), anyObject(), anyObject(), anyBoolean(),
+                anyBoolean(), anyBoolean())).thenReturn(null);
+
+        List<ScanDetail> expectedCarrierNetworks = new ArrayList<>();
+        expectedCarrierNetworks.add(
+                new ScanDetail(
+                        new ScanResult(WifiSsid.createFromAsciiEncoded(CANDIDATE_SSID),
+                                CANDIDATE_SSID, CANDIDATE_BSSID, 1245, 0, "[EAP][ESS]", -78, 2450,
+                                1025, 22, 33, 20, 0, 0, true), null));
+
+        when(mWifiNS.getFilteredScanDetailsForCarrierUnsavedNetworks(any()))
+                .thenReturn(expectedCarrierNetworks);
+        when(mCarrierNetworkConfig.isCarrierEncryptionInfoAvailable()).thenReturn(false);
+
+        // Set WiFi to disconnected state to trigger PNO scan
+        mWifiConnectivityManager.handleConnectionStateChanged(
+                WifiConnectivityManager.WIFI_STATE_DISCONNECTED);
+
+        verify(mCarrierNetworkNotifier, never()).handleScanResults(expectedCarrierNetworks);
+    }
+
+    /**
      * When wifi is connected, {@link CarrierNetworkNotifier} handles the Wi-Fi connected behavior.
      *
      * Expected behavior: CarrierNetworkNotifier handles connected behavior
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiNativeInterfaceManagementTest.java b/tests/wifitests/src/com/android/server/wifi/WifiNativeInterfaceManagementTest.java
index 2fe4452..56ba66a 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiNativeInterfaceManagementTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiNativeInterfaceManagementTest.java
@@ -618,8 +618,6 @@
         // Trigger wificond death
         mWificondDeathHandlerCaptor.getValue().onDeath();
 
-        validateOnDestroyedClientInterface(false, false, IFACE_NAME_0, mIfaceCallback0,
-                mNetworkObserverCaptor0.getValue());
         mInOrder.verify(mWifiMetrics).incrementNumWificondCrashes();
 
         verify(mStatusListener).onStatusChanged(false);
@@ -640,8 +638,6 @@
         // Trigger vendor HAL death
         mWifiVendorHalDeathHandlerCaptor.getValue().onDeath();
 
-        validateOnDestroyedSoftApInterface(false, false, IFACE_NAME_0, mIfaceCallback0,
-                mNetworkObserverCaptor0.getValue());
         mInOrder.verify(mWifiMetrics).incrementNumHalCrashes();
 
         verify(mStatusListener).onStatusChanged(false);
@@ -661,8 +657,6 @@
         // Trigger wificond death
         mSupplicantDeathHandlerCaptor.getValue().onDeath();
 
-        validateOnDestroyedClientInterface(false, false, IFACE_NAME_0, mIfaceCallback0,
-                mNetworkObserverCaptor0.getValue());
         mInOrder.verify(mWifiMetrics).incrementNumSupplicantCrashes();
 
         verify(mStatusListener).onStatusChanged(false);
@@ -694,8 +688,6 @@
         // Trigger vendor HAL death
         mHostapdDeathHandlerCaptor.getValue().onDeath();
 
-        validateOnDestroyedSoftApInterface(false, false, IFACE_NAME_0, mIfaceCallback0,
-                mNetworkObserverCaptor0.getValue());
         mInOrder.verify(mWifiMetrics).incrementNumHostapdCrashes();
 
         verify(mStatusListener).onStatusChanged(false);
diff --git a/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java b/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java
index 6f99f55..efd9b9f 100644
--- a/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java
@@ -89,7 +89,6 @@
 import android.os.RemoteException;
 import android.os.UserManager;
 import android.os.test.TestLooper;
-import android.provider.Settings;
 import android.support.test.filters.SmallTest;
 
 import com.android.internal.os.PowerProfile;
@@ -126,9 +125,7 @@
 
     private static final String TAG = "WifiServiceImplTest";
     private static final String SCAN_PACKAGE_NAME = "scanPackage";
-    private static final String WHITE_LIST_SCAN_PACKAGE_NAME = "whiteListScanPackage";
     private static final int DEFAULT_VERBOSE_LOGGING = 0;
-    private static final long WIFI_BACKGROUND_SCAN_INTERVAL = 10000;
     private static final String ANDROID_SYSTEM_PACKAGE = "android";
     private static final String TEST_PACKAGE_NAME = "TestPackage";
     private static final String SYSUI_PACKAGE_NAME = "com.android.systemui";
@@ -286,15 +283,6 @@
                 anyBoolean(), any());
         when(mContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn(mActivityManager);
         when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
-        when(mFrameworkFacade.getLongSetting(
-                eq(mContext),
-                eq(Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_INTERVAL_MS),
-                anyLong()))
-                .thenReturn(WIFI_BACKGROUND_SCAN_INTERVAL);
-        when(mFrameworkFacade.getStringSetting(
-                eq(mContext),
-                eq(Settings.Global.WIFI_SCAN_BACKGROUND_THROTTLE_PACKAGE_WHITELIST)))
-                .thenReturn(WHITE_LIST_SCAN_PACKAGE_NAME);
         IPowerManager powerManagerService = mock(IPowerManager.class);
         mPowerManager = new PowerManager(mContext, powerManagerService, new Handler());
         when(mContext.getSystemServiceName(PowerManager.class)).thenReturn(Context.POWER_SERVICE);
@@ -1029,80 +1017,6 @@
     }
 
     /**
-     * Ensure foreground apps can always do wifi scans.
-     */
-    @Test
-    public void testWifiScanStartedForeground() {
-        setupWifiStateMachineHandlerForRunWithScissors();
-
-        when(mActivityManager.getPackageImportance(SCAN_PACKAGE_NAME)).thenReturn(
-                ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE);
-        mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy).startScan(Process.myUid());
-        verifyCheckChangePermission(SCAN_PACKAGE_NAME);
-    }
-
-    /**
-     * Ensure background apps get throttled when the previous scan is too close.
-     */
-    @Test
-    public void testWifiScanBackgroundThrottled() {
-        setupWifiStateMachineHandlerForRunWithScissors();
-
-        when(mActivityManager.getPackageImportance(SCAN_PACKAGE_NAME)).thenReturn(
-                ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED);
-        long startMs = 1000;
-        when(mClock.getElapsedSinceBootMillis()).thenReturn(startMs);
-        mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy).startScan(Process.myUid());
-
-        when(mClock.getElapsedSinceBootMillis()).thenReturn(
-                startMs + WIFI_BACKGROUND_SCAN_INTERVAL - 1000);
-        mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy, times(1)).startScan(Process.myUid());
-    }
-
-    /**
-     * Ensure background apps can do wifi scan when the throttle interval reached.
-     */
-    @Test
-    public void testWifiScanBackgroundNotThrottled() {
-        setupWifiStateMachineHandlerForRunWithScissors();
-
-        when(mActivityManager.getPackageImportance(SCAN_PACKAGE_NAME)).thenReturn(
-                ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED);
-        long startMs = 1000;
-        when(mClock.getElapsedSinceBootMillis()).thenReturn(startMs);
-        mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy).startScan(Process.myUid());
-
-        when(mClock.getElapsedSinceBootMillis()).thenReturn(
-                startMs + WIFI_BACKGROUND_SCAN_INTERVAL + 1000);
-        mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy, times(2)).startScan(Process.myUid());
-    }
-
-    /**
-     * Ensure background apps can do wifi scan when the throttle interval reached.
-     */
-    @Test
-    public void testWifiScanBackgroundWhiteListed() {
-        setupWifiStateMachineHandlerForRunWithScissors();
-
-        when(mActivityManager.getPackageImportance(WHITE_LIST_SCAN_PACKAGE_NAME)).thenReturn(
-                ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED);
-        long startMs = 1000;
-        when(mClock.getElapsedSinceBootMillis()).thenReturn(startMs);
-        mWifiServiceImpl.startScan(null, null, WHITE_LIST_SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy).startScan(Process.myUid());
-
-        when(mClock.getElapsedSinceBootMillis()).thenReturn(
-                startMs + WIFI_BACKGROUND_SCAN_INTERVAL - 1000);
-        mWifiServiceImpl.startScan(null, null, WHITE_LIST_SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy, times(2)).startScan(Process.myUid());
-    }
-
-    /**
      * Ensure that we handle scan request failure when posting the runnable to handler fails.
      */
     @Ignore
@@ -1111,11 +1025,8 @@
         setupWifiStateMachineHandlerForRunWithScissors();
         doReturn(false).when(mHandlerSpyForWsmRunWithScissors)
                 .runWithScissors(any(), anyLong());
-
-        when(mActivityManager.getPackageImportance(SCAN_PACKAGE_NAME)).thenReturn(
-                ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE);
         mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy, never()).startScan(Process.myUid());
+        verify(mScanRequestProxy, never()).startScan(Process.myUid(), SCAN_PACKAGE_NAME);
     }
 
     static final String TEST_SSID = "Sid's Place";
@@ -2614,14 +2525,14 @@
         // Send a scan request while the device is idle.
         mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
         // No scans must be made yet as the device is idle.
-        verify(mScanRequestProxy, never()).startScan(Process.myUid());
+        verify(mScanRequestProxy, never()).startScan(Process.myUid(), SCAN_PACKAGE_NAME);
 
         // Tell the wifi service that idle mode ended.
         when(mPowerManager.isDeviceIdleMode()).thenReturn(false);
         TestUtil.sendIdleModeChanged(mBroadcastReceiverCaptor.getValue(), mContext);
 
         // Must scan now.
-        verify(mScanRequestProxy, times(1)).startScan(Process.myUid());
+        verify(mScanRequestProxy).startScan(Process.myUid(), TEST_PACKAGE_NAME);
         // The app ops check is executed with this package's identity (not the identity of the
         // original remote caller who requested the scan while idle).
         verify(mAppOpsManager).noteOp(
@@ -2630,7 +2541,7 @@
         // Send another scan request. The device is not idle anymore, so it must be executed
         // immediately.
         mWifiServiceImpl.startScan(null, null, SCAN_PACKAGE_NAME);
-        verify(mScanRequestProxy, times(2)).startScan(Process.myUid());
+        verify(mScanRequestProxy).startScan(Process.myUid(), SCAN_PACKAGE_NAME);
     }
 
     private class IdleModeIntentMatcher implements ArgumentMatcher<IntentFilter> {