softap: Add blocked/allowed client list support

Bug: 142752869
Test: atest frameworks/opt/net/wifi/tests/wifitests
Test: Manuel test, enable softap with feature enable. check client
connect log and use test code to simulate the real use case.

Change-Id: I59cdcf9e7976f4f8424ebb52bf2fc563817245cb
diff --git a/service/java/com/android/server/wifi/HostapdHal.java b/service/java/com/android/server/wifi/HostapdHal.java
index bac04c6..4b56703 100644
--- a/service/java/com/android/server/wifi/HostapdHal.java
+++ b/service/java/com/android/server/wifi/HostapdHal.java
@@ -28,6 +28,7 @@
 import android.net.MacAddress;
 import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.SoftApConfiguration.BandType;
+import android.net.wifi.WifiManager;
 import android.os.Handler;
 import android.os.HwRemoteBinder;
 import android.os.RemoteException;
@@ -592,10 +593,10 @@
                 byte[] clientMacByteArray = client.toByteArray();
                 short disconnectReason;
                 switch (reasonCode) {
-                    case ApConfigUtil.DISCONNECT_REASON_CODE_INVALID_AUTHENTICATION:
+                    case WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER:
                         disconnectReason = Ieee80211ReasonCode.WLAN_REASON_PREV_AUTH_NOT_VALID;
                         break;
-                    case ApConfigUtil.DISCONNECT_REASON_CODE_NO_MORE_STAS:
+                    case WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS:
                         disconnectReason = Ieee80211ReasonCode.WLAN_REASON_DISASSOC_AP_BUSY;
                         break;
                     default:
diff --git a/service/java/com/android/server/wifi/SoftApManager.java b/service/java/com/android/server/wifi/SoftApManager.java
index 742a3d0..58b3355 100644
--- a/service/java/com/android/server/wifi/SoftApManager.java
+++ b/service/java/com/android/server/wifi/SoftApManager.java
@@ -59,9 +59,11 @@
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 
 /**
  * Manage WiFi in AP mode.
@@ -117,6 +119,12 @@
 
     private @Role int mRole = ROLE_UNSPECIFIED;
 
+    @NonNull
+    private Set<MacAddress> mBlockedClientList = new HashSet<>();
+
+    @NonNull
+    private Set<MacAddress> mAllowedClientList = new HashSet<>();
+
     /**
      * Listener for soft AP events.
      */
@@ -181,6 +189,10 @@
         mSarManager = sarManager;
         mWifiDiagnostics = wifiDiagnostics;
         mStateMachine = new SoftApStateMachine(looper);
+        if (softApConfig != null) {
+            mBlockedClientList = new HashSet<>(softApConfig.getBlockedClientList());
+            mAllowedClientList = new HashSet<>(softApConfig.getAllowedClientList());
+        }
     }
 
     /**
@@ -394,20 +406,11 @@
             Log.d(TAG, "SoftAP is a hidden network");
         }
 
-        if (config.getMaxNumberOfClients() != 0
-                && !mCurrentSoftApCapability.isFeatureSupported(
-                SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
-            Log.d(TAG, "Error, Max Client control requires HAL support");
+        if (!ApConfigUtil.checkSupportAllConfiguration(config, mCurrentSoftApCapability)) {
+            Log.d(TAG, "Unsupported Configuration detect! config = " + config);
             return ERROR_UNSUPPORTED_CONFIGURATION;
         }
 
-        if ((config.getSecurityType() == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION
-                || config.getSecurityType() == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE)
-                && !mCurrentSoftApCapability.isFeatureSupported(
-                SoftApCapability.SOFTAP_FEATURE_WPA3_SAE)) {
-            Log.d(TAG, "Error, SAE requires HAL support");
-            return ERROR_UNSUPPORTED_CONFIGURATION;
-        }
         if (!mWifiNative.startSoftAp(mApInterfaceName,
                   localConfigBuilder.build(), mSoftApListener)) {
             Log.e(TAG, "Soft AP start failed");
@@ -430,24 +433,39 @@
         Log.d(TAG, "Soft AP is stopped");
     }
 
-    private boolean checkSoftApMaxClient(SoftApConfiguration config, WifiClient newClient) {
-        boolean isAllow = true;
+    private boolean checkSoftApClient(SoftApConfiguration config, WifiClient newClient) {
+        if (!mCurrentSoftApCapability.isFeatureSupported(
+                SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
+            return true;
+        }
+
+        if (config.isClientControlByUserEnabled()
+                && !mAllowedClientList.contains(newClient.getMacAddress())) {
+            if (!mBlockedClientList.contains(newClient.getMacAddress())) {
+                mSoftApCallback.onBlockedClientConnecting(newClient,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+            }
+            Log.d(TAG, "Force disconnect for unauthorized client: " + newClient);
+            mWifiNative.forceClientDisconnect(
+                    mApInterfaceName, newClient.getMacAddress(),
+                    WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+            return false;
+        }
         int maxConfig = mCurrentSoftApCapability.getMaxSupportedClients();
         if (config.getMaxNumberOfClients() > 0) {
             maxConfig = Math.min(maxConfig, config.getMaxNumberOfClients());
         }
-        if (mConnectedClients.size() == maxConfig) {
+
+        if (mConnectedClients.size() >= maxConfig) {
             Log.i(TAG, "No more room for new client:" + newClient);
-            if (mCurrentSoftApCapability.isFeatureSupported(
-                       SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
-                Log.d(TAG, "Force disconnect for client: " + newClient);
-                mWifiNative.forceClientDisconnect(
-                        mApInterfaceName, newClient.getMacAddress(),
-                        ApConfigUtil.DISCONNECT_REASON_CODE_NO_MORE_STAS);
-            }
-            isAllow = false;
+            mWifiNative.forceClientDisconnect(
+                    mApInterfaceName, newClient.getMacAddress(),
+                    WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
+            mSoftApCallback.onBlockedClientConnecting(newClient,
+                    WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
+            return false;
         }
-        return isAllow;
+        return true;
     }
 
     private class SoftApStateMachine extends StateMachine {
@@ -555,8 +573,11 @@
                         break;
                     case CMD_UPDATE_CONFIG:
                         SoftApConfiguration newConfig = (SoftApConfiguration) message.obj;
+                        Log.d(TAG, "Configuration changed to " + newConfig);
                         mApConfig = new SoftApModeConfiguration(mApConfig.getTargetMode(),
                                 newConfig, mCurrentSoftApCapability);
+                        mBlockedClientList = new HashSet<>(newConfig.getBlockedClientList());
+                        mAllowedClientList = new HashSet<>(newConfig.getAllowedClientList());
                         break;
                     default:
                         // Ignore all other commands.
@@ -630,6 +651,10 @@
              * configuration.
              */
             private void updateClientConnection() {
+                if (!mCurrentSoftApCapability.isFeatureSupported(
+                        SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
+                    return;
+                }
                 final int maxAllowedClientsByHardwareAndCarrier =
                         mCurrentSoftApCapability.getMaxSupportedClients();
                 final int userApConfigMaxClientCount =
@@ -639,22 +664,39 @@
                     finalMaxClientCount = Math.min(userApConfigMaxClientCount,
                             maxAllowedClientsByHardwareAndCarrier);
                 }
-                if (mConnectedClients.size() > finalMaxClientCount) {
-                    Log.d(TAG, "Capability Changed, update connected client");
+                int targetDisconnectClientNumber = mConnectedClients.size() - finalMaxClientCount;
+                List<WifiClient> allowedConnectedList = new ArrayList<>();
+                if (mApConfig.getSoftApConfiguration().isClientControlByUserEnabled()) {
+                    // Check allow list first
                     Iterator<WifiClient> iterator = mConnectedClients.iterator();
-                    int remove_count = mConnectedClients.size() - finalMaxClientCount;
-                    if (mCurrentSoftApCapability.isFeatureSupported(
-                                SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
-                        while (iterator.hasNext()) {
-                            if (remove_count == 0) break;
-                            WifiClient client = iterator.next();
-                            Log.d(TAG, "Force disconnect for client: " + client);
+                    while (iterator.hasNext()) {
+                        WifiClient client = iterator.next();
+                        if (mAllowedClientList.contains(client.getMacAddress())) {
+                            allowedConnectedList.add(client);
+                        } else {
+                            Log.d(TAG, "Force disconnect for not allowed client: " + client);
                             mWifiNative.forceClientDisconnect(
                                     mApInterfaceName, client.getMacAddress(),
-                                    ApConfigUtil.DISCONNECT_REASON_CODE_NO_MORE_STAS);
-                            remove_count--;
+                                    WifiManager
+                                    .SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+                            targetDisconnectClientNumber--;
                         }
                     }
+                } else {
+                    allowedConnectedList = new ArrayList<>(mConnectedClients);
+                }
+                if (targetDisconnectClientNumber > 0) {
+                    Iterator<WifiClient> allowedClientIterator = allowedConnectedList.iterator();
+                    while (allowedClientIterator.hasNext()) {
+                        if (targetDisconnectClientNumber == 0) break;
+                        WifiClient allowedClient = allowedClientIterator.next();
+                        Log.d(TAG, "Force disconnect for client due to no more room: "
+                                + allowedClient);
+                        mWifiNative.forceClientDisconnect(
+                                mApInterfaceName, allowedClient.getMacAddress(),
+                                WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
+                        targetDisconnectClientNumber--;
+                    }
                 }
             }
 
@@ -676,7 +718,7 @@
                     return;
                 }
                 if (isConnected) {
-                    boolean isAllow = checkSoftApMaxClient(
+                    boolean isAllow = checkSoftApClient(
                             mApConfig.getSoftApConfiguration(), client);
                     if (isAllow) {
                         mConnectedClients.add(client);
@@ -926,6 +968,8 @@
                             boolean needRescheduleTimer =
                                     mApConfig.getSoftApConfiguration().getShutdownTimeoutMillis()
                                     != newConfig.getShutdownTimeoutMillis();
+                            mBlockedClientList = new HashSet<>(newConfig.getBlockedClientList());
+                            mAllowedClientList = new HashSet<>(newConfig.getAllowedClientList());
                             mApConfig = new SoftApModeConfiguration(mApConfig.getTargetMode(),
                                     newConfig, mCurrentSoftApCapability);
                             updateClientConnection();
diff --git a/service/java/com/android/server/wifi/WifiServiceImpl.java b/service/java/com/android/server/wifi/WifiServiceImpl.java
index eb8a502..9ef5dab 100644
--- a/service/java/com/android/server/wifi/WifiServiceImpl.java
+++ b/service/java/com/android/server/wifi/WifiServiceImpl.java
@@ -1111,6 +1111,27 @@
                 }
             }
         }
+
+        /**
+         * Called when client trying to connect but device blocked the client with specific reason.
+         *
+         * @param client the currently blocked client.
+         * @param blockedReason one of blocked reason from
+         * {@link WifiManager.SapClientBlockedReason}
+         */
+        @Override
+        public void onBlockedClientConnecting(WifiClient client, int blockedReason) {
+            Iterator<ISoftApCallback> iterator =
+                    mRegisteredSoftApCallbacks.getCallbacks().iterator();
+            while (iterator.hasNext()) {
+                ISoftApCallback callback = iterator.next();
+                try {
+                    callback.onBlockedClientConnecting(client, blockedReason);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "onBlockedClientConnecting: remote exception -- " + e);
+                }
+            }
+        }
     }
 
     /**
@@ -1467,6 +1488,18 @@
         public void onCapabilityChanged(SoftApCapability capability) {
             // Nothing to do
         }
+
+        /**
+         * Called when client trying to connect but device blocked the client with specific reason.
+         *
+         * @param client the currently blocked client.
+         * @param blockedReason one of blocked reason from
+         * {@link WifiManager.SapClientBlockedReason}
+         */
+        @Override
+        public void onBlockedClientConnecting(WifiClient client, int blockedReason) {
+            // Nothing to do
+        }
     }
 
     /**
@@ -1492,6 +1525,7 @@
             throw new IllegalArgumentException("Callback must not be null");
         }
 
+
         enforceNetworkSettingsPermission();
         if (mVerboseLoggingEnabled) {
             mLog.info("registerSoftApCallback uid=%").c(Binder.getCallingUid()).flush();
diff --git a/service/java/com/android/server/wifi/util/ApConfigUtil.java b/service/java/com/android/server/wifi/util/ApConfigUtil.java
index 45d3a9a..77d0a55 100644
--- a/service/java/com/android/server/wifi/util/ApConfigUtil.java
+++ b/service/java/com/android/server/wifi/util/ApConfigUtil.java
@@ -50,11 +50,6 @@
     public static final int ERROR_GENERIC = 2;
     public static final int ERROR_UNSUPPORTED_CONFIGURATION = 3;
 
-    /* Reason code in IEEE Std 802.11-2016, 9.4.1.7, Table 9-45. */
-    public static final int DISCONNECT_REASON_CODE_UNSPECIFIED_REASON = 1;
-    public static final int DISCONNECT_REASON_CODE_INVALID_AUTHENTICATION = 2;
-    public static final int DISCONNECT_REASON_CODE_NO_MORE_STAS = 5;
-
     /* Random number generator used for AP channel selection. */
     private static final Random sRandom = new Random();
 
@@ -410,6 +405,9 @@
      * Helper function for converting SoftapConfiguration to WifiConfiguration.
      * Note that WifiConfiguration only Supports 2GHz, 5GHz, 2GHz+5GHz bands,
      * so conversion is limited to these bands.
+     *
+     * @param softApConfig the SoftApConfiguration which need to convert.
+     * @return the WifiConfiguration which convert from SoftApConfiguration.
      */
     @NonNull
     public static WifiConfiguration convertToWifiConfiguration(
@@ -454,6 +452,9 @@
      * Only Support None and WPA2 configuration conversion.
      * Note that WifiConfiguration only Supports 2GHz, 5GHz, 2GHz+5GHz bands,
      * so conversion is limited to these bands.
+     *
+     * @param wifiConfig the WifiConfiguration which need to convert.
+     * @return the SoftApConfiguration which convert from WifiConfiguration.
      */
     @NonNull
     public static SoftApConfiguration fromWifiConfiguration(
@@ -492,6 +493,9 @@
 
     /**
      * Helper function to creating SoftApCapability instance with initial field from resource file.
+     *
+     * @param context the caller context used to get value from resource file.
+     * @return SoftApCapability which updated the feature support or not from resource.
      */
     @NonNull
     public static SoftApCapability updateCapabilityFromResource(@NonNull Context context) {
@@ -525,6 +529,9 @@
 
     /**
      * Helper function to get SAE support or not.
+     *
+     * @param context the caller context used to get value from resource file.
+     * @return true if supported, false otherwise.
      */
     public static boolean isWpa3SaeSupported(@NonNull Context context) {
         return context.getResources().getBoolean(
@@ -534,7 +541,10 @@
     /**
      * Helper function for comparing two SoftApConfiguration.
      *
-     * Return true if the difference between the two configurations requires a restart to apply.
+     * @param currentConfig the original configuration.
+     * @param newConfig the new configuration which plan to apply.
+     * @return true if the difference between the two configurations requires a restart to apply,
+     *         false otherwise.
      */
     public static boolean checkConfigurationChangeNeedToRestart(
             SoftApConfiguration currentConfig, SoftApConfiguration newConfig) {
@@ -546,4 +556,31 @@
                 || currentConfig.getBand() != newConfig.getBand()
                 || currentConfig.getChannel() != newConfig.getChannel();
     }
+
+
+    /**
+     * Helper function for checking all of the configuration are supported or not.
+     *
+     * @param config target configuration want to check.
+     * @param capability the capability which indicate feature support or not.
+     * @return true if supported, false otherwise.
+     */
+    public static boolean checkSupportAllConfiguration(SoftApConfiguration config,
+            SoftApCapability capability) {
+        if (!capability.isFeatureSupported(
+                SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)
+                && (config.getMaxNumberOfClients() != 0 || config.isClientControlByUserEnabled())) {
+            Log.d(TAG, "Error, Client control requires HAL support");
+            return false;
+        }
+
+        if (!capability.isFeatureSupported(SoftApCapability.SOFTAP_FEATURE_WPA3_SAE)
+                && (config.getSecurityType()
+                == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION
+                || config.getSecurityType() == SoftApConfiguration.SECURITY_TYPE_WPA3_SAE)) {
+            Log.d(TAG, "Error, SAE requires HAL support");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/service/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java b/service/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java
index d46f7dd..66043dd 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/SoftApManagerTest.java
@@ -70,7 +70,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.WakeupMessage;
-import com.android.server.wifi.util.ApConfigUtil;
 import com.android.wifi.resources.R;
 
 import org.junit.Before;
@@ -1149,6 +1148,217 @@
     }
 
     @Test
+    public void testForceClientDisconnectInvokeBecauseClientAuthorizationEnabled()
+            throws Exception {
+        mTestSoftApCapability.setMaxSupportedClients(10);
+        Builder configBuilder = new SoftApConfiguration.Builder();
+        configBuilder.setBand(SoftApConfiguration.BAND_2GHZ);
+        configBuilder.setSsid(TEST_SSID);
+        configBuilder.enableClientControlByUser(true);
+        SoftApModeConfiguration apConfig =
+                new SoftApModeConfiguration(WifiManager.IFACE_IP_MODE_TETHERED,
+                configBuilder.build(), mTestSoftApCapability);
+        startSoftApAndVerifyEnabled(apConfig);
+
+        verify(mCallback).onConnectedClientsChanged(new ArrayList<>());
+
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT, true);
+        mLooper.dispatchAll();
+
+        // Client is not allow verify
+        verify(mWifiNative).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        verify(mWifiMetrics, never()).addSoftApNumAssociatedStationsChangedEvent(
+                1, apConfig.getTargetMode());
+        verify(mCallback, never()).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT))
+        );
+
+    }
+
+    @Test
+    public void testClientConnectedAfterUpdateToAllowListwhenClientAuthorizationEnabled()
+            throws Exception {
+        mTestSoftApCapability.setMaxSupportedClients(10);
+        ArrayList<MacAddress> allowedClientList = new ArrayList<>();
+        Builder configBuilder = new SoftApConfiguration.Builder();
+        configBuilder.setBand(SoftApConfiguration.BAND_2GHZ);
+        configBuilder.setSsid(TEST_SSID);
+        configBuilder.enableClientControlByUser(true);
+        SoftApModeConfiguration apConfig =
+                new SoftApModeConfiguration(WifiManager.IFACE_IP_MODE_TETHERED,
+                configBuilder.build(), mTestSoftApCapability);
+        startSoftApAndVerifyEnabled(apConfig);
+
+        verify(mCallback).onConnectedClientsChanged(new ArrayList<>());
+
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT, true);
+        mLooper.dispatchAll();
+
+        // Client is not allow verify
+        verify(mWifiNative).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        verify(mWifiMetrics, never()).addSoftApNumAssociatedStationsChangedEvent(
+                1, apConfig.getTargetMode());
+        verify(mCallback, never()).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT))
+        );
+        verify(mCallback).onBlockedClientConnecting(TEST_CONNECTED_CLIENT,
+                WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        reset(mCallback);
+        reset(mWifiNative);
+        // Update configuration
+        allowedClientList.add(TEST_MAC_ADDRESS);
+        configBuilder.setClientList(new ArrayList<MacAddress>(), allowedClientList);
+        mSoftApManager.updateConfiguration(configBuilder.build());
+        mLooper.dispatchAll();
+        // Client connected again
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT, true);
+        mLooper.dispatchAll();
+        verify(mWifiNative, never()).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        verify(mWifiMetrics).addSoftApNumAssociatedStationsChangedEvent(
+                1, apConfig.getTargetMode());
+        verify(mCallback).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT))
+        );
+        verify(mCallback, never()).onBlockedClientConnecting(TEST_CONNECTED_CLIENT,
+                WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+    }
+
+    @Test
+    public void testClientConnectedAfterUpdateToBlockListwhenClientAuthorizationEnabled()
+            throws Exception {
+        mTestSoftApCapability.setMaxSupportedClients(10);
+        ArrayList<MacAddress> blockedClientList = new ArrayList<>();
+        Builder configBuilder = new SoftApConfiguration.Builder();
+        configBuilder.setBand(SoftApConfiguration.BAND_2GHZ);
+        configBuilder.setSsid(TEST_SSID);
+        configBuilder.enableClientControlByUser(true);
+        SoftApModeConfiguration apConfig =
+                new SoftApModeConfiguration(WifiManager.IFACE_IP_MODE_TETHERED,
+                configBuilder.build(), mTestSoftApCapability);
+        startSoftApAndVerifyEnabled(apConfig);
+
+        verify(mCallback).onConnectedClientsChanged(new ArrayList<>());
+
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT, true);
+        mLooper.dispatchAll();
+
+        // Client is not allow verify
+        verify(mWifiNative).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        verify(mWifiMetrics, never()).addSoftApNumAssociatedStationsChangedEvent(
+                1, apConfig.getTargetMode());
+        verify(mCallback, never()).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT))
+        );
+        verify(mCallback).onBlockedClientConnecting(TEST_CONNECTED_CLIENT,
+                WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        reset(mCallback);
+        reset(mWifiNative);
+        // Update configuration
+        blockedClientList.add(TEST_MAC_ADDRESS);
+        configBuilder.setClientList(blockedClientList, new ArrayList<MacAddress>());
+        mSoftApManager.updateConfiguration(configBuilder.build());
+        mLooper.dispatchAll();
+        // Client connected again
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT, true);
+        mLooper.dispatchAll();
+        verify(mWifiNative).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        verify(mWifiMetrics, never()).addSoftApNumAssociatedStationsChangedEvent(
+                1, apConfig.getTargetMode());
+        verify(mCallback, never()).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT))
+        );
+        verify(mCallback, never()).onBlockedClientConnecting(TEST_CONNECTED_CLIENT,
+                WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+    }
+
+    @Test
+    public void testConfigChangeToSmallAndClientAddBlockListCauseClientDisconnect()
+            throws Exception {
+        mTestSoftApCapability.setMaxSupportedClients(10);
+        ArrayList<MacAddress> allowedClientList = new ArrayList<>();
+        allowedClientList.add(TEST_MAC_ADDRESS);
+        allowedClientList.add(TEST_MAC_ADDRESS_2);
+        ArrayList<MacAddress> blockedClientList = new ArrayList<>();
+
+        Builder configBuilder = new SoftApConfiguration.Builder();
+        configBuilder.setBand(SoftApConfiguration.BAND_2GHZ);
+        configBuilder.setSsid(TEST_SSID);
+        configBuilder.enableClientControlByUser(true);
+        configBuilder.setMaxNumberOfClients(2);
+        configBuilder.setClientList(new ArrayList<MacAddress>(), allowedClientList);
+        SoftApModeConfiguration apConfig =
+                new SoftApModeConfiguration(WifiManager.IFACE_IP_MODE_TETHERED,
+                configBuilder.build(), mTestSoftApCapability);
+        startSoftApAndVerifyEnabled(apConfig);
+
+        verify(mCallback).onConnectedClientsChanged(new ArrayList<>());
+
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT, true);
+        mLooper.dispatchAll();
+
+        verify(mCallback, times(2)).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT))
+        );
+
+        verify(mWifiMetrics).addSoftApNumAssociatedStationsChangedEvent(
+                1, apConfig.getTargetMode());
+        // Verify timer is canceled at this point
+        verify(mAlarmManager.getAlarmManager()).cancel(any(WakeupMessage.class));
+
+        // Second client connect and max client set is 1.
+        mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
+                TEST_NATIVE_CLIENT_2, true);
+        mLooper.dispatchAll();
+
+        verify(mCallback, times(3)).onConnectedClientsChanged(
+                Mockito.argThat((List<WifiClient> clients) ->
+                        clients.contains(TEST_CONNECTED_CLIENT_2))
+        );
+        verify(mWifiMetrics).addSoftApNumAssociatedStationsChangedEvent(
+                2, apConfig.getTargetMode());
+        reset(mCallback);
+        reset(mWifiNative);
+        // Update configuration
+        allowedClientList.clear();
+        allowedClientList.add(TEST_MAC_ADDRESS_2);
+
+        blockedClientList.add(TEST_MAC_ADDRESS);
+        configBuilder.setClientList(blockedClientList, allowedClientList);
+        configBuilder.setMaxNumberOfClients(1);
+        mSoftApManager.updateConfiguration(configBuilder.build());
+        mLooper.dispatchAll();
+        verify(mWifiNative).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
+        verify(mWifiNative, never()).forceClientDisconnect(
+                        TEST_INTERFACE_NAME, TEST_MAC_ADDRESS,
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
+    }
+
+
+    @Test
     public void schedulesTimeoutTimerOnStart() throws Exception {
         SoftApModeConfiguration apConfig =
                 new SoftApModeConfiguration(WifiManager.IFACE_IP_MODE_TETHERED, null,
@@ -1410,7 +1620,7 @@
         mLooper.dispatchAll();
         verify(mWifiNative).forceClientDisconnect(
                         TEST_INTERFACE_NAME, TEST_MAC_ADDRESS_2,
-                        ApConfigUtil.DISCONNECT_REASON_CODE_NO_MORE_STAS);
+                        WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
         verify(mWifiMetrics, never()).addSoftApNumAssociatedStationsChangedEvent(
                 2, apConfig.getTargetMode());
     }
@@ -1616,9 +1826,11 @@
         mSoftApListenerCaptor.getValue().onConnectedClientsChanged(
                 TEST_NATIVE_CLIENT_2, true);
         mLooper.dispatchAll();
+        // feature not support thus it should not trigger disconnect
         verify(mWifiNative, never()).forceClientDisconnect(
                         any(), any(), anyInt());
-        verify(mWifiMetrics, never()).addSoftApNumAssociatedStationsChangedEvent(
+        // feature not support thus client still allow connected.
+        verify(mWifiMetrics).addSoftApNumAssociatedStationsChangedEvent(
                 2, apConfig.getTargetMode());
     }
 
diff --git a/service/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java b/service/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java
index f7c2722..1bae2dc 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/WifiServiceImplTest.java
@@ -2273,6 +2273,7 @@
         verify(mClientSoftApCallback, never()).onStateChanged(WIFI_AP_STATE_DISABLED, 0);
         verify(mClientSoftApCallback, never()).onConnectedClientsChanged(any());
         verify(mClientSoftApCallback, never()).onInfoChanged(any());
+        verify(mClientSoftApCallback, never()).onCapabilityChanged(any());
     }
 
 
