Settings: SoftAp: Add client manager

[jhonboy121]: adapted to A13

Signed-off-by: cjybyjk <cjybyjk@zjnu.edu.cn>
Change-Id: If9f0c1000ff4e7dd0b602a61299f1eb2c7608ac5
Signed-off-by: jhonboy121 <alfredmathew05@gmail.com>
diff --git a/res/values/leaf_plurals.xml b/res/values/leaf_plurals.xml
new file mode 100644
index 0000000..757ce30
--- /dev/null
+++ b/res/values/leaf_plurals.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="wifi_hotspot_client_limit_summary">
+        <item quantity="other">Up to <xliff:g id="device_num">%1$d</xliff:g> devices can connect to this hotspot</item>
+        <item quantity="one">Up to <xliff:g id="device_num">%1$d</xliff:g> device can connect to this hotspot</item>
+    </plurals>
+</resources>
diff --git a/res/values/leaf_strings.xml b/res/values/leaf_strings.xml
index bc9d3e1..7e34ca1 100644
--- a/res/values/leaf_strings.xml
+++ b/res/values/leaf_strings.xml
@@ -101,4 +101,15 @@
     <!-- Hotspot -->
     <string name="wifi_hotspot_hidden_ssid_title">Hidden network</string>
     <string name="wifi_hotspot_hidden_ssid_summary">Your mobile hotspot\'s name won\'t appear in the list of available WLAN networks.</string>
+    <string name="wifi_hotspot_client_manager_title">Connected devices</string>
+    <string name="wifi_hotspot_client_manager_summary">List and manage devices connected to the hotspot</string>
+    <string name="wifi_hotspot_client_manager_list_only_summary">List devices connected to the hotspot</string>
+    <string name="wifi_hotspot_client_limit_title">Limit of connected devices</string>
+    <string name="wifi_hotspot_blocked_clients_list_title">Blocked Devices</string>
+    <string name="wifi_hotspot_connected_clients_list_title">Connected Devices</string>
+    <string name="wifi_hotspot_block_client_dialog_title">Block device</string>
+    <string name="wifi_hotspot_block_client_dialog_text">This action will disconnect \"<xliff:g id="device_name">%1$s</xliff:g>\" from this device and add it to blocklist</string>
+    <string name="wifi_hotspot_unblock_client_dialog_title">Unblock device</string>
+    <string name="wifi_hotspot_unblock_client_dialog_text">The device \"<xliff:g id="device_name">%1$s</xliff:g>\" will be able to connect to this hotspot</string>
+    <string name="wifi_hotspot_client_manager_footer_text">No clients are currently available. Connected and blocked devices will be listed here when available.</string>
 </resources>
diff --git a/res/xml/hotspot_client_manager.xml b/res/xml/hotspot_client_manager.xml
new file mode 100644
index 0000000..5e5a869
--- /dev/null
+++ b/res/xml/hotspot_client_manager.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 Project Kaleidoscope
+
+  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.
+  -->
+
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <ink.kscope.settings.wifi.tether.preference.WifiTetherClientLimitPreference
+        android:key="client_limit"
+        android:title="@string/wifi_hotspot_client_limit_title" />
+
+    <PreferenceCategory
+        android:key="connected_client_list"
+        android:title="@string/wifi_hotspot_connected_clients_list_title" />
+
+    <PreferenceCategory
+        android:key="blocked_client_list"
+        android:title="@string/wifi_hotspot_blocked_clients_list_title" />
+
+    <com.android.settingslib.widget.FooterPreference
+        android:key="footer"
+        android:title="@string/wifi_hotspot_client_manager_footer_text"
+        android:selectable="false" />
+</PreferenceScreen>
diff --git a/res/xml/wifi_tether_settings.xml b/res/xml/wifi_tether_settings.xml
index a40480e..332cab0 100644
--- a/res/xml/wifi_tether_settings.xml
+++ b/res/xml/wifi_tether_settings.xml
@@ -69,5 +69,10 @@
     <SwitchPreference
         android:key="wifi_tether_hidden_ssid"
         android:title="@string/wifi_hotspot_hidden_ssid_title"
-        android:summary="@string/wifi_hotspot_hidden_ssid_summary"/>
+        android:summary="@string/wifi_hotspot_hidden_ssid_summary" />
+
+    <Preference
+        android:key="wifi_tether_client_manager"
+        android:title="@string/wifi_hotspot_client_manager_title"
+        android:fragment="ink.kscope.settings.wifi.tether.WifiTetherClientManager" />
 </PreferenceScreen>
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index d68f2c8..48c69eb 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -192,6 +192,8 @@
 import com.android.settings.wifi.savedaccesspoints2.SavedAccessPointsWifiSettings2;
 import com.android.settings.wifi.tether.WifiTetherSettings;
 
