概要
本篇主要介绍Settings应用的WiFi模块首页如何控制WiFi功能的开关,及如何扫描及连接一个WiFi热点。
使用到的类说明
WiFiManager:提供基础的api来管理WiFi的联通性等操作。
Scanner: 负责扫描WiFi。
WiFiEnabler:内部包含SwitchUI控件,用户可点击此控件打开或者关闭WiFi。
AccessPoint:继承自Preference,标识每个可连接的WiFi。
WifiDialog:wifi信息弹框,可编辑。
WiFiSettings
WiFiSettings是当用户在Settings页面点击WiFi按钮时填充到SettingsActivity的main_content中的PreferenceFragment。由顶部的WiFiEnabler开关和下部分的多个AccessPoint组成。可通过menu按键调出添加网络、保存网络、重新扫描等功能,当点击AccessPoint时弹出WiFiDialog,可编辑此AccessPoint的信息。
- 构造方法
public WifiSettings() {
super(DISALLOW_CONFIG_WIFI);
mFilter = new IntentFilter();
//WiFi状态变化,enable or disable 会携带wifi状态信息返回
mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
//当我们调用startScan后返回的扫描结果,扫描到可用WiFi
mFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
mFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
//声明正在建立连接的ap发生变化,在此处可判断WiFi是否建立连接
mFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
//声明网络配置发生变化,作为添加、更新、删除一个网络的result的广播
mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
//声明连接的配置发生变化
mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
//网络状态发生变化,如果连接性可用,可能在广播中携带BSSID和NetWorkInfo信息返回
mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
//wifi信号强度发生变化
mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//处理广播信息
handleEvent(intent);
}
};
mScanner = new Scanner(this);
}
其构造方法添加了各种WiFi变化监听,同时初始化了WiFi扫描器Scanner。
- 生命周期方法
1.onActivityCreated:
首先在其中通过Context.getSystemService方法获取WiFiManager。 然后监听了一些WiFi操作事件如保存WiFi、忘记WiFi、WiFi连接等事件的成功或失败。Activity的saveState现场恢复。
//WiFi服务
mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
WiFiSettings作为PreferenceFragment,在onActivityCreated方法中同时使用addPreferencesFromResource方法提供了根控件为PreferenceScreen的xml文件,后期的AccessPoint其实就是添加到此PreferenceScreen中。
2.onStart:
在onStart中创建并显示了WiFi开关控制控件WiFiEnabler,在显示时会根据WiFiState把当前Switch置为合适状态。同时设置了WiFiEnabler监听WiFiState变换状态。
3.onResume:
在此生命周期中注册了构造方法中的广播接收者,并根据WiFiState更新下部分的UI,如果当前WiFi状态时打开的则需通过WiFiManager的getConfiguredNetworks方法和getScanResults方法获取到所有可用(getLevel()!=-1即可用)的AccessPoint添加到PreferenceScreen中。如果是其它状态则需根据状态显示不同的文字提示信息。关键的AccessPoint获取操作如下:
private static List<AccessPoint> constructAccessPoints(Context context,
WifiManager wifiManager, WifiInfo lastInfo, DetailedState lastState) {
ArrayList<AccessPoint> accessPoints = new ArrayList<AccessPoint>();
/** Lookup table to more quickly update AccessPoints by only considering objects with the
* correct SSID. Maps SSID -> List of AccessPoints with the given SSID. */
Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>();
final List<WifiConfiguration> configs = wifiManager.getConfiguredNetworks();
if (configs != null) {
// Update "Saved Networks" menu option.
if (savedNetworksExist != (configs.size() > 0)) {
savedNetworksExist = !savedNetworksExist;
if (context instanceof Activity) {
((Activity) context).invalidateOptionsMenu();
}
}
for (WifiConfiguration config : configs) {
if (config.selfAdded && config.numAssociation == 0) {
continue;
}
AccessPoint accessPoint = new AccessPoint(context, config);
if (lastInfo != null && lastState != null) {
accessPoint.update(lastInfo, lastState);
}
accessPoints.add(accessPoint);
apMap.put(accessPoint.ssid, accessPoint);
}
}
final List<ScanResult> results = wifiManager.getScanResults();
if (results != null) {
for (ScanResult result : results) {
// Ignore hidden and ad-hoc networks.
if (result.SSID == null || result.SSID.length() == 0 ||
result.capabilities.contains("[IBSS]")) {
continue;
}
boolean found = false;
for (AccessPoint accessPoint : apMap.getAll(result.SSID)) {
if (accessPoint.update(result))
found = true;
}
if (!found) {
AccessPoint accessPoint = new AccessPoint(context, result);
accessPoints.add(accessPoint);
apMap.put(accessPoint.ssid, accessPoint);
}
}
}
// Pre-sort accessPoints to speed preference insertion
Collections.sort(accessPoints);
return accessPoints;
}
4.onPause:
在可视但非前台时需要取消注册WiFi状态变化广播的接收,同时通知Scanner停止扫描。
@Override
public void onPause() {
super.onPause();
if (mWifiEnabler != null) {
mWifiEnabler.pause();
}
//停止WiFi状态监听
getActivity().unregisterReceiver(mReceiver);
//停止扫描
mScanner.pause();
}
5.onDestroyView:
在此生命周期中取消了WiFiEnabler控件对WiFi状态的监听。
- WiFi广播事件的处理HandleEvent
private void handleEvent(Intent intent) {
String action = intent.getAction();
if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
//更新WiFi状态,在extra中携带了state
updateWifiState(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
WifiManager.WIFI_STATE_UNKNOWN));
} else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action) ||
WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) ||
WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) {
//更新各WiFi条目
updateAccessPoints();
} else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(
WifiManager.EXTRA_NETWORK_INFO);
mConnected.set(info.isConnected());
changeNextButtonState(info.isConnected());
//更新个WiFi条目
updateAccessPoints();
updateConnectionState(info.getDetailedState());
} else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
//信号强度变化
updateConnectionState(null);
}
}
1.WiFi状态变化:
WiFi状态在intent的extra即EXTRA_WIFI_STATE中,更新WiFi状态方法updateWifiState中根据wifi是否可用让Scanner扫描器启动扫描或停止扫描,并根据状态显示提示信息。
2.扫描完成、新增or删除网络、已连接网络发生变化:
当广播接收者接收到此广播时需要WiFiSettings需要更新页面上的AccessPoint个数。关于如何获取AccessPoint的数量参见上边的constructAccessPoints方法。
3.WiFi信号强度发生改变:
当某WiFi的信号强度发生变化时会调用各AccessPoint的update方法,在update内部会根据AccessPoint中的networkId(通过WifiInfo可获取)是否相同来确定是否是当前WiFi的信号强度发生改变。
- 弹框(区分为1.点击AccessPoint,2.OptionsMenu,3.ContextMenu)
@Override
public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) {
if (preference instanceof AccessPoint) {
mSelectedAccessPoint = (AccessPoint) preference;
/** Bypass dialog for unsecured, unsaved networks */
if (mSelectedAccessPoint.security == AccessPoint.SECURITY_NONE &&
mSelectedAccessPoint.networkId == INVALID_NETWORK_ID) {
mSelectedAccessPoint.generateOpenNetworkConfig();
if (!savedNetworksExist) {
savedNetworksExist = true;
getActivity().invalidateOptionsMenu();
}
connect(mSelectedAccessPoint.getConfig());
} else {
showDialog(mSelectedAccessPoint, false);
}
} else {
return super.onPreferenceTreeClick(screen, preference);
}
return true;
}
从中可见,如果我们点击的是AccessPoint则先判断当前被点击的AccessPoint如不需要密码且networkId=-1则直接连接,如果需要密码则显示弹框showDialog。在showDialog中有点绕,他会显示一个SettingsDialogFragment,但此DialogFragment的Dialog来源则是WiFiSettings的onCreateDialog方法。在onCreateDialog中则根据dialogId可知是case1,在其中新建了WifiDialog返回。
@Override
public Dialog onCreateDialog(int dialogId) {
switch (dialogId) {
case WIFI_DIALOG_ID:
AccessPoint ap = mDlgAccessPoint; // For manual launch
if (ap == null) { // For re-launch from saved state
if (mAccessPointSavedState != null) {
ap = new AccessPoint(getActivity(), mAccessPointSavedState);
// For repeated orientation changes
mDlgAccessPoint = ap;
// Reset the saved access point data
mAccessPointSavedState = null;
}
}
// If it's null, fine, it's for Add Network
mSelectedAccessPoint = ap;
mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit);
return mDialog;
//...
}
//...
}
2.点击OptionsMenu时(onOptionsItemSelected)弹出的菜单中有添加网络、保存网络、扫描WiFi、高级选项。其中扫描Wifi直接调用Scanner,高级选项和保存网络则是启动其他fragment,这里不展开。其他则是弹框操作,同样是弹出WifiDialog。
3.当我们长按页面上的某个view控件时会在onCreateContextMenu中响应到,我们的WifiSettings在其中判断如果我们长按的是AccessPoint则会弹出menu,弹出的menu中有连接、忘记、修改、WriteNFC等按钮。在onContextItemSelected中除了forget(忘记网络)按钮会直接调用WiFiManager的forget api之外,其他都会showDialog。
@Override
public boolean onContextItemSelected(MenuItem item) {
if (mSelectedAccessPoint == null) {
return super.onContextItemSelected(item);
}
switch (item.getItemId()) {
case MENU_ID_CONNECT: {
if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) {
connect(mSelectedAccessPoint.networkId);
} else if (mSelectedAccessPoint.security == AccessPoint.SECURITY_NONE) {
/** Bypass dialog for unsecured networks */
mSelectedAccessPoint.generateOpenNetworkConfig();
connect(mSelectedAccessPoint.getConfig());
} else {
showDialog(mSelectedAccessPoint, true);
}
return true;
}
case MENU_ID_FORGET: {
mWifiManager.forget(mSelectedAccessPoint.networkId, mForgetListener);
return true;
}
case MENU_ID_MODIFY: {
showDialog(mSelectedAccessPoint, true);
return true;
}
case MENU_ID_WRITE_NFC:
showDialog(WRITE_NFC_DIALOG_ID);
return true;
}
return super.onContextItemSelected(item);
}
- WiFiSettings中其余关键方法
/* package */ void forget() {
if (mSelectedAccessPoint.networkId == INVALID_NETWORK_ID) {
// Should not happen, but a monkey seems to trigger it
Log.e(TAG, "Failed to forget invalid network " + mSelectedAccessPoint.getConfig());
return;
}
mWifiManager.forget(mSelectedAccessPoint.networkId, mForgetListener);
if (mWifiManager.isWifiEnabled()) {
mScanner.resume();
}
//更新WiFi条目
updateAccessPoints();
// We need to rename/replace "Next" button in wifi setup context.
changeNextButtonState(false);
}
2.连接网络:
/* package */ void submit(WifiConfigController configController) {
final WifiConfiguration config = configController.getConfig();
if (config == null) {
if (mSelectedAccessPoint != null
&& mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) {
//调用WiFiManager的connect方法
connect(mSelectedAccessPoint.networkId);
}
} else if (config.networkId != INVALID_NETWORK_ID) {
if (mSelectedAccessPoint != null) {
mWifiManager.save(config, mSaveListener);
}
} else {
if (configController.isEdit()) {
mWifiManager.save(config, mSaveListener);
} else {
//调用WiFiManager的connect方法
connect(config);
}
}
//重新调用startScan
if (mWifiManager.isWifiEnabled()) {
mScanner.resume();
}
//更新Wifi列表
updateAccessPoints();
}
Scanner
Scanner扫描器时WifiSettings的内部类,其本质上是一个Handler。拥有resume(开始扫描)、pause(暂停扫描)、forceScan(强制扫描-立即执行)等方法。private static class Scanner extends Handler {
private int mRetry = 0;
private WifiSettings mWifiSettings = null;
Scanner(WifiSettings wifiSettings) {
mWifiSettings = wifiSettings;
}
void resume() {
if (!hasMessages(0)) {
sendEmptyMessage(0);
}
}
void forceScan() {
removeMessages(0);
sendEmptyMessage(0);
}
void pause() {
mRetry = 0;
removeMessages(0);
}
@Override
public void handleMessage(Message message) {
if (mWifiSettings.mWifiManager.startScan()) {
mRetry = 0;
} else if (++mRetry >= 3) {
mRetry = 0;
Activity activity = mWifiSettings.getActivity();
if (activity != null) {
Toast.makeText(activity, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
}
return;
}
sendEmptyMessageDelayed(0, WIFI_RESCAN_INTERVAL_MS);
}
}
WifiDialog
当我们长按wifi列表或者点击菜单键等操作时弹出的弹窗dialog,在弹窗中可显示该WiFi的ip配置、代理、名称、安全协议等信息,同时可根据创建弹窗时的edit属性来控制这些信息的可编辑性。@Override
protected void onCreate(Bundle savedInstanceState) {
mView = getLayoutInflater().inflate(R.layout.wifi_dialog, null);
setView(mView);
setInverseBackgroundForced(true);
mController = new WifiConfigController(this, mView, mAccessPoint, mEdit);
super.onCreate(savedInstanceState);
if (mHideSubmitButton) {
mController.hideSubmitButton();
} else {
/* During creation, the submit button can be unavailable to determine
* visibility. Right after creation, update button visibility */
mController.enableSubmitIfAppropriate();
}
}
而在WifiConfigController中,对各个控件进行可编辑区分,如果是新建Wifi则处于编辑状态,其它则具体根据WifiDialog的传参mEdit有关。
//安全信息 - 身份认证协议
showSecurityFields();
//ip配置信息 - ip、网关、dns
showIpConfigFields();
//代理信息 - static、pac、none
showProxyFields();
当我们提交弹框中的wifi信息时,会调用上文中WifiSettings中其余关键方法中的submit方法,在submit方法中,我们可以看见其会去获取WifiConfigController的config信息(通过getConfig方法)。
/* package */ WifiConfiguration getConfig() {
if (mAccessPoint != null && mAccessPoint.networkId != INVALID_NETWORK_ID && !mEdit) {
return null;
}
WifiConfiguration config = new WifiConfiguration();
if (mAccessPoint == null) {
//wifi名称
config.SSID = AccessPoint.convertToQuotedString(
mSsidView.getText().toString());
// If the user adds a network manually, assume that it is hidden.
//新建的网络默认名称hide
config.hiddenSSID = true;
} else if (mAccessPoint.networkId == INVALID_NETWORK_ID) {
//networkId=-1
config.SSID = AccessPoint.convertToQuotedString(
mAccessPoint.ssid);
} else {
//networkId用于区分各个wifi
config.networkId = mAccessPoint.networkId;
}
//加密类型区分为 none、wep、wpa/wpa2 psk
switch (mAccessPointSecurity) {
case AccessPoint.SECURITY_NONE:
config.allowedKeyManagement.set(KeyMgmt.NONE);
break;
case AccessPoint.SECURITY_WEP:
config.allowedKeyManagement.set(KeyMgmt.NONE);
config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
config.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED);
if (mPasswordView.length() != 0) {
int length = mPasswordView.length();
String password = mPasswordView.getText().toString();
// WEP-40, WEP-104, and 256-bit WEP (WEP-232?)
if ((length == 10 || length == 26 || length == 58) &&
password.matches("[0-9A-Fa-f]*")) {
config.wepKeys[0] = password;
} else {
config.wepKeys[0] = '"' + password + '"';
}
}
break;
case AccessPoint.SECURITY_PSK:
config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
if (mPasswordView.length() != 0) {
String password = mPasswordView.getText().toString();
if (password.matches("[0-9A-Fa-f]{64}")) {
config.preSharedKey = password;
} else {
config.preSharedKey = '"' + password + '"';
}
}
break;
case AccessPoint.SECURITY_EAP:
config.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
config.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
config.enterpriseConfig = new WifiEnterpriseConfig();
int eapMethod = mEapMethodSpinner.getSelectedItemPosition();
int phase2Method = mPhase2Spinner.getSelectedItemPosition();
config.enterpriseConfig.setEapMethod(eapMethod);
switch (eapMethod) {
case Eap.PEAP:
// PEAP supports limited phase2 values
// Map the index from the PHASE2_PEAP_ADAPTER to the one used
// by the API which has the full list of PEAP methods.
switch(phase2Method) {
case WIFI_PEAP_PHASE2_NONE:
config.enterpriseConfig.setPhase2Method(Phase2.NONE);
break;
case WIFI_PEAP_PHASE2_MSCHAPV2:
config.enterpriseConfig.setPhase2Method(Phase2.MSCHAPV2);
break;
case WIFI_PEAP_PHASE2_GTC:
config.enterpriseConfig.setPhase2Method(Phase2.GTC);
break;
default:
Log.e(TAG, "Unknown phase2 method" + phase2Method);
break;
}
break;
default:
// The default index from PHASE2_FULL_ADAPTER maps to the API
config.enterpriseConfig.setPhase2Method(phase2Method);
break;
}
//身份认证协议
String caCert = (String) mEapCaCertSpinner.getSelectedItem();
if (caCert.equals(unspecifiedCert)) caCert = "";
config.enterpriseConfig.setCaCertificateAlias(caCert);
String clientCert = (String) mEapUserCertSpinner.getSelectedItem();
if (clientCert.equals(unspecifiedCert)) clientCert = "";
config.enterpriseConfig.setClientCertificateAlias(clientCert);
config.enterpriseConfig.setIdentity(mEapIdentityView.getText().toString());
config.enterpriseConfig.setAnonymousIdentity(
mEapAnonymousView.getText().toString());
if (mPasswordView.isShown()) {
// For security reasons, a previous password is not displayed to user.
// Update only if it has been changed.
if (mPasswordView.length() > 0) {
config.enterpriseConfig.setPassword(mPasswordView.getText().toString());
}
} else {
// clear password
config.enterpriseConfig.setPassword(mPasswordView.getText().toString());
}
break;
default:
return null;
}
config.setIpConfiguration(
new IpConfiguration(mIpAssignment, mProxySettings,
mStaticIpConfiguration, mHttpProxy));
return config;
}
可以看见,其中配置了当前wifi网络的SSID、networkId、根据加密类型得到的密码、ip信息。事实上,创建wifi的config信息参考此处getConfig方法。以下是我们的WifiDialog的各个按钮点击的回调,forget最终会调用WiFiManager的forget方法,而submit最终会调用WiFiManager的connect方法尝试连接wifi网络。
@Override
public void onClick(DialogInterface dialogInterface, int button) {
if (button == WifiDialog.BUTTON_FORGET && mSelectedAccessPoint != null) {
forget();
} else if (button == WifiDialog.BUTTON_SUBMIT) {
if (mDialog != null) {
submit(mDialog.getController());
}
}
}
AccessPoint
在这里我们只讲解AccessPoint解析WiFiConfiguration或ScanResult的过程。在上边WiFiSetings的生命周期onResume中会加载可用的AccessPoint到UI页面。而在关键的constructAccessPoints方法中,新建的AccessPoint有两种构造传参方式,一种传入WiFiConfiguration,一种传入ScanResult。
AccessPoint(Context context, WifiConfiguration config) {
super(context);
loadConfig(config);
refresh();
}
AccessPoint(Context context, ScanResult result) {
super(context);
loadResult(result);
refresh();
}
其中构造方法内部的loadConfig和loadResult就是对两种参数分别进行解析的过程。
private void loadConfig(WifiConfiguration config) {
ssid = (config.SSID == null ? "" : removeDoubleQuotes(config.SSID));
bssid = config.BSSID;
security = getSecurity(config);
networkId = config.networkId;
mConfig = config;
}
private void loadResult(ScanResult result) {
ssid = result.SSID;
bssid = result.BSSID;
security = getSecurity(result);
wpsAvailable = security != SECURITY_EAP && result.capabilities.contains("WPS");
if (security == SECURITY_PSK)
pskType = getPskType(result);
mRssi = result.level;
mScanResult = result;
if (result.seen > mSeen) {
mSeen = result.seen;
}
}
在两者内部:
1.取出SSID,即当前wifi的名称。
2.取出BSSID,当前wifi的mac地址。
3.判断当前的加密类型,通过getSecurity方法。如果是PSK加密则需继续通过getPskType得知。
4.loadConfig中取出networkId。
5.loadResult中获取信号强度。
通过WiFiConfiguration和ScanResult判断加密类型的getSecurity方法如下:
static int getSecurity(WifiConfiguration config) {
if (config.allowedKeyManagement.get(KeyMgmt.WPA_PSK)) {
return SECURITY_PSK;
}
if (config.allowedKeyManagement.get(KeyMgmt.WPA_EAP) ||
config.allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
return SECURITY_EAP;
}
return (config.wepKeys[0] != null) ? SECURITY_WEP : SECURITY_NONE;
}
private static int getSecurity(ScanResult result) {
if (result.capabilities.contains("WEP")) {
return SECURITY_WEP;
} else if (result.capabilities.contains("PSK")) {
return SECURITY_PSK;
} else if (result.capabilities.contains("EAP")) {
return SECURITY_EAP;
}
return SECURITY_NONE;
}
从上可知,如果是从config中判断加密类型,可追溯到WifiConfigController中geConfig方法的设置。此处通过判断WifiConfiguration的allowedKeyManagement中设置的值判断加密类型。如果是从ScanResult中获取加密类型,则需从ScanResult的capabilities中判断,如果包含WEP,这说明时WEP加密类型,如果包含PSK,则需进一步判断是WPA还是WPA2加密类型,如果capabilities中包含EAP,这说明是身份认证协议类型,需通过enterpriseConfig字段判断是使用的哪种身份认证协议。
最后
以上就是陶醉白云最近收集整理的关于【Settings开发】WiFi模块(一)的全部内容,更多相关【Settings开发】WiFi模块(一)内容请搜索靠谱客的其他文章。
发表评论 取消回复