分享

Android开启热点进行UDP通信中的坑

 DeepReading 2019-06-04

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了,遇到的问题都贴出来代码了。希望可以帮到小伙伴们。哦,如果文中有错误,尤其是对子网掩码的解释,欢迎大牛们提出批评意见!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多