1. 程式人生 > >Android開啟熱點進行UDP通訊中的坑

Android開啟熱點進行UDP通訊中的坑

1、寫在前面:

2018年的第一篇文章,最近在使用UDP協議進行硬體通訊,大家都知道UDP協議通訊必須在同一個區域網內,但是每個使用者家的wifi都是不一樣的,硬體裝置是無法只值連線到使用者家的wifi的。所以為了解決這個問題,提出一個思路,讓手機開啟熱點,然後把硬體連結到手機的熱點上。再由手機告訴硬體去連結使用者家裡的wifi,這樣手機和裝置就都能連線到使用者家的wifi了,就能愉快的進行通訊了。那麼怎麼解決這個問題呢?繼續往下看!

2、實現思路:

  • 1、獲取當前網路wifi名稱
  • 2、開啟熱點
  • 3、讓使用者輸入wifi密碼
  • 4、獲取當前網路的廣播地址,掃描裝置
  • 5、給裝置發命令,配置資訊
  • 6、把繫結的裝置存起來
  • 7、迴圈4-6直到沒有新裝置了
  • 8、退出的時候先把裝置資訊提交
  • 9、關閉熱點、開啟wifi

3、中間遇到坑:

測試真機: 魅族4 Android5.1 、小米5 Android 7.0
這裡就不說怎麼進行UDP通訊了,只說在中間遇到的問題。兩個坑吧,一個是開啟熱點相容6.x+,另一個是獲取廣播地址,相容wifi環境,乙太網環境,無網路環境。

3.1 開啟熱點,相容android6.x

這裡先提供一個開啟/關閉熱點的工具類WifiUtils:

import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;

import
java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * 作者:dell or Xiaomi Li * 時間: 2018/1/17 * 內容:開啟/關閉熱點 * 最後修改: */ public class WifiUtils { private final static String APName = "XiaomiLi8"; private final static String APPassword = "5311925577"; /** * 建立熱點 * * @return
*/
public static boolean CreatHotspot(WifiManager wifiManager) { boolean request; //開啟熱點 if (wifiManager.isWifiEnabled()) { //如果wifi處於開啟狀態,則關閉wifi, wifiManager.setWifiEnabled(false); } WifiConfiguration config = new WifiConfiguration(); config.SSID = APName; config.preSharedKey = APPassword; config.hiddenSSID = false;//是否隱藏網路 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.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP); config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP); config.status = WifiConfiguration.Status.ENABLED; //通過反射呼叫設定熱點 try { Method method = wifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, Boolean.TYPE); boolean enable = (Boolean) method.invoke(wifiManager, config, true); if (enable) { request = true; } else { request = false; LogUtils.Loge("建立失敗"); } } catch (Exception e) { e.printStackTrace(); LogUtils.Loge(e.toString() + "建立失敗"); request = false; } return request; } /** * 關閉熱點,並開啟wifi */ public static void closeWifiHotspot(WifiManager wifiManager) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = wifiManager.getClass().getMethod("getWifiApConfiguration"); method.setAccessible(true); WifiConfiguration config = (WifiConfiguration) method.invoke(wifiManager); Method method2 = wifiManager.getClass().getMethod("setWifiApEnabled", WifiConfiguration.class, boolean.class); method2.invoke(wifiManager, config, false); //開啟wifi wifiManager.setWifiEnabled(true); } }

需要用到許可權:

<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" />

這裡來說坑,經過測試發現在Android6.0一下的手機是可以正常開啟熱點的,但是在6.0以上的手機。開啟熱點會報如下錯誤:

java.lang.reflect.InvocationTargetException

大家都能猜到是許可權的問題,確實如此,經過查詢資料很多部落格也都說是許可權的問題,也有人說直接把版本設定在22,這樣就不用管許可權問題了,但是這種方法也行可以歸納為不正常手段。所以這裡就不用這種方法了。然後繼續查詢,發現是android.permission.WRITE_SETTINGS這個許可權發生的錯誤,既然是許可權問題,那就去動態申請許可權就好了。然後就會發現,申請之後根本沒有用。還是沒有許可權。那這是為何呢?
因為在android 6.0及以後,WRITE_SETTINGS許可權的保護等級已經由原來的dangerous升級為signature,這意味著我們的APP需要用系統簽名或者成為系統預裝軟體才能夠申請此許可權,並且還需要提示使用者跳轉到修改系統的設定介面去授予此許可權。所以我們動態申請許可權是沒有用的。這裡先給出參考地址:http://blog.csdn.net/XieGaoXiong/article/details/52317155 然後給出申請的方法,如下:

    /**
     * WIFI設定請求碼
     */
    private final int REQUEST_CODE_ASK_WRITE_SETTINGS = 0X1;

    /**
     * 請求許可權
     */
    private void getWifiPreMission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.System.canWrite(this)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS,
                        Uri.parse("package:" + getPackageName()));
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivityForResult(intent, REQUEST_CODE_ASK_WRITE_SETTINGS);
            } else {
                //有了許可權去做什麼呢?
                getConnectWifiSsid();
            }
        } else {
            getConnectWifiSsid();
        }
    }

