概述
WIFI就是一种无线联网技术,常见的是使用无线路由器。那么在这个无线路由器的信号覆盖的范围内都可以采用WIFI连接的方式进行联网。如果无线路由器连接了一个ADSL线路或其他的联网线路,则又被称为“热点”。
在实际使用中,我们进入wifi列表后可以看到下面这样
那wifi列表里面的数据是怎么来的?这里就是本文章主要讲的内容wifi如何进行扫描以及如何去更新数据。
限制
权限
在我们开始使用前需要在AndroidManifest.xml中加入wifi相关的权限:
//用于扫描结束后读取wifi信息
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
//用于扫描WiFi列表
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
Android 8.0 & 8.1
成功调用WifiManager.getScanResults() 方法必须具备以下权限之一,否则会抛异常SecurityException。或者,在搭载 Android 8.0(API 级别 26)及更高版本的设备上,您可以使用 CompanionDeviceManager 代表应用对附近的配套设备执行扫描,而不需要位置权限。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
实际从Android6.0以上调用getScanResults()获取WiFi列表,就必须要打开GPS才能获取,否则为空。是因为6.0之前,APP在不开启GPS定位的情况下,依然可以调用getScanResults获取周边Wifi的相关信息,比如SSID、BSSID、level等,去请求谷歌位置服务器(“http://www.google.com/loc/json”),获取用户所在的位置信息;这显得GPS定位开关毫无意义,显然不合理,是位置权限设计的Bug;Google出于设计考虑,6.0之后版本,获取WiFi列表必须要设备开启了GPS定位,并且APP具备位置权限,才能获取WiFi列表。
Android 9
成功调用 WifiManager.startScan() 需要满足以下所有条件:
- 应用拥有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限。
- 应用拥有 CHANGE_WIFI_STATE 权限。
- 设备已启用位置信息服务(位于设置 > 位置信息下)。
Android 10(API 级别 29)及更高版本:
成功调用 WifiManager.startScan() 需要满足以下所有条件:
- 如果您的应用以 Android 10(API 级别 29)SDK 或更高版本为目标平台,应用需要拥有
ACCESS_FINE_LOCATION 权限。 - 如果您的应用以低于 Android 10(API 级别 29)的 SDK 为目标平台,应用需要拥有
ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION 权限。 - 应用拥有 CHANGE_WIFI_STATE 权限。
- 设备已启用位置信息服务(位于设置 > 位置信息下)。
若要成功调用 WifiManager.getScanResults(),请确保满足以下所有条件:
- 如果您的应用以 Android 10(API 级别 29)SDK 或更高版本为目标平台,应用需要拥有
ACCESS_FINE_LOCATION 权限。 - 如果您的应用以低于 Android 10(API 级别 29)的 SDK 为目标平台,应用需要拥有
ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION 权限。 - 应用拥有 ACCESS_WIFI_STATE 权限。
- 设备已启用位置信息服务(位于设置 > 位置信息下)。
如果调用应用无法满足上述所有要求,调用将失败,并显示 SecurityException。
节流
使用 WifiManager.startScan() 扫描的频率适用以下限制。
Android 8.0 和 Android 8.1:
每个后台应用可以在 30 分钟内扫描一次。
Android 9:
每个前台应用可以在 2 分钟内扫描四次。这样便可在短时间内进行多次扫描。
所有后台应用总共可以在 30 分钟内扫描一次。
Android 10 及更高版本:
适用 Android 9 的节流限制。新增一个开发者选项,用户可以关闭节流功能以便进行本地测试(位于开发者选项 > 网络 > WLAN 扫描调节下)。
实现
扫描流程分为三步:
- 为 SCAN_RESULTS_AVAILABLE_ACTION注册一个广播监听器,系统会在完成扫描请求时调用此监听器,提供其成功/失败状态。对于搭载 Android 10(API 级别29)及更高版本的设备,系统将针对平台或其他应用在设备上执行的所有完整 WLAN扫描发送此广播。应用可以使用广播被动监听设备上所有扫描的完成情况,无需发出自己的扫描。
- 使用 WifiManager.startScan() 请求扫描。请务必检查方法的返回状态,因为调用可能因以下任一原因失败:
由于短时间扫描过多,扫描请求可能遭到节流。
设备处于空闲状态,扫描已停用。
WLAN 硬件报告扫描失败。 - 使用 WifiManager.getScanResults() 获取扫描结果。系统返回的扫描结果为最近更新的结果,但如果当前扫描尚未完成或成功,可能会返回以前扫描的结果。也就是说,如果在收到成功的 SCAN_RESULTS_AVAILABLE_ACTION 广播前调用此方法,您可能会获得较旧的扫描结果。
下面看代码部分的实现,主要代码如下
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
//注册监听
initListener();
//在活动创建时开始扫描WiFi
wifiManager.startScan();
}
private void initListener() {
//注册相关广播的监听
IntentFilter scanFilter = new IntentFilter();
scanFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
if (wifiListReceiver == null) {
wifiListReceiver = new WifiListReceiver();
}
registerReceiver(wifiListReceiver, scanFilter);
......
}
private class WifiListReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
//接收到扫描完成的广播
if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
//开始过滤数据,筛选有效数据。
getRealWifi();
Message message = Message.obtain();
message.what = MSG_SCAN_WIFI;
//因为Wifi的信号是实时变化的,这里我们在拿到上一个数据后再过10s开始下一次的扫描
wifiHandler.sendMessageDelayed(message, 10000);
}
}
}
public void getRealWifi() {
//获取wifi扫描结果
wifiList = wifiManager.getScanResults();
if (wifiList.size() == 0) {
return;
}
//新建一个list存储有效wifi,有些WiFi是无效的需要过滤
if (realWifiList == null) {
realWifiList = new ArrayList();
} else {
realWifiList.clear();
}
for (ScanResult result : wifiList) {
if (result.SSID == null || result.SSID.length() == 0 || result.capabilities.contains("[IBSS]")) {
continue;
}
boolean found = false;
for (ScanResult item : realWifiList) {
if (item.SSID.equals(result.SSID) && item.capabilities.equals(result.capabilities)) {
found = true;
break;
}
}
if (!found) {
realWifiList.add(result);
}
}
if (realWifiList.size() != 0) {
currentWifiInfo = wifiManager.getConnectionInfo();
if (wifiAdapter == null) {
//第一次扫描列表
wifiAdapter = new MyAdapter(WifiActivity.this, realWifiList);
wifiListView.setAdapter(wifiAdapter);
} else {
//更新列表数据
wifiAdapter.list = realWifiList;
wifiAdapter.notifyDataSetChanged();
}
}
}
//负责相关消息处理
private static class WifiHandler extends Handler {
final WeakReference<WifiActivity> mActivity;
private WifiHandler(WifiActivity activity) {
mActivity = new WeakReference<WifiActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
WifiActivity currentActivity = mActivity.get();
if (currentActivity == null) {
return;
}
//开始下一次的扫描
switch (msg.what) {
case MSG_SCAN_WIFI:
currentActivity.wifiManager.startScan();
break;
}
}
}
通过以上几个方法我们就可以拿到扫描到的有效WiFi数据。然后我们就可以通过Listview或Recyclerview去加载数据。这里主要看下加载布局的部分:
public class MyAdapter extends BaseAdapter {
LayoutInflater inflater;
List<ScanResult> list;
......
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
VIewHolder viewHolder;
ScanResult scanResult = getItem(position);
if (convertView == null) {
view = LayoutInflater.from(getBaseContext()).inflate(R.layout.item_wifi_list, parent, false);
viewHolder = new VIewHolder();
viewHolder.textView = (TextView) view.findViewById(R.id.item_title);
viewHolder.keyType = (TextView) view.findViewById(R.id.item_key_type);
viewHolder.wifiState = (TextView) view.findViewById(R.id.item_state);
viewHolder.tag = (TextView) view.findViewById(R.id.tag);
viewHolder.imageView = (ImageView) view.findViewById(R.id.item_level);
// 将 viewHolder 存储在 View 中
view.setTag(viewHolder);
} else {
view = convertView;
// 重新获取 viewHolder
viewHolder = (VIewHolder) view.getTag();
}
viewHolder.textView.setText(scanResult.SSID);
viewHolder.keyType.setText(WifiUtil.getEncrypt(scanResult));
//判断信号强度,显示对应的指示图标,数值越小代表信号越高。
if (Math.abs(scanResult.level) > 100) {
viewHolder.imageView.setImageDrawable(getResources().getDrawable(R.drawable.stat_wifi_signal_1));
} else if (Math.abs(scanResult.level) > 80) {
viewHolder.imageView.setImageDrawable(getResources().getDrawable(R.drawable.stat_wifi_signal_2));
} else if (Math.abs(scanResult.level) > 70) {
viewHolder.imageView.setImageDrawable(getResources().getDrawable(R.drawable.stat_wifi_signal_3));
} else if (Math.abs(scanResult.level) > 60) {
viewHolder.imageView.setImageDrawable(getResources().getDrawable(R.drawable.stat_wifi_signal_4));
} else {
viewHolder.imageView.setImageDrawable(getResources().getDrawable(R.drawable.stat_wifi_signal_5));
}
return view;
}
class VIewHolder {
TextView textView;
TextView keyType;
TextView wifiState;
TextView tag;
ImageView imageView;
}
}
item_wifi_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/x300"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"
android:textSize="@dimen/x16"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/item_key_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NONE"
android:textSize="@dimen/x16"
android:layout_marginTop="@dimen/x3"
app:layout_constraintLeft_toLeftOf="@+id/item_title"
app:layout_constraintTop_toBottomOf="@+id/item_title"/>
<TextView
android:id="@+id/tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/x45"
android:visibility="gone"
android:text="|"
android:textSize="@dimen/x18"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBaseline_toBaselineOf="@+id/item_key_type"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/item_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="right"
app:constraint_referenced_ids="item_title,item_key_type"/>
<TextView
android:id="@+id/item_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="@dimen/x16"
android:layout_marginStart="@dimen/x60"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBaseline_toBaselineOf="@+id/item_key_type"/>
<ImageView
android:id="@+id/item_level"
android:layout_width="@dimen/x32"
android:layout_height="@dimen/x32"
android:src="@drawable/ic_back"
android:layout_marginRight="@dimen/x20"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
总结
Wifi扫描的代码流程上不难,主要是权限方面的问题。保证权限申请正确的前提下以上流程就可以扫描到正确的WiFi.
PS:本文是在Android TV上开发测试的,安卓模拟器可能无法扫描。建议真机模拟实验,另各家厂商的手机可能有修改本文的方法不一定生效。
如果想要了解更多,请参考官方文档。