Browse Source

add android

liuyuqi-dellpc 5 years ago
commit
a7ab43ab2b
67 changed files with 5336 additions and 0 deletions
  1. 6 0
      Android/.gitignore
  2. 8 0
      Android/README.md
  3. 1 0
      Android/app/.gitignore
  4. 34 0
      Android/app/build.gradle
  5. 25 0
      Android/app/proguard-rules.pro
  6. 49 0
      Android/app/src/main/AndroidManifest.xml
  7. 35 0
      Android/app/src/main/java/com/example/socketdemo/activity/MainActivity.java
  8. 793 0
      Android/app/src/main/java/com/example/socketdemo/activity/ReceiveFilesActivity.java
  9. 644 0
      Android/app/src/main/java/com/example/socketdemo/activity/SendFilesActivity.java
  10. 165 0
      Android/app/src/main/java/com/example/socketdemo/base/AppContext.java
  11. 363 0
      Android/app/src/main/java/com/example/socketdemo/base/BaseActivity.java
  12. 19 0
      Android/app/src/main/java/com/example/socketdemo/base/BaseTransfer.java
  13. 25 0
      Android/app/src/main/java/com/example/socketdemo/base/Transferable.java
  14. 146 0
      Android/app/src/main/java/com/example/socketdemo/bean/FileInfo.java
  15. 67 0
      Android/app/src/main/java/com/example/socketdemo/common/Consts.java
  16. 225 0
      Android/app/src/main/java/com/example/socketdemo/common/FileReceiver.java
  17. 228 0
      Android/app/src/main/java/com/example/socketdemo/common/FileSender.java
  18. 52 0
      Android/app/src/main/java/com/example/socketdemo/common/SpaceItemDecoration.java
  19. 33 0
      Android/app/src/main/java/com/example/socketdemo/receiver/HotSpotBroadcaseReceiver.java
  20. 66 0
      Android/app/src/main/java/com/example/socketdemo/receiver/WifiBroadcaseReceiver.java
  21. 89 0
      Android/app/src/main/java/com/example/socketdemo/utils/FileUtils.java
  22. 60 0
      Android/app/src/main/java/com/example/socketdemo/utils/LogUtils.java
  23. 31 0
      Android/app/src/main/java/com/example/socketdemo/utils/NetUtils.java
  24. 132 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/ApMgr.java
  25. 47 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/ChangingAwareEditText.java
  26. 34 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/ConfigurationSecurities.java
  27. 186 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/ConfigurationSecuritiesOld.java
  28. 180 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/ConfigurationSecuritiesV8.java
  29. 102 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/ReenableAllApsWhenNetworkStateChanged.java
  30. 31 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/Version.java
  31. 319 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/Wifi.java
  32. 414 0
      Android/app/src/main/java/com/example/socketdemo/wifitools/WifiMgr.java
  33. 20 0
      Android/app/src/main/res/drawable/bg_item_choose_hotspot_selector.xml
  34. 18 0
      Android/app/src/main/res/layout/activity_base.xml
  35. 21 0
      Android/app/src/main/res/layout/activity_main.xml
  36. 47 0
      Android/app/src/main/res/layout/activity_receive_files.xml
  37. 31 0
      Android/app/src/main/res/layout/activity_send_files.xml
  38. 26 0
      Android/app/src/main/res/layout/item_choose_hotspot.xml
  39. 43 0
      Android/app/src/main/res/layout/item_file_transfer.xml
  40. 52 0
      Android/app/src/main/res/layout/item_files_selector.xml
  41. 12 0
      Android/app/src/main/res/layout/layout_dialog_with_edittext.xml
  42. 42 0
      Android/app/src/main/res/layout/layout_open_hotspot.xml
  43. 34 0
      Android/app/src/main/res/layout/layout_toolbar.xml
  44. BIN
      Android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  45. BIN
      Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  46. BIN
      Android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  47. BIN
      Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  48. BIN
      Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  49. BIN
      Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  50. BIN
      Android/app/src/main/res/mipmap-xhdpi/icon_back.png
  51. BIN
      Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  52. BIN
      Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  53. BIN
      Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  54. BIN
      Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  55. 11 0
      Android/app/src/main/res/values/colors.xml
  56. 52 0
      Android/app/src/main/res/values/dimens.xml
  57. 8 0
      Android/app/src/main/res/values/strings.xml
  58. 11 0
      Android/app/src/main/res/values/styles.xml
  59. 25 0
      Android/build.gradle
  60. 17 0
      Android/gradle.properties
  61. BIN
      Android/gradle/wrapper/gradle-wrapper.jar
  62. 6 0
      Android/gradle/wrapper/gradle-wrapper.properties
  63. 160 0
      Android/gradlew
  64. 90 0
      Android/gradlew.bat
  65. BIN
      Android/screenshot/mi5.gif
  66. BIN
      Android/screenshot/mx5.gif
  67. 1 0
      Android/settings.gradle

+ 6 - 0
Android/.gitignore

@@ -0,0 +1,6 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+/build

+ 8 - 0
Android/README.md