然後在Activity的onActivityResult()方法中去操作一波:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE_ASK_WRITE_SETTINGS || requestCode == PreMissionDialog.ACTION_APPLICATION_DETAILS_SETTINGS) {
            if (!Settings.System.canWrite(this)) {
                //如果還是沒有許可權,就彈框提醒使用者
                PreMissionDialog.showPermissionDialog(SearchEqListActivity.this, "系統設定");
            } else {
                getConnectWifiSsid();
            }
        }
    }

這裡把提醒使用者的彈框也給出來,可以拿去用,覺得醜可以自己寫一個:

import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.provider.Settings;
import android.support.v7.app.AlertDialog;

/**
 * 作者:dell or Xiaomi Li
 * 時間: 2018/1/17
 * 內容:提醒使用者開啟許可權彈框
 * 最後修改:
 */

public class PreMissionDialog {
    public final static int ACTION_APPLICATION_DETAILS_SETTINGS = 0x100;

    /**
     * 申請許可權
     *
     * @param message
     */
    public static void showPermissionDialog(final Activity mActivity, String message) {
        AlertDialog.Builder dialog = new AlertDialog.Builder(mActivity);
        dialog.setTitle("許可權提醒")
                .setMessage("請在許可權管理中允許" + message + "許可權")
                .setPositiveButton("許可權設定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        Uri uri = Uri.fromParts("package", mActivity.getPackageName(), null);
                        intent.setData(uri);
                        mActivity.startActivityForResult(intent, ACTION_APPLICATION_DETAILS_SETTINGS);
                    }
                })
                .setNegativeButton("取消", null)
                .create()
                .show();
    }
}

進行上述操作,就可以成功開啟熱點了。

3.2 獲取當前網路的廣播地址。相容wifi環境/乙太網環境/無網路環境下。

UDP傳送一個廣播命令,需要有一個廣播地址。先說一下廣播地址是怎麼組成的。通常大家連線到的wifi是IP地址是:1xx.1xx.2xx.45 這樣樣子的。那麼廣播地址就是:1xx.1xx.2xx.255 這樣。那麼問題來了,這個廣播地址是怎麼來的呢?是用IP地址的最後一個“.”後邊的數字改成“255” 麼?不是的,這裡就要提一下子網掩碼了。先看一下圖片:

子網掩碼1

子網掩碼2

通過上邊的圖片大家可以發現,並不是所有的子網掩碼都是255.255.255.0 這樣子的。那麼廣播地址和子網掩碼又有什麼關係呢?其實廣播地址是當前網段的最後一個地址,而通過子網掩碼就能看出來當前網段有多多少個地址。像最後一個是0的,就是有1-255個地址,最後一個.255就是廣播地址。而第二個240的呢,就是有1-15個地址,最後一個.15就是廣播地址。
也就是說用255-子網掩碼的最後一個值就是廣播地址。
那麼結論就出來了,最後的廣播地址等於IP地址的前三個+(255-子網掩碼的最後一個)。這樣拼起來就是真正的廣播地址。(這裡不知道解釋的清楚不清楚,或者說的就有錯誤,歡迎各位大牛提意見!)

