本篇博客主要记录一下 Android 中打开 Wifi、获取 Wifi 接入点信息及连接指接入点的方法。
自己写的 demo 主要用于测试接口的基本功能,因此界面及底层逻辑比较粗糙。
demo 的整体界面如下所示:
上图中的 OPEN 按键负责开启 Wifi;
GET 按键负责获取扫描到的接入点信息。
当获取到接入点信息后,我选取了其中的名称及信号强度,以列表的形式显示在主界面下方,如下图:
当点击列表中的 Item 时,就会去连接对应的接入点。
自己的逻辑比较简单,测试时的代码,假定连接的是不许要密码或密码已知的接入点。
demo 的布局文件就不介绍了,就是 Button 和 RecyclerView。
主要记录一下,使用到的核心代码。
- ....................
- //Open按键点击后的逻辑
- mOpenWifiButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- //WifiManager的isWifiEnabled接口,用于判断Wifi开关是否已经开启
- if (!mWifiManager.isWifiEnabled()) {
- //setWifiEnabled接口用于开启Wifi
- mWifiManager.setWifiEnabled(true);
- mMainHandler.post(mMainRunnable);
- }
- }
- });
- ....................
mMainRunnable 的代码如下,主要用于判断 Wifi 是否开启成功。
- ................
- private Runnable mMainRunnable = new Runnable() {
- @Override
- public void run() {
- if (mWifiManager.isWifiEnabled()) {
- //开启成功后,使能Get按键
- mGetWifiInfoButton.setEnabled(true);
- } else {
- mMainHandler.postDelayed(mMainRunnable, 1000);
- }
- }
- };
- ...............
这部分代码,主要使用了 WifiManager 的公有接口,开启 Wifi 开关及判断开启状态。
这部分操作需要的权限是:
- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
- <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
Get 按键被点击后,对应的代码如下:
- .................
- mGetWifiInfoButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (mWifiManager.isWifiEnabled()) {
- //getScanResults接口将返回List<ScanResult>
- //ScanResult中保留了每个接入点的基本信息
- mScanResultList = mWifiManager.getScanResults();
- //多个接入点可能携带相同的信息,形成一个整体的Wifi覆盖网络
- //因此,筛除一些冗余信息
- sortList(mScanResultList);
- //我使用的是RecyclerView,得到数据后,刷新界面进行显示
- mWifiInfoRecyclerView.getAdapter().notifyDataSetChanged();
- }
- }
- });
- .................
上面这部分代码也比较简单,主要利用 WifiManager 的 getScanResults 接口,获取终端探索到的接入点信息。
其中,sortList 的代码如下:
- ..............
- private void sortList(List<ScanResult> list) {
- TreeMap<String, ScanResult> map = new TreeMap<>();
- //demo中仅按照SSID进行筛选
- //实际使用时,还可以参考信号强度等条件
- for (ScanResult scanResult : list) {
- map.put(scanResult.SSID, scanResult);
- }
- list.clear();
- list.addAll(map.values());
- }
- .............
这部分代码唯一需要注意的地方是,需要申明权限:
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
- />
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
- />
同时,在高版本中还需要主动获取运行时权限。
权限的要求,是由 WifiServiceImpl 的实现决定的,我们以 Android 7.0 为例,看看对应的代码:
- public List<ScanResult> getScanResults(String callingPackage) {
- //这里要求的是ACCESS_WIFI_STATE
- enforceAccessPermission();
- ............
- try {
- ...........
- if (!canReadPeerMacAddresses && !isActiveNetworkScorer
- //在checkCallerCanAccessScanResults中检查了ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION
- //如果没有这两个权限,就会返回一个empty List
- && !checkCallerCanAccessScanResults(callingPackage, uid)) {
- return new ArrayList<ScanResult>();
- }
- ...........
- } fianlly {
- ..........
- }
- }
获取到信息后,就可以显示和点击列表中的 Item 了。
由于自己使用的是 RecyclerView,因此这部分工作全部交给了对应 ViewHolder:
- ...............
- private class ScanResultViewHolder extends RecyclerView.ViewHolder {
- private View mView;
- private TextView mWifiName;
- private TextView mWifiLevel;
- ScanResultViewHolder(View itemView) {
- super(itemView);
- mView = itemView;
- mWifiName = (TextView) itemView.findViewById(R.id.ssid);
- mWifiLevel = (TextView) itemView.findViewById(R.id.level);
- }
- void bindScanResult(final ScanResult scanResult) {
- //将接入点的名称和强度显示到界面上
- mWifiName.setText(
- getString(R.string.scan_wifi_name, "" + scanResult.SSID));
- mWifiLevel.setText(
- getString(R.string.scan_wifi_level, "" + scanResult.level));
- //点击Item后,就连接对应的接入点
- mView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- //createWifiConfig主要用于构建一个WifiConfiguration,代码中的例子主要用于连接不需要密码的Wifi
- //WifiManager的addNetwork接口,传入WifiConfiguration后,得到对应的NetworkId
- int netId = mWifiManager.addNetwork(createWifiConfig(scanResult.SSID, "", WIFICIPHER_NOPASS));
- //WifiManager的enableNetwork接口,就可以连接到netId对应的wifi了
- //其中boolean参数,主要用于指定是否需要断开其它Wifi网络
- boolean enable = mWifiManager.enableNetwork(netId, true);
- Log.d("ZJTest", "enable: " + enable);
- //可选操作,让Wifi重新连接最近使用过的接入点
- //如果上文的enableNetwork成功,那么reconnect同样连接netId对应的网络
- //若失败,则连接之前成功过的网络
- boolean reconnect = mWifiManager.reconnect();
- Log.d("ZJTest", "reconnect: " + reconnect);
- }
- });
- }
- }
- .................
以上就是连接指定 Wifi 的基本套路,从代码中容易看出,关键问题是如何创建出有效的 WifiConfiguration。
自己测试时,初始创建 WifiConfiguration 失败,手机怎么都没法连接到热点上,后来修改后,基本功能终于能够实现:
- ....................
- private static final int WIFICIPHER_NOPASS = 0;
- private static final int WIFICIPHER_WEP = 1;
- private static final int WIFICIPHER_WPA = 2;
- private WifiConfiguration createWifiConfig(String ssid, String password, int type) {
- //初始化WifiConfiguration
- WifiConfiguration config = new WifiConfiguration();
- config.allowedAuthAlgorithms.clear();
- config.allowedGroupCiphers.clear();
- config.allowedKeyManagement.clear();
- config.allowedPairwiseCiphers.clear();
- config.allowedProtocols.clear();
- //指定对应的SSID
- config.SSID = "\"" + ssid + "\"";
- //如果之前有类似的配置
- WifiConfiguration tempConfig = isExist(ssid);
- if(tempConfig != null) {
- //则清除旧有配置
- mWifiManager.removeNetwork(tempConfig.networkId);
- }
- //不需要密码的场景
- if(type == WIFICIPHER_NOPASS) {
- config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
- //以WEP加密的场景
- } else if(type == WIFICIPHER_WEP) {
- config.hiddenSSID = true;
- config.wepKeys[0]= "\""+password+"\"";
- config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
- config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
- config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
- config.wepTxKeyIndex = 0;
- //以WPA加密的场景,自己测试时,发现热点以WPA2建立时,同样可以用这种配置连接
- } else if(type == WIFICIPHER_WPA) {
- config.preSharedKey = "\""+password+"\"";
- config.hiddenSSID = true;
- 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;
- }
- return config;
- }
- .................
- private WifiConfiguration isExist(String ssid) {
- List configs = mWifiManager.getConfiguredNetworks();
- for (WifiConfiguration config : configs) {
- if (config.SSID.equals("\""+ssid+"\"")) {
- return config;
- }
- }
- return null;
- }
- .................
自己写完 demo 后,以一个手机建立热点,分别测试了有密码和无密码的场景(对应的,需要修改 createWifiConfig 的传入参数)。
发现 demo 运行的手机在两种场景下,均能够连接到指定热点。
Demo 地址如下:
在本文的最后,补充一下终端作为热点时的接口。
@hide 注解的公有接口,判断手机的热点是否开启。
在 Android 5.1 之前,,
于是有很多代码会利用 Java 发射机制,获取该方法并判断手机热点是否开启。
现在那些老代码已经没法使用了。
现在的做法(以 5.1 以上为例),应该利用广播接收器监听 WifiManager 中定义的 WIFI_AP_STATE_CHANGED_ACTION。
,所以要直接监听对应的字符串,示例如下 (上面链接中的 demo 也有涉及):
- ...................
- private BroadcastReceiver mBroadcastReceiver;
- private void registerBroadcastReceiver() {
- mBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- //收到广播后,利用"wifi_state"的字段,得到AP的状态
- int state = intent.getIntExtra("wifi_state", 11);
- Log.d("ZJTest", "AP state: " + state);
- }
- };
- IntentFilter intentFilter = new IntentFilter();
- //添加Action对应的字符信息
- intentFilter.addAction("android.net.wifi.WIFI_AP_STATE_CHANGED");
- this.registerReceiver(mBroadcastReceiver, intentFilter);
- }
- .........
- private void unregisterBroadcastReceiver() {
- this.unregisterReceiver(mBroadcastReceiver);
- }
- ..........
我暂时没有深究 Wifi 模块开启 AP 的流程。
不过从自己的测试结果来看,Wifi 开启或关闭 AP 时,推测发送的应该是 Sticky 类型的广播。
于是,只要 APK 注册了广播监听器,立马就会得到回复,明白当前 AP 的状态。
例如,我在开启 AP 后,再打开自己的测试 Demo,立马会收到如下信息:
- //对应WIFI_AP_STATE_ENABLED,定义于WifiManager中,@SystemApi
- 02-20 17:48:52.470 12773-12773/? D/ZJTest: AP state: 13
手动关闭 AP 后可以得到如下结果:
- //WIFI_AP_STATE_DISABLING
- 02-20 17:49:35.803 12773-12773/stark.a.is.zhang.wifitest D/ZJTest: AP state: 10
- //WIFI_AP_STATE_DISABLED
- 02-20 17:49:36.960 12773-12773/stark.a.is.zhang.wifitest D/ZJTest: AP state: 11
@SystemApi,设置和获取 Wifi-AP 的配置信息。
可以看出不论手机作为 AP 还是 STA,在 Framework 中均利用 WifiConfiguration 抽象对应的配置信息,包括鉴权算法、密码、SSID、协议等。
这种设计是符合 802.11 协议精神的,毕竟在物理设备的角度上,AP 和 STA 是完全对等的。只不过在实际情况中,根据各自的需求,特质化了一些组件。
实际上从底层协议来看,仅在传输这个角度上,AP 和 STA 的主要区别仅在于收到数据帧后的处理流程不同。AP 收到数据帧后,发现目的地址不是自己,就会进入转发流程;而 STA 可能就直接丢弃该数据帧了。当然如果从控制的角度来看,即考虑通信信令,AP 和 STA 还是主从的关系。
@SystemApi,改变 Wifi-AP 的开关状态。开启的 AP,将使用参数定义的 WifiConfiguration 信息。
可以看出,手机热点对应接口全部变成了 SystemApi,因此在 android 的高版本上,应用基本上是无法再操作热点了。
来源: http://www.bubuko.com/infodetail-1990952.html