@@ -0,0 +1,8 @@
+## **一个创建热点、连接WiFi、Socket传输文件Demo.**</br>
+![](https://github.com/WhoIsAA/SocketDemo/blob/master/screenshot/mx5.gif)![](https://github.com/WhoIsAA/SocketDemo/blob/master/screenshot/mi5.gif)
+- 发送端创建WiFi热点
+- 接收端连接热点
+- 发送端使用UDP发送文件列表
+- 接收端收到后展示文件列表,选择要接收的文件发送给发送端
+- 发送端通过TCP发送所选文件
+- 接收端开始接收...

+ 1 - 0
Android/app/.gitignore

@@ -0,0 +1 @@
+/build

+ 34 - 0
Android/app/build.gradle

@@ -0,0 +1,34 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 29
+    defaultConfig {
+        applicationId "com.example.socketdemo"
+        minSdkVersion 16
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+//    implementation 'androidx.appcompat:appcompat:1.1.0'
+//    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+//    testimplementation 'junit:junit:4.12'
+
+//    //ButterKnife注入
+//    implementation 'com.jakewharton:butterknife:+'
+//    annotationProcessor 'com.jakewharton:butterknife-compiler:+'
+//
+//    //万能适配器(RecyclerView):https://github.com/hongyangAndroid/baseAdapter
+//    implementation 'com.zhy:base-rvadapter:+'
+//    implementation 'com.google.code.gson:gson:+'
+}

+ 25 - 0
Android/app/proguard-rules.pro

@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\android_sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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

+ 49 - 0
Android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.socketdemo">
+
+
+    <!-- 在sdcard中创建/删除文件的权限 -->
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
+    <!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位 -->
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <!-- 这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位 -->
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <!-- 往sdcard中读写数据的权限 -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <!-- 定位权限 -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+
+
+
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:name=".base.AppContext"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".activity.MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+
+        <activity android:name=".activity.SendFilesActivity" />
+        <activity android:name=".activity.ReceiveFilesActivity" />
+    </application>
+
+</manifest>

+ 35 - 0
Android/app/src/main/java/com/example/socketdemo/activity/MainActivity.java

@@ -0,0 +1,35 @@
+package com.example.socketdemo.activity;
+
+import android.view.View;
+
+import com.example.socketdemo.R;
+import com.example.socketdemo.base.BaseActivity;
+
+public class MainActivity extends BaseActivity {
+
+    @Override
+    protected int getLayoutId() {
+        return R.layout.activity_main;
+    }
+
+    @Override
+    protected String getTitleText() {
+        return "首页";
+    }
+
+    @Override
+    protected void initData() {
+        setToolbarLeftIcon(0);
+
+    }
+
+    public void sendFiles(View view) {
+        //发送文件
+        pushActivity(SendFilesActivity.class);
+    }
+
+    public void receiveFiles(View view) {
+        //接收文件
+        pushActivity(ReceiveFilesActivity.class);
+    }
+}

+ 793 - 0
Android/app/src/main/java/com/example/socketdemo/activity/ReceiveFilesActivity.java

@@ -0,0 +1,793 @@
+package com.example.socketdemo.activity;
+
+import android.content.DialogInterface;
+import android.content.IntentFilter;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.example.socketdemo.R;
+import com.example.socketdemo.base.AppContext;
+import com.example.socketdemo.base.BaseActivity;
+import com.example.socketdemo.base.BaseTransfer;
+import com.example.socketdemo.bean.FileInfo;
+import com.example.socketdemo.common.Consts;
+import com.example.socketdemo.common.FileReceiver;
+import com.example.socketdemo.common.SpaceItemDecoration;
+import com.example.socketdemo.receiver.WifiBroadcaseReceiver;
+import com.example.socketdemo.utils.FileUtils;
+import com.example.socketdemo.utils.LogUtils;
+import com.example.socketdemo.utils.NetUtils;
+import com.example.socketdemo.wifitools.WifiMgr;
+import com.zhy.adapter.recyclerview.CommonAdapter;
+import com.zhy.adapter.recyclerview.MultiItemTypeAdapter;
+import com.zhy.adapter.recyclerview.base.ViewHolder;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import butterknife.BindView;
+import butterknife.OnClick;
+
+/**
+ * Created by AA on 2017/3/28.
+ */
+public class ReceiveFilesActivity extends BaseActivity implements MultiItemTypeAdapter.OnItemClickListener {
+
+    /**
+     * 接收端初始化完毕
+     */
+    public static final int MSG_FILE_RECEIVER_INIT_SUCCESS = 0x661;
+
+    /**
+     * 更新适配器
+     */
+    public static final int MSG_UPDATE_ADAPTER = 0x662;
+
+    /**
+     * 发送选中要接收的文件列表
+     */
+    public static final int MSG_SEND_RECEIVE_FILE_LIST = 0x663;
+
+    /**
+     * 添加接收文件
+     */
+    public static final int MSG_ADD_FILEINFO = 0x664;
+
+    /**
+     * 更新进度条
+     */
+    public static final int MSG_UPDATE_PROGRESS = 0x665;
+
+    /**
+     * 设置当前状态
+     */
+    public static final int MSG_SET_STATUS = 0x666;
+
+    @BindView(R.id.tv_receive_files_status)
+    TextView tvStatus;
+
+    @BindView(R.id.btn_receive_files)
+    Button btnSendFileList;
+
+    @BindView(R.id.rv_receive_files_choose_hotspot)
+    RecyclerView mChooseHotspotRecyclerView;
+    private CommonAdapter<ScanResult> mChooseHotspotAdapter;
+
+    @BindView(R.id.rv_receive_files)
+    RecyclerView mReceiveFilesRecyclerView;
+    private CommonAdapter<Map.Entry<String, FileInfo>> mReceiveFilesAdapter;
+
+    /**
+     * 选中待发送的文件列表
+     */
+    private List<FileInfo> mSendFileInfos = new ArrayList<>();
+
+    /**
+     * 接收文件线程列表数据
+     */
+    private List<FileReceiver> mFileReceiverList = new ArrayList<>();
+
+    /**
+     * WiFi工具类
+     */
+    private WifiMgr mWifiMgr;
+
+    /**
+     * 扫描到的可用WiFi列表
+     */
+    private List<ScanResult> mScanResults = new ArrayList<>();
+
+    /**
+     * 用来接收文件的Socket
+     */
+    private Socket mClientSocket;
+
+    /**
+     * UDP Socket
+     */
+    private DatagramSocket mDatagramSocket;
+
+    /**
+     * 接收文件线程
+     */
+    private ReceiveServerRunnable mReceiveServerRunnable;
+
+    /**
+     * 是否已发送初始化指令
+     */
+    private boolean mIsSendInitOrder;
+
+    /**
+     * 获取权限是否成功
+     */
+    private boolean mIsPermissionGranted;
+
+    /**
+     * 当前所选WiFi的SSID
+     */
+    private String mSelectedSSID;
+
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            super.handleMessage(msg);
+            if (msg.what == MSG_FILE_RECEIVER_INIT_SUCCESS) {
+                //告知发送端,接收端初始化完毕
+                sendInitSuccessToFileSender();
+            } else if (msg.what == MSG_UPDATE_ADAPTER) {
+                //更新适配器
+                setupReceiveFilesAdapter();
+            } else if (msg.what == MSG_SEND_RECEIVE_FILE_LIST) {
+                //发送选中要接收的文件列表
+                sendFileListToFileSender();
+            } else if (msg.what == MSG_ADD_FILEINFO) {
+                //添加接收文件
+                mReceiveFilesAdapter.notifyDataSetChanged();
+            } else if (msg.what == MSG_UPDATE_PROGRESS) {
+                //更新进度条
+                int position = msg.arg1;
+                int progress = msg.arg2;
+                if (position >= 0 && position < mReceiveFilesAdapter.getItemCount()) {
+                    updateProgress(position, progress);
+                }
+            } else if (msg.what == MSG_SET_STATUS) {
+                //设置当前状态
+                setStatus(msg.obj.toString());
+            }
+        }
+    };
+
+    @Override
+    protected int getLayoutId() {
+        return R.layout.activity_receive_files;
+    }
+
+    @Override
+    protected String getTitleText() {
+        return "接收文件";
+    }
+
+    @Override
+    protected void initData() {
+        //请求权限
+        requestPermission(PERMISSION_CONNECT_WIFI, PERMISSION_REQ_CONNECT_WIFI);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if(mIsPermissionGranted && mWifiBroadcaseReceiver == null) {
+            registerWifiReceiver();
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if(mWifiBroadcaseReceiver != null) {
+            unregisterWifiReceiver();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if(hasFileReceiving()) {
+            showTipsDialog("文件正在接收,是否退出?", "是", new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    finishActivity();
+                }
+            }, "否", null);
+        } else {
+            finishActivity();
+        }
+    }
+
+    @Override
+    protected void permissionSuccess(int requestCode) {
+        super.permissionSuccess(requestCode);
+        if(requestCode == PERMISSION_REQ_CONNECT_WIFI) {
+            //权限请求成功
+            mIsPermissionGranted = true;
+
+            //开启WiFi,监听WiFi广播
+            registerWifiReceiver();
+            mWifiMgr = new WifiMgr(getContext());
+            if(mWifiMgr.isWifiEnabled()) {
+                setStatus("正在扫描可用WiFi...");
+                mWifiMgr.startScan();
+            } else {
+                mWifiMgr.openWifi();
+            }
+        }
+    }
+
+    @Override
+    protected void permissionFail(int requestCode) {
+        super.permissionFail(requestCode);
+        if(requestCode == PERMISSION_REQ_CONNECT_WIFI) {
+            //权限请求失败
+            mIsPermissionGranted = false;
+            showTipsDialog("WiFi权限获取失败", new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    onBackPressed();
+                }
+            });
+        }
+    }
+
+    /**
+     * 注册监听WiFi操作的系统广播
+     */
+    private void registerWifiReceiver() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+        registerReceiver(mWifiBroadcaseReceiver, filter);
+    }
+
+    /**
+     * 反注册WiFi相关的系统广播
+     */
+    private void unregisterWifiReceiver() {
+        if (mWifiBroadcaseReceiver != null) {
+            unregisterReceiver(mWifiBroadcaseReceiver);
+            mWifiBroadcaseReceiver = null;
+        }
+    }
+
+    /**
+     * 开启文件接收服务
+     */
+    private void initReceiverServer() {
+        mReceiveServerRunnable = new ReceiveServerRunnable();
+        new Thread(mReceiveServerRunnable).start();
+    }
+
+    /**
+     * 告知发送端初始化完毕
+     */
+    private void sendInitSuccessToFileSender() {
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    //确保WiFi连接后获取正确IP地址
+                    int tryCount = 0;
+                    String serverIp = mWifiMgr.getIpAddressFromHotspot();
+                    while (serverIp.equals(Consts.DEFAULT_UNKNOW_IP) && tryCount < Consts.DEFAULT_TRY_COUNT) {
+                        Thread.sleep(1000);
+                        serverIp = mWifiMgr.getIpAddressFromHotspot();
+                        tryCount ++;
+                    }
+
+                    //是否可以ping通指定IP地址
+                    tryCount = 0;
+                    while (!NetUtils.pingIpAddress(serverIp) && tryCount < Consts.DEFAULT_TRY_COUNT) {
+                        Thread.sleep(500);
+                        LogUtils.i("Try to ping ------" + serverIp + " - " + tryCount);
+                        tryCount ++;
+                    }
+
+                    //创建UDP通信
+                    if(mDatagramSocket == null) {
+                        //解决:java.net.BindException: bind failed: EADDRINUSE (Address already in use)
+                        mDatagramSocket = new DatagramSocket(null);
+                        mDatagramSocket.setReuseAddress(true);
+                        mDatagramSocket.bind(new InetSocketAddress(Consts.DEFAULT_SERVER_UDP_PORT));
+                    }
+                    //发送初始化完毕指令
+                    InetAddress ipAddress = InetAddress.getByName(serverIp);
+                    byte[] sendData = Consts.MSG_FILE_RECEIVER_INIT_SUCCESS.getBytes(BaseTransfer.UTF_8);
+                    DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress, Consts.DEFAULT_SERVER_UDP_PORT);
+                    mDatagramSocket.send(sendPacket);
+                    LogUtils.i("发送消息 ------->>>" + Consts.MSG_FILE_RECEIVER_INIT_SUCCESS);
+
+                    //接收文件列表
+                    while (true) {
+                        byte[] receiveData = new byte[1024];
+                        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
+                        mDatagramSocket.receive(receivePacket);
+                        String response = new String(receivePacket.getData()).trim();
+                        if(isNotEmptyString(response)) {
+                            //发送端发来的文件列表
+                            LogUtils.e("接收到的消息 -------->>>" + response);
+                            parseFileInfoList(response);
+                        }
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }.start();
+    }
+
+    /**
+     * 设置WiFi列表适配器
+     */
+    private void setupWifiAdapter() {
+        if(mChooseHotspotAdapter == null) {
+            mChooseHotspotAdapter = new CommonAdapter<ScanResult>(getContext(), R.layout.item_choose_hotspot, mScanResults) {
+                @Override
+                protected void convert(ViewHolder holder, ScanResult scanResult, int position) {
+                    holder.setText(R.id.tv_item_choose_hotspot_ssid, scanResult.SSID);
+                    holder.setText(R.id.tv_item_choose_hotspot_level, String.format(getString(R.string.item_level), scanResult.level));
+                }
+            };
+            //设置点击事件
+            mChooseHotspotAdapter.setOnItemClickListener(this);
+            //设置适配器
+            mChooseHotspotRecyclerView.setAdapter(mChooseHotspotAdapter);
+            //设置间隔
+            mChooseHotspotRecyclerView.addItemDecoration(new SpaceItemDecoration(10));
+            mChooseHotspotRecyclerView.setVisibility(View.VISIBLE);
+        } else {
+            mChooseHotspotAdapter.notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * 设置接收文件列表适配器
+     */
+    private void setupReceiveFilesAdapter() {
+        List<Map.Entry<String, FileInfo>> fileInfos = AppContext.getAppContext().getReceiverFileInfoMap();
+        Collections.sort(fileInfos, Consts.DEFAULT_COMPARATOR);
+        //设置适配器
+        mReceiveFilesAdapter = new CommonAdapter<Map.Entry<String, FileInfo>>(getContext(), R.layout.item_files_selector, fileInfos) {
+            @Override
+            protected void convert(ViewHolder holder, Map.Entry<String, FileInfo> fileInfoMap, int position) {
+                final FileInfo fileInfo = fileInfoMap.getValue();
+                //文件路径
+                holder.setText(R.id.tv_item_files_selector_file_path, fileInfo.getFilePath());
+                //文件大小
+                holder.setText(R.id.tv_item_files_selector_size, FileUtils.FormetFileSize(fileInfo.getSize()));
+                //文件接收状态
+                if(fileInfo.getProgress() >= 100) {
+                    holder.setText(R.id.tv_item_files_selector_status, "接收完毕");
+                } else if(fileInfo.getProgress() == 0) {
+                    holder.setText(R.id.tv_item_files_selector_status, "准备接收");
+                } else if(fileInfo.getProgress() < 100) {
+                    holder.setText(R.id.tv_item_files_selector_status, "正在接收");
+                } else {
+                    holder.setText(R.id.tv_item_files_selector_status, "接收失败");
+                }
+                //文件接收进度
+                ProgressBar progressBar = holder.getView(R.id.pb_item_files_selector);
+                progressBar.setProgress(fileInfo.getProgress());
+
+                //选中文件
+                CheckBox checkBox = holder.getView(R.id.cb_item_files_selector);
+                checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+                    @Override
+                    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                        if(isChecked) {
+                            mSendFileInfos.add(fileInfo);
+                        } else {
+                            mSendFileInfos.remove(fileInfo);
+                        }
+                        //选中的文件个数大于零才可点击底部按钮
+                        btnSendFileList.setEnabled(mSendFileInfos.size() > 0);
+                    }
+                });
+            }
+        };
+        mReceiveFilesRecyclerView.setAdapter(mReceiveFilesAdapter);
+        //设置ListView样式
+        mReceiveFilesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        //分割线
+        mReceiveFilesRecyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
+    }
+
+    /**
+     * 更新文件接收进度
+     * @param position 文件索引
+     * @param progress 接收进度
+     */
+    private void updateProgress(int position, int progress) {
+        FileInfo fileInfo = mReceiveFilesAdapter.getDatas().get(position).getValue();
+        fileInfo.setProgress(progress);
+        mReceiveFilesAdapter.notifyItemChanged(position);
+
+        if(position == AppContext.getAppContext().getReceiverFileInfoMap().size() - 1 && progress == 100) {
+            toast("所有文件接收完毕");
+            LogUtils.e("所有文件接收完毕");
+        }
+    }
+
+    /**
+     * 将字符串解析成FileInfo列表
+     * @param jsonStr
+     */
+    private void parseFileInfoList(String jsonStr) {
+        if(isNotEmptyString(jsonStr)) {
+            List<FileInfo> fileInfos = FileInfo.toObjectList(jsonStr);
+            if(!isEmptyList(fileInfos)) {
+                for(FileInfo fileInfo : fileInfos) {
+                    if(fileInfo != null && isNotEmptyString(fileInfo.getFilePath())) {
+                        AppContext.getAppContext().addReceiverFileInfo(fileInfo);
+                    }
+                }
+                //更新适配器
+                mHandler.sendEmptyMessage(MSG_UPDATE_ADAPTER);
+            }
+        }
+    }
+
+    /**
+     * 发送选中的文件列表给发送端
+     */
+    private void sendFileListToFileSender() {
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    //确保WiFi连接后获取正确IP地址
+                    String serverIp = mWifiMgr.getIpAddressFromHotspot();
+                    if(mDatagramSocket == null) {
+                        //解决:java.net.BindException: bind failed: EADDRINUSE (Address already in use)
+                        mDatagramSocket = new DatagramSocket(null);
+                        mDatagramSocket.setReuseAddress(true);
+                        mDatagramSocket.bind(new InetSocketAddress(Consts.DEFAULT_SERVER_UDP_PORT));
+                    }
+
+                    //发送选中的文件列表
+                    InetAddress ipAddress = InetAddress.getByName(serverIp);
+                    String jsonStr = FileInfo.toJsonStr(mSendFileInfos);
+                    DatagramPacket sendPacket = new DatagramPacket(jsonStr.getBytes(), jsonStr.getBytes().length, ipAddress, Consts.DEFAULT_SERVER_UDP_PORT);
+                    mDatagramSocket.send(sendPacket);
+                    LogUtils.i("Send Msg To FileSender ------->>>" + jsonStr);
+
+                    //发送开始发送文件指令
+                    byte[] sendData = Consts.MSG_START_SEND.getBytes(BaseTransfer.UTF_8);
+                    DatagramPacket sendPacket2 = new DatagramPacket(sendData, sendData.length, ipAddress, Consts.DEFAULT_SERVER_UDP_PORT);
+                    mDatagramSocket.send(sendPacket2);
+                    LogUtils.i("Send Msg To FileSender ------->>>" + sendData);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }.start();
+    }
+
+    /**
+     * 显示WiFi密码输入框
+     * @param title
+     * @param listener
+     */
+    protected void showDialogWithEditText(String title, final OnWifiPasswordConfirmListener listener) {
+        View dialogView = LayoutInflater.from(this).inflate(R.layout.layout_dialog_with_edittext, null);
+        final EditText etPassword = (EditText) dialogView.findViewById(R.id.et_dialog_with_edittext);
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle(title);
+        builder.setView(dialogView);
+        builder.setPositiveButton(getString(R.string.confirm), new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                if (listener != null) {
+                    listener.onConfirm(etPassword.getText().toString().trim());
+                }
+            }
+        });
+        builder.setNegativeButton(getString(R.string.cancel), null);
+        builder.create().show();
+    }
+
+    /**
+     * 设置状态
+     * @param status
+     */
+    private void setStatus(String status) {
+        tvStatus.setText(status);
+        LogUtils.e(status);
+    }
+
+    /**
+     * 是否还有文件在接收
+     * @return
+     */
+    private boolean hasFileReceiving() {
+        for(FileReceiver fileReceiver : mFileReceiverList) {
+            if(fileReceiver != null && fileReceiver.isRunning()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 停止所有文件发送任务
+     */
+    private void stopAllFileReceivingTask() {
+        for(FileReceiver fileReceiver : mFileReceiverList) {
+            if(fileReceiver != null) {
+                fileReceiver.stop();
+            }
+        }
+    }
+
+    /**
+     * 关闭此Activity
+     */
+    private void finishActivity() {
+        //断开UDP Socket
+        closeUdpSocket();
+
+        //停止所有文件接收任务
+        stopAllFileReceivingTask();
+
+        //断开接收文件的Socket
+        closeClientSocket();
+
+        //清除WiFi网络
+        mWifiMgr.clearWifiConfig();
+
+        //清空接收文件列表
+        AppContext.getAppContext().clearReceiverFileInfoMap();
+
+        finish();
+    }
+
+    /**
+     * 断开接收文件的Socket
+     */
+    private void closeClientSocket() {
+        if(mClientSocket != null) {
+            try {
+                mClientSocket.close();
+                mClientSocket = null;
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 关闭UDP Socket
+     */
+    private void closeUdpSocket() {
+        if(mDatagramSocket != null) {
+            mDatagramSocket.disconnect();
+            mDatagramSocket.close();
+            mDatagramSocket = null;
+        }
+    }
+
+    @OnClick(R.id.btn_receive_files)
+    public void sendReceiveFileListToFileSender() {
+        //将选择的文件列表发给发送端,开始接收文件
+        AppContext.getAppContext().clearReceiverFileInfoMap();
+        for(FileInfo fileInfo : mSendFileInfos) {
+            fileInfo.setPosition(mSendFileInfos.indexOf(fileInfo));
+            AppContext.getAppContext().addReceiverFileInfo(fileInfo);
+        }
+        setupReceiveFilesAdapter();
+        initReceiverServer();
+    }
+
+    @Override
+    public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) {
+        if(position < mChooseHotspotAdapter.getItemCount() && position >= 0) {
+            //获取当前点击WiFi的SSID
+            ScanResult scanResult = mChooseHotspotAdapter.getDatas().get(position);
+            mSelectedSSID = scanResult.SSID;
+
+            if((scanResult.capabilities != null && !scanResult.capabilities.equals(WifiMgr.NO_PASSWORD)) || (scanResult.capabilities != null && !scanResult.capabilities.equals(WifiMgr.NO_PASSWORD_WPS))){
+                //弹出密码输入框
+                showDialogWithEditText(mSelectedSSID, new OnWifiPasswordConfirmListener() {
+                    @Override
+                    public void onConfirm(String password) {
+                        //使用密码连接WiFi
+                        if(isNotEmptyString(password)) {
+                            try {
+                                setStatus("正在连接Wifi...");
+                                mWifiMgr.connectWifi(mSelectedSSID, password, mScanResults);
+                            } catch (InterruptedException e) {
+                                e.printStackTrace();
+                            }
+                        } else {
+                            toast("密码不能为空");
+                        }
+                    }
+                });
+            } else {
+                //连接免密码WiFi
+                try {
+                    setStatus("正在连接Wifi...");
+                    mWifiMgr.connectWifi(mSelectedSSID, "", mScanResults);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) {
+        return false;
+    }
+
+    /**
+     * WiFi广播接收器
+     */
+    private WifiBroadcaseReceiver mWifiBroadcaseReceiver = new WifiBroadcaseReceiver() {
+        @Override
+        public void onWifiEnabled() {
+            //WiFi已开启,开始扫描可用WiFi
+            setStatus("正在扫描可用WiFi...");
+            mWifiMgr.startScan();
+        }
+
+        @Override
+        public void onWifiDisabled() {
+            //WiFi已关闭,清除可用WiFi列表
+            mSelectedSSID = "";
+            mScanResults.clear();
+            setupWifiAdapter();
+        }
+
+        @Override
+        public void onScanResultsAvailable(List<ScanResult> scanResults) {
+            //扫描周围可用WiFi成功,设置可用WiFi列表
+            mScanResults.clear();
+            mScanResults.addAll(scanResults);
+            setupWifiAdapter();
+        }
+
+        @Override
+        public void onWifiConnected(String connectedSSID) {
+            //判断指定WiFi是否连接成功
+            if (connectedSSID.equals(mSelectedSSID) && !mIsSendInitOrder) {
+                //连接成功
+                setStatus("Wifi连接成功...");
+                //显示发送列表,隐藏WiFi选择列表
+                mChooseHotspotRecyclerView.setVisibility(View.GONE);
+                mReceiveFilesRecyclerView.setVisibility(View.VISIBLE);
+
+                //告知发送端,接收端初始化完毕
+                mHandler.sendEmptyMessage(MSG_FILE_RECEIVER_INIT_SUCCESS);
+                mIsSendInitOrder = true;
+            } else {
+//                //连接成功的不是设备WiFi,清除该WiFi,重新扫描周围WiFi
+//                LogUtils.e("连接到错误WiFi,正在断开重连...");
+//                mWifiMgr.disconnectWifi(connectedSSID);
+//                mWifiMgr.startScan();
+            }
+        }
+
+        @Override
+        public void onWifiDisconnected() {
+
+        }
+    };
+
+    /**
+     * ServerSocket启动线程
+     */
+    private class ReceiveServerRunnable implements Runnable {
+
+        @Override
+        public void run() {
+            try {
+                //发送选择接收的文件
+                mHandler.sendEmptyMessage(MSG_SEND_RECEIVE_FILE_LIST);
+
+                Thread.sleep(3000);
+                //开始接收文件
+                String serverIp = mWifiMgr.getIpAddressFromHotspot();
+                List<Map.Entry<String, FileInfo>> fileInfoList = AppContext.getAppContext().getReceiverFileInfoMap();
+                Collections.sort(fileInfoList, Consts.DEFAULT_COMPARATOR);
+                for(final Map.Entry<String, FileInfo> fileInfoMap : fileInfoList) {
+                    //连接发送端,逐个文件进行接收
+                    final int position = fileInfoList.indexOf(fileInfoMap);
+                    mClientSocket = new Socket(serverIp, Consts.DEFAULT_FILE_RECEIVE_SERVER_PORT);
+                    FileReceiver fileReceiver = new FileReceiver(mClientSocket, fileInfoMap.getValue());
+                    fileReceiver.setOnReceiveListener(new FileReceiver.OnReceiveListener() {
+                        @Override
+                        public void onStart() {
+                            mHandler.obtainMessage(MSG_SET_STATUS, "开始接收"+ FileUtils.getFileName(fileInfoMap.getValue().getFilePath())).sendToTarget();
+                        }
+
+                        @Override
+                        public void onProgress(FileInfo fileInfo, long progress, long total) {
+                            //更新接收进度视图
+                            int i_progress = (int) (progress * 100 / total);
+                            LogUtils.e("正在接收:" + fileInfo.getFilePath() + "\n当前进度:" + i_progress);
+
+                            Message msg = new Message();
+                            msg.what = MSG_UPDATE_PROGRESS;
+                            msg.arg1 = position;
+                            msg.arg2 = i_progress;
+                            mHandler.sendMessage(msg);
+                        }
+
+                        @Override
+                        public void onSuccess(FileInfo fileInfo) {
+                            //接收成功
+                            mHandler.obtainMessage(MSG_SET_STATUS, "文件:" + FileUtils.getFileName(fileInfo.getFilePath()) + "接收成功").sendToTarget();
+                            fileInfo.setResult(FileInfo.FLAG_SUCCESS);
+                            AppContext.getAppContext().updateReceiverFileInfo(fileInfo);
+
+                            Message msg = new Message();
+                            msg.what = MSG_UPDATE_PROGRESS;
+                            msg.arg1 = position;
+                            msg.arg2 = 100;
+                            mHandler.sendMessage(msg);
+                        }
+
+                        @Override
+                        public void onFailure(Throwable throwable, FileInfo fileInfo) {
+                            if(fileInfo != null) {
+                                //接收失败
+                                mHandler.obtainMessage(MSG_SET_STATUS, "文件:" + FileUtils.getFileName(fileInfo.getFilePath()) + "接收失败").sendToTarget();
+                                fileInfo.setResult(FileInfo.FLAG_FAILURE);
+                                AppContext.getAppContext().updateReceiverFileInfo(fileInfo);
+
+                                Message msg = new Message();
+                                msg.what = MSG_UPDATE_PROGRESS;
+                                msg.arg1 = position;
+                                msg.arg2 = -1;
+                                mHandler.sendMessage(msg);
+                            }
+                        }
+                    });
+
+                    //加入线程池执行
+                    mFileReceiverList.add(fileReceiver);
+                    AppContext.getAppContext().MAIN_EXECUTOR.execute(fileReceiver);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private interface OnWifiPasswordConfirmListener {
+        void onConfirm(String password);
+    }
+}

+ 644 - 0
Android/app/src/main/java/com/example/socketdemo/activity/SendFilesActivity.java

@@ -0,0 +1,644 @@
+package com.example.socketdemo.activity;
+
+import android.content.DialogInterface;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.example.socketdemo.R;
+import com.example.socketdemo.base.AppContext;
+import com.example.socketdemo.base.BaseActivity;
+import com.example.socketdemo.bean.FileInfo;
+import com.example.socketdemo.common.Consts;
+import com.example.socketdemo.common.FileSender;
+import com.example.socketdemo.receiver.HotSpotBroadcaseReceiver;
+import com.example.socketdemo.utils.FileUtils;
+import com.example.socketdemo.utils.LogUtils;
+import com.example.socketdemo.wifitools.ApMgr;
+import com.zhy.adapter.recyclerview.CommonAdapter;
+import com.zhy.adapter.recyclerview.base.ViewHolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import butterknife.BindView;
+
+/**
+ * Created by AA on 2017/3/28.
+ */
+public class SendFilesActivity extends BaseActivity {
+
+    /**
+     * 更新进度条
+     */
+    public static final int MSG_UPDATE_PROGRESS = 0x661;
+
+    /**
+     * 更新列表适配器
+     */
+    public static final int MSG_UPDATE_ADAPTER = 0x662;
+
+    /**
+     * 接收端初始化成功
+     */
+    public static final int MSG_FILE_RECEIVER_INIT_SUCCESS = 0x663;
+
+    /**
+     * 设置当前状态
+     */
+    public static final int MSG_SET_STATUS = 0x664;
+
+    @BindView(R.id.tv_send_files_status)
+    TextView tvStatus;
+
+    @BindView(R.id.vs_send_files_open_hotspot)
+    ViewStub vsOpenHotspot;
+
+    @BindView(R.id.rv_send_files)
+    RecyclerView mSendFileRecyclerView;
+
+    private EditText etSSID;
+    private EditText etPassword;
+
+    /**
+     * 发送文件列表适配器
+     */
+    private CommonAdapter<Map.Entry<String, FileInfo>> mSendFileAdapter;
+
+    /**
+     * 便携热点状态接收器
+     */
+    private HotSpotBroadcaseReceiver mHotSpotBroadcaseReceiver;
+
+    /**
+     * Udp Socket
+     */
+    private DatagramSocket mDatagramSocket;
+
+    /**
+     * 文件发送线程
+     */
+    private SenderServerRunnable mSenderServerRunnable;
+
+    /**
+     * 发送端所有待发送的文件列表
+     */
+    private List<FileInfo> mAllFileInfos = new ArrayList<>();
+
+    /**
+     * 发送文件线程列表数据
+     */
+    private List<FileSender> mFileSenderList = new ArrayList<>();
+
+    /**
+     * 获取权限是否成功
+     */
+    private boolean mIsPermissionGranted;
+
+    /**
+     * 是否初始化成功
+     */
+    private boolean mIsInitialized;
+
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            super.handleMessage(msg);
+            if(msg.what == MSG_UPDATE_PROGRESS) {
+                //更新文件发送进度
+                int position = msg.arg1;
+                int progress = msg.arg2;
+                if(position >= 0 && position < mSendFileAdapter.getItemCount()) {
+                    updateProgress(position, progress);
+                }
+            } else if(msg.what == MSG_UPDATE_ADAPTER) {
+                //更新列表适配器
+                initSendFilesLayout();
+            } else if(msg.what == MSG_SET_STATUS) {
+                //设置当前状态
+                setStatus(msg.obj.toString());
+            } else if(msg.what == MSG_FILE_RECEIVER_INIT_SUCCESS) {
+                //接收端初始化完毕
+                setStatus("接收端初始化成功...");
+                //显示发送文件视图
+                initSendFilesLayout();
+            }
+        }
+    };
+
+    @Override
+    protected int getLayoutId() {
+        return R.layout.activity_send_files;
+    }
+
+    @Override
+    protected String getTitleText() {
+        return "发送文件";
+    }
+
+    @Override
+    protected void initData() {
+        //假装添加文件
+        String file1 = Environment.getExternalStorageDirectory() + File.separator + "2.rar";
+        String file2 = Environment.getExternalStorageDirectory() + File.separator + "test.jpg";
+
+        try {
+            FileInfo fileInfo1 = new FileInfo(1, file1, FileUtils.getFileSizes(new File(file1)));
+            FileInfo fileInfo2 = new FileInfo(2, file2, FileUtils.getFileSizes(new File(file2)));
+            mAllFileInfos.add(fileInfo1);
+            mAllFileInfos.add(fileInfo2);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        //初始化开启热点视图
+        initOpenHotspotLayout();
+        //请求权限,开启热点
+        requestPermission(PERMISSION_CREATE_HOTSPOT, PERMISSION_REQ_CREATE_HOTSPOT);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if(mIsPermissionGranted && mHotSpotBroadcaseReceiver == null) {
+            //注册便携热点状态接收器
+            registerHotSpotReceiver();
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        if(mHotSpotBroadcaseReceiver != null) {
+            //反注册便携热点状态接收器
+            unregisterHotSpotReceiver();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if(hasFileSending()) {
+            showTipsDialog("文件正在发送,是否退出?", "是", new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    finishActivity();
+                }
+            }, "否", null);
+        } else {
+            finishActivity();
+        }
+    }
+
+    @Override
+    protected void permissionSuccess(int requestCode) {
+        super.permissionSuccess(requestCode);
+        if(requestCode == PERMISSION_REQ_CREATE_HOTSPOT) {
+            //获取创建便携热点权限成功
+            mIsPermissionGranted = true;
+        }
+    }
+
+    @Override
+    protected void permissionFail(int requestCode) {
+        super.permissionFail(requestCode);
+        if(requestCode == PERMISSION_REQ_CREATE_HOTSPOT) {
+            //获取创建便携热点权限失败
+            mIsPermissionGranted = false;
+        }
+    }
+
+    /**
+     * 初始化开启热点视图
+     */
+    private void initOpenHotspotLayout() {
+        View view = vsOpenHotspot.inflate();
+        etSSID = (EditText) view.findViewById(R.id.et_open_hotspot_ssid);
+        etPassword = (EditText) view.findViewById(R.id.et_open_hotspot_password);
+    }
+
+    /**
+     * 初始化发送文件视图
+     */
+    private void initSendFilesLayout() {
+        vsOpenHotspot.setVisibility(View.GONE);
+        mSendFileRecyclerView.setVisibility(View.VISIBLE);
+
+        //设置适配器
+        List<Map.Entry<String, FileInfo>> fileInfos = AppContext.getAppContext().getSendFileInfoMap();
+        Collections.sort(fileInfos, Consts.DEFAULT_COMPARATOR);
+        mSendFileAdapter = new CommonAdapter<Map.Entry<String, FileInfo>>(getContext(), R.layout.item_file_transfer, fileInfos) {
+            @Override
+            protected void convert(ViewHolder holder, Map.Entry<String, FileInfo> fileInfoMap, int position) {
+                FileInfo fileInfo = fileInfoMap.getValue();
+                //文件路径
+                holder.setText(R.id.tv_item_file_transfer_file_path, fileInfo.getFilePath());
+                //文件大小
+                holder.setText(R.id.tv_item_file_transfer_size, FileUtils.FormetFileSize(fileInfo.getSize()));
+                //文件发送状态
+                if(fileInfo.getProgress() >= 100) {
+                    holder.setText(R.id.tv_item_file_transfer_status, "发送完毕");
+                } else if(fileInfo.getProgress() == 0) {
+                    holder.setText(R.id.tv_item_file_transfer_status, "准备发送");
+                } else if(fileInfo.getProgress() < 100) {
+                    holder.setText(R.id.tv_item_file_transfer_status, "正在发送");
+                } else {
+                    holder.setText(R.id.tv_item_file_transfer_status, "发送失败");
+                }
+                //文件发送进度
+                ProgressBar progressBar = holder.getView(R.id.pb_item_file_transfer);
+                progressBar.setProgress(fileInfo.getProgress());
+            }
+        };
+        mSendFileRecyclerView.setAdapter(mSendFileAdapter);
+        //设置ListView样式
+        mSendFileRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+        //分割线
+        mSendFileRecyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
+    }
+
+    /**
+     * 开启便携热点
+     * @param view
+     */
+    public void openHotspot(View view) {
+        String ssid = etSSID.getText().toString().trim();
+        String password = etPassword.getText().toString().trim();
+        if(isEmptyString(ssid)) {
+            ssid = Build.MODEL;
+        }
+
+        //是否有权限
+        if(mIsPermissionGranted) {
+            //开启热点前,先关闭WiFi,如有其他热点已开启,先关闭
+            ApMgr.closeWifi(getContext());
+            if(ApMgr.isApOn(getContext())) {
+                ApMgr.closeAp(getContext());
+            }
+
+            //注册便携热点状态接收器
+            registerHotSpotReceiver();
+
+            //以手机型号为SSID,开启热点
+            boolean isSuccess = ApMgr.openAp(getContext(), ssid, password);
+            if(!isSuccess) {
+                setStatus("创建热点失败");
+            }
+        } else {
+            showTipsDialog("获取权限失败,开启热点", new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    finish();
+                }
+            });
+        }
+    }
+
+    /**
+     * 注册便携热点状态接收器
+     */
+    private void registerHotSpotReceiver() {
+        if(mHotSpotBroadcaseReceiver == null) {
+            mHotSpotBroadcaseReceiver = new HotSpotBroadcaseReceiver() {
+                @Override
+                public void onHotSpotEnabled() {
+                    //热点成功开启
+                    if(!mIsInitialized) {
+                        mIsInitialized = true;
+                        setStatus("成功开启热点...");
+
+                        tvStatus.postDelayed(new Runnable() {
+                            @Override
+                            public void run() {
+                                setStatus("正在等待连接...");
+
+                                //等待接收端连接
+                                Runnable mUdpServerRunnable = receiveInitSuccessOrderRunnable();
+                                AppContext.MAIN_EXECUTOR.execute(mUdpServerRunnable);
+                            }
+                        }, 2000);
+                    }
+                }
+            };
+        }
+        IntentFilter filter = new IntentFilter(HotSpotBroadcaseReceiver.ACTION_HOTSPOT_STATE_CHANGED);
+        registerReceiver(mHotSpotBroadcaseReceiver, filter);
+    }
+
+    /**
+     * 反注册便携热点状态接收器
+     */
+    private void unregisterHotSpotReceiver() {
+        if(mHotSpotBroadcaseReceiver != null) {
+            unregisterReceiver(mHotSpotBroadcaseReceiver);
+            mHotSpotBroadcaseReceiver = null;
+        }
+    }
+
+    /**
+     * 等待接收端发送初始化完成指令线程
+     * @return
+     */
+    private Runnable receiveInitSuccessOrderRunnable() {
+        return new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    //开始接收接收端发来的指令
+                    receiveInitSuccessOrder(Consts.DEFAULT_SERVER_UDP_PORT);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        };
+    }
+
+    /**
+     * 等待接收端发送初始化完成指令,向其发送文件列表
+     * @param serverPort
+     * @throws Exception
+     */
+    private void receiveInitSuccessOrder(int serverPort) throws Exception {
+        //确保WiFi连接后获取正确IP地址
+        int tryCount = 0;
+        String localIpAddress = ApMgr.getHotspotLocalIpAddress(getContext());
+        while (localIpAddress.equals(Consts.DEFAULT_UNKNOW_IP) && tryCount < Consts.DEFAULT_TRY_COUNT) {
+            Thread.sleep(1000);
+            localIpAddress = ApMgr.getHotspotLocalIpAddress(getContext());
+            tryCount ++;
+        }
+
+        /** 这里使用UDP发送和接收指令 */
+        mDatagramSocket = new DatagramSocket(serverPort);
+        while (true) {
+            byte[] receiveData = new byte[1024];
+            DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
+            mDatagramSocket.receive(receivePacket);
+            String response = new String(receivePacket.getData()).trim();
+            if(isNotEmptyString(response)) {
+                LogUtils.e("接收到的消息 -------->>>" + response);
+                if(response.equals(Consts.MSG_FILE_RECEIVER_INIT_SUCCESS)) {
+                    //初始化成功指令
+                    mHandler.sendEmptyMessage(MSG_FILE_RECEIVER_INIT_SUCCESS);
+                    //发送文件列表
+                    InetAddress inetAddress = receivePacket.getAddress();
+                    int port = receivePacket.getPort();
+                    //通过UDP发送文件列表给接收端
+                    sendFileInfoListToFileReceiverWithUdp(inetAddress, port);
+                } else if(response.equals(Consts.MSG_START_SEND)) {
+                    //开始发送指令
+                    initSenderServer();
+                } else {
+                    //接收端发来的待发送文件列表
+                    parseFileInfo(response);
+                }
+            }
+        }
+    }
+
+    /**
+     * 通过UDP发送文件列表给接收端
+     * @param ipAddress IP地址
+     * @param serverPort 端口号
+     */
+    private void sendFileInfoListToFileReceiverWithUdp(InetAddress ipAddress, int serverPort) {
+        if(!isEmptyList(mAllFileInfos)) {
+            String jsonStr = FileInfo.toJsonStr(mAllFileInfos);
+            DatagramPacket sendFileInfoPacket = new DatagramPacket(jsonStr.getBytes(), jsonStr.getBytes().length, ipAddress, serverPort);
+            try {
+                //发送文件列表
+                mDatagramSocket.send(sendFileInfoPacket);
+                LogUtils.i("发送消息 --------->>>" + jsonStr + "=== Success!");
+                mHandler.obtainMessage(MSG_SET_STATUS, "成功发送文件列表...").sendToTarget();
+            } catch (IOException e) {
+                e.printStackTrace();
+                LogUtils.i("发送消息 --------->>>" + jsonStr + "=== 失败!");
+            }
+        }
+    }
+
+    /**
+     * 初始化发送端服务,开始发送文件
+     */
+    private void initSenderServer() {
+        mSenderServerRunnable = new SenderServerRunnable();
+        new Thread(mSenderServerRunnable).start();
+    }
+
+    /**
+     * 将字符串解析成FileInfo
+     * @param jsonStr
+     */
+    private void parseFileInfo(String jsonStr) {
+        if(isNotEmptyString(jsonStr)) {
+            List<FileInfo> fileInfoList = FileInfo.toObjectList(jsonStr);
+            if(!isEmptyList(fileInfoList)) {
+                for(FileInfo fileInfo : fileInfoList) {
+                    if(fileInfo != null && isNotEmptyString(fileInfo.getFilePath())) {
+                        fileInfo.setPosition(fileInfoList.indexOf(fileInfo));
+                        AppContext.getAppContext().addSendFileInfo(fileInfo);
+                        mHandler.sendEmptyMessage(MSG_UPDATE_ADAPTER);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 设置状态
+     * @param status
+     */
+    private void setStatus(String status) {
+        tvStatus.setText(status);
+        LogUtils.e(status);
+    }
+
+    /**
+     * 更新文件接收进度
+     * @param position 文件索引
+     * @param progress 接收进度
+     */
+    private void updateProgress(int position, int progress) {
+        if(position < 0 || position >= mSendFileAdapter.getItemCount()) {
+            return;
+        }
+
+        FileInfo fileInfo = mSendFileAdapter.getDatas().get(position).getValue();
+        fileInfo.setProgress(progress);
+        mSendFileAdapter.notifyItemChanged(position);
+
+        if(position == AppContext.getAppContext().getSendFileInfoMap().size() - 1 && progress == 100) {
+            toast("所有文件发送完毕");
+            LogUtils.e("所有文件发送完毕");
+        }
+    }
+
+    /**
+     * 是否还有文件在发送
+     * @return
+     */
+    private boolean hasFileSending() {
+        for(FileSender fileSender : mFileSenderList) {
+            if(fileSender != null && fileSender.isRunning()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 关闭此Activity
+     */
+    private void finishActivity() {
+        //关闭UDP Socket连接
+        closeUdpSocket();
+
+        //停止所有文件发送任务
+        stopAllFileSendingTask();
+
+        //关闭发送端Socket
+        if(mSenderServerRunnable != null) {
+            mSenderServerRunnable.closeServerSocket();
+            mSenderServerRunnable = null;
+        }
+
+        //关闭便携热点
+        ApMgr.closeAp(getContext());
+
+        //清除待发送的文件列表
+        AppContext.getAppContext().clearSendFileInfoMap();
+
+        finish();
+    }
+
+    /**
+     * 停止所有文件发送任务
+     */
+    private void stopAllFileSendingTask() {
+        for(FileSender fileSender : mFileSenderList) {
+            if(fileSender != null) {
+                fileSender.stop();
+            }
+        }
+    }
+
+    /**
+     * 关闭UDP Socket
+     */
+    private void closeUdpSocket() {
+        if(mDatagramSocket != null) {
+            mDatagramSocket.disconnect();
+            mDatagramSocket.close();
+            mDatagramSocket = null;
+        }
+    }
+
+    /**
+     * 文件发送线程
+     */
+    private class SenderServerRunnable implements Runnable {
+
+        private ServerSocket mServerSocket;
+
+        @Override
+        public void run() {
+            try {
+                //获取待发送的文件列表数据,按position索引排序
+                List<Map.Entry<String, FileInfo>> fileInfoList = AppContext.getAppContext().getSendFileInfoMap();
+                Collections.sort(fileInfoList, Consts.DEFAULT_COMPARATOR);
+                mServerSocket = new ServerSocket(Consts.DEFAULT_FILE_RECEIVE_SERVER_PORT);
+                //逐个文件进行发送
+                for(final Map.Entry<String, FileInfo> fileInfoMap : fileInfoList) {
+                    final FileInfo fileInfo = fileInfoMap.getValue();
+                    Socket socket = mServerSocket.accept();
+                    FileSender fileSender = new FileSender(getContext(), socket, fileInfo);
+                    fileSender.setOnSendListener(new FileSender.OnSendListener() {
+                        @Override
+                        public void onStart() {
+                            mHandler.obtainMessage(MSG_SET_STATUS, "开始发送"+ FileUtils.getFileName(fileInfo.getFilePath())).sendToTarget();
+                        }
+
+                        @Override
+                        public void onProgress(long progress, long total) {
+                            //更新发送进度视图
+                            int i_progress = (int) (progress * 100 / total);
+                            LogUtils.e("正在发送:" + fileInfo.getFilePath() + "\n当前进度:" + i_progress);
+
+                            Message msg = new Message();
+                            msg.what = MSG_UPDATE_PROGRESS;
+                            msg.arg1 = fileInfo.getPosition();
+                            msg.arg2 = i_progress;
+                            mHandler.sendMessage(msg);
+                        }
+
+                        @Override
+                        public void onSuccess(FileInfo fileInfo) {
+                            //发送成功
+                            mHandler.obtainMessage(MSG_SET_STATUS, "文件:" + FileUtils.getFileName(fileInfo.getFilePath()) + "发送成功").sendToTarget();
+                            fileInfo.setResult(FileInfo.FLAG_SUCCESS);
+                            AppContext.getAppContext().updateSendFileInfo(fileInfo);
+
+                            Message msg = new Message();
+                            msg.what = MSG_UPDATE_PROGRESS;
+                            msg.arg1 = fileInfo.getPosition();
+                            msg.arg2 = 100;
+                            mHandler.sendMessage(msg);
+                        }
+
+                        @Override
+                        public void onFailure(Throwable throwable, FileInfo fileInfo) {
+                            //发送失败
+                            mHandler.obtainMessage(MSG_SET_STATUS, "文件:" + FileUtils.getFileName(fileInfo.getFilePath()) + "发送失败").sendToTarget();
+                            fileInfo.setResult(FileInfo.FLAG_FAILURE);
+                            AppContext.getAppContext().updateSendFileInfo(fileInfo);
+
+                            Message msg = new Message();
+                            msg.what = MSG_UPDATE_PROGRESS;
+                            msg.arg1 = fileInfo.getPosition();
+                            msg.arg2 = -1;
+                            mHandler.sendMessage(msg);
+                        }
+                    });
+                    //添加到线程池执行
+                    mFileSenderList.add(fileSender);
+                    AppContext.FILE_SENDER_EXECUTOR.execute(fileSender);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        /**
+         * 关闭Socket连接
+         */
+        public void closeServerSocket() {
+            if(mServerSocket != null) {
+                try {
+                    mServerSocket.close();
+                    mServerSocket = null;
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+}

+ 165 - 0
Android/app/src/main/java/com/example/socketdemo/base/AppContext.java

@@ -0,0 +1,165 @@
+package com.example.socketdemo.base;
+
+import android.app.Application;
+
+import com.example.socketdemo.bean.FileInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by AA on 2017/3/23.
+ */
+public class AppContext extends Application {
+
+    /**
+     * App全局上下文
+     */
+    private static AppContext mInstance;
+
+    /**
+     * 主线程池
+     */
+    public static Executor MAIN_EXECUTOR = Executors.newFixedThreadPool(5);
+
+    /**
+     * 文件发送端单线程
+     */
+    public static Executor FILE_SENDER_EXECUTOR = Executors.newSingleThreadExecutor();
+
+    /**
+     * 待发送的文件数据
+     */
+    public Map<String, FileInfo> mSendFileInfoMap = new HashMap<>();
+
+    /**
+     * 接收到的文件数据
+     */
+    public Map<String, FileInfo> mReceivedFileInfoMap = new HashMap<>();
+
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mInstance = this;
+    }
+
+    /**
+     * 获取Application全局变量
+     * @return
+     */
+    public static AppContext getAppContext() {
+        return mInstance;
+    }
+
+
+    /**************************************************************************************
+     ********************************************发送端************************************
+     **************************************************************************************/
+
+    /**
+     * 删除待发送的文件Map
+     */
+    public void clearSendFileInfoMap() {
+        mSendFileInfoMap.clear();
+    }
+
+    /**
+     * 获取待发送的文件Map
+     * @return
+     */
+    public List<Map.Entry<String, FileInfo>> getSendFileInfoMap() {
+        List<Map.Entry<String, FileInfo>> fileInfoMapList = new ArrayList<>(mSendFileInfoMap.entrySet());
+        return fileInfoMapList;
+    }
+
+    /**
+     * 获取待发送的文件总长度
+     * @return
+     */
+    public long getAllSendFileInfoSize() {
+        long totalSize = 0;
+        for(FileInfo fileInfo : mSendFileInfoMap.values()) {
+            if(fileInfo != null) {
+                totalSize += fileInfo.getSize();
+            }
+        }
+        return totalSize;
+    }
+
+    /**
+     * 添加FileInfo
+     * @param fileInfo
+     */
+    public void addSendFileInfo(FileInfo fileInfo) {
+        if(!mSendFileInfoMap.containsKey(fileInfo)) {
+            mSendFileInfoMap.put(fileInfo.getFilePath(), fileInfo);
+        }
+    }
+
+    /**
+     * 更新FileInfo
+     * @param fileInfo
+     */
+    public void updateSendFileInfo(FileInfo fileInfo) {
+        mSendFileInfoMap.put(fileInfo.getFilePath(), fileInfo);
+    }
+
+
+    /**************************************************************************************
+     ********************************************接收端************************************
+     **************************************************************************************/
+
+    /**
+     * 删除接收到的文件Map
+     */
+    public void clearReceiverFileInfoMap() {
+        mReceivedFileInfoMap.clear();
+    }
+
+    /**
+     * 获取接收到的文件Map
+     * @return
+     */
+    public List<Map.Entry<String, FileInfo>> getReceiverFileInfoMap() {
+        List<Map.Entry<String, FileInfo>> fileInfoMapList = new ArrayList<>(mReceivedFileInfoMap.entrySet());
+        return fileInfoMapList;
+    }
+
+    /**
+     * 获取接收到的文件总长度
+     * @return
+     */
+    public long getAllReceiverFileInfoSize() {
+        long totalSize = 0;
+        for(FileInfo fileInfo : mReceivedFileInfoMap.values()) {
+            if(fileInfo != null) {
+                totalSize += fileInfo.getSize();
+            }
+        }
+        return totalSize;
+    }
+
+    /**
+     * 添加FileInfo
+     * @param fileInfo
+     */
+    public void addReceiverFileInfo(FileInfo fileInfo) {
+        if(!mReceivedFileInfoMap.containsKey(fileInfo.getFilePath())) {
+            mReceivedFileInfoMap.put(fileInfo.getFilePath(), fileInfo);
+        }
+    }
+
+    /**
+     * 更新FileInfo
+     * @param fileInfo
+     */
+    public void updateReceiverFileInfo(FileInfo fileInfo) {
+        mReceivedFileInfoMap.put(fileInfo.getFilePath(), fileInfo);
+    }
+
+}

+ 363 - 0
Android/app/src/main/java/com/example/socketdemo/base/BaseActivity.java

@@ -0,0 +1,363 @@
+package com.example.socketdemo.base;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.ViewStub;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.example.socketdemo.R;
+import com.example.socketdemo.utils.LogUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import butterknife.ButterKnife;
+
+/**
+ * Activity基类
+ */
+public abstract class BaseActivity extends AppCompatActivity {
+
+    /** WiFi热点连接和创建权限请求码 */
+    protected static final int PERMISSION_REQ_CONNECT_WIFI = 3020;
+
+    /** 创建便携热点权限请求码 */
+    protected static final int PERMISSION_REQ_CREATE_HOTSPOT = 3021;
+
+    /** 连接WiFi所需权限 */
+    protected static final String[] PERMISSION_CONNECT_WIFI = new String[] {
+            Manifest.permission.ACCESS_WIFI_STATE,
+            Manifest.permission.ACCESS_FINE_LOCATION,
+            Manifest.permission.ACCESS_COARSE_LOCATION,
+            android.Manifest.permission.WRITE_EXTERNAL_STORAGE};
+
+    /** 创建便携热点所需权限 */
+    protected static final String[] PERMISSION_CREATE_HOTSPOT = new String[] {
+            Manifest.permission.WRITE_SETTINGS,
+            android.Manifest.permission.WRITE_EXTERNAL_STORAGE};
+
+    /** 标题栏相关控件 */
+    private Toolbar mToolbar;
+    private TextView tvTitle;
+    /** 内容视图控件 */
+    private ViewStub mViewStub;
+
+    /**
+     * 当前Activity上下文
+     */
+    private Context mContext;
+
+    /**
+     * 权限请求码
+     */
+    private int mRequestCode;
+
+    /**
+     * 获取当前Activity视图ID
+     */
+    protected abstract int getLayoutId();
+
+    /**
+     * 获取当前Activity标题
+     * @return
+     */
+    protected abstract String getTitleText();
+
+    /**
+     *  初始化数据
+     */
+    protected abstract void initData();
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContext = this;
+
+        setContentView(R.layout.activity_base);
+        mToolbar = (Toolbar) findViewById(R.id.base_toolbar);
+        tvTitle = (TextView) findViewById(R.id.tv_toolbar_title);
+        mViewStub = (ViewStub) findViewById(R.id.base_viewstub);
+        mToolbar.setTitle("");
+        setSupportActionBar(mToolbar);
+
+        //设置视图
+        int layoutId = getLayoutId();
+        if(layoutId > 0) {
+            mViewStub.setLayoutResource(getLayoutId());
+            mViewStub.inflate();
+        }
+        //设置标题
+        String titleText = getTitleText();
+        if(isNotEmptyString(titleText)) {
+            tvTitle.setText(getTitleText());
+        }
+        //绑定注解类
+        ButterKnife.bind(this);
+        //初始化数据
+        initData();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                onBackPressed();
+                break;
+        }
+        return true;
+    }
+
+    /**
+     * 获取当前Activity上下文
+     * @return
+     */
+    protected Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * 设置标题栏左边Icon
+     * @param resId
+     */
+    protected void setToolbarLeftIcon(int resId) {
+        if(resId > 0) {
+            mToolbar.setNavigationIcon(resId);
+            getSupportActionBar().setHomeButtonEnabled(true);
+            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        } else {
+            mToolbar.setNavigationIcon(null);
+            getSupportActionBar().setHomeButtonEnabled(false);
+            getSupportActionBar().setDisplayHomeAsUpEnabled(false);
+        }
+    }
+
+    /**
+     * 请求权限
+     * @param permissions 需要的权限列表
+     * @param requestCode 请求码
+     */
+    protected void requestPermission(String[] permissions, int requestCode) {
+        this.mRequestCode = requestCode;
+        if(checkPermissions(permissions)) {
+            permissionSuccess(mRequestCode);
+        } else {
+            List<String> needPermissions = getDeniedPermissions(permissions);
+            ActivityCompat.requestPermissions(this, needPermissions.toArray(new String[needPermissions.size()]), mRequestCode);
+        }
+    }
+
+    /**
+     * 检查所需的权限是否都已授权
+     * @param permissions
+     * @return
+     */
+    private boolean checkPermissions(String[] permissions) {
+        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            return true;
+        }
+
+        for(String permission : permissions) {
+            if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 获取所需权限列表中需要申请权限的列表
+     * @param permissions
+     * @return
+     */
+    private List<String> getDeniedPermissions(String[] permissions) {
+        List<String> needRequestPermissionList = new ArrayList<>();
+        for(String permission : permissions) {
+            if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
+                    || ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+                needRequestPermissionList.add(permission);
+            }
+        }
+        return  needRequestPermissionList;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        //系统请求权限回调
+        if(requestCode == mRequestCode) {
+            if(verifyPermissions(grantResults)) {
+                permissionSuccess(mRequestCode);
+            } else {
+                permissionFail(mRequestCode);
+                showPermissionTipsDialog();
+            }
+        }
+    }
+
+    /**
+     * 确认所需权限是否都已授权
+     * @param grantResults
+     * @return
+     */
+    private boolean verifyPermissions(int[] grantResults) {
+        for(int grantResult : grantResults) {
+            if(grantResult != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 显示权限提示对话框
+     */
+    private void showPermissionTipsDialog() {
+        showTipsDialogWithTitle("提示", "当前应用缺少必要权限,该功能暂时无法使用。如若需要,请点击【确定】按钮前往设置中心进行权限授权", new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                startAppSettings();
+            }
+        }, null);
+    }
+
+    /**
+     * 权限请求成功
+     * @param requestCode
+     */
+    protected void permissionSuccess(int requestCode) {
+        LogUtils.e("获取权限成功:" + requestCode);
+    }
+
+    /**
+     * 权限请求失败
+     * @param requestCode
+     */
+    protected void permissionFail(int requestCode) {
+        LogUtils.e("获取权限失败:" + requestCode);
+    }
+
+    /**
+     * 启动当前应用设置页面
+     */
+    private void startAppSettings() {
+        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+        intent.setData(Uri.parse("package:" + getPackageName()));
+        startActivity(intent);
+    }
+
+    /**
+     * 弹出Toast提示
+     * @param text 提示内容
+     */
+    protected void toast(String text) {
+        if(this.isFinishing()) {
+            return;
+        }
+
+        if(!TextUtils.isEmpty(text)) {
+            Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    /**
+     * 显示提示对话框
+     * @param content 内容
+     * @param confirmListener 确定按钮点击事件
+     */
+    protected void showTipsDialog(String content, DialogInterface.OnClickListener confirmListener) {
+        showTipsDialogWithTitle(null, content, getString(R.string.confirm), confirmListener, null, null);
+    }
+
+    /**
+     * 显示提示对话框
+     * @param content 内容
+     * @param confirmText 确定按钮文字
+     * @param confirmListener 确定按钮点击事件
+     * @param cancelText 取消按钮文字
+     * @param cancelListener 取消按钮点击事件
+     */
+    protected void showTipsDialog(String content, String confirmText, DialogInterface.OnClickListener confirmListener, String cancelText, DialogInterface.OnClickListener cancelListener) {
+        showTipsDialogWithTitle("", content, confirmText, confirmListener, cancelText, cancelListener);
+    }
+
+    /**
+     * 显示提示对话框(带标题)
+     * @param title 标题
+     * @param content 内容
+     * @param confirmListener 确定按钮点击事件
+     * @param cancelListener 取消按钮点击事件
+     */
+    protected void showTipsDialogWithTitle(String title, String content, DialogInterface.OnClickListener confirmListener, DialogInterface.OnClickListener cancelListener) {
+        showTipsDialogWithTitle(title, content, getString(R.string.confirm), confirmListener, getString(R.string.cancel), cancelListener);
+    }
+
+    /**
+     * 显示提示对话框(带标题)
+     * @param title 标题
+     * @param content 内容
+     * @param confirmText 确定按钮文字
+     * @param confirmListener 确定按钮点击事件
+     * @param cancelText 取消按钮文字
+     * @param cancelListener 取消按钮点击事件
+     */
+    protected void showTipsDialogWithTitle(String title, String content, String confirmText, DialogInterface.OnClickListener confirmListener, String cancelText, DialogInterface.OnClickListener cancelListener) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        if(isNotEmptyString(title)) {
+            builder.setTitle(title);
+        }
+        builder.setMessage(content);
+        builder.setPositiveButton(confirmText, confirmListener);
+        if(isNotEmptyString(cancelText)) {
+            builder.setNegativeButton(cancelText, cancelListener);
+        }
+        builder.create().show();
+    }
+
+    protected void pushActivity(Class<?> mClass) {
+        startActivity(new Intent(mContext, mClass));
+    }
+
+    /**
+     * 判断字符串是否不为空
+     * @param text
+     * @return
+     */
+    protected boolean isNotEmptyString(String text) {
+        return !TextUtils.isEmpty(text) && !text.equals("null");
+    }
+
+    /**
+     * 判断字符串是否为空
+     * @param text
+     * @return
+     */
+    protected boolean isEmptyString(String text) {
+        return TextUtils.isEmpty(text) || text.equals("null");
+    }
+
+    /**
+     * 判断列表是否为空
+     * @param list
+     * @return
+     */
+    protected boolean isEmptyList(List<?> list) {
+        return list == null || list.size() <= 0;
+    }
+
+}

+ 19 - 0
Android/app/src/main/java/com/example/socketdemo/base/BaseTransfer.java

@@ -0,0 +1,19 @@
+package com.example.socketdemo.base;
+
+/**
+ * Created by AA on 2017/3/24.
+ */
+public abstract class BaseTransfer implements Transferable {
+
+    /**
+     * 字节数组长度
+     */
+    public static final int BYTE_SIZE_HEADER = 1024 * 10;
+    public static final int BYTE_SIZE_DATA = 1024 * 4;
+
+    /**
+     * 传输字节类型
+     */
+    public static final String UTF_8 = "UTF-8";
+
+}

+ 25 - 0
Android/app/src/main/java/com/example/socketdemo/base/Transferable.java

@@ -0,0 +1,25 @@
+package com.example.socketdemo.base;
+
+/**
+ * Created by AA on 2017/3/24.
+ */
+public interface Transferable {
+
+    /**
+     * 初始化
+     * @throws Exception
+     */
+    void init() throws Exception;
+
+    /**
+     * 发送/接收文件实体数据
+     * @throws Exception
+     */
+    void parseBody() throws Exception;
+
+    /**
+     * 发送/接收完毕
+     * @throws Exception
+     */
+    void finishTransfer() throws Exception;
+}

+ 146 - 0
Android/app/src/main/java/com/example/socketdemo/bean/FileInfo.java

@@ -0,0 +1,146 @@
+package com.example.socketdemo.bean;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Created by AA on 2017/3/23.
+ */
+public class FileInfo implements Serializable {
+
+    /**
+     * 文件传输结果:1 成功  -1 失败
+     */
+    public static final int FLAG_SUCCESS = 1;
+    public static final int FLAG_FAILURE = -1;
+
+
+    /**
+     * 文件路径
+     */
+    private String filePath;
+
+    /**
+     * 文件类型
+     */
+    private int fileType;
+
+    /**
+     * 文件大小
+     */
+    private long size;
+
+    /***
+     * 文件名
+     */
+    private String fileName;
+
+    /**
+     * 文件传送结果
+     */
+    private int result;
+
+    /**
+     * 传输进度
+     */
+    private int progress;
+
+
+    private int position;
+
+
+    public FileInfo(int position, String filePath, long size) {
+        this.position = position;
+        this.filePath = filePath;
+        this.size = size;
+    }
+
+    public FileInfo() {
+
+    }
+
+    public String getFilePath() {
+        return filePath;
+    }
+
+    public void setFilePath(String filePath) {
+        this.filePath = filePath;
+    }
+
+    public int getFileType() {
+        return fileType;
+    }
+
+    public void setFileType(int fileType) {
+        this.fileType = fileType;
+    }
+
+    public long getSize() {
+        return size;
+    }
+
+    public void setSize(long size) {
+        this.size = size;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public int getResult() {
+        return result;
+    }
+
+    public void setResult(int result) {
+        this.result = result;
+    }
+
+    public int getProgress() {
+        return progress;
+    }
+
+    public void setProgress(int progress) {
+        this.progress = progress;
+    }
+
+    public int getPosition() {
+        return position;
+    }
+
+    public void setPosition(int position) {
+        this.position = position;
+    }
+
+    public static String toJsonStr(FileInfo fileInfo) {
+        return new Gson().toJson(fileInfo);
+    }
+
+    public static String toJsonStr(List<FileInfo> fileInfoList) {
+        return new Gson().toJson(fileInfoList);
+    }
+
+    public static FileInfo toObject(String jsonStr) {
+        return new Gson().fromJson(jsonStr, FileInfo.class);
+    }
+
+    public static List<FileInfo> toObjectList(String jsonStr) {
+        return new Gson().fromJson(jsonStr, new TypeToken<List<FileInfo>>(){}.getType());
+    }
+
+    @Override
+    public String toString() {
+        return "FileInfo:{" +
+                "filePath='" + filePath + '\'' +
+                ", fileType=" + fileType +
+                ", size=" + size +
+                ", position=" + position +
+                '}';
+    }
+}

+ 67 - 0
Android/app/src/main/java/com/example/socketdemo/common/Consts.java

@@ -0,0 +1,67 @@
+package com.example.socketdemo.common;
+
+import com.example.socketdemo.bean.FileInfo;
+
+import java.util.Comparator;
+import java.util.Map;
+
+/**
+ * Created by AA on 2017/3/22.
+ */
+public class Consts {
+
+    public static final String KEY_EXIT = "exit";
+    public static final String KEY_IPPORT_INFO = "ipport_info";
+
+    /**
+     * 最大尝试次数
+     */
+    public static final int DEFAULT_TRY_COUNT = 10;
+
+    /**
+     * WiFi连接成功时未分配的默认IP地址
+     */
+    public static final String DEFAULT_UNKNOW_IP = "0.0.0.0";
+
+    /**
+     * UDP通信服务端默认端口号
+     */
+    public static final int DEFAULT_SERVER_UDP_PORT = 8204;
+
+    /**
+     * 文件接收端监听默认端口号
+     */
+    public static final int DEFAULT_FILE_RECEIVE_SERVER_PORT = 8284;
+
+    /**
+     * UDP通知:文件接收端初始化
+     */
+    public static final String MSG_FILE_RECEIVER_INIT = "MSG_FILE_RECEIVER_INIT";
+
+    /**
+     * UDP通知:文件接收端初始化完毕
+     */
+    public static final String MSG_FILE_RECEIVER_INIT_SUCCESS = "MSG_FILE_RECEIVER_INIT_SUCCESS";
+
+    /**
+     * UDP通知:开始发送文件
+     */
+    public static final String MSG_START_SEND = "MSG_START_SEND";
+
+
+
+    public static final Comparator<Map.Entry<String, FileInfo>> DEFAULT_COMPARATOR = new Comparator<Map.Entry<String, FileInfo>>() {
+        @Override
+        public int compare(Map.Entry<String, FileInfo> o1, Map.Entry<String, FileInfo> o2) {
+            if(o1.getValue().getPosition() > o2.getValue().getPosition()) {
+                return 1;
+            } else if(o1.getValue().getPosition() < o2.getValue().getPosition()) {
+                return -1;
+            } else {
+                return 0;
+            }
+        }
+    };
+
+
+}

+ 225 - 0
Android/app/src/main/java/com/example/socketdemo/common/FileReceiver.java

@@ -0,0 +1,225 @@
+package com.example.socketdemo.common;
+
+import com.example.socketdemo.base.BaseTransfer;
+import com.example.socketdemo.bean.FileInfo;
+import com.example.socketdemo.utils.FileUtils;
+import com.example.socketdemo.utils.LogUtils;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * Created by AA on 2017/3/24.
+ */
+public class FileReceiver extends BaseTransfer implements Runnable {
+
+    /**
+     * 接收文件的Socket的输入输出流
+     */
+    private Socket mSocket;
+    private InputStream mInputStream;
+
+    /**
+     * 待接收的文件数据
+     */
+    private FileInfo mFileInfo;
+
+    /**
+     * 用来控制线程暂停、恢复
+     */
+    private final Object LOCK = new Object();
+    private boolean mIsPaused = false;
+
+    /**
+     * 设置未执行线程的不执行标识
+     */
+    private boolean mIsStop;
+
+    /**
+     * 该线程是否执行完毕
+     */
+    private boolean mIsFinish;
+
+    /**
+     * 文件接收监听事件
+     */
+    private OnReceiveListener mOnReceiveListener;
+
+
+    public FileReceiver(Socket socket, FileInfo fileInfo) {
+        mSocket = socket;
+        mFileInfo = fileInfo;
+    }
+
+    /**
+     * 设置接收监听事件
+     * @param onReceiveListener
+     */
+    public void setOnReceiveListener(OnReceiveListener onReceiveListener) {
+        mOnReceiveListener = onReceiveListener;
+    }
+
+    @Override
+    public void run() {
+        if(mIsStop) {
+            return;
+        }
+
+        //初始化
+        try {
+            if(mOnReceiveListener != null) {
+                mOnReceiveListener.onStart();
+            }
+            init();
+        } catch (Exception e) {
+            e.printStackTrace();
+            LogUtils.i("FileReceiver init() ------->>> occur expection");
+            if(mOnReceiveListener != null) {
+                mOnReceiveListener.onFailure(e, mFileInfo);
+            }
+        }
+
+        //发送文件实体数据
+        try {
+            parseBody();
+        } catch (Exception e) {
+            e.printStackTrace();
+            LogUtils.i("FileReceiver parseBody() ------->>> occur expection");
+            if(mOnReceiveListener != null) {
+                mOnReceiveListener.onFailure(e, mFileInfo);
+            }
+        }
+
+        //文件传输完毕
+        try {
+            finishTransfer();
+        } catch (Exception e) {
+            e.printStackTrace();
+            LogUtils.i("FileReceiver finishTransfer() ------->>> occur expection");
+            if(mOnReceiveListener != null) {
+                mOnReceiveListener.onFailure(e, mFileInfo);
+            }
+        }
+    }
+
+    @Override
+    public void init() throws Exception {
+        if(mSocket != null) {
+            mInputStream = mSocket.getInputStream();
+        }
+    }
+
+    @Override
+    public void parseBody() throws Exception {
+        if(mFileInfo == null) {
+            return;
+        }
+
+        long fileSize = mFileInfo.getSize();
+        OutputStream fos = new FileOutputStream(FileUtils.gerateLocalFile(mFileInfo.getFilePath()));
+
+        byte[] bytes = new byte[BYTE_SIZE_DATA];
+        long total = 0;
+        int len = 0;
+
+        long sTime = System.currentTimeMillis();
+        long eTime = 0;
+        while ((len = mInputStream.read(bytes)) != -1) {
+            synchronized (LOCK) {
+                if(mIsPaused) {
+                    try {
+                        LOCK.wait();
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+
+                //写入文件
+                fos.write(bytes, 0, len);
+                total = total + len;
+
+                //每隔200毫秒返回一次进度
+                eTime = System.currentTimeMillis();
+                if(eTime - sTime > 200) {
+                    sTime = eTime;
+                    if(mOnReceiveListener != null) {
+                        mOnReceiveListener.onProgress(mFileInfo, total, fileSize);
+                    }
+                }
+            }
+        }
+
+        //文件接收成功
+        if(mOnReceiveListener != null) {
+            mOnReceiveListener.onSuccess(mFileInfo);
+        }
+        mIsFinish = true;
+    }
+
+    @Override
+    public void finishTransfer() throws Exception {
+        if(mInputStream != null) {
+            try {
+                mInputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+
+        if(mSocket != null && mSocket.isConnected()) {
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 暂停接收线程
+     */
+    public void pause() {
+        synchronized (LOCK) {
+            mIsPaused = true;
+            LOCK.notifyAll();
+        }
+    }
+
+    /**
+     * 恢复接收线程
+     */
+    public void resume() {
+        synchronized (LOCK) {
+            mIsPaused = false;
+            LOCK.notifyAll();
+        }
+    }
+
+    /**
+     * 设置当前的接收任务不执行
+     */
+    public void stop() {
+        mIsStop = true;
+    }
+
+    /**
+     * 文件是否在接收中
+     * @return
+     */
+    public boolean isRunning() {
+        return !mIsFinish;
+    }
+
+    /**
+     * 文件接收监听事件
+     */
+    public interface OnReceiveListener {
+        void onStart();
+        void onProgress(FileInfo fileInfo, long progress, long total);
+        void onSuccess(FileInfo fileInfo);
+        void onFailure(Throwable throwable, FileInfo fileInfo);
+    }
+}

+ 228 - 0
Android/app/src/main/java/com/example/socketdemo/common/FileSender.java

@@ -0,0 +1,228 @@
+package com.example.socketdemo.common;
+
+import android.content.Context;
+
+import com.example.socketdemo.base.BaseTransfer;
+import com.example.socketdemo.bean.FileInfo;
+import com.example.socketdemo.utils.LogUtils;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * Created by AA on 2017/3/24.
+ */
+public class FileSender extends BaseTransfer implements Runnable {
+
+    private Context mContext;
+
+    /**
+     * 待发送的文件数据
+     */
+    private FileInfo mFileInfo;
+
+    /**
+     * 传送文件的Socket输入输出流
+     */
+    private Socket mSocket;
+    private OutputStream mOutputStream;
+
+    /**
+     * 用来控制线程暂停、恢复
+     */
+    private final Object LOCK = new Object();
+    private boolean mIsPause;
+
+    /**
+     * 该线程是否执行完毕
+     */
+    private boolean mIsFinish;
+
+    /**
+     * 设置未执行线程的不执行标识
+     */
+    private boolean mIsStop;
+
+    /**
+     * 文件传送监听事件
+     */
+    private OnSendListener mOnSendListener;
+
+
+    public FileSender(Context context, Socket socket, FileInfo fileInfo) {
+        mContext = context;
+        mSocket = socket;
+        mFileInfo = fileInfo;
+    }
+
+    /**
+     * 设置发送监听事件
+     * @param onSendListener
+     */
+    public void setOnSendListener(OnSendListener onSendListener) {
+        mOnSendListener = onSendListener;
+    }
+
+    @Override
+    public void run() {
+        if(mIsStop) {
+            return;
+        }
+
+        //初始化
+        try {
+            if(mOnSendListener != null) {
+                mOnSendListener.onStart();
+            }
+            init();
+        } catch (Exception e) {
+            e.printStackTrace();
+            LogUtils.i("FileSender init() ------->>> occur expection");
+            if(mOnSendListener != null) {
+                mOnSendListener.onFailure(e, mFileInfo);
+            }
+        }
+
+        //发送文件实体数据
+        try {
+            parseBody();
+        } catch (Exception e) {
+            e.printStackTrace();
+            LogUtils.i("FileSender parseBody() ------->>> occur expection");
+            if(mOnSendListener != null) {
+                mOnSendListener.onFailure(e, mFileInfo);
+            }
+        }
+
+        //文件传输完毕
+        try {
+            finishTransfer();
+        } catch (Exception e) {
+            e.printStackTrace();
+            LogUtils.i("FileSender finishTransfer() ------->>> occur expection");
+            if(mOnSendListener != null) {
+                mOnSendListener.onFailure(e, mFileInfo);
+            }
+        }
+    }
+
+    @Override
+    public void init() throws Exception {
+        mSocket.setSoTimeout(30 * 1000);
+        OutputStream os = mSocket.getOutputStream();
+        mOutputStream = new BufferedOutputStream(os);
+    }
+
+    @Override
+    public void parseBody() throws Exception {
+        long fileSize = mFileInfo.getSize();
+        File file = new File(mFileInfo.getFilePath());
+        InputStream fis = new FileInputStream(file);
+
+        int len = 0;
+        long total = 0;
+        byte[] bytes = new byte[BYTE_SIZE_DATA];
+
+        long sTime = System.currentTimeMillis();
+        long eTime = 0;
+        while ((len = fis.read(bytes)) != -1) {
+            synchronized (LOCK) {
+                if(mIsPause) {
+                    try {
+                        LOCK.wait();
+                    } catch (InterruptedException e) {
+                        e.printStackTrace();
+                    }
+                }
+
+                //写入文件
+                mOutputStream.write(bytes, 0, len);
+                total += len;
+
+                //每隔200毫秒返回一次进度
+                eTime = System.currentTimeMillis();
+                if(eTime - sTime > 200) {
+                    sTime = eTime;
+                    if(mOnSendListener != null) {
+                        mOnSendListener.onProgress(total, fileSize);
+                    }
+                }
+            }
+        }
+
+        //关闭Socket输入输出流
+        mOutputStream.flush();
+        mOutputStream.close();
+        //文件发送成功
+        if(mOnSendListener != null) {
+            mOnSendListener.onSuccess(mFileInfo);
+        }
+        mIsFinish = true;
+    }
+
+    @Override
+    public void finishTransfer() throws Exception {
+        if(mOutputStream != null) {
+            try {
+                mOutputStream.close();
+            } catch (IOException e) {
+
+            }
+        }
+
+        if(mSocket != null && mSocket.isConnected()) {
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * 暂停发送线程
+     */
+    public void pause() {
+        synchronized (LOCK) {
+            mIsPause = true;
+            LOCK.notifyAll();
+        }
+    }
+
+    /**
+     * 恢复发送线程
+     */
+    public void resume() {
+        synchronized (LOCK) {
+            mIsPause = false;
+            LOCK.notifyAll();
+        }
+    }
+
+    /**
+     * 设置当前的发送任务不执行
+     */
+    public void stop() {
+        mIsStop = true;
+    }
+
+    /**
+     * 文件是否在发送中
+     * @return
+     */
+    public boolean isRunning() {
+        return !mIsFinish;
+    }
+
+    public interface OnSendListener {
+        void onStart();
+        void onProgress(long progress, long total);
+        void onSuccess(FileInfo fileInfo);
+        void onFailure(Throwable throwable, FileInfo fileInfo);
+    }
+}

+ 52 - 0
Android/app/src/main/java/com/example/socketdemo/common/SpaceItemDecoration.java

@@ -0,0 +1,52 @@
+package com.example.socketdemo.common;
+
+import android.graphics.Rect;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * Created by AA on 2017/3/29.
+ */
+public class SpaceItemDecoration extends RecyclerView.ItemDecoration{
+
+    private int space;
+
+    public SpaceItemDecoration(int space) {
+        this.space = space;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
+        //判断总的数量是否可以整除
+        int totalCount = layoutManager.getItemCount();
+        int surplusCount = totalCount % layoutManager.getSpanCount();
+        int childPosition = parent.getChildAdapterPosition(view);
+        if (layoutManager.getOrientation() == GridLayoutManager.VERTICAL) {//竖直方向的
+            if (surplusCount == 0 && childPosition > totalCount - layoutManager.getSpanCount() - 1) {
+                //后面几项需要bottom
+                outRect.bottom = space;
+            } else if (surplusCount != 0 && childPosition > totalCount - surplusCount - 1) {
+                outRect.bottom = space;
+            }
+            if ((childPosition + 1) % layoutManager.getSpanCount() == 0) {//被整除的需要右边
+                outRect.right = space;
+            }
+            outRect.top = space;
+            outRect.left = space;
+        } else {
+            if (surplusCount == 0 && childPosition > totalCount - layoutManager.getSpanCount() - 1) {
+                //后面几项需要右边
+                outRect.right = space;
+            } else if (surplusCount != 0 && childPosition > totalCount - surplusCount - 1) {
+                outRect.right = space;
+            }
+            if ((childPosition + 1) % layoutManager.getSpanCount() == 0) {//被整除的需要下边
+                outRect.bottom = space;
+            }
+            outRect.top = space;
+            outRect.left = space;
+        }
+    }
+}

+ 33 - 0
Android/app/src/main/java/com/example/socketdemo/receiver/HotSpotBroadcaseReceiver.java

@@ -0,0 +1,33 @@
+package com.example.socketdemo.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+
+/**
+ * Created by AA on 2017/3/23.
+ */
+public abstract class HotSpotBroadcaseReceiver extends BroadcastReceiver {
+
+    public static final String ACTION_HOTSPOT_STATE_CHANGED = "android.net.wifi.WIFI_AP_STATE_CHANGED";
+
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if(action.equals(ACTION_HOTSPOT_STATE_CHANGED)) {
+            //便携热点状态监听
+            int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0);
+            if(WifiManager.WIFI_STATE_ENABLED == state % 10) {
+                //便携热点可用
+                onHotSpotEnabled();
+            }
+        }
+    }
+
+    /**
+     * 便携热点可用
+     */
+    public abstract void onHotSpotEnabled();
+}

+ 66 - 0
Android/app/src/main/java/com/example/socketdemo/receiver/WifiBroadcaseReceiver.java

@@ -0,0 +1,66 @@
+package com.example.socketdemo.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkInfo;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+
+import com.example.socketdemo.wifitools.WifiMgr;
+
+import java.util.List;
+
+/**
+ * Created by AA on 2017/3/24.
+ */
+public abstract class WifiBroadcaseReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if(intent != null) {
+            if(intent.getAction().equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
+                //监听WiFi开启/关闭事件
+                int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0);
+                if(wifiState == WifiManager.WIFI_STATE_ENABLED) {
+                    //WiFi已开启
+                    onWifiEnabled();
+                } else if(wifiState == WifiManager.WIFI_STATE_DISABLED) {
+                    //WiFi已关闭
+                    onWifiDisabled();
+                }
+            } else if(intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                WifiMgr wifiMgr = new WifiMgr(context);
+                List<ScanResult> scanResults = wifiMgr.getScanResults();
+                if(wifiMgr.isWifiEnabled() && scanResults != null && scanResults.size() > 0) {
+                    //成功扫描
+                    onScanResultsAvailable(scanResults);
+                }
+            } else if(intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+                //网络状态改变的广播
+                NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+                if (info != null) {
+                    if (info.getState().equals(NetworkInfo.State.CONNECTED)) {
+                        //WiFi已连接
+                        WifiMgr wifiMgr = new WifiMgr(context);
+                        String connectedSSID = wifiMgr.getConnectedSSID();
+                        onWifiConnected(connectedSSID);
+                    } else if (info.getState().equals(NetworkInfo.State.DISCONNECTED)) {
+                        //WiFi已断开连接
+                        onWifiDisconnected();
+                    }
+                }
+            }
+        }
+    }
+
+    public abstract void onWifiEnabled();
+
+    public abstract void onWifiDisabled();
+
+    public abstract void onScanResultsAvailable(List<ScanResult> scanResults);
+
+    public abstract void onWifiConnected(String connectedSSID);
+
+    public abstract void onWifiDisconnected();
+}

+ 89 - 0
Android/app/src/main/java/com/example/socketdemo/utils/FileUtils.java

@@ -0,0 +1,89 @@
+package com.example.socketdemo.utils;
+
+import android.os.Environment;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.text.DecimalFormat;
+
+/**
+ * Created by AA on 2017/3/24.
+ */
+public class FileUtils {
+
+    public static final String ROOT_PATH = Environment.getExternalStorageDirectory() + File.separator + "socketDemo/";
+
+
+    /**
+     * 根据文件路径获取文件名称
+     * @param filePath
+     * @return
+     */
+    public static String getFileName(String filePath) {
+        if(TextUtils.isEmpty(filePath)) {
+            return "";
+        }
+        return filePath.substring(filePath.lastIndexOf(File.separator) + 1);
+    }
+
+    /**
+     * 生成本地文件路径
+     * @param filePath
+     * @return
+     */
+    public static File gerateLocalFile(String filePath) {
+        String fileNmae = getFileName(filePath);
+        File dirFile = new File(ROOT_PATH);
+        if(!dirFile.exists()) {
+            dirFile.mkdirs();
+        }
+        return new File(dirFile, fileNmae);
+    }
+
+    /**
+     * 转换文件大小
+     *
+     * @param fileSize
+     * @return
+     */
+    public static String FormetFileSize(long fileSize) {
+        if(fileSize <= 0) {
+            return "0KB";
+        }
+
+        DecimalFormat df = new DecimalFormat("#.00");
+        String fileSizeString = "";
+        if (fileSize < 1024) {
+            fileSizeString = df.format((double) fileSize) + "B";
+        } else if (fileSize < 1048576) {
+            fileSizeString = df.format((double) fileSize / 1024) + "K";
+        } else if (fileSize < 1073741824) {
+            fileSizeString = df.format((double) fileSize / 1048576) + "M";
+        } else {
+            fileSizeString = df.format((double) fileSize / 1073741824) + "G";
+        }
+        return fileSizeString;
+    }
+
+    /**
+     * 取得文件大小
+     *
+     * @param f
+     * @return
+     * @throws Exception
+     */
+    @SuppressWarnings("resource")
+    public static long getFileSizes(File f) throws Exception {
+        long size = 0;
+        if (f.exists()) {
+            FileInputStream fis = null;
+            fis = new FileInputStream(f);
+            size = fis.available();
+        } else {
+            f.createNewFile();
+        }
+        return size;
+    }
+
+}

+ 60 - 0
Android/app/src/main/java/com/example/socketdemo/utils/LogUtils.java

@@ -0,0 +1,60 @@
+package com.example.socketdemo.utils;
+
+import android.util.Log;
+
+/**
+ * Log统一管理类
+ */
+public class LogUtils {
+
+    private LogUtils() {
+        /* cannot be instantiated */
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    // 是否需要打印bug,可以在application的onCreate函数里面初始化
+    public static boolean isDebug = true;
+    private static final String TAG = "FUXING_LOG";
+
+    // 下面四个是默认tag的函数
+    public static void i(String msg) {
+        if (isDebug)
+            Log.i(TAG, msg);
+    }
+
+    public static void d(String msg) {
+        if (isDebug)
+            Log.d(TAG, msg);
+    }
+
+    public static void e(String msg) {
+        if (isDebug)
+            Log.e(TAG, msg);
+    }
+
+    public static void v(String msg) {
+        if (isDebug)
+            Log.v(TAG, msg);
+    }
+
+    // 下面是传入自定义tag的函数
+    public static void i(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+    public static void d(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+    public static void e(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+
+    public static void v(String tag, String msg) {
+        if (isDebug)
+            Log.i(tag, msg);
+    }
+}

+ 31 - 0
Android/app/src/main/java/com/example/socketdemo/utils/NetUtils.java

@@ -0,0 +1,31 @@
+package com.example.socketdemo.utils;
+
+import java.io.IOException;
+
+/**
+ * Created by AA on 2017/3/23.
+ */
+public class NetUtils {
+
+    /**
+     * 是否可以ping通指定IP地址
+     * @param ipAddress
+     * @return
+     */
+    public static boolean pingIpAddress(String ipAddress) {
+        try {
+            Process process = Runtime.getRuntime().exec("/system/bin/ping -c 1 -w 100 " + ipAddress);
+            int status = process.waitFor();
+            if (status == 0) {
+                return true;
+            } else {
+                return false;
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+}

+ 132 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/ApMgr.java

@@ -0,0 +1,132 @@
+package com.example.socketdemo.wifitools;
+
+import android.content.Context;
+import android.net.DhcpInfo;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+
+import java.lang.reflect.Method;
+
+/**
+ * Created by AA on 2017/3/22.
+ */
+public class ApMgr {
+
+    /**
+     * 便携热点是否开启
+     * @param context 上下文
+     * @return
+     */
+    public static boolean isApOn(Context context) {
+        WifiManager wifimanager = (WifiManager) context.getSystemService(context.WIFI_SERVICE);
+        try {
+            Method method = wifimanager.getClass().getDeclaredMethod("isWifiApEnabled");
+            method.setAccessible(true);
+            return (Boolean) method.invoke(wifimanager);
+        } catch (Throwable ignored) {}
+        return false;
+    }
+
+    /**
+     * 关闭Wi-Fi
+     * @param context 上下文
+     */
+    public static void closeWifi(Context context) {
+        WifiManager wifimanager = (WifiManager) context.getSystemService(context.WIFI_SERVICE);
+        if (wifimanager.isWifiEnabled()) {
+            wifimanager.setWifiEnabled(false);
+        }
+    }
+
+    /**
+     * 开启便携热点
+     * @param context 上下文
+     * @param SSID 便携热点SSID
+     * @param password 便携热点密码
+     * @return
+     */
+    public static boolean openAp(Context context, String SSID, String password) {
+        if(TextUtils.isEmpty(SSID)) {
+            return false;
+        }
+
+        WifiManager wifimanager = (WifiManager) context.getSystemService(context.WIFI_SERVICE);
+        if (wifimanager.isWifiEnabled()) {
+            wifimanager.setWifiEnabled(false);
+        }
+
+        WifiConfiguration wifiConfiguration = getApConfig(SSID, password);
+        try {
+            if(isApOn(context)) {
+                wifimanager.setWifiEnabled(false);
+                closeAp(context);
+            }
+
+            //使用反射开启Wi-Fi热点
+            Method method = wifimanager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class);
+            method.invoke(wifimanager, wifiConfiguration, true);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+    /**
+     * 关闭便携热点
+     * @param context 上下文
+     */
+    public static void closeAp(Context context) {
+        WifiManager wifimanager = (WifiManager) context.getSystemService(context.WIFI_SERVICE);
+        try {
+            Method method = wifimanager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class);
+            method.invoke(wifimanager, null, false);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取开启便携热点后自身热点IP地址
+     * @param context
+     * @return
+     */
+    public static String getHotspotLocalIpAddress(Context context) {
+        WifiManager wifimanager = (WifiManager) context.getSystemService(context.WIFI_SERVICE);
+        DhcpInfo dhcpInfo = wifimanager.getDhcpInfo();
+        if(dhcpInfo != null) {
+            int address = dhcpInfo.serverAddress;
+            return ((address & 0xFF)
+                    + "." + ((address >> 8) & 0xFF)
+                    + "." + ((address >> 16) & 0xFF)
+                    + "." + ((address >> 24) & 0xFF));
+        }
+        return null;
+    }
+
+    /**
+     * 设置有密码的热点信息
+     * @param SSID 便携热点SSID
+     * @param pwd 便携热点密码
+     * @return
+     */
+    private static WifiConfiguration getApConfig(String SSID, String pwd) {
+        if(TextUtils.isEmpty(pwd)) {
+            return null;
+        }
+
+        WifiConfiguration config = new WifiConfiguration();
+        config.SSID = SSID;
+        config.preSharedKey = pwd;
+//        config.hiddenSSID = true;
+        config.status = WifiConfiguration.Status.ENABLED;
+        config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+        config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+        config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
+        config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
+        config.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
+        return config;
+    }
+}

+ 47 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/ChangingAwareEditText.java

@@ -0,0 +1,47 @@
+/*
+ * Wifi Connecter
+ * 
+ * Copyright (c) 2011 Kevin Yuan (farproc@gmail.com)
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ * 
+ **/ 
+
+package com.example.socketdemo.wifitools;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+public class ChangingAwareEditText extends EditText {
+
+	public ChangingAwareEditText(Context context, AttributeSet attrs) {
+		super(context, attrs);
+	}
+	
+	private boolean mChanged = false;
+	
+	public boolean getChanged() {
+		return mChanged;
+	}
+	
+	protected void onTextChanged (CharSequence text, int start, int before, int after) {
+		mChanged = true;
+	}
+}

+ 34 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/ConfigurationSecurities.java

@@ -0,0 +1,34 @@
+
+package com.example.socketdemo.wifitools;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+
+public abstract class ConfigurationSecurities {
+	/**
+     * @return The security of a given {@link WifiConfiguration}.
+     */
+	public abstract String getWifiConfigurationSecurity(WifiConfiguration wifiConfig);
+	/**
+     * @return The security of a given {@link ScanResult}.
+     */
+	public abstract String getScanResultSecurity(ScanResult scanResult);
+	/**
+     * Fill in the security fields of WifiConfiguration config.
+     * @param config The object to fill.
+     * @param security If is OPEN, password is ignored.
+     * @param password Password of the network if security is not OPEN.
+     */
+	public abstract void setupSecurity(WifiConfiguration config, String security, final String password);
+	public abstract String getDisplaySecirityString(final ScanResult scanResult);
+	public abstract boolean isOpenNetwork(final String security);
+	
+	public static ConfigurationSecurities newInstance() {
+		if(Version.SDK < 8) {
+			return new ConfigurationSecuritiesOld();
+		} else {
+			return new ConfigurationSecuritiesV8();
+		}
+	}
+	
+}

+ 186 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/ConfigurationSecuritiesOld.java

@@ -0,0 +1,186 @@
+
+package com.example.socketdemo.wifitools;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.AuthAlgorithm;
+import android.net.wifi.WifiConfiguration.GroupCipher;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.net.wifi.WifiConfiguration.PairwiseCipher;
+import android.net.wifi.WifiConfiguration.Protocol;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class ConfigurationSecuritiesOld extends ConfigurationSecurities {
+	
+	// Constants used for different security types
+	public static final String WPA2 = "WPA2";
+	public static final String WPA = "WPA";
+	public static final String WEP = "WEP";
+	public static final String OPEN = "Open";
+    // For EAP Enterprise fields
+    public static final String WPA_EAP = "WPA-EAP";
+    public static final String IEEE8021X = "IEEE8021X";
+
+    public static final String[] EAP_METHOD = { "PEAP", "TLS", "TTLS" };
+    
+    public static final int WEP_PASSWORD_AUTO = 0;
+    public static final int WEP_PASSWORD_ASCII = 1;
+    public static final int WEP_PASSWORD_HEX = 2;
+    
+	static final String[] SECURITY_MODES = { WEP, WPA, WPA2, WPA_EAP, IEEE8021X };
+    
+    private static final String TAG = "ConfigurationSecuritiesOld";
+
+	@Override
+	public String getWifiConfigurationSecurity(WifiConfiguration wifiConfig) {
+
+        if (wifiConfig.allowedKeyManagement.get(KeyMgmt.NONE)) {
+            // If we never set group ciphers, wpa_supplicant puts all of them.
+            // For open, we don't set group ciphers.
+            // For WEP, we specifically only set WEP40 and WEP104, so CCMP
+            // and TKIP should not be there.
+            if (!wifiConfig.allowedGroupCiphers.get(GroupCipher.CCMP)
+                    && 
+                    (wifiConfig.allowedGroupCiphers.get(GroupCipher.WEP40)
+                            || wifiConfig.allowedGroupCiphers.get(GroupCipher.WEP104))) {
+                return WEP;
+            } else {
+                return OPEN;
+            }
+        } else if (wifiConfig.allowedProtocols.get(Protocol.RSN)) {
+            return WPA2;
+        } else if (wifiConfig.allowedKeyManagement.get(KeyMgmt.WPA_EAP)) {
+            return WPA_EAP;
+        } else if (wifiConfig.allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
+            return IEEE8021X;
+        } else if (wifiConfig.allowedProtocols.get(Protocol.WPA)) {
+            return WPA;
+        } else {
+            Log.w(TAG, "Unknown security type from WifiConfiguration, falling back on open.");
+            return OPEN;
+        }
+    }
+
+	@Override
+    public String getScanResultSecurity(ScanResult scanResult) {
+        final String cap = scanResult.capabilities;
+        for (int i = SECURITY_MODES.length - 1; i >= 0; i--) {
+            if (cap.contains(SECURITY_MODES[i])) {
+                return SECURITY_MODES[i];
+            }
+        }
+        
+        return OPEN;
+    }
+	@Override
+	public String getDisplaySecirityString(final ScanResult scanResult) {
+		return getScanResultSecurity(scanResult);
+	}
+	
+	private static boolean isHexWepKey(String wepKey) {
+        final int len = wepKey.length();
+        
+        // WEP-40, WEP-104, and some vendors using 256-bit WEP (WEP-232?)
+        if (len != 10 && len != 26 && len != 58) {
+            return false;
+        }
+        
+        return isHex(wepKey);
+    }
+    
+    private static boolean isHex(String key) {
+        for (int i = key.length() - 1; i >= 0; i--) {
+            final char c = key.charAt(i);
+            if (!(c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f')) {
+                return false;
+            }
+        }
+        
+        return true;
+    }
+
+	@Override
+	public void setupSecurity(WifiConfiguration config, String security, final String password) {
+        config.allowedAuthAlgorithms.clear();
+        config.allowedGroupCiphers.clear();
+        config.allowedKeyManagement.clear();
+        config.allowedPairwiseCiphers.clear();
+        config.allowedProtocols.clear();
+        
+        if (TextUtils.isEmpty(security)) {
+            security = OPEN;
+            Log.w(TAG, "Empty security, assuming open");
+        }
+        
+        if (security.equals(WEP)) {
+        	 int wepPasswordType = WEP_PASSWORD_AUTO;
+            // If password is empty, it should be left untouched
+            if (!TextUtils.isEmpty(password)) {
+				if (wepPasswordType == WEP_PASSWORD_AUTO) {
+                    if (isHexWepKey(password)) {
+                        config.wepKeys[0] = password;
+                    } else {
+                        config.wepKeys[0] = Wifi.convertToQuotedString(password);
+                    }
+                } else {
+                    config.wepKeys[0] = wepPasswordType == WEP_PASSWORD_ASCII
+                            ? Wifi.convertToQuotedString(password)
+                            : password;
+                }
+            }
+            
+            config.wepTxKeyIndex = 0;
+            
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);
+
+            config.allowedKeyManagement.set(KeyMgmt.NONE);
+            
+            config.allowedGroupCiphers.set(GroupCipher.WEP40);
+            config.allowedGroupCiphers.set(GroupCipher.WEP104);
+            
+        } else if (security.equals(WPA) || security.equals(WPA2)){
+            config.allowedGroupCiphers.set(GroupCipher.TKIP);
+            config.allowedGroupCiphers.set(GroupCipher.CCMP);
+            
+            config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
+            
+            config.allowedPairwiseCiphers.set(PairwiseCipher.CCMP);
+            config.allowedPairwiseCiphers.set(PairwiseCipher.TKIP);
+
+            config.allowedProtocols.set(security.equals(WPA2) ? Protocol.RSN : Protocol.WPA);
+            
+            // If password is empty, it should be left untouched
+            if (!TextUtils.isEmpty(password)) {
+                if (password.length() == 64 && isHex(password)) {
+                    // Goes unquoted as hex
+                    config.preSharedKey = password;
+                } else {
+                    // Goes quoted as ASCII
+                    config.preSharedKey = Wifi.convertToQuotedString(password);
+                }
+            }
+            
+        } else if (security.equals(OPEN)) {
+            config.allowedKeyManagement.set(KeyMgmt.NONE);
+        } else if (security.equals(WPA_EAP) || security.equals(IEEE8021X)) {
+            config.allowedGroupCiphers.set(GroupCipher.TKIP);
+            config.allowedGroupCiphers.set(GroupCipher.CCMP);
+            if (security.equals(WPA_EAP)) {
+                config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
+            } else {
+                config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
+            }
+            if (!TextUtils.isEmpty(password)) {
+                config.preSharedKey = Wifi.convertToQuotedString(password);
+            }
+        }
+    }
+
+	@Override
+	public boolean isOpenNetwork(String security) {
+		return OPEN.equals(security);
+	}
+
+}

+ 180 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/ConfigurationSecuritiesV8.java

@@ -0,0 +1,180 @@
+package com.example.socketdemo.wifitools;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.AuthAlgorithm;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.util.Log;
+
+public class ConfigurationSecuritiesV8 extends ConfigurationSecurities {
+	
+	static final int SECURITY_NONE = 0;
+    static final int SECURITY_WEP = 1;
+    static final int SECURITY_PSK = 2;
+    static final int SECURITY_EAP = 3;
+    
+    enum PskType {
+        UNKNOWN,
+        WPA,
+        WPA2,
+        WPA_WPA2
+    }
+    
+    private static final String TAG = "ConfigurationSecuritiesV14";
+    
+    private static int getSecurity(WifiConfiguration config) {
+        if (config.allowedKeyManagement.get(KeyMgmt.WPA_PSK)) {
+            return SECURITY_PSK;
+        }
+        if (config.allowedKeyManagement.get(KeyMgmt.WPA_EAP) ||
+                config.allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
+            return SECURITY_EAP;
+        }
+        return (config.wepKeys[0] != null) ? SECURITY_WEP : SECURITY_NONE;
+    }
+
+    private static int getSecurity(ScanResult result) {
+        if (result.capabilities.contains("WEP")) {
+            return SECURITY_WEP;
+        } else if (result.capabilities.contains("PSK")) {
+            return SECURITY_PSK;
+        } else if (result.capabilities.contains("EAP")) {
+            return SECURITY_EAP;
+        }
+        return SECURITY_NONE;
+    }
+
+	@Override
+	public String getWifiConfigurationSecurity(WifiConfiguration wifiConfig) {
+		return String.valueOf(getSecurity(wifiConfig));
+	}
+
+	@Override
+	public String getScanResultSecurity(ScanResult scanResult) {
+		return String.valueOf(getSecurity(scanResult));
+	}
+
+	@Override
+	public void setupSecurity(WifiConfiguration config, String security, String password) {
+		config.allowedAuthAlgorithms.clear();
+        config.allowedGroupCiphers.clear();
+        config.allowedKeyManagement.clear();
+        config.allowedPairwiseCiphers.clear();
+        config.allowedProtocols.clear();
+        
+        final int sec = security == null ? SECURITY_NONE : Integer.valueOf(security);
+        final int passwordLen = password == null ? 0 : password.length();
+        switch (sec) {
+        case SECURITY_NONE:
+            config.allowedKeyManagement.set(KeyMgmt.NONE);
+            break;
+
+        case SECURITY_WEP:
+            config.allowedKeyManagement.set(KeyMgmt.NONE);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
+            config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);
+            if (passwordLen != 0) {
+                // WEP-40, WEP-104, and 256-bit WEP (WEP-232?)
+                if ((passwordLen == 10 || passwordLen == 26 || passwordLen == 58) &&
+                        password.matches("[0-9A-Fa-f]*")) {
+                    config.wepKeys[0] = password;
+                } else {
+                    config.wepKeys[0] = '"' + password + '"';
+                }
+            }
+            break;
+
+        case SECURITY_PSK:
+            config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
+            if (passwordLen != 0) {
+                if (password.matches("[0-9A-Fa-f]{64}")) {
+                    config.preSharedKey = password;
+                } else {
+                    config.preSharedKey = '"' + password + '"';
+                }
+            }
+            break;
+
+        case SECURITY_EAP:
+            config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
+            config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
+//            config.eap.setValue((String) mEapMethodSpinner.getSelectedItem());
+//
+//            config.phase2.setValue((mPhase2Spinner.getSelectedItemPosition() == 0) ? "" :
+//                    "auth=" + mPhase2Spinner.getSelectedItem());
+//            config.ca_cert.setValue((mEapCaCertSpinner.getSelectedItemPosition() == 0) ? "" :
+//                    KEYSTORE_SPACE + Credentials.CA_CERTIFICATE +
+//                    (String) mEapCaCertSpinner.getSelectedItem());
+//            config.client_cert.setValue((mEapUserCertSpinner.getSelectedItemPosition() == 0) ?
+//                    "" : KEYSTORE_SPACE + Credentials.USER_CERTIFICATE +
+//                    (String) mEapUserCertSpinner.getSelectedItem());
+//            config.private_key.setValue((mEapUserCertSpinner.getSelectedItemPosition() == 0) ?
+//                    "" : KEYSTORE_SPACE + Credentials.USER_PRIVATE_KEY +
+//                    (String) mEapUserCertSpinner.getSelectedItem());
+//            config.identity.setValue((mEapIdentityView.length() == 0) ? "" :
+//                    mEapIdentityView.getText().toString());
+//            config.anonymous_identity.setValue((mEapAnonymousView.length() == 0) ? "" :
+//                    mEapAnonymousView.getText().toString());
+//            if (mPasswordView.length() != 0) {
+//                config.password.setValue(mPasswordView.getText().toString());
+//            }
+            break;
+
+        default:
+                Log.e(TAG, "Invalid security type: " + sec);
+    }
+
+//    config.proxySettings = mProxySettings;
+//    config.ipAssignment = mIpAssignment;
+//    config.linkProperties = new LinkProperties(mLinkProperties);
+		
+	}
+	
+	private static PskType getPskType(ScanResult result) {
+        boolean wpa = result.capabilities.contains("WPA-PSK");
+        boolean wpa2 = result.capabilities.contains("WPA2-PSK");
+        if (wpa2 && wpa) {
+            return PskType.WPA_WPA2;
+        } else if (wpa2) {
+            return PskType.WPA2;
+        } else if (wpa) {
+            return PskType.WPA;
+        } else {
+            Log.w(TAG, "Received abnormal flag string: " + result.capabilities);
+            return PskType.UNKNOWN;
+        }
+    }
+
+	@Override
+	public String getDisplaySecirityString(final ScanResult scanResult) {
+		final int security = getSecurity(scanResult);
+		if(security == SECURITY_PSK) {
+			switch(getPskType(scanResult)) {
+			case WPA:
+				return "WPA";
+			case WPA_WPA2:
+			case WPA2:
+				return "WPA2";
+			default:
+				return "?";
+			}
+		} else {
+			switch(security) {
+			case SECURITY_NONE:
+				return "OPEN";
+			case SECURITY_WEP:
+				return "WEP";
+			case SECURITY_EAP:
+				return "EAP";
+			}
+		}
+		
+		return "?";
+	}
+
+	@Override
+	public boolean isOpenNetwork(String security) {
+		return String.valueOf(SECURITY_NONE).equals(security);
+	}
+
+}

+ 102 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/ReenableAllApsWhenNetworkStateChanged.java

@@ -0,0 +1,102 @@
+/*
+ * Wifi Connecter
+ * 
+ * Copyright (c) 2011 Kevin Yuan (farproc@gmail.com)
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ * 
+ **/ 
+
+package com.example.socketdemo.wifitools;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.IBinder;
+
+import java.util.List;
+
+public class ReenableAllApsWhenNetworkStateChanged {
+	public static void schedule(final Context ctx) {
+		ctx.startService(new Intent(ctx, BackgroundService.class));
+	}
+	
+	private static void reenableAllAps(final Context ctx) {
+		final WifiManager wifiMgr = (WifiManager)ctx.getSystemService(Context.WIFI_SERVICE);
+		final List<WifiConfiguration> configurations = wifiMgr.getConfiguredNetworks();
+		if(configurations != null) {
+			for(final WifiConfiguration config:configurations) {
+				wifiMgr.enableNetwork(config.networkId, false);
+			}
+		}
+	}
+	
+	public static class BackgroundService extends Service {
+
+		private boolean mReenabled;
+		
+		private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+			
+			@Override
+			public void onReceive(Context context, Intent intent) {
+				final String action = intent.getAction();
+				if(WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
+					final NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
+					final NetworkInfo.DetailedState detailed = networkInfo.getDetailedState();
+					if(detailed != NetworkInfo.DetailedState.DISCONNECTED
+							&& detailed != NetworkInfo.DetailedState.DISCONNECTING
+							&& detailed != NetworkInfo.DetailedState.SCANNING) {
+						if(!mReenabled) {
+							mReenabled = true;
+							reenableAllAps(context);
+							stopSelf();
+						}
+					}
+				}
+			}
+		};
+		
+		private IntentFilter mIntentFilter;
+		
+		@Override
+		public IBinder onBind(Intent intent) {
+			return null; // We need not bind to it at all.
+		}
+		
+		@Override
+		public void onCreate() {
+			super.onCreate();
+			mReenabled = false;
+			mIntentFilter = new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+			registerReceiver(mReceiver, mIntentFilter);
+		}
+		
+		@Override
+		public void onDestroy() {
+			super.onDestroy();
+			unregisterReceiver(mReceiver);
+		}
+
+	}
+}

+ 31 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/Version.java

@@ -0,0 +1,31 @@
+package com.example.socketdemo.wifitools;
+
+import android.os.Build.VERSION;
+
+import java.lang.reflect.Field;
+
+;
+
+/**
+ * Get Android version in different Android versions. :)
+ * @author yuanxiaohui
+ *
+ */
+public class Version {
+	
+	public final static int SDK = get();
+	
+	private static int get() {
+		 final Class<VERSION> versionClass = VERSION.class;
+		 try {
+			 // First try to read the recommended field android.os.Build.VERSION.SDK_INT.
+			final Field sdkIntField = versionClass.getField("SDK_INT");
+			return sdkIntField.getInt(null);
+		}catch (NoSuchFieldException e) {
+			// If SDK_INT does not exist, read the deprecated field SDK.
+			return Integer.valueOf(VERSION.SDK);
+		} catch (Exception e) {
+			throw new RuntimeException(e);
+		}
+	}
+}

+ 319 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/Wifi.java

@@ -0,0 +1,319 @@
+/*
+ * Wifi Connecter
+ * 
+ * Copyright (c) 2011 Kevin Yuan (farproc@gmail.com)
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ * 
+ **/ 
+
+package com.example.socketdemo.wifitools;
+
+import android.content.Context;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Comparator;
+import java.util.List;
+
+public class Wifi {
+	
+	public static final ConfigurationSecurities ConfigSec = ConfigurationSecurities.newInstance();
+    
+	private static final String TAG = "Wifi Connecter";
+	
+	/**
+	 * Change the password of an existing configured network and connect to it
+	 * @param wifiMgr
+	 * @param config
+	 * @param newPassword
+	 * @return
+	 */
+	public static boolean changePasswordAndConnect(final Context ctx, final WifiManager wifiMgr, final WifiConfiguration config, final String newPassword, final int numOpenNetworksKept) {
+		ConfigSec.setupSecurity(config, ConfigSec.getWifiConfigurationSecurity(config), newPassword);
+		final int networkId = wifiMgr.updateNetwork(config);
+		if(networkId == -1) {
+			// Update failed.
+			return false;
+		}
+		// Force the change to apply.
+		wifiMgr.disconnect();
+		return connectToConfiguredNetwork(ctx, wifiMgr, config, true);
+	}
+	
+	/**
+	 * Configure a network, and connect to it.
+	 * @param wifiMgr
+	 * @param scanResult
+	 * @param password Password for secure network or is ignored.
+	 * @return
+	 */
+	public static boolean connectToNewNetwork(final Context ctx, final WifiManager wifiMgr, final ScanResult scanResult, final String password, final int numOpenNetworksKept) {
+		final String security = ConfigSec.getScanResultSecurity(scanResult);
+		
+		if(ConfigSec.isOpenNetwork(security)) {
+			checkForExcessOpenNetworkAndSave(wifiMgr, numOpenNetworksKept);
+		}
+		
+		WifiConfiguration config = new WifiConfiguration();
+		config.SSID = convertToQuotedString(scanResult.SSID);
+		config.BSSID = scanResult.BSSID;
+		ConfigSec.setupSecurity(config, security, password);
+		
+		int id = -1;
+		try {
+			id = wifiMgr.addNetwork(config);
+		} catch(NullPointerException e) {
+			Log.e(TAG, "Weird!! Really!! What's wrong??", e);
+			// Weird!! Really!!
+			// This exception is reported by user to Android Developer Console(https://market.android.com/publish/Home)
+		}
+		if(id == -1) {
+			return false;
+		}
+		
+		if(!wifiMgr.saveConfiguration()) {
+			return false;
+		}
+		
+		config = getWifiConfiguration(wifiMgr, config, security);
+		if(config == null) {
+			return false;
+		}
+		
+		return connectToConfiguredNetwork(ctx, wifiMgr, config, true);
+	}
+	
+	/**
+	 * Connect to a configured network.
+	 * @return
+	 */
+	public static boolean connectToConfiguredNetwork(final Context ctx, final WifiManager wifiMgr, WifiConfiguration config, boolean reassociate) {
+        if(Version.SDK >= 23) {
+            return connectToConfiguredNetworkV23(ctx, wifiMgr, config, reassociate);
+        }
+		final String security = ConfigSec.getWifiConfigurationSecurity(config);
+		
+		int oldPri = config.priority;
+		// Make it the highest priority.
+		int newPri = getMaxPriority(wifiMgr) + 1;
+		if(newPri > MAX_PRIORITY) {
+			newPri = shiftPriorityAndSave(wifiMgr);
+			config = getWifiConfiguration(wifiMgr, config, security);
+			if(config == null) {
+				return false;
+			}
+		}
+		
+		// Set highest priority to this configured network
+		config.priority = newPri;
+		int networkId = wifiMgr.updateNetwork(config);
+		if(networkId == -1) {
+			return false;
+		}
+		
+		// Do not disable others
+		if(!wifiMgr.enableNetwork(networkId, false)) {
+			config.priority = oldPri;
+			return false;
+		}
+		
+		if(!wifiMgr.saveConfiguration()) {
+			config.priority = oldPri;
+			return false;
+		}
+		
+		// We have to retrieve the WifiConfiguration after save.
+		config = getWifiConfiguration(wifiMgr, config, security);
+		if(config == null) {
+			return false;
+		}
+		
+		ReenableAllApsWhenNetworkStateChanged.schedule(ctx);
+		
+		// Disable others, but do not save.
+		// Just to force the WifiManager to connect to it.
+		if(!wifiMgr.enableNetwork(config.networkId, true)) {
+			return false;
+		}
+		
+		final boolean connect = reassociate ? wifiMgr.reassociate() : wifiMgr.reconnect();
+		if(!connect) {
+			return false;
+		}
+		
+		return true;
+	}
+
+    private static boolean connectToConfiguredNetworkV23(final Context ctx, final WifiManager wifiMgr, WifiConfiguration config, boolean reassociate) {
+        if(!wifiMgr.enableNetwork(config.networkId, true)) {
+            return false;
+        }
+
+        return reassociate ? wifiMgr.reassociate() : wifiMgr.reconnect();
+    }
+	
+	private static void sortByPriority(final List<WifiConfiguration> configurations) {
+		java.util.Collections.sort(configurations, new Comparator<WifiConfiguration>() {
+
+			@Override
+			public int compare(WifiConfiguration object1,
+					WifiConfiguration object2) {
+				return object1.priority - object2.priority;
+			}
+		});
+	}
+	
+	/**
+	 * Ensure no more than numOpenNetworksKept open networks in configuration list.
+	 * @param wifiMgr
+	 * @param numOpenNetworksKept
+	 * @return Operation succeed or not.
+	 */
+	private static boolean checkForExcessOpenNetworkAndSave(final WifiManager wifiMgr, final int numOpenNetworksKept) {
+		final List<WifiConfiguration> configurations = wifiMgr.getConfiguredNetworks();
+		sortByPriority(configurations);
+		
+		boolean modified = false;
+		int tempCount = 0;
+		for(int i = configurations.size() - 1; i >= 0; i--) {
+			final WifiConfiguration config = configurations.get(i);
+			if(ConfigSec.isOpenNetwork(ConfigSec.getWifiConfigurationSecurity(config))) {
+				tempCount++;
+				if(tempCount >= numOpenNetworksKept) {
+					modified = true;
+					wifiMgr.removeNetwork(config.networkId);
+				}
+			}
+		}
+		if(modified) {
+			return wifiMgr.saveConfiguration();
+		}
+		
+		return true;
+	}
+	
+	private static final int MAX_PRIORITY = 99999;
+	
+	private static int shiftPriorityAndSave(final WifiManager wifiMgr) {
+		final List<WifiConfiguration> configurations = wifiMgr.getConfiguredNetworks();
+		sortByPriority(configurations);
+		final int size = configurations.size();
+		for(int i = 0; i < size; i++) {
+			final WifiConfiguration config = configurations.get(i);
+			config.priority = i;
+			wifiMgr.updateNetwork(config);
+		}
+		wifiMgr.saveConfiguration();
+		return size;
+	}
+
+	private static int getMaxPriority(final WifiManager wifiManager) {
+		final List<WifiConfiguration> configurations = wifiManager.getConfiguredNetworks();
+		int pri = 0;
+		for(final WifiConfiguration config : configurations) {
+			if(config.priority > pri) {
+				pri = config.priority;
+			}
+		}
+		return pri;
+	}
+	
+	private static final String BSSID_ANY = "any";
+
+	public static WifiConfiguration getWifiConfiguration(final WifiManager wifiMgr, final ScanResult hotsopt, String hotspotSecurity) {
+		final String ssid = convertToQuotedString(hotsopt.SSID);
+		if(ssid.length() == 0) {
+			return null;
+		}
+		
+		final String bssid = hotsopt.BSSID;
+		if(bssid == null) {
+			return null;
+		}
+		
+		if(hotspotSecurity == null) {
+			hotspotSecurity = ConfigSec.getScanResultSecurity(hotsopt);
+		}
+		
+		final List<WifiConfiguration> configurations = wifiMgr.getConfiguredNetworks();
+		if(configurations == null) {
+			return null;
+		}
+
+		for(final WifiConfiguration config : configurations) {
+			if(config.SSID == null || !ssid.equals(config.SSID)) {
+				continue;
+			}
+			if(config.BSSID == null || BSSID_ANY.equals(config.BSSID) ||  bssid.equals(config.BSSID)) {
+				final String configSecurity = ConfigSec.getWifiConfigurationSecurity(config);
+				if(hotspotSecurity.equals(configSecurity)) {
+					return config;
+				}
+			}
+		}
+		return null;
+	}
+	
+	public static WifiConfiguration getWifiConfiguration(final WifiManager wifiMgr, final WifiConfiguration configToFind, String security) {
+		final String ssid = configToFind.SSID;
+		if(ssid.length() == 0) {
+			return null;
+		}
+		
+		final String bssid = configToFind.BSSID;
+
+		
+		if(security == null) {
+			security = ConfigSec.getWifiConfigurationSecurity(configToFind);
+		}
+		
+		final List<WifiConfiguration> configurations = wifiMgr.getConfiguredNetworks();
+
+		for(final WifiConfiguration config : configurations) {
+			if(config.SSID == null || !ssid.equals(config.SSID)) {
+				continue;
+			}
+			if(config.BSSID == null || BSSID_ANY.equals(config.BSSID) || bssid == null || bssid.equals(config.BSSID)) {
+				final String configSecurity = ConfigSec.getWifiConfigurationSecurity(config);
+				if(security.equals(configSecurity)) {
+					return config;
+				}
+			}
+		}
+		return null;
+	}
+	
+	public static String convertToQuotedString(String string) {
+        if (TextUtils.isEmpty(string)) {
+            return "";
+        }
+        
+        final int lastPos = string.length() - 1;
+        if(lastPos > 0 && (string.charAt(0) == '"' && string.charAt(lastPos) == '"')) {
+            return string;
+        }
+        
+        return "\"" + string + "\"";
+    }
+   
+}

+ 414 - 0
Android/app/src/main/java/com/example/socketdemo/wifitools/WifiMgr.java

@@ -0,0 +1,414 @@
+package com.example.socketdemo.wifitools;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.NetworkInfo;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class WifiMgr {
+    //过滤免密码连接的WiFi
+    public static final String NO_PASSWORD = "[ESS]";
+    public static final String NO_PASSWORD_WPS = "[WPS][ESS]";
+	
+	private Context mContext;
+	private WifiManager mWifiManager;
+	
+	
+	public WifiMgr(Context context) {
+		mContext = context;
+        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+	}
+	
+	
+	/**
+     * 打开Wi-Fi
+     */
+    public void openWifi() {
+        if (!mWifiManager.isWifiEnabled()) {
+            mWifiManager.setWifiEnabled(true);
+        }
+    }
+
+    /**
+     * 关闭Wi-Fi
+     */
+    public void closeWifi() {
+        if (mWifiManager.isWifiEnabled()) {
+            mWifiManager.setWifiEnabled(false);
+        }
+    }
+    
+    /**
+     * 当前WiFi是否开启
+     */
+    public boolean isWifiEnabled() {
+    	return mWifiManager.isWifiEnabled();
+    }
+    
+    /**
+     * 清除指定网络
+     * @param SSID
+     */
+    public void clearWifiInfo(String SSID) {
+    	WifiConfiguration tempConfig = isExsits(SSID);
+        if (tempConfig != null) {
+            mWifiManager.removeNetwork(tempConfig.networkId);
+        }
+    }
+    
+    /**
+     * 判断当前网络是否WiFi
+     * @param context
+     * @return
+     */
+    public boolean isWifi(final Context context) {
+        ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+        return networkInfo != null && networkInfo.getType() == 1;
+    }
+    
+    /**
+     * 扫描周围可用WiFi
+     * @return
+     */
+    public boolean startScan() {
+        if(isWifiEnabled()) {
+            return mWifiManager.startScan();
+        }
+        return false;
+    }
+    
+    /**
+     * 获取周围可用WiFi扫描结果
+     * @return
+     */
+    public List<ScanResult> getScanResults() {
+        List<ScanResult> scanResults = mWifiManager.getScanResults();
+        if(scanResults != null && scanResults.size() > 0) {
+            return filterScanResult(scanResults);
+        } else {
+            return new ArrayList<>();
+        }
+    }
+	
+    /**
+     * 获取周围信号强度大于-80的WiFi列表(Wifi强度为负数,值越大信号越好)
+     * @return
+     * @throws InterruptedException 
+     */
+	public List<ScanResult> getWifiScanList() throws InterruptedException {
+		List<ScanResult> resList = new ArrayList<ScanResult>();
+		if(mWifiManager.startScan()) {
+			List<ScanResult> tmpList = mWifiManager.getScanResults();
+			Thread.sleep(2000);
+			if(tmpList != null && tmpList.size() > 0) {
+//				resList = sortByLevel(tmpList);
+				for(ScanResult scanResult : tmpList) {
+					if(scanResult.level > -80) {
+						resList.add(scanResult);
+					}
+				}
+			} else {
+				System.err.println("扫描为空");
+			}
+		}
+		return resList;
+	}
+	
+	/**
+	 * 判断当前WiFi是否正确连接指定WiFi
+	 * @param SSID
+	 * @return
+	 */
+	public boolean isWifiConnected(String SSID) {
+		WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
+		return wifiInfo != null && wifiInfo.getSSID().equals(SSID);
+	}
+	
+	/**
+	 * 获取当前连接WiFi的SSID
+	 * @return
+	 */
+	public String getConnectedSSID() {
+		WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
+		return wifiInfo != null ? wifiInfo.getSSID().replaceAll("\"", "") : "";
+	}
+	
+	/**
+	 * 连接WiFi
+	 * @param ssid
+	 * @param pwd
+     * @param scanResults
+	 * @return
+	 * @throws InterruptedException 
+	 */
+	public boolean connectWifi(final String ssid, final String pwd, List<ScanResult> scanResults) throws InterruptedException {
+		if(scanResults == null || scanResults.size() == 0) {
+			return false;
+		}
+		
+		//匹配SSID相同的WiFi
+        ScanResult result = null;
+		for(ScanResult tmpResult : scanResults) {
+			if(tmpResult.SSID.equals(ssid)) {
+				result = tmpResult;
+				break;
+			}
+		}
+		
+		if(result == null) {
+			return false;
+		}
+		
+		if(isAdHoc(result)) {
+			return false;
+		}
+		
+		final String security = Wifi.ConfigSec.getScanResultSecurity(result);
+		final WifiConfiguration config = Wifi.getWifiConfiguration(mWifiManager, result, security);
+		
+		if(config == null) {
+            //连接新WiFi
+			boolean connResult;
+            int numOpenNetworksKept =  Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.WIFI_NUM_OPEN_NETWORKS_KEPT, 10);
+            String scanResultSecurity = Wifi.ConfigSec.getScanResultSecurity(result);
+            boolean isOpenNetwork = Wifi.ConfigSec.isOpenNetwork(scanResultSecurity);
+            if(isOpenNetwork) {
+                connResult = Wifi.connectToNewNetwork(mContext, mWifiManager, result, null, numOpenNetworksKept);
+            } else {
+                connResult = Wifi.connectToNewNetwork(mContext, mWifiManager, result, pwd, numOpenNetworksKept);
+            }
+			return connResult;
+		} else {
+			final boolean isCurrentNetwork_ConfigurationStatus = config.status == WifiConfiguration.Status.CURRENT;
+			final WifiInfo info = mWifiManager.getConnectionInfo();
+			final boolean isCurrentNetwork_WifiInfo = info != null 
+				&& TextUtils.equals(info.getSSID(), result.SSID)
+				&& TextUtils.equals(info.getBSSID(), result.BSSID);
+			if(!isCurrentNetwork_ConfigurationStatus && !isCurrentNetwork_WifiInfo) {
+				//连接已保存的WiFi
+                String scanResultSecurity = Wifi.ConfigSec.getScanResultSecurity(result);
+				final WifiConfiguration wcg = Wifi.getWifiConfiguration(mWifiManager, result, scanResultSecurity);
+				boolean connResult = false;
+				if(wcg != null) {
+					connResult = Wifi.connectToConfiguredNetwork(mContext, mWifiManager, wcg, false);
+				}
+				return connResult;
+			} else {
+				//点击的是当前已连接的WiFi
+				return true;
+			}
+		}
+	}
+    
+    /**
+     * 断开指定ID的网络
+     * @param SSID
+     */
+    public boolean disconnectWifi(String SSID) {
+    	return mWifiManager.disableNetwork(getNetworkIdBySSID(SSID)) && mWifiManager.disconnect();
+    }
+
+    /**
+     * 清除指定SSID的网络
+     * @param SSID
+     */
+    public void clearWifiConfig(String SSID) {
+        SSID = SSID.replace("\"", "");
+        List<WifiConfiguration> wifiConfigurations = mWifiManager.getConfiguredNetworks();
+        if(wifiConfigurations != null && wifiConfigurations.size() > 0) {
+            for(WifiConfiguration wifiConfiguration : wifiConfigurations) {
+                if(wifiConfiguration.SSID.replace("\"", "").contains(SSID)) {
+                    mWifiManager.removeNetwork(wifiConfiguration.networkId);
+                    mWifiManager.saveConfiguration();
+                }
+            }
+        }
+    }
+
+    /**
+     * 清除当前连接的WiFi网络
+     */
+    public void clearWifiConfig() {
+        String SSID = mWifiManager.getConnectionInfo().getSSID().replace("\"", "");
+        List<WifiConfiguration> wifiConfigurations = mWifiManager.getConfiguredNetworks();
+        if(wifiConfigurations != null && wifiConfigurations.size() > 0) {
+            for(WifiConfiguration wifiConfiguration : wifiConfigurations) {
+                if(wifiConfiguration.SSID.replace("\"", "").contains(SSID)) {
+                    mWifiManager.removeNetwork(wifiConfiguration.networkId);
+                    mWifiManager.saveConfiguration();
+                }
+            }
+        }
+    }
+    
+    private boolean isAdHoc(final ScanResult scanResule) {
+		return scanResule.capabilities.indexOf("IBSS") != -1;
+	}
+    
+    /**
+     * 根据SSID查networkID
+     *
+     * @param SSID
+     * @return
+     */
+    public int getNetworkIdBySSID(String SSID) {
+        if (TextUtils.isEmpty(SSID)) {
+            return 0;
+        }
+        WifiConfiguration config = isExsits(SSID);
+        if (config != null) {
+            return config.networkId;
+        }
+        return 0;
+    }
+
+    /**
+     * 获取连接WiFi后的IP地址
+     * @return
+     */
+    public String getIpAddressFromHotspot() {
+        DhcpInfo dhcpInfo = mWifiManager.getDhcpInfo();
+        if(dhcpInfo != null) {
+            int address = dhcpInfo.gateway;
+            return ((address & 0xFF)
+                    + "." + ((address >> 8) & 0xFF)
+                    + "." + ((address >> 16) & 0xFF)
+                    + "." + ((address >> 24) & 0xFF));
+        }
+        return null;
+    }
+    
+    /**
+     * 创建WifiConfiguration对象 分为三种情况:1没有密码;2用wep加密;3用wpa加密
+     * 
+     * @param SSID
+     * @param Password
+     * @param Type
+     * @return
+     */
+    public WifiConfiguration CreateWifiInfo(String SSID, String Password,
+            int Type) {
+        WifiConfiguration config = new WifiConfiguration();
+        config.allowedAuthAlgorithms.clear();
+        config.allowedGroupCiphers.clear();
+        config.allowedKeyManagement.clear();
+        config.allowedPairwiseCiphers.clear();
+        config.allowedProtocols.clear();
+        config.SSID = "\"" + SSID + "\"";
+
+        WifiConfiguration tempConfig = isExsits(SSID);
+        if (tempConfig != null) {
+            mWifiManager.removeNetwork(tempConfig.networkId);
+        }
+
+        if (Type == 1) // WIFICIPHER_NOPASS
+        {
+            config.wepKeys[0] = "";
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+            config.wepTxKeyIndex = 0;
+        }
+        if (Type == 2) // WIFICIPHER_WEP
+        {
+            config.hiddenSSID = true;
+            config.wepKeys[0] = "\"" + Password + "\"";
+            config.allowedAuthAlgorithms
+                    .set(WifiConfiguration.AuthAlgorithm.SHARED);
+            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
+            config.allowedGroupCiphers
+                    .set(WifiConfiguration.GroupCipher.WEP104);
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+            config.wepTxKeyIndex = 0;
+        }
+        if (Type == 3) // WIFICIPHER_WPA
+        {
+            config.preSharedKey = "\"" + Password + "\"";
+            config.hiddenSSID = true;
+            config.allowedAuthAlgorithms
+                    .set(WifiConfiguration.AuthAlgorithm.OPEN);
+            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+            config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+            config.allowedPairwiseCiphers
+                    .set(WifiConfiguration.PairwiseCipher.TKIP);
+            // config.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
+            config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+            config.allowedPairwiseCiphers
+                    .set(WifiConfiguration.PairwiseCipher.CCMP);
+            config.status = WifiConfiguration.Status.ENABLED;
+        }
+        return config;
+    }
+    
+    /**
+     * 添加一个网络并连接 传入参数:WIFI发生配置类WifiConfiguration
+     */
+    public boolean addNetwork(WifiConfiguration wcg) {
+        int wcgID = mWifiManager.addNetwork(wcg);
+        return mWifiManager.enableNetwork(wcgID, true);
+    }
+	
+    /**
+     * 获取当前手机所连接的wifi信息
+     */
+    public WifiInfo getCurrentWifiInfo() {
+        return mWifiManager.getConnectionInfo();
+    }
+	
+    /**
+     * 获取指定WiFi信息
+     * @param SSID
+     * @return
+     */
+    private WifiConfiguration isExsits(String SSID) {
+        List<WifiConfiguration> existingConfigs = mWifiManager.getConfiguredNetworks();
+        if(existingConfigs != null && existingConfigs.size() > 0) {
+            for (WifiConfiguration existingConfig : existingConfigs) {
+                if (existingConfig.SSID.equals(SSID) || existingConfig.SSID.equals("\"" + SSID + "\"")) {
+                    return existingConfig;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 过滤WiFi扫描结果
+     * @return
+     */
+    private List<ScanResult> filterScanResult(List<ScanResult> scanResults) {
+        List<ScanResult> result = new ArrayList<>();
+        if(scanResults == null) {
+            return result;
+        }
+
+        for (ScanResult scanResult : scanResults) {
+            if (!TextUtils.isEmpty(scanResult.SSID) && scanResult.level > -80) {
+                result.add(scanResult);
+            }
+        }
+
+        for (int i = 0; i < result.size(); i++) {
+            for (int j = 0; j < result.size(); j++) {
+                //将搜索到的wifi根据信号强度从强到弱进行排序
+                if (result.get(i).level > result.get(j).level) {
+                    ScanResult temp = result.get(i);
+                    result.set(i, result.get(j));
+                    result.set(j, temp);
+                }
+            }
+        }
+        return result;
+    }
+
+}

+ 20 - 0
Android/app/src/main/res/drawable/bg_item_choose_hotspot_selector.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true">
+        <shape>
+            <corners android:radius="@dimen/dist_10" />
+            <stroke android:width="@dimen/dist_2" android:color="@color/textColor_item_orange" />
+            <padding android:bottom="@dimen/dist_10" android:left="@dimen/dist_10" android:right="@dimen/dist_10" android:top="@dimen/dist_10" />
+            <solid android:color="@color/colorPrimaryDark" />
+        </shape>
+    </item>
+
+    <item android:state_pressed="false">
+        <shape>
+            <corners android:radius="@dimen/dist_10" />
+            <stroke android:width="@dimen/dist_2" android:color="@color/textColor_item_orange" />
+            <padding android:bottom="@dimen/dist_10" android:left="@dimen/dist_10" android:right="@dimen/dist_10" android:top="@dimen/dist_10" />
+            <solid android:color="@color/colorPrimary" />
+        </shape>
+    </item>
+</selector>

+ 18 - 0
Android/app/src/main/res/layout/activity_base.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:background="@color/white"
+              android:orientation="vertical">
+
+    <include
+        android:id="@+id/base_toolbar"
+        layout="@layout/layout_toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/height_toolbar"/>
+
+    <ViewStub
+        android:id="@+id/base_viewstub"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</LinearLayout>

+ 21 - 0
Android/app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:layout_height="match_parent">
+
+    <Button
+        android:layout_width="wrap_content"
+        android:text="发送文件"
+        android:onClick="sendFiles"
+        android:layout_height="wrap_content"/>
+
+    <Button
+        android:layout_width="wrap_content"
+        android:text="接收文件"
+        android:onClick="receiveFiles"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>

+ 47 - 0
Android/app/src/main/res/layout/activity_receive_files.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:padding="@dimen/dist_10">
+
+    <TextView
+        android:id="@+id/tv_receive_files_status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:textColor="@color/textColor_content"
+        android:textSize="@dimen/textSize_content"/>
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/rv_receive_files_choose_hotspot"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/btn_receive_files"
+        android:layout_below="@+id/tv_receive_files_status"
+        android:layout_marginTop="@dimen/dist_10"
+        android:overScrollMode="never"
+        android:visibility="gone"
+        app:layoutManager="GridLayoutManager"
+        app:spanCount="3"/>
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/rv_receive_files"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/btn_receive_files"
+        android:layout_below="@+id/tv_receive_files_status"
+        android:layout_marginTop="@dimen/dist_10"
+        android:overScrollMode="never"
+        android:visibility="gone"
+        app:layoutManager="LinearLayoutManager"/>
+
+    <Button
+        android:id="@+id/btn_receive_files"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:enabled="false"
+        android:text="开始发送"/>
+</RelativeLayout>

+ 31 - 0
Android/app/src/main/res/layout/activity_send_files.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical"
+              android:padding="@dimen/dist_10">
+
+    <TextView
+        android:id="@+id/tv_send_files_status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:textColor="@color/textColor_content"
+        android:textSize="@dimen/textSize_content"/>
+
+    <ViewStub
+        android:id="@+id/vs_send_files_open_hotspot"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="@dimen/dist_10"
+        android:layout="@layout/layout_open_hotspot"/>
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/rv_send_files"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="@dimen/dist_10"
+        android:overScrollMode="never"
+        android:visibility="gone"/>
+
+</LinearLayout>

+ 26 - 0
Android/app/src/main/res/layout/item_choose_hotspot.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:background="@drawable/bg_item_choose_hotspot_selector"
+              android:gravity="center"
+              android:orientation="vertical"
+              android:padding="@dimen/dist_10">
+
+    <TextView
+        android:id="@+id/tv_item_choose_hotspot_ssid"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:gravity="center"
+        android:lines="2"
+        android:textSize="@dimen/textSize_content"/>
+
+    <TextView
+        android:id="@+id/tv_item_choose_hotspot_level"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/dist_3"
+        android:textSize="@dimen/textSize_content"/>
+
+</LinearLayout>

+ 43 - 0
Android/app/src/main/res/layout/item_file_transfer.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:padding="@dimen/dist_10">
+
+    <TextView
+        android:id="@+id/tv_item_file_transfer_file_path"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_toLeftOf="@+id/tv_item_file_transfer_size"
+        android:ellipsize="end"
+        android:lines="1"
+        android:textSize="@dimen/textSize_content"/>
+
+    <TextView
+        android:id="@+id/tv_item_file_transfer_size"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_marginLeft="@dimen/dist_10"
+        android:textSize="@dimen/textSize_caption"/>
+
+    <TextView
+        android:id="@+id/tv_item_file_transfer_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tv_item_file_transfer_size"
+        android:layout_marginTop="@dimen/dist_5"
+        android:textSize="@dimen/textSize_caption"/>
+
+    <ProgressBar
+        android:id="@+id/pb_item_file_transfer"
+        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tv_item_file_transfer_size"
+        android:layout_marginLeft="@dimen/dist_10"
+        android:layout_marginTop="@dimen/dist_5"
+        android:layout_toRightOf="@+id/tv_item_file_transfer_status"
+        android:max="100"/>
+
+</RelativeLayout>

+ 52 - 0
Android/app/src/main/res/layout/item_files_selector.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:padding="@dimen/dist_10">
+
+    <TextView
+        android:id="@+id/tv_item_files_selector_file_path"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_toLeftOf="@+id/tv_item_files_selector_size"
+        android:ellipsize="end"
+        android:lines="1"
+        android:textSize="@dimen/textSize_content"/>
+
+    <TextView
+        android:id="@+id/tv_item_files_selector_size"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="@dimen/dist_10"
+        android:layout_toLeftOf="@+id/cb_item_files_selector"
+        android:textSize="@dimen/textSize_caption"/>
+
+    <TextView
+        android:id="@+id/tv_item_files_selector_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tv_item_files_selector_size"
+        android:layout_marginTop="@dimen/dist_5"
+        android:textSize="@dimen/textSize_caption"/>
+
+    <ProgressBar
+        android:id="@+id/pb_item_files_selector"
+        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/tv_item_files_selector_size"
+        android:layout_marginLeft="@dimen/dist_10"
+        android:layout_marginTop="@dimen/dist_5"
+        android:layout_toLeftOf="@+id/cb_item_files_selector"
+        android:layout_toRightOf="@+id/tv_item_files_selector_status"
+        android:max="100"/>
+
+    <CheckBox
+        android:id="@+id/cb_item_files_selector"
+        android:layout_width="@dimen/dist_30"
+        android:layout_height="@dimen/dist_30"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        android:layout_marginLeft="@dimen/dist_10"/>
+
+</RelativeLayout>

+ 12 - 0
Android/app/src/main/res/layout/layout_dialog_with_edittext.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<EditText xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/et_dialog_with_edittext"
+          android:layout_width="match_parent"
+          android:layout_height="wrap_content"
+          android:layout_marginBottom="@dimen/dist_0"
+          android:layout_marginLeft="@dimen/dist_10"
+          android:layout_marginRight="@dimen/dist_10"
+          android:layout_marginTop="@dimen/dist_10"
+          android:hint="密码"
+          android:inputType="textVisiblePassword"
+          android:padding="@dimen/dist_10"/>

+ 42 - 0
Android/app/src/main/res/layout/layout_open_hotspot.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:gravity="center"
+              android:orientation="vertical">
+
+    <EditText
+        android:id="@+id/et_open_hotspot_ssid"
+        android:layout_width="@dimen/dist_150"
+        android:layout_height="wrap_content"
+        android:inputType="text"
+        android:textColorHint="@color/textColor_caption"
+        android:hint="输入热点名称"
+        android:gravity="center"
+        android:text="aa"
+        android:padding="@dimen/dist_10"
+        android:textColor="@color/textColor_content"
+        android:textSize="@dimen/textSize_content"/>
+
+    <EditText
+        android:id="@+id/et_open_hotspot_password"
+        android:layout_width="@dimen/dist_150"
+        android:layout_height="wrap_content"
+        android:inputType="text"
+        android:textColorHint="@color/textColor_caption"
+        android:hint="输入热点密码"
+        android:text="88888888"
+        android:gravity="center"
+        android:layout_marginTop="@dimen/dist_10"
+        android:padding="@dimen/dist_10"
+        android:textColor="@color/textColor_content"
+        android:textSize="@dimen/textSize_content"/>
+
+    <Button
+        android:id="@+id/btn_open_hotspot"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="openHotspot"
+        android:text="开启热点"/>
+
+</LinearLayout>

+ 34 - 0
Android/app/src/main/res/layout/layout_toolbar.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
+                                   xmlns:app="http://schemas.android.com/apk/res-auto"
+                                   android:id="@+id/tb_toolbar"
+                                   android:layout_width="match_parent"
+                                   android:layout_height="@dimen/height_toolbar"
+                                   android:background="?attr/colorPrimary"
+                                   app:navigationIcon="@mipmap/icon_back"
+                                   app:titleTextColor="@color/white">
+
+    <TextView
+        android:id="@+id/tv_toolbar_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:drawablePadding="@dimen/dist_2"
+        android:gravity="center"
+        android:lines="1"
+        android:textColor="@color/white"
+        android:textSize="@dimen/textSize_title"/>
+
+    <TextView
+        android:id="@+id/tv_toolbar_right_text"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="right"
+        android:layout_marginRight="@dimen/dist_10"
+        android:background="?android:attr/selectableItemBackground"
+        android:padding="@dimen/dist_10"
+        android:textColor="@color/white"
+        android:textSize="@dimen/textSize_content"
+        android:visibility="gone"/>
+
+</android.support.v7.widget.Toolbar>

BIN
Android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
Android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
Android/app/src/main/res/mipmap-xhdpi/icon_back.png


BIN
Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 11 - 0
Android/app/src/main/res/values/colors.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#18b4ec</color>
+    <color name="colorPrimaryDark">#18b4ec</color>
+    <color name="colorAccent">#18b4ec</color>
+    <color name="white">#fff</color>
+    <color name="black">#000</color>
+    <color name="textColor_content">#333</color>
+    <color name="textColor_caption">#999</color>
+    <color name="textColor_item_orange">#FF9200</color>
+</resources>

+ 52 - 0
Android/app/src/main/res/values/dimens.xml

@@ -0,0 +1,52 @@
+<resources>
+
+    <!-- BEGIN * 常用距离值 -->
+    <dimen name="dist_0">0dp</dimen>
+    <dimen name="dist_1">1dp</dimen>
+    <dimen name="dist_2">2dp</dimen>
+    <dimen name="dist_3">3dp</dimen>
+    <dimen name="dist_5">5dp</dimen>
+    <dimen name="dist_8">8dp</dimen>
+    <dimen name="dist_10">10dp</dimen>
+    <dimen name="dist_15">15dp</dimen>
+    <dimen name="dist_20">20dp</dimen>
+    <dimen name="dist_25">25dp</dimen>
+    <dimen name="dist_30">30dp</dimen>
+    <dimen name="dist_35">35dp</dimen>
+    <dimen name="dist_40">40dp</dimen>
+    <dimen name="dist_45">45dp</dimen>
+    <dimen name="dist_50">50dp</dimen>
+    <dimen name="dist_55">55dp</dimen>
+    <dimen name="dist_60">60dp</dimen>
+    <dimen name="dist_70">70dp</dimen>
+    <dimen name="dist_80">80dp</dimen>
+    <dimen name="dist_90">90dp</dimen>
+    <dimen name="dist_100">100dp</dimen>
+    <dimen name="dist_110">110dp</dimen>
+    <dimen name="dist_120">120dp</dimen>
+    <dimen name="dist_130">130dp</dimen>
+    <dimen name="dist_150">150dp</dimen>
+    <dimen name="dist_180">180dp</dimen>
+    <dimen name="dist_200">200dp</dimen>
+    <!-- END * 常用距离值 -->
+
+
+    <!-- BEGIN * 常用字体大小 -->
+    <dimen name="textSize_big_btn">20sp</dimen>
+    <dimen name="textSize_title">18sp</dimen>
+    <dimen name="textSize_content">16sp</dimen>
+    <dimen name="textSize_caption">14sp</dimen>
+    <dimen name="textSize_small">12sp</dimen>
+
+    <!-- END * 常用字体大小 -->
+
+    <!-- BEGIN * 常用高度值 -->
+    <dimen name="height_toolbar">48dp</dimen>
+    <dimen name="height_big_head">90dp</dimen>
+    <dimen name="height_middle_head">60dp</dimen>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+    <!-- END * 常用高度值 -->
+
+</resources>

+ 8 - 0
Android/app/src/main/res/values/strings.xml

@@ -0,0 +1,8 @@
+<resources>
+    <string name="app_name">SocketDemo</string>
+    <string name="confirm">确定</string>
+    <string name="cancel">取消</string>
+    <string name="item_ssid">SSID:%1$s</string>
+    <string name="item_mac">MAC地址:%1$s</string>
+    <string name="item_level">Level:%1$d</string>
+</resources>

+ 11 - 0
Android/app/src/main/res/values/styles.xml

@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>

+ 25 - 0
Android/build.gradle

@@ -0,0 +1,25 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath "com.android.tools.build:gradle:4.0.0"
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 17 - 0
Android/gradle.properties

@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true

BIN
Android/gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
Android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Wed Mar 22 13:33:36 CST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

+ 160 - 0
Android/gradlew

@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 90 - 0
Android/gradlew.bat

@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

BIN
Android/screenshot/mi5.gif


BIN
Android/screenshot/mx5.gif


+ 1 - 0
Android/settings.gradle

@@ -0,0 +1 @@
+include ':app'