+import ink.kscope.settings.wifi.tether.WifiTetherClientManager;
+
 public class SettingsGateway {
 
     /**
@@ -372,6 +374,7 @@
             BatteryInfoFragment.class.getName(),
             UserAspectRatioDetails.class.getName(),
             ScreenTimeoutSettings.class.getName(),
+            WifiTetherClientManager.class.getName()
     };
 
     public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/src/com/android/settings/wifi/tether/WifiTetherSettings.java b/src/com/android/settings/wifi/tether/WifiTetherSettings.java
index 5264ce0..b5cb622 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherSettings.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherSettings.java
@@ -49,6 +49,7 @@
 import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
 
 import ink.kscope.settings.wifi.tether.WifiTetherHiddenSsidPreferenceController;
+import ink.kscope.settings.wifi.tether.WifiTetherClientManagerPreferenceController;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -81,6 +82,9 @@
     @VisibleForTesting
     static final String KEY_WIFI_TETHER_HIDDEN_SSID =
             WifiTetherHiddenSsidPreferenceController.PREF_KEY;
+    @VisibleForTesting
+    static final String KEY_WIFI_TETHER_CLIENT_MANAGER =
+            WifiTetherClientManagerPreferenceController.PREF_KEY;
 
     @VisibleForTesting
     SettingsMainSwitchBar mMainSwitchBar;
@@ -97,6 +101,8 @@
     WifiTetherAutoOffPreferenceController mWifiTetherAutoOffPreferenceController;
     @VisibleForTesting
     WifiTetherHiddenSsidPreferenceController mHiddenSsidPrefController;
+    @VisibleForTesting
+    WifiTetherClientManagerPreferenceController mClientPrefController;
 
     @VisibleForTesting
     boolean mUnavailable;
@@ -207,6 +213,7 @@
                 use(WifiTetherMaximizeCompatibilityPreferenceController.class);
         mWifiTetherAutoOffPreferenceController = use(WifiTetherAutoOffPreferenceController.class);
         mHiddenSsidPrefController = use(WifiTetherHiddenSsidPreferenceController.class);
+        mClientPrefController = use(WifiTetherClientManagerPreferenceController.class);
     }
 
     @Override
@@ -292,6 +299,7 @@
                 new WifiTetherAutoOffPreferenceController(context, KEY_WIFI_TETHER_AUTO_OFF));
         controllers.add(new WifiTetherMaximizeCompatibilityPreferenceController(context, listener));
         controllers.add(new WifiTetherHiddenSsidPreferenceController(context, listener));
+        controllers.add(new WifiTetherClientManagerPreferenceController(context, listener));
         return controllers;
     }
 
@@ -339,6 +347,7 @@
         configBuilder.setAutoShutdownEnabled(
                 mWifiTetherAutoOffPreferenceController.isEnabled());
         configBuilder.setHiddenSsid(mHiddenSsidPrefController.isHiddenSsidEnabled());
+        mClientPrefController.updateConfig(configBuilder);
         return configBuilder.build();
     }
 
@@ -381,6 +390,7 @@
                 keys.add(KEY_WIFI_TETHER_AUTO_OFF);
                 keys.add(KEY_WIFI_TETHER_MAXIMIZE_COMPATIBILITY);
                 keys.add(KEY_WIFI_TETHER_HIDDEN_SSID);
+                keys.add(KEY_WIFI_TETHER_CLIENT_MANAGER);
             }
 
             // Remove duplicate
diff --git a/src/ink/kscope/settings/wifi/tether/WifiTetherClientManager.java b/src/ink/kscope/settings/wifi/tether/WifiTetherClientManager.java
new file mode 100644
index 0000000..a661f0e
--- /dev/null
+++ b/src/ink/kscope/settings/wifi/tether/WifiTetherClientManager.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2022 Project Kaleidoscope
+ *
+ * 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 ink.kscope.settings.wifi.tether;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.net.MacAddress;
+import android.net.TetheringManager;
+import android.net.TetheredClient;
+import android.net.wifi.SoftApCapability;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settingslib.widget.FooterPreference;
+
+import ink.kscope.settings.wifi.tether.preference.WifiTetherClientLimitPreference;
+
+public class WifiTetherClientManager extends SettingsPreferenceFragment implements
+        Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
+        WifiManager.SoftApCallback, TetheringManager.TetheringEventCallback {
+
+    private static final String TAG = "WifiTetherClientManager";
+
+    private static final String PREF_KEY_CLIENT_LIMIT = "client_limit";
+    private static final String PREF_KEY_BLOCKED_CLIENT_LIST = "blocked_client_list";
+    private static final String PREF_KEY_CONNECTED_CLIENT_LIST = "connected_client_list";
+    private static final String PREF_KEY_FOOTER = "footer";
+
+    private WifiManager mWifiManager;
+    private TetheringManager mTetheringManager;
+
+    private WifiTetherClientLimitPreference mClientLimitPref;
+    private PreferenceCategory mConnectedClientsPref;
+    private PreferenceCategory mBlockedClientsPref;
+    private FooterPreference mFooterPref;
+
+    private boolean mSupportForceDisconnect;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mWifiManager = getSystemService(WifiManager.class);
+        mTetheringManager = getSystemService(TetheringManager.class);
+
+        mWifiManager.registerSoftApCallback(getActivity().getMainExecutor(), this);
+
+        addPreferencesFromResource(R.xml.hotspot_client_manager);
+
+        getActivity().setTitle(R.string.wifi_hotspot_client_manager_title);
+
+        mClientLimitPref = findPreference(PREF_KEY_CLIENT_LIMIT);
+        mConnectedClientsPref = findPreference(PREF_KEY_CONNECTED_CLIENT_LIST);
+        mBlockedClientsPref = findPreference(PREF_KEY_BLOCKED_CLIENT_LIST);
+        mFooterPref = findPreference(PREF_KEY_FOOTER);
+
+        mClientLimitPref.setOnPreferenceChangeListener(this);
+
+        updateBlockedClients();
+        updatePreferenceVisible();
+    }
+
+    @Override
+    public void onCapabilityChanged(@NonNull SoftApCapability softApCapability) {
+        mSupportForceDisconnect =
+                softApCapability.areFeaturesSupported(
+                    SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT);
+        mWifiManager.unregisterSoftApCallback(this);
+
+        if (mSupportForceDisconnect) {
+            mClientLimitPref.setMin(1);
+            mClientLimitPref.setMax(softApCapability.getMaxSupportedClients());
+            final SoftApConfiguration softApConfiguration = mWifiManager.getSoftApConfiguration();
+            final int maxNumberOfClients = softApConfiguration.getMaxNumberOfClients();
+            mClientLimitPref.setValue(maxNumberOfClients, false);
+        }
+        updatePreferenceVisible();
+    }
+
+    private void updatePreferenceVisible() {
+        if (mBlockedClientsPref == null || mClientLimitPref == null ||
+                mConnectedClientsPref == null || mFooterPref == null) return;
+        boolean hasConnectedClient = mConnectedClientsPref.getPreferenceCount() > 0;
+        boolean hasBlockedClient = mBlockedClientsPref.getPreferenceCount() > 0;
+        mClientLimitPref.setVisible(mSupportForceDisconnect);
+        mBlockedClientsPref.setVisible(mSupportForceDisconnect && hasBlockedClient);
+        mConnectedClientsPref.setVisible(hasConnectedClient);
+        mFooterPref.setVisible(!hasBlockedClient && !hasConnectedClient);
+    }
+
+    private void updateBlockedClients() {
+        final SoftApConfiguration softApConfiguration = mWifiManager.getSoftApConfiguration();
+        final List<MacAddress> blockedClientList = softApConfiguration.getBlockedClientList();
+        mBlockedClientsPref.removeAll();
+        for (MacAddress mac : blockedClientList) {
+            BlockedClientPreference preference = new BlockedClientPreference(getActivity(), mac);
+            preference.setOnPreferenceClickListener(this);
+            mBlockedClientsPref.addPreference(preference);
+        }
+        updatePreferenceVisible();
+    }
+
+    @Override
+    public void onClientsChanged(Collection<TetheredClient> clients) {
+        mConnectedClientsPref.removeAll();
+        for (TetheredClient client : clients) {
+            if (client.getTetheringType() != TetheringManager.TETHERING_WIFI) {
+                continue;
+            }
+            ConnectedClientPreference preference =
+                new ConnectedClientPreference(getActivity(), client);
+            preference.setOnPreferenceClickListener(this);
+            mConnectedClientsPref.addPreference(preference);
+        }
+        updatePreferenceVisible();
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        if (mSupportForceDisconnect) {
+            if (preference instanceof ConnectedClientPreference) {
+                showBlockClientDialog(
+                    ((ConnectedClientPreference)preference).getMacAddress(),
+                    preference.getTitle());
+                return true;
+            } else if (preference instanceof BlockedClientPreference) {
+                showUnblockClientDialog(((BlockedClientPreference)preference).getMacAddress());
+                return true;
+            }
+        }
+        return super.onPreferenceTreeClick(preference);
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        if (preference == mClientLimitPref) {
+            int value = (int) newValue;
+            SoftApConfiguration softApConfiguration = mWifiManager.getSoftApConfiguration();
+            SoftApConfiguration newSoftApConfiguration =
+                    new SoftApConfiguration.Builder(softApConfiguration)
+                            .setMaxNumberOfClients(value)
+                            .build();
+            return mWifiManager.setSoftApConfiguration(newSoftApConfiguration);
+        }
+        return false;
+    }
+
+    private void blockClient(MacAddress mac, boolean isBlock) {
+        final SoftApConfiguration softApConfiguration = mWifiManager.getSoftApConfiguration();
+        final List<MacAddress> blockedClientList = softApConfiguration.getBlockedClientList();
+        if (isBlock) {
+            if (blockedClientList.contains(mac)) return;
+            blockedClientList.add(mac);
+        } else {
+            if (!blockedClientList.contains(mac)) return;
+            blockedClientList.remove(mac);
+        }
+        SoftApConfiguration newSoftApConfiguration =
+                new SoftApConfiguration.Builder(softApConfiguration)
+                        .setBlockedClientList(blockedClientList)
+                        .build();
+        mWifiManager.setSoftApConfiguration(newSoftApConfiguration);
+        updateBlockedClients();
+    }
+
+    private void showBlockClientDialog(MacAddress mac, CharSequence deviceName) {
+        final Activity activity = getActivity();
+        new AlertDialog.Builder(activity)
+            .setTitle(R.string.wifi_hotspot_block_client_dialog_title)
+            .setMessage(activity.getString(
+                R.string.wifi_hotspot_block_client_dialog_text, deviceName))
+            .setPositiveButton(android.R.string.ok,
+                (dialog, which) -> {
+                    blockClient(mac, true);
+                })
+            .setNegativeButton(android.R.string.cancel, null)
+            .create().show();
+    }
+
+    private void showUnblockClientDialog(MacAddress mac) {
+        final Activity activity = getActivity();
+        new AlertDialog.Builder(activity)
+            .setTitle(R.string.wifi_hotspot_unblock_client_dialog_title)
+            .setMessage(activity.getString(
+                R.string.wifi_hotspot_unblock_client_dialog_text, mac.toString()))
+            .setPositiveButton(android.R.string.ok,
+                (dialog, which) -> {
+                    blockClient(mac, false);
+                })
+            .setNegativeButton(android.R.string.cancel, null)
+            .create().show();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mTetheringManager.registerTetheringEventCallback(getActivity().getMainExecutor(), this);
+    }
+
+    @Override
+    public void onStop() {
+        mTetheringManager.unregisterTetheringEventCallback(this);
+        super.onStop();
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.WIFI_TETHER_SETTINGS;
+    }
+
+    private class ConnectedClientPreference extends Preference {
+        private MacAddress mMacAddress;
+
+        public ConnectedClientPreference(Context context, TetheredClient client) {
+            super(context);
+            mMacAddress = client.getMacAddress();
+
+            String hostName = null;
+            String macAddress = client.getMacAddress().toString();
+
+            for (TetheredClient.AddressInfo addressInfo : client.getAddresses()) {
+                if (!TextUtils.isEmpty(addressInfo.getHostname())) {
+                    hostName = addressInfo.getHostname();
+                    break;
+                }
+            }
+
+            setKey(macAddress);
+            if (!TextUtils.isEmpty(hostName)) {
+                setTitle(hostName);
+                setSummary(macAddress);
+            } else {
+                setTitle(macAddress);
+            }
+        }
+
+        public MacAddress getMacAddress() {
+            return mMacAddress;
+        }
+    }
+
+    private class BlockedClientPreference extends Preference {
+        private MacAddress mMacAddress;
+
+        public BlockedClientPreference(Context context, MacAddress mac) {
+            super(context);
+            mMacAddress = mac;
+            setKey(mac.toString());
+            setTitle(mac.toString());
+        }
+
+        public MacAddress getMacAddress() {
+            return mMacAddress;
+        }
+    }
+}
diff --git a/src/ink/kscope/settings/wifi/tether/WifiTetherClientManagerPreferenceController.java b/src/ink/kscope/settings/wifi/tether/WifiTetherClientManagerPreferenceController.java
new file mode 100644
index 0000000..95572ce
--- /dev/null
+++ b/src/ink/kscope/settings/wifi/tether/WifiTetherClientManagerPreferenceController.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 Project Kaleidoscope
+ *
+ * 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 ink.kscope.settings.wifi.tether;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.MacAddress;
+import android.net.wifi.SoftApCapability;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
+import android.util.FeatureFlagUtils;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.FeatureFlags;
+import com.android.settings.wifi.tether.WifiTetherBasePreferenceController;
+
+import java.util.List;
+
+public class WifiTetherClientManagerPreferenceController extends WifiTetherBasePreferenceController
+        implements WifiManager.SoftApCallback {
+
+    public static final String DEDUP_POSTFIX = "_2";
+    public static final String PREF_KEY = "wifi_tether_client_manager";
+
+    private boolean mSupportForceDisconnect;
+
+    public WifiTetherClientManagerPreferenceController(Context context,
+            WifiTetherBasePreferenceController.OnTetherConfigUpdateListener listener) {
+        super(context, listener);
+
+        mWifiManager.registerSoftApCallback(context.getMainExecutor(), this);
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.TETHER_ALL_IN_ONE)
+                ? PREF_KEY + DEDUP_POSTFIX : PREF_KEY;
+    }
+
+    @Override
+    public void onCapabilityChanged(@NonNull SoftApCapability softApCapability) {
+        mSupportForceDisconnect =
+                softApCapability.areFeaturesSupported(
+                    SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT);
+        mWifiManager.unregisterSoftApCallback(this);
+        updateDisplay();
+    }
+
+    @Override
+    public void updateDisplay() {
+        if (mPreference != null) {
+            if (mSupportForceDisconnect) {
+                mPreference.setSummary(R.string.wifi_hotspot_client_manager_summary);
+            } else {
+                mPreference.setSummary(R.string.wifi_hotspot_client_manager_list_only_summary);
+            }
+        }
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        return true;
+    }
+
+    public void updateConfig(SoftApConfiguration.Builder builder) {
+        if (builder == null || !mSupportForceDisconnect) return;
+        final SoftApConfiguration softApConfiguration = mWifiManager.getSoftApConfiguration();
+        final int maxNumberOfClients = softApConfiguration.getMaxNumberOfClients();
+        final List<MacAddress> blockedClientList = softApConfiguration.getBlockedClientList();
+        builder.setMaxNumberOfClients(maxNumberOfClients)
+                .setBlockedClientList(blockedClientList);
+    }
+}
diff --git a/src/ink/kscope/settings/wifi/tether/preference/WifiTetherClientLimitPreference.java b/src/ink/kscope/settings/wifi/tether/preference/WifiTetherClientLimitPreference.java
new file mode 100644
index 0000000..87d78b3
--- /dev/null
+++ b/src/ink/kscope/settings/wifi/tether/preference/WifiTetherClientLimitPreference.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 Project Kaleidoscope
+ *
+ * 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 ink.kscope.settings.wifi.tether.preference;
+
+import android.content.Context;
+import android.widget.SeekBar;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.settings.R;
+import com.android.settings.SeekBarDialogPreference;
+
+public class WifiTetherClientLimitPreference extends SeekBarDialogPreference implements
+        SeekBar.OnSeekBarChangeListener {
+
+    private Context mContext;
+    private SeekBar mSeekBar;
+    private int mValue;
+    private int mMin;
+    private int mMax;
+
+    public WifiTetherClientLimitPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    @Override
+    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
+        setText(getSummaryForValue(progress + mMin));
+    }
+
+    @Override
+    protected void onBindDialogView(View view) {
+        super.onBindDialogView(view);
+
+        mSeekBar = getSeekBar(view);
+        mSeekBar.setOnSeekBarChangeListener(this);
+        mSeekBar.setMax(mMax - mMin);
+        mSeekBar.setProgress(mValue - mMin);
+    }
+
+    @Override
+    public void onStartTrackingTouch(SeekBar seekBar) {
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar seekBar) {
+    }
+
+    @Override
+    protected void onDialogClosed(boolean positiveResult) {
+        super.onDialogClosed(positiveResult);
+        if (positiveResult) {
+            setValue(mSeekBar.getProgress() + mMin, true);
+        }
+    }
+
+    private String getSummaryForValue(int value) {
+        return mContext.getResources().getQuantityString(
+            R.plurals.wifi_hotspot_client_limit_summary, value, value);
+    }
+
+    public void setMin(int min) {
+        mMin = min;
+    }
+
+    public void setMax(int max) {
+        mMax = max;
+    }
+
+    public void setValue(int value, boolean callListener) {
+        if (value == 0) value = mMax;
+        mValue = value;
+        String summary = getSummaryForValue(value);
+        setSummary(summary);
+        setText(summary);
+        if (callListener) callChangeListener(value);
+    }
+
+    public int getValue() {
+        return mValue;
+    }
+}