1.写在前面
项目中需要获取wifi列表同时连接wifi,本来这种需要以前做过的,感觉挺简单的,后面发现坑比较多,看了很多博客和资料总是有些问题解决不了,比如兼容android6.0版本,到底需要动态申请哪些权限,以什么样的方式友好提示用户,怎样监听wifi连接成功或者失败 或者wifi列表的变化等等,现在需求完成,总结下吧!
2.步骤分析
- 进入页面。检测wifi开关是否打开,权限是否已经获取,如果没,动态申请权限,如果是请求wifi列表。
- 获取扫描wifi列表,循环遍历转成自己的wifi对象和集合。更新适配器,刷新界面列表
- 书写适配器,Recyclerview设置适配器。
- 注册广播,监听wifi开关状态,监听wifi是否已经连接,监听wifi列表是否变化。
- 点击wifi列表item连接。具体情况具体分析,已连接–>wifi详情;未连接–>连接;
3.代码
3.1 android6.0系统权限申请
进入页面检测权限以及wifi开启状态
//权限请求码 private static final int PERMISSION_REQUEST_CODE = 0; //两个危险权限需要动态申请 private static final String[] NEEDED_PERMISSIONS = new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; private boolean mHasPermission; mHasPermission = checkPermission(); if (!mHasPermission) { requestPermission(); } /** * 检查是否已经授予权限 * @return */ private boolean checkPermission() { for (String permission : NEEDED_PERMISSIONS) { if (ActivityCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { return false; } } return true; } /** * 申请权限 */ private void requestPermission() { ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, PERMISSION_REQUEST_CODE); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); boolean hasAllPermission = true; if (requestCode == PERMISSION_REQUEST_CODE) { for (int i : grantResults) { if (i != PackageManager.PERMISSION_GRANTED) { hasAllPermission = false; //判断用户是否同意获取权限 break; } } //如果同意权限 if (hasAllPermission) { mHasPermission = true; if(WifiSupport.isOpenWifi(MainActivity.this) && mHasPermission){ //如果wifi开关是开 并且 已经获取权限 sortScaResult(); }else{ Toast.makeText(MainActivity.this,"WIFI处于关闭状态或权限获取失败",Toast.LENGTH_SHORT).show(); } } else { //用户不同意权限 mHasPermission = false; Toast.makeText(MainActivity.this,"获取权限失败",Toast.LENGTH_SHORT).show(); } } }
对于如上的代码主要是针对android6.0系统,获取wifi信息。首先进入此页面就检测是否已经获取权限或者wifi已经打开,如果wifi打开并且权限已经获取,搜索周围wifi数据,如果没有获取权限则申请权限。这一部分有关权限,我找了很多资料,最后确定为这两个是正确的
Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION
1.通过WiFi或移动基站的方式获取用户错略的经纬度信息,定位精度大概误差在30~1500米,这是第一个。
2.通过GPS芯片接收卫星的定位信息,定位精度达10米以内,这是第二个权限
3.2 获取wifi列表集合数据
这里特别要注意,对于wifi对象,我们不能使用原生的对象ScanResult,因为它的属性中不能确定wifi的状态,不能知道此wifi是否处于连接状态,所以我们需要新建bean对象作为wifi的信息,然后将ScanResult的集合循环遍历,转成自己bean的集合。
ScanResult的属性如下:
public String SSID; public String capabilities; public int centerFreq0; public int centerFreq1; public int channelWidth; public int frequency; public int level; public CharSequence operatorFriendlyName; public long timestamp; public CharSequence venueName;
自己定义的bean如下,并且为了保证集合中按照信号强度排序,需要实现Comparable接口,然后重写compareTo方法,代码如下:
public class WifiBean implements Comparable<WifiBean> { private String wifiName; private String level; private String state; //已连接 正在连接 未连接 三种状态 private String capabilities;//加密方式 ....... 省略get set方法 ....... @Override public int compareTo(WifiBean o) { int level1 = Integer.parseInt(this.getLevel()); int level2 = Integer.parseInt(o.getLevel()); return level1 - level2; } }
所以接下来的思路就简单了,两个bean,一个是原生的ScanResult,一个是自定义的WifiBean,首先获取
ScanResult的集合,去重,循环遍历获取WifiBean的集合。
/** * 去除同名WIFI * * @param oldSr 需要去除同名的列表 * @return 返回不包含同命的列表 */ public static List<ScanResult> noSameName(List<ScanResult> oldSr) { List<ScanResult> newSr = new ArrayList<ScanResult>(); for (ScanResult result : oldSr) { if (!TextUtils.isEmpty(result.SSID) && !containName(newSr, result.SSID)) newSr.add(result); } return newSr; } /** * 判断一个扫描结果中,是否包含了某个名称的WIFI * @param sr 扫描结果 * @param name 要查询的名称 * @return 返回true表示包含了该名称的WIFI,返回false表示不包含 */ public static boolean containName(List<ScanResult> sr, String name) { for (ScanResult result : sr) { if (!TextUtils.isEmpty(result.SSID) && result.SSID.equals(name)) return true; } return false; } List<ScanResult> scanResults = WifiSupport.noSameName(WifiSupport.getWifiScanResult(this)); // List<WifiBean> realWifiList = new ArrayList<>(); /** * 获取wifi列表然后将bean转成自己定义的WifiBean */ public void sortScaResult(){ List<ScanResult> scanResults = WifiSupport.noSameName(WifiSupport.getWifiScanResult(this)); realWifiList.clear(); if(!CollectionUtils.isNullOrEmpty(scanResults)){ for(int i = 0;i < scanResults.size();i++){ WifiBean wifiBean = new WifiBean(); wifiBean.setWifiName(scanResults.get(i).SSID); wifiBean.setState(AppContants.WIFI_STATE_UNCONNECT); //只要获取都假设设置成未连接,真正的状态都通过广播来确定 wifiBean.setCapabilities(scanResults.get(i).capabilities); wifiBean.setLevel(WifiSupport.getLevel(scanResults.get(i).level)+""); realWifiList.add(wifiBean); //排序 Collections.sort(realWifiList); adapter.notifyDataSetChanged(); } } }
这个sortScaResult()使用的比较多,比较简单,就是循环遍历list,然后set,组成一个新的list,特别要注意的是
public static final String WIFI_STATE_CONNECT = "已连接"; public static final String WIFI_STATE_ON_CONNECTING = "正在连接"; public static final String WIFI_STATE_UNCONNECT = "未连接"; wifiBean.setState(AppContants.WIFI_STATE_UNCONNECT); //只要获取都假设设置成未连接,真正的状态都通过广播来确定
假设进入此页面,wifi列表中一个wifi已经处于连接状态,这里获取是我们还是假设它处于未连接状态,因为后面我们都是需要通过广播来确定网络连接状态,并且为了提高用户体验,还需要将已经连接的wifi放到列表首位。
3.3 Adapter适配器的书写
对于此列表的适配器比较简单,主要就是onBindViewHolder和点击事件,对于onBindViewHolder看效果图中,我们可以知道如果某一个wifi处于已连接或者正在连接状态,wifi名称的颜色需要改变。然后对于点击事件,这里也是采用常规的方法,在activity中回调,代码如下
@Override public void onBindViewHolder(MyViewHolder holder, final int position) { final WifiBean bean = resultList.get(position); holder.tvItemWifiName.setText(bean.getWifiName()); holder.tvItemWifiStatus.setText("("+bean.getState()+")"); //可以传递给adapter的数据都是经过处理的,已连接或者正在连接状态的wifi都是处于集合中的首位,所以可以写出如下判断 if(position == 0 && (AppContants.WIFI_STATE_ON_CONNECTING.equals(bean.getState()) || AppContants.WIFI_STATE_CONNECT.equals(bean.getState()))){ holder.tvItemWifiName.setTextColor(mContext.getResources().getColor(R.color.homecolor1)); holder.tvItemWifiStatus.setTextColor(mContext.getResources().getColor(R.color.homecolor1)); }else{ holder.tvItemWifiName.setTextColor(mContext.getResources().getColor(R.color.gray_home)); holder.tvItemWifiStatus.setTextColor(mContext.getResources().getColor(R.color.gray_home)); } holder.itemview.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onItemClick(view,position,bean); } }); }
3.4 注册监听广播
这里我把广播注册在onResume()中,因为有一些需求是点击wifi连接后可能会跳转 或者 点击wifi连接后有fragmentdialog或者activitydialog等等,万一需要回到这个页面,就不会在经历onCreate(),这样不太好,这里注册了三个广播
@Override protected void onResume() { super.onResume(); //注册广播 wifiReceiver = new WifiBroadcastReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);//监听wifi是开关变化的状态 filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);//监听wifiwifi连接状态广播 filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);//监听wifi列表变化(开启一个热点或者关闭一个热点) this.registerReceiver(wifiReceiver, filter); } @Override protected void onPause() { super.onPause(); WifiListActivity.this.unregisterReceiver(wifiReceiver); }
其实上面杀那个广播中最主要的是第二个,当然其他两个也需要,但是接收到广播后需要做的事情并不复杂,先看第一个简单的
if(WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())){ int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0); switch (state){ /** * WIFI_STATE_DISABLED WLAN已经关闭 * WIFI_STATE_DISABLING WLAN正在关闭 * WIFI_STATE_ENABLED WLAN已经打开 * WIFI_STATE_ENABLING WLAN正在打开 * WIFI_STATE_UNKNOWN 未知 */ case WifiManager.WIFI_STATE_DISABLED:{ Log.d(TAG,"已经关闭"); break; } case WifiManager.WIFI_STATE_DISABLING:{ Log.d(TAG,"正在关闭"); break; } case WifiManager.WIFI_STATE_ENABLED:{ Log.d(TAG,"已经打开"); sortScaResult(); break; } case WifiManager.WIFI_STATE_ENABLING:{ Log.d(TAG,"正在打开"); break; } case WifiManager.WIFI_STATE_UNKNOWN:{ Log.d(TAG,"未知状态"); break; } } }
这是在广播接收器中接收到第一个广播wifi开关状态的,并不需要做什么,如果wifi是关闭的 直接弹出toast,如果是打开 搜索下wifi列表集合,
然后看第二个广播,监听下wifi是否连接了一个有效的路由,比如wifi进入这个页面并有连接wifi,是处于4G或者既不是wifi也不是4G,那么将所有wifi设置成未连接状态
for(int i = 0;i < realWifiList.size();i++){//没连接上将 所有的连接状态都置为“未连接” realWifiList.get(i).setState(AppContants.WIFI_STATE_UNCONNECT); } adapter.notifyDataSetChanged();
如果进入此页面本身就已经连接了一个wifi,那么获取到已经连接wifi的信息数据,并且将它放到集合中的首位,设置成已连接状态
Log.d(TAG,"wifi连接上了"); hidingProgressBar(); WifiInfo connectedWifiInfo = WifiSupport.getConnectedWifiInfo(MainActivity.this); //连接成功 跳转界面 传递ip地址 Toast.makeText(MainActivity.this,"wifi连接上了",Toast.LENGTH_SHORT).show(); connectType = 1; wifiListSet(connectedWifiInfo.getSSID(),connectType);
正在连接状态也是如上,和已连接一样
第三个广播很简单,如果周围某一wifi刚好发生变化,比如关闭了,刷新下wifi列表数据
总的代码如下
//监听wifi状态 public class WifiBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if(WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())){ int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0); switch (state){ /** * WIFI_STATE_DISABLED WLAN已经关闭 * WIFI_STATE_DISABLING WLAN正在关闭 * WIFI_STATE_ENABLED WLAN已经打开 * WIFI_STATE_ENABLING WLAN正在打开 * WIFI_STATE_UNKNOWN 未知 */ case WifiManager.WIFI_STATE_DISABLED:{ Log.d(TAG,"已经关闭"); Toast.makeText(MainActivity.this,"WIFI处于关闭状态",Toast.LENGTH_SHORT).show(); break; } case WifiManager.WIFI_STATE_DISABLING:{ Log.d(TAG,"正在关闭"); break; } case WifiManager.WIFI_STATE_ENABLED:{ Log.d(TAG,"已经打开"); sortScaResult(); break; } case WifiManager.WIFI_STATE_ENABLING:{ Log.d(TAG,"正在打开"); break; } case WifiManager.WIFI_STATE_UNKNOWN:{ Log.d(TAG,"未知状态"); break; } } }else if(WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())){ NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); Log.d(TAG, "--NetworkInfo--" + info.toString()); if(NetworkInfo.State.DISCONNECTED == info.getState()){//wifi没连接上 Log.d(TAG,"wifi没连接上"); hidingProgressBar(); for(int i = 0;i < realWifiList.size();i++){//没连接上将 所有的连接状态都置为“未连接” realWifiList.get(i).setState(AppContants.WIFI_STATE_UNCONNECT); } adapter.notifyDataSetChanged(); }else if(NetworkInfo.State.CONNECTED == info.getState()){//wifi连接上了 Log.d(TAG,"wifi连接上了"); hidingProgressBar(); WifiInfo connectedWifiInfo = WifiSupport.getConnectedWifiInfo(MainActivity.this); //连接成功 跳转界面 传递ip地址 Toast.makeText(MainActivity.this,"wifi连接上了",Toast.LENGTH_SHORT).show(); connectType = 1; wifiListSet(connectedWifiInfo.getSSID(),connectType); }else if(NetworkInfo.State.CONNECTING == info.getState()){//正在连接 Log.d(TAG,"wifi正在连接"); showProgressBar(); WifiInfo connectedWifiInfo = WifiSupport.getConnectedWifiInfo(MainActivity.this); connectType = 2; wifiListSet(connectedWifiInfo.getSSID(),connectType ); } }else if(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(intent.getAction())){ Log.d(TAG,"网络列表变化了"); wifiListChange(); } } }
上面的将以连接或者正在连接放到首位去方法
/** * 将"已连接"或者"正在连接"的wifi热点放置在第一个位置 * @param wifiName * @param type */ public void wifiListSet(String wifiName , int type){ int index = -1; WifiBean wifiInfo = new WifiBean(); if(CollectionUtils.isNullOrEmpty(realWifiList)){ return; } for(int i = 0;i < realWifiList.size();i++){ realWifiList.get(i).setState(AppContants.WIFI_STATE_UNCONNECT); } Collections.sort(realWifiList);//根据信号强度排序 for(int i = 0;i < realWifiList.size();i++){ WifiBean wifiBean = realWifiList.get(i); if(index == -1 && ("\"" + wifiBean.getWifiName() + "\"").equals(wifiName)){ index = i; wifiInfo.setLevel(wifiBean.getLevel()); wifiInfo.setWifiName(wifiBean.getWifiName()); wifiInfo.setCapabilities(wifiBean.getCapabilities()); if(type == 1){ wifiInfo.setState(AppContants.WIFI_STATE_CONNECT); }else{ wifiInfo.setState(AppContants.WIFI_STATE_ON_CONNECTING); } } } if(index != -1){ realWifiList.remove(index); realWifiList.add(0, wifiInfo); adapter.notifyDataSetChanged(); } }
3.5 点击wifi连接
点击事件这里比较复杂,需要根据自己的需求来,首先当然一个item处于“未连接”或者“已连接”才可以点击,然后如果“已连接”点击后出现什么情况,不同的需求有不同的代码,一般常规的可能就是弹出dialog显示下已经连接wifi的信息数据,这里我没考虑这种情况了,然后“未连接”分为很多情况,比如需不需要密码,不需要密码直接用方法连接,如果需要密码 又分为好几种情况,以前连接过没,有没有保存的密码,以前从没连接过,弹出dialog输入密码。
这里我没有分为这么多情况,就两种,有密码 和 没密码 ,只要是要输入密码的wifi全部重新输入密码
adapter.setOnItemClickListener(new WifiListAdapter.onItemClickListener() { @Override public void onItemClick(View view, int postion, Object o) { WifiBean wifiBean = realWifiList.get(postion); if(wifiBean.getState().equals(AppContants.WIFI_STATE_UNCONNECT) || wifiBean.getState().equals(AppContants.WIFI_STATE_CONNECT)){ String capabilities = realWifiList.get(postion).getCapabilities(); if(WifiSupport.getWifiCipher(capabilities) == WifiSupport.WifiCipherType.WIFICIPHER_NOPASS){//无需密码 WifiConfiguration exsits = WifiSupport.createWifiConfig(wifiBean.getWifiName(), null, WifiSupport.WifiCipherType.WIFICIPHER_NOPASS); WifiSupport.addNetWork(exsits, MainActivity.this); }else{ //需要密码,弹出输入密码dialog noConfigurationWifi(postion); } } } });
4.感谢
注意 :比较尴尬,本来以为没问题的了,我在华为荣耀4A android5.1上和小米 5S android7.0上测试,有无密码都可以连接,但是今天领导被用户投诉了一大堆,直接找到我说华为P系列全部连不上,然后测试了下,还真的是,他们的手机大部分都是6.0以上的,但是我觉得我能在7.0上没问题,应该不是版本的问题,难道只是华为荣耀这种情况,然后debug 打log对比华为荣耀4A android5.1 和 华为荣耀P android6.0招了下就是WifiConfiguration对象总是有点不一样,然后搜了下 还真的搜到相关内容,
十分感谢这位大佬
通过他的思路,修改了下代码,解决了,这博客代码没改过来,github上代码提交更新了,如果帮到你 给个star吧
本文由老郭种树原创,转载请注明:https://guozh.net/adnroid-wifi-adapter-6-0/
你的github是啥地址啊。
这个是否只适合安卓开发,web不行