(九十九) Android O wps 流程簡單梳理
1.wps簡介
wifi的wps連線主要分兩種,一種是按鈕,一種是pin碼。比如說測試機使用wps連線,那麼選擇按鈕或者pin碼方式,服務端比如是路由器或者輔測機在限定時間內按下按鈕或者輸入測試機提供的pin碼即可連線,相當於一種不需要選擇ssid並簡化輸入密碼的連線方式,保密性也比較好,畢竟不要輸密碼嘛。
注:本文梳理忽略WifiStateMachine以下流程。
2.流程梳理
2.1 WpsDialog
private enum DialogState { WPS_INIT, WPS_START, WPS_COMPLETE, CONNECTED, //WPS + IP config is done WPS_FAILED } DialogState mDialogState = DialogState.WPS_INIT;
首先這個對話方塊有5種如上狀態,初始狀態為WPS_INIT
public WpsDialog(Context context, int wpsSetup) { super(context); mContext = context; mWpsSetup = wpsSetup; class WpsListener extends WifiManager.WpsCallback { public void onStarted(String pin) { if (pin != null) { updateDialog(DialogState.WPS_START, String.format( mContext.getString(R.string.wifi_wps_onstart_pin), pin)); } else { updateDialog(DialogState.WPS_START, mContext.getString( R.string.wifi_wps_onstart_pbc)); } } public void onSucceeded() { updateDialog(DialogState.WPS_COMPLETE, mContext.getString(R.string.wifi_wps_complete)); } public void onFailed(int reason) { String msg; switch (reason) { case WifiManager.WPS_OVERLAP_ERROR: msg = mContext.getString(R.string.wifi_wps_failed_overlap); break; case WifiManager.WPS_WEP_PROHIBITED: msg = mContext.getString(R.string.wifi_wps_failed_wep); break; case WifiManager.WPS_TKIP_ONLY_PROHIBITED: msg = mContext.getString(R.string.wifi_wps_failed_tkip); break; case WifiManager.IN_PROGRESS: msg = mContext.getString(R.string.wifi_wps_in_progress); break; default: msg = mContext.getString(R.string.wifi_wps_failed_generic); break; } updateDialog(DialogState.WPS_FAILED, msg); } } mWpsListener = new WpsListener(); mFilter = new IntentFilter(); mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { handleEvent(context, intent); } }; setCanceledOnTouchOutside(false); }
這邊進行一些初始化,主要初始化了一個mWpsListener,並註冊了一個廣播接收器,訊息處理如下。
private void handleEvent(Context context, Intent intent) { String action = intent.getAction(); if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { final int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN); if (state == WifiManager.WIFI_STATE_DISABLED) { if (mTimer != null) { mTimer.cancel(); mTimer = null; } String msg = mContext.getString(R.string.wifi_wps_failed_wifi_disconnected); updateDialog(DialogState.WPS_FAILED, msg); } } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { NetworkInfo info = (NetworkInfo) intent.getParcelableExtra( WifiManager.EXTRA_NETWORK_INFO); final NetworkInfo.DetailedState state = info.getDetailedState(); if (state == DetailedState.CONNECTED && mDialogState == DialogState.WPS_COMPLETE) { WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); if (wifiInfo != null) { String msg = String.format(mContext.getString( R.string.wifi_wps_connected), wifiInfo.getSSID()); updateDialog(DialogState.CONNECTED, msg); } } } }
最關鍵的還是wps流程了,這邊onCreate方法裡直接就是開始startWps流程了。
@Override
protected void onCreate(Bundle savedInstanceState) {
mView = getLayoutInflater().inflate(R.layout.wifi_wps_dialog, null);
mTextView = (TextView) mView.findViewById(R.id.wps_dialog_txt);
mTextView.setText(R.string.wifi_wps_setup_msg);
mTimeoutBar = ((ProgressBar) mView.findViewById(R.id.wps_timeout_bar));
mTimeoutBar.setMax(WPS_TIMEOUT_S);
mTimeoutBar.setProgress(0);
mProgressBar = ((ProgressBar) mView.findViewById(R.id.wps_progress_bar));
mProgressBar.setVisibility(View.GONE);
mButton = ((Button) mView.findViewById(R.id.wps_dialog_btn));
mButton.setText(R.string.wifi_cancel);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
setView(mView);
if (savedInstanceState == null) {
startWps();
}
super.onCreate(savedInstanceState);
}
private void startWps() {
WpsInfo wpsConfig = new WpsInfo();
wpsConfig.setup = mWpsSetup;
mWifiManager.startWps(wpsConfig, mWpsListener);
}
呼叫到WifiManager.startWps
PS:
1)
/**
* A class representing Wi-Fi Protected Setup
*
* {@see WifiP2pConfig}
*/
public class WpsInfo implements Parcelable {
/** Push button configuration */
public static final int PBC = 0;
/** Display pin method configuration - pin is generated and displayed on device */
public static final int DISPLAY = 1;
/** Keypad pin method configuration - pin is entered on device */
public static final int KEYPAD = 2;
/** Label pin method configuration - pin is labelled on device */
public static final int LABEL = 3;
/** Invalid configuration */
public static final int INVALID = 4;
2)設定中的wps連線
wps按鈕對應於WpsInfo.PBC,wps pin對應於WpsInfo.DISPLAY
WpsPreferenceController.java
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mWpsPushPref = screen.findPreference(KEY_WPS_PUSH);
mWpsPinPref = screen.findPreference(KEY_WPS_PIN);
if (mWpsPushPref == null || mWpsPinPref == null) {
return;
}
// WpsDialog: Create the dialog like WifiSettings does.
mWpsPushPref.setOnPreferenceClickListener((arg) -> {
WpsFragment wpsFragment = new WpsFragment(WpsInfo.PBC);
wpsFragment.show(mFragmentManager, KEY_WPS_PUSH);
return true;
}
);
// WpsDialog: Create the dialog like WifiSettings does.
mWpsPinPref.setOnPreferenceClickListener((arg) -> {
WpsFragment wpsFragment = new WpsFragment(WpsInfo.DISPLAY);
wpsFragment.show(mFragmentManager, KEY_WPS_PIN);
return true;
});
togglePreferences();
}
2.2 WifiManager
/**
* Start Wi-fi Protected Setup
*
* @param config WPS configuration (does not support {@link WpsInfo#LABEL})
* @param listener for callbacks on success or failure. Can be null.
* @throws IllegalStateException if the WifiManager instance needs to be
* initialized again
*/
public void startWps(WpsInfo config, WpsCallback listener) {
if (config == null) throw new IllegalArgumentException("config cannot be null");
getChannel().sendMessage(START_WPS, 0, putListener(listener), config);
}
傳送訊息給服務端,也就是WifiServiceImpl
2.3 WifiServiceImpl
case WifiManager.START_WPS:
if (checkChangePermissionAndReplyIfNotAuthorized(msg, WifiManager.WPS_FAILED)) {
mWifiStateMachine.sendMessage(Message.obtain(msg));
}
break;
傳送給WifiStateMachine繼續處理
2.4 WifiStateMachine
class ConnectModeState extends State {
...
case WifiManager.START_WPS:
WpsInfo wpsInfo = (WpsInfo) message.obj;
if (wpsInfo == null) {
loge("Cannot start WPS with null WpsInfo object");
replyToMessage(message, WifiManager.WPS_FAILED, WifiManager.ERROR);
break;
}
WpsResult wpsResult = new WpsResult();
// TODO(b/32898136): Not needed when we start deleting networks from supplicant
// on disconnect.
if (!mWifiNative.removeAllNetworks()) {
loge("Failed to remove networks before WPS");
}
switch (wpsInfo.setup) {
case WpsInfo.PBC:
if (mWifiNative.startWpsPbc(wpsInfo.BSSID)) {
wpsResult.status = WpsResult.Status.SUCCESS;
} else {
Log.e(TAG, "Failed to start WPS push button configuration");
wpsResult.status = WpsResult.Status.FAILURE;
}
break;
case WpsInfo.KEYPAD:
if (mWifiNative.startWpsRegistrar(wpsInfo.BSSID, wpsInfo.pin)) {
wpsResult.status = WpsResult.Status.SUCCESS;
} else {
Log.e(TAG, "Failed to start WPS push button configuration");
wpsResult.status = WpsResult.Status.FAILURE;
}
break;
case WpsInfo.DISPLAY:
wpsResult.pin = mWifiNative.startWpsPinDisplay(wpsInfo.BSSID);
if (!TextUtils.isEmpty(wpsResult.pin)) {
wpsResult.status = WpsResult.Status.SUCCESS;
} else {
Log.e(TAG, "Failed to start WPS pin method configuration");
wpsResult.status = WpsResult.Status.FAILURE;
}
break;
default:
wpsResult = new WpsResult(Status.FAILURE);
loge("Invalid setup for WPS");
break;
}
if (wpsResult.status == Status.SUCCESS) {
replyToMessage(message, WifiManager.START_WPS_SUCCEEDED, wpsResult);
transitionTo(mWpsRunningState);
} else {
loge("Failed to start WPS with config " + wpsInfo.toString());
replyToMessage(message, WifiManager.WPS_FAILED, WifiManager.ERROR);
}
break;
這邊啟動成功就會發送一個WifiManager.START_WPS_SUCCEEDED給WifiManager,WifiManager繼而會回撥設定傳來的listener的onStarted方法。
case WifiManager.START_WPS_SUCCEEDED:
if (listener != null) {
WpsResult result = (WpsResult) message.obj;
((WpsCallback) listener).onStarted(result.pin);
//Listener needs to stay until completion or failure
synchronized (mListenerMapLock) {
mListenerMap.put(message.arg2, listener);
}
}
break;
start完了後切換到WpsRunningState
/**
* WPS connection flow:
* 1. WifiStateMachine receive WPS_START message from WifiManager API.
* 2. WifiStateMachine initiates the appropriate WPS operation using WifiNative methods:
* {@link WifiNative#startWpsPbc(String)}, {@link WifiNative#startWpsPinDisplay(String)}, etc.
* 3. WifiStateMachine then transitions to this WpsRunningState.
* 4a. Once WifiStateMachine receive the connected event:
* {@link WifiMonitor#NETWORK_CONNECTION_EVENT},
* 4a.1 Load the network params out of wpa_supplicant.
* 4a.2 Add the network with params to WifiConfigManager.
* 4a.3 Enable the network with |disableOthers| set to true.
* 4a.4 Send a response to the original source of WifiManager API using {@link #mSourceMessage}.
* 4b. Any failures are notified to the original source of WifiManager API
* using {@link #mSourceMessage}.
* 5. We then transition to disconnected state and let network selection reconnect to the newly
* added network.
*/
class WpsRunningState extends State {
// Tracks the source to provide a reply
private Message mSourceMessage;
@Override
public void enter() {
mSourceMessage = Message.obtain(getCurrentMessage());
}
@Override
public boolean processMessage(Message message) {
logStateAndMessage(message, this);
switch (message.what) {
case WifiMonitor.WPS_SUCCESS_EVENT:
// Ignore intermediate success, wait for full connection
break;
case WifiMonitor.NETWORK_CONNECTION_EVENT:
Pair<Boolean, Integer> loadResult = loadNetworksFromSupplicantAfterWps();
boolean success = loadResult.first;
int netId = loadResult.second;
if (success) {
message.arg1 = netId;
replyToMessage(mSourceMessage, WifiManager.WPS_COMPLETED);
} else {
replyToMessage(mSourceMessage, WifiManager.WPS_FAILED,
WifiManager.ERROR);
}
mSourceMessage.recycle();
mSourceMessage = null;
deferMessage(message);
transitionTo(mDisconnectedState);
break;
如註釋所述,接收到NETWORK_CONNECTION_EVENT訊息後表示wps已完成,後續就開始和一般WiFi連線一樣的流程,載入網路配置,啟用連線的網路並禁用其他的,傳送WifiManager.WPS_COMPLETED訊息告訴WifiManager,WifiManager相應地回撥設定傳來的listener的onSucceeded()方法。
case WifiManager.WPS_COMPLETED:
if (listener != null) {
((WpsCallback) listener).onSucceeded();
}
break;
dhcp走完了後網路狀態會設為connected,這時設定接收到廣播訊息,將wps流程結束
} else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(
WifiManager.EXTRA_NETWORK_INFO);
final NetworkInfo.DetailedState state = info.getDetailedState();
if (state == DetailedState.CONNECTED &&
mDialogState == DialogState.WPS_COMPLETE) {
WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
if (wifiInfo != null) {
String msg = String.format(mContext.getString(
R.string.wifi_wps_connected), wifiInfo.getSSID());
updateDialog(DialogState.CONNECTED, msg);
}
}
}
3.總結
對比wps連線和普通輸密碼流程,其實流程不同在校驗上,校驗完成後回到WifiStateMachine還是dhcp一套後就連線上了,話說連線流程到supplicant還是看的很痛苦,待續。。。