前言
本文代码只在Android8及以下正常使用
Android9上WIFI连接后无法获取ssid
Android10上完全无效
相关知识
涉及到的权限
这里需要说明的是
android.permission.ACCESS_FINE_LOCATION
这个权限在Android6.0以上是必须的因为在Android6.0以上必须开启位置获取位置权限 才能获取WI-FI列表 否则列表会为空
android.permission.WRITE_SECURE_SETTINGS
这个权限在Android6.0以上是系统权限普通应用是无法获取的 所以其实不用引 如果APP定制的Android系统在6.0以下可以引 用来修改配置
1 | <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> |
涉及到的类
WifiManager 入口类,Wifi相关的所有操作均通过此类
WifiConfiguration 进行热点连接时,通过该类为热点创建一个配置,并由WifiManager以此配置生成一个networkId,后开始连接;
此外,也用于表示一个已连接的热点在本地的记录
WifiInfo 表示当前的wifi网络连接信息
ScanResult 扫描到的热点信息类,每一个对象代表一个扫描到的热点,其中包括若干该热点信息
相关属性及概念
- networkId——连接某个wifi热点时,系统会为该热点生成一个networkId,在同一设备上,不同热点的networkId是唯一的,通常情况下为大于0的整数,在某些设备上,恢复出厂后连接的第一个热点networkId为0
- ssid——wifi热点名称,可重复
- bssid——类似于mac地址,但并不是路由器的mac地址,与ssid一起可作为热点的唯一标识,同时该属性每个热点唯一不重复
- 亲属热点——(本文设定概念)ssid相同,但bssid不同的所有热点,互为亲属热点,android设备会将ssid相同的所有亲属热点当做一个热点进行处理
ScanResult 和 WifiInfo 中的ssid是有差异的
- WifiInfo中的ssid是包括了双引号的,如
"CCMC"
- ScanResult中的ssid是不包括双引号的,如
CCMC
热点加密类型
目前,常见及需要处理的热点,包括以下3大类:
- open——开放型网络,即无加密,可直接连接
- wep——采用wep加密类型的热点,已过时,不安全,容易被破解,目前使用率已不足10%
- wpa/wpa2——目前使用最广泛,相对最安全,破解难度最大的加密类型
wps(wifi protected setup):是为了进一步增强wpa热点及简化连接过程的技术,不属于加密类型。
获取加密类型的方法
1 | var capabilities = scanResult!!.capabilities.toLowerCase() |
涉及到的广播
- WifiManager.WIFI_STATE_CHANGED_ACTION ——wifi开关变化广播
- WifiManager.SCAN_RESULTS_AVAILABLE_ACTION ——热点扫描结果通知广播
- WifiManager.SUPPLICANT_STATE_CHANGED_ACTION ——热点连接结果通知广播
- WifiManager.NETWORK_STATE_CHANGED_ACTION ——网络状态变化广播(与上一广播协同完成连接过程通知)
开发细节
1 获取WifiManager实例
1 | wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); |
2 打开及关闭wifi
1 | wifiManager.setWifiEnabled(true) |
true表示打开wifi开关,false表示关闭,该方法的返回值仅代表操作是否成功,不代表wifi状态的变化;
通过监听广播WifiManager.WIFI_STATE_CHANGED_ACTION
,来判断真正的wifi开关变化,该广播带有一个int型的值来表示wifi状态:
1 | int wifistate = intent.getIntExtra( |
该操作其实是一个异步操作,一般耗时在1~3秒之间。
3 周围热点扫描
收到WI-FI已打开的广播后 开始扫描
1 | wifiManager.startScan() |
以上方法为开始扫描的接口,其返回值代表操作是否成功,扫描结果通过另外一个接口获取:
1 | List<ScanResult> results = wifiManager.getScanResults(); |
一般在主动调用startScan之后,大概2秒左右,会收到WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
广播通知,该广播包括一个boolean型的额外参数:
1 | boolean isScanned = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true); |
上面的值表示,扫描结果是否已可用,若可用,则可以使用getScanResults获取结果,在结果没有就绪之前,会返回null。
一般系统本身会调用startScan接口,而该操作相对比较耗电,因此在应用中要酌情使用,并不需要频繁调用。
4 获取已连接过的热点
所有已经连接过的热点,都会存在本地一个文件中,一般路径为/data/misc/wifi/wpa_supplicant.conf(查看需root),而在程序中获取则通过以下接口:
1 | List<WifiConfiguration> configurations = wifiManager.getConfiguredNetworks(); |
获取到的WiFiConfiguration对象中,只有ssid和networkId是一定有的,可以用于直接连接该热点,其他信息如bssid,密钥等信息基本都是空的。(如何直接连接热点,下文叙述)
5 获取当前wifi连接信息
1 | WifiInfo info = wifiManager.getConnectionInfo(); |
该对象代表当前已连接的热点,信息,无连接时返回对象的networkId为-1;
该对象可获取包括ssid,bssid,networkId等信息,而ssid是包括了双引号的,如“CCMC”,在之前的扫描结果ScanResult中,ssid并不带双引号。
6 连接指定热点
连接一个未连接过的热点时,需3步:
1)创建一个配置:WifiConfiguration
1 | public WifiConfiguration createConfiguration(AccessPoint ap) { |
2)生成一个networkId
1 | WifiConfiguration config = createConfiguration(ap); |
一般情况下,对一个已经连接过的热点(本地有连接记录),进行以上操作时,在api21及以上会返回一个小于0的networkId,此时,进行下一步连接是没有意义的,获得一个小于0的networkId已经表示连接失败。
3)开始连接
1 | wifiManager.enableNetwork(networkId, true) |
对于已经连接过的热点,通过小项4 中的方式,获取到该热点的networkId之后,可直接进行第三步的连接,无需1)2);
若有必要进行12步(如尝试一个新密码,因为即使使用了错误的密码连接,系统还是会为本次连接生成一个本地记录),则必须在一开始,将本地记录remove掉,remove操作将在下文介绍。
连接结果通过两个广播反馈:
- WifiManager.NETWORK_STATE_CHANGED_ACTION
- WifiManager.SUPPLICANT_STATE_CHANGED_ACTION
其中,密码错误的结果通知需通过第二个广播判断:
1 | int error = intent.getIntExtra(WifiManager.EXTRA_SUPPLICANT_ERROR, 0); |
其他结果均通过第一个广播接收:
1 | if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { |
1 | public enum DetailedState { |
7 断开当前wifi连接
1 | wifiManager.disconnect() |
以上接口返回值代表当前操作是否成功,操作的最终结果,会在两个广播中有所反馈:
- WifiManager.SUPPLICANT_STATE_CHANGED_ACTION
- WifiManager.NETWORK_STATE_CHANGED_ACTION
并且断开成功的广播会发送若干次。
8 遗忘一个已连接过的热点
1 | boolean isRemoved = wifiManager.removeNetwork(networkId) |
返回值代表操作是否成功,该操作在api21以上的系统中,成功率在10%以下,在api21以下,基本都可以成功;
可以通过反复进行此操作来提高成功率,但效果不大。
实战
工具类
1 | import android.content.Context; |
获取Wi-Fi列表
全局变量
1 | private var mListAdapter: SWifiAdapter? = null |
这里之所以定义了一个wifiMap
是因为获取的ScanResult
的列表中 会有相同SSID的WI-FI 使用Map来过滤掉
在接收到WI-FI打开的广播后 扫描WI-FI
1 | var locManager = mContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager |
刷新列表
1 | private fun reloadData() { |
添加广播接收
1 | var receiver = object : BroadcastReceiver() { |