@@ -2296,6 +2297,9 @@
         verify(callback).onStateChanged(WIFI_AP_STATE_DISABLED, 0);
         verify(callback).onConnectedClientsChanged(Mockito.<WifiClient>anyList());
         verify(callback).onInfoChanged(new SoftApInfo());
+        verify(callback).onCapabilityChanged(ApConfigUtil.updateCapabilityFromResource(mContext));
+        // Don't need to invoke callback when register.
+        verify(callback, never()).onBlockedClientConnecting(any(), anyInt());
     }
 
     /**
@@ -2369,14 +2373,18 @@
     @Test
     public void correctCallbackIsCalledAfterAddingTwoCallbacksAndRemovingOne() throws Exception {
         final int callbackIdentifier = 1;
+        WifiClient testWifiClient = new WifiClient(MacAddress.fromString("22:33:44:55:66:77"));
         mWifiServiceImpl.registerSoftApCallback(mAppBinder, mClientSoftApCallback,
                 callbackIdentifier);
+        mLooper.dispatchAll();
 
         // Change state from default before registering the second callback
         final List<WifiClient> testClients = new ArrayList();
         mStateMachineSoftApCallback.onStateChanged(WIFI_AP_STATE_ENABLED, 0);
         mStateMachineSoftApCallback.onConnectedClientsChanged(testClients);
         mStateMachineSoftApCallback.onInfoChanged(mTestSoftApInfo);
+        mStateMachineSoftApCallback.onBlockedClientConnecting(testWifiClient, 0);
+
 
         // Register another callback and verify the new state is returned in the immediate callback
         final int anotherUid = 2;
@@ -2385,6 +2393,10 @@
         verify(mAnotherSoftApCallback).onStateChanged(WIFI_AP_STATE_ENABLED, 0);
         verify(mAnotherSoftApCallback).onConnectedClientsChanged(testClients);
         verify(mAnotherSoftApCallback).onInfoChanged(mTestSoftApInfo);
+        // Verify only first callback will receive onBlockedClientConnecting since it call after
+        // first callback register but before another callback register.
+        verify(mClientSoftApCallback).onBlockedClientConnecting(testWifiClient, 0);
+        verify(mAnotherSoftApCallback, never()).onBlockedClientConnecting(testWifiClient, 0);
 
         // unregister the fisrt callback
         mWifiServiceImpl.unregisterSoftApCallback(callbackIdentifier);
@@ -2470,9 +2482,11 @@
     @Test
     public void updatesSoftApStateAndConnectedClientsOnSoftApEvents() throws Exception {
         final List<WifiClient> testClients = new ArrayList();
+        WifiClient testWifiClient = new WifiClient(MacAddress.fromString("22:33:44:55:66:77"));
         mStateMachineSoftApCallback.onStateChanged(WIFI_AP_STATE_ENABLED, 0);
         mStateMachineSoftApCallback.onConnectedClientsChanged(testClients);
         mStateMachineSoftApCallback.onInfoChanged(mTestSoftApInfo);
+        mStateMachineSoftApCallback.onBlockedClientConnecting(testWifiClient, 0);
 
         // Register callback after num clients and soft AP are changed.
         final int callbackIdentifier = 1;
@@ -2482,6 +2496,8 @@
         verify(mClientSoftApCallback).onStateChanged(WIFI_AP_STATE_ENABLED, 0);
         verify(mClientSoftApCallback).onConnectedClientsChanged(testClients);
         verify(mClientSoftApCallback).onInfoChanged(mTestSoftApInfo);
+        // Don't need to invoke callback when register.
+        verify(mClientSoftApCallback, never()).onBlockedClientConnecting(any(), anyInt());
     }
 
     private class IntentFilterMatcher implements ArgumentMatcher<IntentFilter> {