然後咱們就去去子網掩碼和IP地址就好了。那麼坑又來了。在wifi環境下,取到這兩個值是沒問題的,但是在乙太網環境下,怎麼獲取IP地址呢,是不是需要必須有網路呢?為什麼在手機資訊裡邊看到的IP地址和電腦連結手機熱點的IP地址不在一個網段呢?唉,問題還真多!直接放一個工具類出來好了,通過這個工具類就能截至獲取到廣播地址了,包括wifi環境和乙太網環境!程式碼如下:

import android.content.Context;
import android.net.DhcpInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;

import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.List;

/**
 * 類描述:獲取ip
 * 作  者:Admin or 李小米
 * 時  間:2018/1/11
 * 修改備註:
 */
public class IPUtils {

    public static String getIp(Context mContext) throws SocketException {
        String ip = "";
        //獲取wifi服務
        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
        if (wifiManager.isWifiEnabled()) {
            WifiInfo wifiInfo = wifiManager.getConnectionInfo();
            int ipAddress = wifiInfo.getIpAddress();
            DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
            String dhcpInfos = intToIp(dhcpInfo.netmask);
            String[] split = intToIp(ipAddress).split("\\.");
            ip = split[0] + "." + split[1] + "." + split[2] + "." + (255 - Integer.parseInt(dhcpInfos.split("\\.")[3]));//根據子網掩碼獲取廣播的IP地址
        } else {
            String asd = getInfo();
            String[] split = asd.split(",");
            String ipStr = split[0];
            String NetMask = split[1];
            String[] split1 = ipStr.split("\\.");
            ip = split1[0] + "." + split1[1] + "." + split1[2] + "." + (255 - Integer.parseInt(NetMask.split("\\.")[3]));//根據子網掩碼獲取廣播的IP地址
        }
        return ip;
    }


    private static String intToIp(int paramInt) {
        return (paramInt & 0xFF) + "." + (0xFF & paramInt >> 8) + "." + (0xFF & paramInt >> 16) + "."
                + (0xFF & paramInt >> 24);
    }


    public static String getInfo() throws SocketException {
        String ipAddress = "";
        String maskAddress = "";

        for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
            NetworkInterface intf = en.nextElement();
            List<InterfaceAddress> mList = intf.getInterfaceAddresses();
            for (InterfaceAddress l : mList) {
                InetAddress inetAddress = l.getAddress();
                if (!inetAddress.isLoopbackAddress()) {
                    String hostAddress = inetAddress.getHostAddress();
                    if (hostAddress.indexOf(":") > 0) {
                        continue;
                    } else {
                        ipAddress = hostAddress;
                        maskAddress = calcMaskByPrefixLength(l.getNetworkPrefixLength());
                    }
                }
            }
        }
        return ipAddress + "," + maskAddress;
    }


    private static String calcMaskByPrefixLength(int length) {
        int mask = -1 << (32 - length);
        int partsNum = 4;
        int bitsOfPart = 8;
        int maskParts[] = new int[partsNum];
        int selector = 0x000000ff;

        for (int i = 0; i < maskParts.length; i++) {
            int pos = maskParts.length - 1 - i;
            maskParts[pos] = (mask >> (i * bitsOfPart)) & selector;
        }

        String result = "";
        result = result + maskParts[0];
        for (int i = 1; i < maskParts.length; i++) {
            result = result + "." + maskParts[i];
        }
        return result;
    }

}

4、結語:

這裡因為是直接在專案裡寫的,就不寫demo了,遇到的問題都貼出來程式碼了。希望可以幫到小夥伴們。哦,如果文中有錯誤,尤其是對子網掩碼的解釋,歡迎大牛們提出批評意見!