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” 麼?不是的,這裡就要提一下子網掩碼了。先看一下圖片:
通過上邊的圖片大家可以發現,並不是所有的子網掩碼都是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了,遇到的問題都貼出來程式碼了。希望可以幫到小夥伴們。哦,如果文中有錯誤,尤其是對子網掩碼的解釋,歡迎大牛們提出批評意見!