inital commit

Signed-off-by: cjybyjk <cjybyjk@zjnu.edu.cn>
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..b32e3b8
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,52 @@
+plugins {
+    id 'com.android.application'
+}
+
+def keystorePropertiesFile = rootProject.file("keystore.properties")
+def keystoreProperties = new Properties()
+keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        applicationId "org.exthmui.softap"
+        minSdkVersion 29
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    signingConfigs {
+        debug {
+            keyAlias keystoreProperties['keyAlias']
+            keyPassword keystoreProperties['keyPassword']
+            storeFile file(keystoreProperties['storeFile'])
+            storePassword keystoreProperties['storePassword']
+        }
+    }
+}
+
+dependencies {
+    implementation 'androidx.preference:preference:1.1.1'
+    compileOnly fileTree(dir: 'system_libs/', include: ['*.jar'])
+
+    implementation 'com.google.android.material:material:1.2.1'
+    testImplementation 'junit:junit:4.+'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/org/exthmui/softap/ExampleInstrumentedTest.java b/app/src/androidTest/java/org/exthmui/softap/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..94e2fe1
--- /dev/null
+++ b/app/src/androidTest/java/org/exthmui/softap/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package org.exthmui.softap;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("org.exthmui.softap", appContext.getPackageName());
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/Android.bp b/app/src/main/Android.bp
new file mode 100644
index 0000000..4b63078
--- /dev/null
+++ b/app/src/main/Android.bp
@@ -0,0 +1,18 @@
+android_app {
+    name: "SoftAPManager",
+
+    resource_dirs: ["res"],
+
+    srcs: ["java/**/*.java"],
+
+    static_libs: [
+        "androidx.preference_preference",
+        "com.google.android.material_material",
+    ],
+
+    platform_apis: true,
+    product_specific: true,
+    privileged: true,
+    certificate: "platform",
+
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..18a73c3
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.exthmui.softap"
+    android:sharedUserId="android.uid.system">
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault.Settings">
+        <activity
+            android:name=".ClientListActivity"
+            android:label="@string/title_activity_client_list">
+
+        </activity>
+        <activity
+            android:name=".ClientInfoActivity"
+            android:label="@string/title_activity_client_info" />
+
+        <service
+            android:name=".SoftApManageService"
+            android:enabled="true"
+            android:exported="false" />
+
+        <receiver
+            android:name=".receivers.BootCompletedReceiver"
+            android:enabled="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+            </intent-filter>
+        </receiver>
+
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/app/src/main/java/org/exthmui/softap/ClientInfoActivity.java b/app/src/main/java/org/exthmui/softap/ClientInfoActivity.java
new file mode 100644
index 0000000..904b53a
--- /dev/null
+++ b/app/src/main/java/org/exthmui/softap/ClientInfoActivity.java
@@ -0,0 +1,176 @@
+package org.exthmui.softap;
+
+import android.app.ActionBar;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.SwitchPreference;
+
+import org.exthmui.softap.model.ClientInfo;
+
+public class ClientInfoActivity extends FragmentActivity implements SoftApManageService.StatusListener {
+
+    private SoftApManageService.SoftApManageBinder mSoftApManageBinder;
+    private SoftApManageConn mSoftApManageConn;
+
+    private ClientInfoFragment mFragment;
+
+    private String mMACAddress;
+    private ClientInfo mClientInfo;
+    private IClientManager mClientManager = new IClientManager() {
+        @Override
+        public boolean block(boolean val) {
+            if (mSoftApManageBinder != null) {
+                if (val) {
+                    return mSoftApManageBinder.blockClient(mMACAddress);
+                } else {
+                    return mSoftApManageBinder.unblockClient(mMACAddress);
+                }
+            }
+            return false;
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        mMACAddress = intent.getStringExtra("mac");
+        if (TextUtils.isEmpty(mMACAddress)) {
+            finish();
+            return;
+        }
+
+        setContentView(R.layout.client_info_activity);
+        if (savedInstanceState == null) {
+            getSupportFragmentManager()
+                    .beginTransaction()
+                    .replace(R.id.content, new ClientInfoFragment())
+                    .commit();
+        }
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+        Intent mSoftApManageService = new Intent(this, SoftApManageService.class);
+        mSoftApManageConn = new SoftApManageConn();
+        bindServiceAsUser(mSoftApManageService, mSoftApManageConn, Context.BIND_AUTO_CREATE, UserHandle.CURRENT_OR_SELF);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        int id = item.getItemId();
+
+        switch (id) {
+            case android.R.id.home:
+                finish();
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mSoftApManageConn != null) {
+            unbindService(mSoftApManageConn);
+        }
+        if (mFragment != null) {
+            mFragment.setClientManager(null);
+        }
+        super.onDestroy();
+    }
+
+    @Override
+    public void onAttachFragment(Fragment fragment) {
+        super.onAttachFragment(fragment);
+        if (mFragment == null && fragment instanceof ClientInfoFragment) {
+            mFragment = (ClientInfoFragment) fragment;
+            mFragment.setClientManager(mClientManager);
+        }
+    }
+
+    private void updateClientInfo() {
+        if (mFragment == null || mSoftApManageBinder == null) {
+            return;
+        }
+        mClientInfo = mSoftApManageBinder.getClientByMAC(mMACAddress);
+        mFragment.updateClientInfo(mClientInfo);
+    }
+
+    @Override
+    public void onStatusChanged(int what) {
+
+    }
+
+    private class SoftApManageConn implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+            mSoftApManageBinder = (SoftApManageService.SoftApManageBinder) iBinder;
+            updateClientInfo();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mSoftApManageBinder = null;
+            finish();
+        }
+    }
+
+
+    public static class ClientInfoFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener {
+
+        private IClientManager mClientManager;
+        private Preference prefName;
+        private Preference prefMAC;
+        private Preference prefIP;
+        private SwitchPreference prefBlocked;
+
+        @Override
+        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+            setPreferencesFromResource(R.xml.client_info_prefs, rootKey);
+            prefName = findPreference("name");
+            prefIP = findPreference("ip_address");
+            prefMAC = findPreference("mac_address");
+            prefBlocked = findPreference("blocked");
+            prefBlocked.setOnPreferenceChangeListener(this);
+        }
+
+        public void setClientManager(IClientManager manager) {
+            mClientManager = manager;
+        }
+
+        public void updateClientInfo(ClientInfo info) {
+            if (info == null) return;
+            prefName.setSummary(info.getName());
+            prefMAC.setSummary(info.getMACAddress());
+            prefIP.setSummary(String.join("\n", info.getIPAddressArray()));
+            prefBlocked.setChecked(info.isBlocked());
+        }
+
+        @Override
+        public boolean onPreferenceChange(Preference preference, Object newValue) {
+            if (preference == prefBlocked) {
+                Boolean val = (Boolean) newValue;
+                if (mClientManager != null) {
+                    return mClientManager.block(val);
+                }
+            }
+            return false;
+        }
+    }
+
+    protected interface IClientManager {
+        boolean block(boolean val);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/exthmui/softap/ClientListActivity.java b/app/src/main/java/org/exthmui/softap/ClientListActivity.java
new file mode 100644
index 0000000..650e4f2
--- /dev/null
+++ b/app/src/main/java/org/exthmui/softap/ClientListActivity.java
@@ -0,0 +1,176 @@
+package org.exthmui.softap;
+
+import android.app.ActionBar;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.view.MenuItem;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragmentCompat;
+
+import org.exthmui.softap.model.ClientInfo;
+
+import java.util.ArrayList;
+
+public class ClientListActivity extends FragmentActivity implements SoftApManageService.StatusListener {
+
+    private SoftApManageService.SoftApManageBinder mSoftApManageBinder;
+    private SoftApManageConn mSoftApManageConn;
+    private ClientListFragment mFragment;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.client_list_activity);
+        if (savedInstanceState == null) {
+            getSupportFragmentManager()
+                    .beginTransaction()
+                    .replace(R.id.content, new ClientListFragment())
+                    .commit();
+        }
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+
+        Intent mSoftApManageService = new Intent(this, SoftApManageService.class);
+        startServiceAsUser(mSoftApManageService, UserHandle.CURRENT_OR_SELF);
+        mSoftApManageConn = new SoftApManageConn();
+        bindServiceAsUser(mSoftApManageService, mSoftApManageConn, Context.BIND_AUTO_CREATE, UserHandle.CURRENT_OR_SELF);
+    }
+
+    private void updateClientList() {
+        if (mFragment != null && mSoftApManageBinder != null) {
+            mFragment.updateClientList(mSoftApManageBinder.getClients());
+        }
+    }
+
+    @Override
+    public void onAttachFragment(Fragment fragment) {
+        super.onAttachFragment(fragment);
+        if (mFragment == null && fragment instanceof ClientListFragment) {
+            mFragment = (ClientListFragment) fragment;
+            updateClientList();
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mSoftApManageConn != null) {
+            mSoftApManageBinder.removeStatusListener(this);
+            unbindService(mSoftApManageConn);
+        }
+        super.onDestroy();
+    }
+
+    @Override
+    public void onStatusChanged(int what) {
+        if (what == SoftApManageService.STATUS_CLIENTS_REFRESH_DONE ||
+            what == SoftApManageService.STATUS_BLOCK_LIST_UPDATED) {
+            updateClientList();
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        int id = item.getItemId();
+
+        switch (id) {
+            case android.R.id.home:
+                finish();
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    public static class ClientListFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener {
+
+        private PreferenceCategory mConnectedClients;
+        private PreferenceCategory mBlockedClients;
+
+        @Override
+        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+            setPreferencesFromResource(R.xml.client_list_prefs, rootKey);
+            mConnectedClients = findPreference("connected_clients");
+            mBlockedClients = findPreference("blocked_clients");
+        }
+
+        @Override
+        public boolean onPreferenceClick(Preference preference) {
+            Intent intent = new Intent(getActivity(), ClientInfoActivity.class);
+            intent.putExtra("mac", preference.getKey());
+            startActivity(intent);
+            return true;
+        }
+
+        public void updateClientList(ClientInfo[] arr) {
+            ArrayList<String> removeConnectedPrefList = new ArrayList<>();
+            ArrayList<String> removeBlockedPrefList = new ArrayList<>();
+
+            addPrefToList(removeConnectedPrefList, mConnectedClients);
+            addPrefToList(removeBlockedPrefList, mBlockedClients);
+
+            for (ClientInfo info : arr) {
+                if (info.isBlocked()) {
+                    removeBlockedPrefList.remove(info.getMACAddress());
+                    if (mBlockedClients.findPreference(info.getMACAddress()) != null) {
+                        continue;
+                    }
+                    mBlockedClients.addPreference(makeClientPreference(info));
+                } else if (info.isConnected()) {
+                    removeConnectedPrefList.remove(info.getMACAddress());
+                    if (mConnectedClients.findPreference(info.getMACAddress()) != null) {
+                        continue;
+                    }
+                    mConnectedClients.addPreference(makeClientPreference(info));
+                }
+            }
+
+            for (String key : removeBlockedPrefList) {
+                mBlockedClients.removePreferenceRecursively(key);
+            }
+            for (String key : removeConnectedPrefList) {
+                mConnectedClients.removePreferenceRecursively(key);
+            }
+        }
+
+        private void addPrefToList(ArrayList<String> list, PreferenceCategory preferenceCategory) {
+            for (int i = 0; i < preferenceCategory.getPreferenceCount(); i++) {
+                Preference pref = preferenceCategory.getPreference(i);
+                list.add(pref.getKey());
+            }
+        }
+
+        private Preference makeClientPreference(ClientInfo clientInfo) {
+            Preference preference = new Preference(getContext());
+            preference.setKey(clientInfo.getMACAddress());
+            preference.setTitle(clientInfo.getName());
+            preference.setSummary(clientInfo.getMACAddress());
+            preference.setOnPreferenceClickListener(this);
+            return preference;
+        }
+    }
+
+    private class SoftApManageConn implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+            mSoftApManageBinder = (SoftApManageService.SoftApManageBinder) iBinder;
+            mSoftApManageBinder.addStatusListener(ClientListActivity.this);
+            updateClientList();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mSoftApManageBinder = null;
+            finish();
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/exthmui/softap/SoftApManageService.java b/app/src/main/java/org/exthmui/softap/SoftApManageService.java
new file mode 100644
index 0000000..bc61cb6
--- /dev/null
+++ b/app/src/main/java/org/exthmui/softap/SoftApManageService.java
@@ -0,0 +1,388 @@
+package org.exthmui.softap;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Message;
+import android.os.ServiceManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.exthmui.softap.model.ClientInfo;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
+
+public class SoftApManageService extends Service implements WifiManager.SoftApCallback {
+
+    private static final String IP_NEIGHBOR_COMMAND_FORMAT = "ip neigh show dev %s";
+    private static final int COLUMN_IP_ADDRESS = 0;
+    private static final int COLUMN_MAC_ADDRESS = 2;
+
+    private static final int MSG_STATUS_CHANGED = 1;
+    private static final int MSG_UPDATE_CLIENT_LIST = 2;
+
+    public static final int STATUS_NORMAL = 0;
+    public static final int STATUS_CLIENTS_REFRESHING = 1;
+    public static final int STATUS_CLIENTS_REFRESH_DONE = 2;
+    public static final int STATUS_ERROR = 3;
+    public static final int STATUS_BLOCK_LIST_UPDATED = 4;
+
+    private static File BLOCKED_MAC_ADDRESS_FILE;
+
+    private INetworkManagementService mNetworkManagementService;
+    private WifiManager mWifiManager;
+
+    private String mInterfaceName;
+    private final ArrayList<String> mBlockedMACList = new ArrayList<>();
+    private final ArrayList<StatusListener> mListeners = new ArrayList<>();
+    private final HashMap<String, ClientInfo> mClients = new HashMap<>();
+
+    private Thread mClientUpdateThread;
+    private Handler mHandler;
+
+    /* Use this receiver to get interface name and register callback */
+    private final BroadcastReceiver mApStatusReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) {
+                mInterfaceName = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+                unregisterReceiver(this);
+                mWifiManager.registerSoftApCallback(SoftApManageService.this, mHandler);
+            }
+        }
+    };
+
+    public SoftApManageService() {
+    }
+
+    private Thread createClientUpdateThread() {
+        return new Thread() {
+            @Override
+            public void run() {
+                try {
+                    IUpdateClientList();
+                    sendStatusChangedMessage(STATUS_NORMAL);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    sendStatusChangedMessage(STATUS_ERROR);
+                }
+            }
+        };
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        int ret = super.onStartCommand(intent, flags, startId);
+        if (BLOCKED_MAC_ADDRESS_FILE == null) {
+            BLOCKED_MAC_ADDRESS_FILE = new File(getDataDir(), "blocked.txt");
+        }
+        if (mHandler == null) {
+            mHandler = new Handler(getMainLooper()) {
+                @Override
+                public void handleMessage(@NonNull Message msg) {
+                    super.handleMessage(msg);
+                    if (msg.what == MSG_STATUS_CHANGED) {
+                        for (StatusListener listener : mListeners) {
+                            listener.onStatusChanged(msg.arg1);
+                        }
+                    } else if (msg.what == MSG_UPDATE_CLIENT_LIST) {
+                        if (mClientUpdateThread != null &&
+                                mClientUpdateThread.getState() != Thread.State.NEW &&
+                                mClientUpdateThread.getState() != Thread.State.TERMINATED) {
+                            mHandler.sendEmptyMessageDelayed(MSG_UPDATE_CLIENT_LIST, 1000);
+                            return;
+                        }
+                        if (mClientUpdateThread == null || mClientUpdateThread.getState() != Thread.State.NEW) {
+                            mClientUpdateThread = createClientUpdateThread();
+                        }
+                        mClientUpdateThread.start();
+                    }
+                }
+            };
+        }
+        if (mNetworkManagementService == null) {
+            mNetworkManagementService = INetworkManagementService.Stub.asInterface(ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
+        }
+        if (mWifiManager == null) {
+            mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
+        }
+        if (mBlockedMACList.isEmpty()) {
+            updateBlockedMACList();
+        }
+        registerReceiver(mApStatusReceiver, new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION));
+
+        return ret;
+    }
+
+    @Override
+    public void onDestroy() {
+        unregisterReceiver(mApStatusReceiver);
+        mWifiManager.unregisterSoftApCallback(this);
+        super.onDestroy();
+    }
+
+    private void updateBlockedMACList() {
+        try {
+            mBlockedMACList.clear();
+            FileInputStream fis = new FileInputStream(BLOCKED_MAC_ADDRESS_FILE);
+            InputStreamReader inputStreamReader = new InputStreamReader(fis);
+            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
+            while (bufferedReader.ready()) {
+                String mac = bufferedReader.readLine();
+                blockMACAddress(mac);
+            }
+            bufferedReader.close();
+            inputStreamReader.close();
+        } catch (IOException e) {
+            // do nothing
+        }
+    }
+
+    private void saveBlockedMACList() {
+        try {
+            BLOCKED_MAC_ADDRESS_FILE.createNewFile();
+            FileWriter fileWriter = new FileWriter(BLOCKED_MAC_ADDRESS_FILE);
+            for (String mac : mBlockedMACList) {
+                fileWriter.write(mac);
+                fileWriter.write('\n');
+            }
+            fileWriter.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private boolean blockMACAddress(String mac) {
+        if (!mBlockedMACList.contains(mac)) {
+            try {
+                mNetworkManagementService.setFirewallMACAddressRule(mac, false);
+                mBlockedMACList.add(mac);
+                if (mClients.containsKey(mac)) {
+                    mClients.get(mac).setBlocked(true);
+                }
+                sendStatusChangedMessage(STATUS_BLOCK_LIST_UPDATED);
+                return true;
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return false;
+    }
+
+    private boolean unblockMACAddress(String mac) {
+        if (mBlockedMACList.contains(mac)) {
+            try {
+                mNetworkManagementService.setFirewallMACAddressRule(mac, true);
+                mBlockedMACList.remove(mac);
+                if (mClients.containsKey(mac)) {
+                    mClients.get(mac).setBlocked(false);
+                }
+                sendStatusChangedMessage(STATUS_BLOCK_LIST_UPDATED);
+                return true;
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return false;
+    }
+
+    private void IUpdateClientList() {
+
+        HashMap<String, ClientInfo> clientInfoHashMap = new HashMap<>();
+
+        sendStatusChangedMessage(STATUS_CLIENTS_REFRESHING);
+        // 取得当前连接的设备
+        String res = execCommand(String.format(IP_NEIGHBOR_COMMAND_FORMAT, mInterfaceName));
+        if (!TextUtils.isEmpty(res)) {
+            String[] lines = res.split("\n");
+            for (String line : lines) {
+                String[] values = line.split(" ");
+                String ipAddress = values[COLUMN_IP_ADDRESS];
+                String macAddress = values[COLUMN_MAC_ADDRESS];
+                if (!isValidMACAddress(macAddress)) continue;
+                ClientInfo info;
+                if (clientInfoHashMap.containsKey(macAddress)) {
+                    info = clientInfoHashMap.get(macAddress);
+                } else {
+                    info = new ClientInfo(macAddress);
+                    clientInfoHashMap.put(macAddress, info);
+                }
+                if (info == null) continue;
+                info.addIPAddress(ipAddress);
+                info.setConnected(info.isConnected() || isReachable(ipAddress));
+            }
+        }
+
+        // 取得被屏蔽的设备列表
+        for (String blockedMAC : mBlockedMACList) {
+            if (clientInfoHashMap.containsKey(blockedMAC)) {
+                clientInfoHashMap.get(blockedMAC).setBlocked(true);
+            } else {
+                ClientInfo info = new ClientInfo(blockedMAC);
+                info.setConnected(false);
+                info.setBlocked(true);
+                clientInfoHashMap.put(blockedMAC, info);
+            }
+        }
+
+        for (ClientInfo clientInfo : clientInfoHashMap.values()) {
+            getClientName(clientInfo);
+        }
+
+        mClients.clear();
+        mClients.putAll(clientInfoHashMap);
+
+        sendStatusChangedMessage(STATUS_CLIENTS_REFRESH_DONE);
+    }
+
+    private boolean isValidMACAddress(String mac) {
+        return mac != null && mac.matches("..:..:..:..:..:..");
+    }
+
+    private boolean isIPV4Address(String ip) {
+        return ip != null && ip.matches("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
+    }
+
+    private boolean isReachable(String ip) {
+        try {
+            return InetAddress.getByName(ip).isReachable(1500);
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    private void sendStatusChangedMessage(int what) {
+        Message msg = mHandler.obtainMessage();
+        msg.what = MSG_STATUS_CHANGED;
+        msg.arg1 = what;
+        mHandler.sendMessage(msg);
+    }
+
+    private void getClientName(ClientInfo client) {
+        String[] ips = client.getIPAddressArray();
+        if (ips == null || ips.length == 0) return;
+        try {
+            boolean isIPV4 = isIPV4Address(ips[0]);
+            String[] ipStr;
+            if (isIPV4) {
+                ipStr = ips[0].split("\\.");
+            } else {
+                ipStr = ips[0].split(":");
+            }
+            byte[] ipBuffer = new byte[ipStr.length];
+            for(int i = 0; i < ipStr.length; i++){
+                ipBuffer[i] = (byte)(Integer.parseInt(ipStr[i], isIPV4 ? 10 : 16) & 0xff);
+            }
+            InetAddress inetAddress = InetAddress.getByAddress(ipBuffer);
+            client.setName(inetAddress.getHostName());
+        } catch (UnknownHostException e) {
+            // do nothing
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new SoftApManageBinder();
+    }
+
+    @Override
+    public void onStateChanged(int state, int failureReason) {
+        mHandler.sendEmptyMessage(MSG_UPDATE_CLIENT_LIST);
+    }
+
+    @Override
+    public void onNumClientsChanged(int numClients) {
+        mHandler.sendEmptyMessageDelayed(MSG_UPDATE_CLIENT_LIST, 1000);
+    }
+
+    private static String execCommand(String cmd) {
+        Runtime runtime = Runtime.getRuntime();
+        try {
+            Process process = runtime.exec(cmd);
+            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+            StringBuffer stringBuffer = new StringBuffer();
+            char[] buff = new char[1024];
+            int ch = 0;
+            while ((ch = reader.read(buff)) != -1) {
+                stringBuffer.append(buff, 0, ch);
+            }
+            reader.close();
+            process.waitFor();
+            if (process.exitValue() == 0) {
+                return stringBuffer.toString();
+            }
+        } catch (IOException | InterruptedException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public class SoftApManageBinder extends Binder {
+        public ClientInfo[] getClients() {
+            ClientInfo[] clientInfoArray = new ClientInfo[mClients.size()];
+            int i = 0;
+            for (ClientInfo clientInfo : mClients.values()) {
+                clientInfoArray[i++] = clientInfo.clone();
+            }
+            return clientInfoArray;
+        }
+
+        public ClientInfo getClientByMAC(String macAddress) {
+            ClientInfo client = mClients.get(macAddress);
+            if (client == null) {
+                return null;
+            } else {
+                return client.clone();
+            }
+        }
+
+        public boolean blockClient(String macAddress) {
+            if (blockMACAddress(macAddress)) {
+                saveBlockedMACList();
+                return true;
+            }
+            return false;
+        }
+
+        public boolean unblockClient(String macAddress) {
+            if (unblockMACAddress(macAddress)) {
+                saveBlockedMACList();
+                return true;
+            }
+            return false;
+        }
+
+        public boolean addStatusListener(StatusListener listener) {
+            if (mListeners.contains(listener)) return false;
+            return mListeners.add(listener);
+        }
+
+        public boolean removeStatusListener(StatusListener listener) {
+            return mListeners.remove(listener);
+        }
+    }
+
+    public interface StatusListener {
+        void onStatusChanged(int what);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/exthmui/softap/model/ClientInfo.java b/app/src/main/java/org/exthmui/softap/model/ClientInfo.java
new file mode 100644
index 0000000..fe17b34
--- /dev/null
+++ b/app/src/main/java/org/exthmui/softap/model/ClientInfo.java
@@ -0,0 +1,63 @@
+package org.exthmui.softap.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class ClientInfo implements Cloneable {
+    private String mName;
+    private final String mMACAddress;
+    private boolean mBlocked;
+    private boolean mConnected;
+    private final ArrayList<String> mIPAddressList = new ArrayList<>();
+
+    public ClientInfo(String mac) {
+        mMACAddress = mac;
+    }
+
+    public String getMACAddress() {
+        return mMACAddress;
+    }
+
+    public void setName(String name) {
+        mName = name;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void addIPAddress(String ip) {
+        mIPAddressList.add(ip);
+    }
+
+    public void setConnected(boolean connected) {
+        mConnected = connected;
+        if (!connected) mIPAddressList.clear();
+    }
+
+    public boolean isConnected() {
+        return mConnected;
+    }
+
+    public void setBlocked(boolean blocked) {
+        mBlocked = blocked;
+    }
+
+    public boolean isBlocked() {
+        return mBlocked;
+    }
+
+    public String[] getIPAddressArray() {
+        Object[] objects = mIPAddressList.toArray();
+        return Arrays.copyOf(objects, objects.length, String[].class);
+    }
+
+    @Override
+    public ClientInfo clone() {
+        try {
+            return (ClientInfo) super.clone();
+        } catch (CloneNotSupportedException e) {
+            return null;
+        }
+    }
+}
diff --git a/app/src/main/java/org/exthmui/softap/receivers/BootCompletedReceiver.java b/app/src/main/java/org/exthmui/softap/receivers/BootCompletedReceiver.java
new file mode 100644
index 0000000..3326ec1
--- /dev/null
+++ b/app/src/main/java/org/exthmui/softap/receivers/BootCompletedReceiver.java
@@ -0,0 +1,19 @@
+package org.exthmui.softap.receivers;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import org.exthmui.softap.SoftApManageService;
+
+public class BootCompletedReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+            Intent serviceIntent = new Intent(context, SoftApManageService.class);
+            context.startServiceAsUser(serviceIntent, UserHandle.CURRENT_OR_SELF);
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/client_info_activity.xml b/app/src/main/res/layout/client_info_activity.xml
new file mode 100644
index 0000000..c94e1a8
--- /dev/null
+++ b/app/src/main/res/layout/client_info_activity.xml
@@ -0,0 +1,9 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <FrameLayout
+        android:id="@+id/content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/client_list_activity.xml b/app/src/main/res/layout/client_list_activity.xml
new file mode 100644
index 0000000..c94e1a8
--- /dev/null
+++ b/app/src/main/res/layout/client_list_activity.xml
@@ -0,0 +1,9 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <FrameLayout
+        android:id="@+id/content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..3731b00
--- /dev/null
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">SoftAPManager</string>
+    <string name="title_activity_client_info">客户端信息</string>
+    <string name="title_activity_client_list">客户端列表</string>
+    <string name="title_blocked_clients">已阻止</string>
+    <string name="title_connected_clients">已连接</string>
+    <string name="title_name">名称</string>
+    <string name="title_mac_address">MAC 地址</string>
+    <string name="title_ip_address">IP 地址</string>
+    <string name="title_blocked">阻止连接</string>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..b00aef5
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+<resources>
+    <string name="app_name">SoftAPManager</string>
+    <string name="title_activity_client_info">Client Info</string>
+    <string name="title_activity_client_list">Client List</string>
+    <string name="title_blocked_clients">Blocked Clients</string>
+    <string name="title_connected_clients">Connected Clients</string>
+    <string name="title_name">Name</string>
+    <string name="title_mac_address">MAC Address</string>
+    <string name="title_ip_address">IP Address</string>
+    <string name="title_blocked">Blocked</string>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/xml/client_info_prefs.xml b/app/src/main/res/xml/client_info_prefs.xml
new file mode 100644
index 0000000..d2643e4
--- /dev/null
+++ b/app/src/main/res/xml/client_info_prefs.xml
@@ -0,0 +1,21 @@
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <Preference
+        android:key="name"
+        android:title="@string/title_name" />
+
+    <Preference
+        android:key="mac_address"
+        android:title="@string/title_mac_address" />
+
+    <Preference
+        android:key="ip_address"
+        android:title="@string/title_ip_address" />
+
+    <SwitchPreference
+        android:defaultValue="false"
+        android:key="blocked"
+        android:title="@string/title_blocked" />
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/app/src/main/res/xml/client_list_prefs.xml b/app/src/main/res/xml/client_list_prefs.xml
new file mode 100644
index 0000000..3c85722
--- /dev/null
+++ b/app/src/main/res/xml/client_list_prefs.xml
@@ -0,0 +1,16 @@
+<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <PreferenceCategory
+        android:title="@string/title_connected_clients"
+        android:key="connected_clients" >
+
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:title="@string/title_blocked_clients"
+        android:key="blocked_clients" >
+
+    </PreferenceCategory>
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/app/src/test/java/org/exthmui/softap/ExampleUnitTest.java b/app/src/test/java/org/exthmui/softap/ExampleUnitTest.java
new file mode 100644
index 0000000..8b636a3
--- /dev/null
+++ b/app/src/test/java/org/exthmui/softap/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package org.exthmui.softap;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}
\ No newline at end of file