/*
|
* Copyright (C) 2010 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.server.wifi;
|
|
import android.content.Context;
|
import android.content.Intent;
|
import android.net.wifi.SupplicantState;
|
import android.net.wifi.WifiConfiguration;
|
import android.net.wifi.WifiManager;
|
import android.os.BatteryStats;
|
import android.os.Handler;
|
import android.os.Message;
|
import android.os.Parcelable;
|
import android.os.RemoteException;
|
import android.os.UserHandle;
|
import android.util.Log;
|
import android.util.Slog;
|
|
import com.android.internal.app.IBatteryStats;
|
import com.android.internal.util.State;
|
import com.android.internal.util.StateMachine;
|
|
import java.io.FileDescriptor;
|
import java.io.PrintWriter;
|
|
/**
|
* Tracks the state changes in supplicant and provides functionality
|
* that is based on these state changes:
|
* - detect a failed WPA handshake that loops indefinitely
|
* - authentication failure handling
|
*/
|
public class SupplicantStateTracker extends StateMachine {
|
|
private static final String TAG = "SupplicantStateTracker";
|
private static boolean DBG = false;
|
private final WifiConfigManager mWifiConfigManager;
|
private FrameworkFacade mFacade;
|
private final IBatteryStats mBatteryStats;
|
/* Indicates authentication failure in supplicant broadcast.
|
* TODO: enhance auth failure reporting to include notification
|
* for all type of failures: EAP, WPS & WPA networks */
|
private boolean mAuthFailureInSupplicantBroadcast = false;
|
|
/* Authentication failure reason
|
* see {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_NONE},
|
* {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_TIMEOUT},
|
* {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_WRONG_PSWD},
|
* {@link android.net.wifi.WifiManager#ERROR_AUTH_FAILURE_EAP_FAILURE}
|
*/
|
private int mAuthFailureReason;
|
|
/* Maximum retries on a authentication failure notification */
|
private static final int MAX_RETRIES_ON_AUTHENTICATION_FAILURE = 2;
|
|
/* Maximum retries on assoc rejection events */
|
private static final int MAX_RETRIES_ON_ASSOCIATION_REJECT = 16;
|
|
/* Tracks if networks have been disabled during a connection */
|
private boolean mNetworksDisabledDuringConnect = false;
|
|
private final Context mContext;
|
|
private final State mUninitializedState = new UninitializedState();
|
private final State mDefaultState = new DefaultState();
|
private final State mInactiveState = new InactiveState();
|
private final State mDisconnectState = new DisconnectedState();
|
private final State mScanState = new ScanState();
|
private final State mConnectionActiveState = new ConnectionActiveState();
|
private final State mHandshakeState = new HandshakeState();
|
private final State mCompletedState = new CompletedState();
|
private final State mDormantState = new DormantState();
|
|
void enableVerboseLogging(int verbose) {
|
if (verbose > 0) {
|
DBG = true;
|
} else {
|
DBG = false;
|
}
|
}
|
|
public String getSupplicantStateName() {
|
return getCurrentState().getName();
|
}
|
|
public SupplicantStateTracker(Context c, WifiConfigManager wcs,
|
FrameworkFacade facade, Handler t) {
|
super(TAG, t.getLooper());
|
|
mContext = c;
|
mWifiConfigManager = wcs;
|
mFacade = facade;
|
mBatteryStats = mFacade.getBatteryService();
|
// CHECKSTYLE:OFF IndentationCheck
|
addState(mDefaultState);
|
addState(mUninitializedState, mDefaultState);
|
addState(mInactiveState, mDefaultState);
|
addState(mDisconnectState, mDefaultState);
|
addState(mConnectionActiveState, mDefaultState);
|
addState(mScanState, mConnectionActiveState);
|
addState(mHandshakeState, mConnectionActiveState);
|
addState(mCompletedState, mConnectionActiveState);
|
addState(mDormantState, mConnectionActiveState);
|
// CHECKSTYLE:ON IndentationCheck
|
|
setInitialState(mUninitializedState);
|
setLogRecSize(50);
|
setLogOnlyTransitions(true);
|
//start the state machine
|
start();
|
}
|
|
private void handleNetworkConnectionFailure(int netId, int disableReason) {
|
if (DBG) {
|
Log.d(TAG, "handleNetworkConnectionFailure netId=" + Integer.toString(netId)
|
+ " reason " + Integer.toString(disableReason)
|
+ " mNetworksDisabledDuringConnect=" + mNetworksDisabledDuringConnect);
|
}
|
|
/* If other networks disabled during connection, enable them */
|
if (mNetworksDisabledDuringConnect) {
|
mNetworksDisabledDuringConnect = false; }
|
/* update network status */
|
mWifiConfigManager.updateNetworkSelectionStatus(netId, disableReason);
|
}
|
|
private void transitionOnSupplicantStateChange(StateChangeResult stateChangeResult) {
|
SupplicantState supState = (SupplicantState) stateChangeResult.state;
|
|
if (DBG) Log.d(TAG, "Supplicant state: " + supState.toString() + "\n");
|
|
switch (supState) {
|
case DISCONNECTED:
|
transitionTo(mDisconnectState);
|
break;
|
case INTERFACE_DISABLED:
|
//we should have received a disconnection already, do nothing
|
break;
|
case SCANNING:
|
transitionTo(mScanState);
|
break;
|
case AUTHENTICATING:
|
case ASSOCIATING:
|
case ASSOCIATED:
|
case FOUR_WAY_HANDSHAKE:
|
case GROUP_HANDSHAKE:
|
transitionTo(mHandshakeState);
|
break;
|
case COMPLETED:
|
transitionTo(mCompletedState);
|
break;
|
case DORMANT:
|
transitionTo(mDormantState);
|
break;
|
case INACTIVE:
|
transitionTo(mInactiveState);
|
break;
|
case UNINITIALIZED:
|
case INVALID:
|
transitionTo(mUninitializedState);
|
break;
|
default:
|
Log.e(TAG, "Unknown supplicant state " + supState);
|
break;
|
}
|
}
|
|
private void sendSupplicantStateChangedBroadcast(SupplicantState state, boolean failedAuth) {
|
sendSupplicantStateChangedBroadcast(state, failedAuth, WifiManager.ERROR_AUTH_FAILURE_NONE);
|
}
|
|
private void sendSupplicantStateChangedBroadcast(SupplicantState state, boolean failedAuth,
|
int reasonCode) {
|
int supplState;
|
switch (state) {
|
case DISCONNECTED: supplState = BatteryStats.WIFI_SUPPL_STATE_DISCONNECTED; break;
|
case INTERFACE_DISABLED:
|
supplState = BatteryStats.WIFI_SUPPL_STATE_INTERFACE_DISABLED; break;
|
case INACTIVE: supplState = BatteryStats.WIFI_SUPPL_STATE_INACTIVE; break;
|
case SCANNING: supplState = BatteryStats.WIFI_SUPPL_STATE_SCANNING; break;
|
case AUTHENTICATING: supplState = BatteryStats.WIFI_SUPPL_STATE_AUTHENTICATING; break;
|
case ASSOCIATING: supplState = BatteryStats.WIFI_SUPPL_STATE_ASSOCIATING; break;
|
case ASSOCIATED: supplState = BatteryStats.WIFI_SUPPL_STATE_ASSOCIATED; break;
|
case FOUR_WAY_HANDSHAKE:
|
supplState = BatteryStats.WIFI_SUPPL_STATE_FOUR_WAY_HANDSHAKE; break;
|
case GROUP_HANDSHAKE: supplState = BatteryStats.WIFI_SUPPL_STATE_GROUP_HANDSHAKE; break;
|
case COMPLETED: supplState = BatteryStats.WIFI_SUPPL_STATE_COMPLETED; break;
|
case DORMANT: supplState = BatteryStats.WIFI_SUPPL_STATE_DORMANT; break;
|
case UNINITIALIZED: supplState = BatteryStats.WIFI_SUPPL_STATE_UNINITIALIZED; break;
|
case INVALID: supplState = BatteryStats.WIFI_SUPPL_STATE_INVALID; break;
|
default:
|
Slog.w(TAG, "Unknown supplicant state " + state);
|
supplState = BatteryStats.WIFI_SUPPL_STATE_INVALID;
|
break;
|
}
|
try {
|
mBatteryStats.noteWifiSupplicantStateChanged(supplState, failedAuth);
|
} catch (RemoteException e) {
|
// Won't happen.
|
}
|
Intent intent = new Intent(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
|
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
|
| Intent.FLAG_RECEIVER_REPLACE_PENDING);
|
intent.putExtra(WifiManager.EXTRA_NEW_STATE, (Parcelable) state);
|
if (failedAuth) {
|
intent.putExtra(
|
WifiManager.EXTRA_SUPPLICANT_ERROR,
|
WifiManager.ERROR_AUTHENTICATING);
|
intent.putExtra(
|
WifiManager.EXTRA_SUPPLICANT_ERROR_REASON,
|
reasonCode);
|
}
|
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
|
}
|
|
/********************************************************
|
* HSM states
|
*******************************************************/
|
|
class DefaultState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
}
|
@Override
|
public boolean processMessage(Message message) {
|
if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
|
switch (message.what) {
|
case WifiMonitor.AUTHENTICATION_FAILURE_EVENT:
|
mAuthFailureInSupplicantBroadcast = true;
|
mAuthFailureReason = message.arg1;
|
break;
|
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
|
StateChangeResult stateChangeResult = (StateChangeResult) message.obj;
|
SupplicantState state = stateChangeResult.state;
|
sendSupplicantStateChangedBroadcast(state, mAuthFailureInSupplicantBroadcast,
|
mAuthFailureReason);
|
mAuthFailureInSupplicantBroadcast = false;
|
mAuthFailureReason = WifiManager.ERROR_AUTH_FAILURE_NONE;
|
transitionOnSupplicantStateChange(stateChangeResult);
|
break;
|
case ClientModeImpl.CMD_RESET_SUPPLICANT_STATE:
|
transitionTo(mUninitializedState);
|
break;
|
case WifiManager.CONNECT_NETWORK:
|
mNetworksDisabledDuringConnect = true;
|
break;
|
case WifiMonitor.ASSOCIATION_REJECTION_EVENT:
|
default:
|
Log.e(TAG, "Ignoring " + message);
|
break;
|
}
|
return HANDLED;
|
}
|
}
|
|
/*
|
* This indicates that the supplicant state as seen
|
* by the framework is not initialized yet. We are
|
* in this state right after establishing a control
|
* channel connection before any supplicant events
|
* or after we have lost the control channel
|
* connection to the supplicant
|
*/
|
class UninitializedState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
}
|
}
|
|
class InactiveState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
}
|
}
|
|
class DisconnectedState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
}
|
}
|
|
class ScanState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
}
|
}
|
|
/* Meta-state that processes supplicant disconnections and broadcasts this event. */
|
class ConnectionActiveState extends State {
|
@Override
|
public boolean processMessage(Message message) {
|
if (message.what == ClientModeImpl.CMD_RESET_SUPPLICANT_STATE) {
|
sendSupplicantStateChangedBroadcast(SupplicantState.DISCONNECTED, false);
|
}
|
|
/* Let parent states handle the state possible transition. */
|
return NOT_HANDLED;
|
}
|
}
|
|
class HandshakeState extends State {
|
/**
|
* The max number of the WPA supplicant loop iterations before we
|
* decide that the loop should be terminated:
|
*/
|
private static final int MAX_SUPPLICANT_LOOP_ITERATIONS = 4;
|
private int mLoopDetectIndex;
|
private int mLoopDetectCount;
|
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
mLoopDetectIndex = 0;
|
mLoopDetectCount = 0;
|
}
|
@Override
|
public boolean processMessage(Message message) {
|
if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
|
switch (message.what) {
|
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
|
StateChangeResult stateChangeResult = (StateChangeResult) message.obj;
|
SupplicantState state = stateChangeResult.state;
|
if (SupplicantState.isHandshakeState(state)) {
|
if (mLoopDetectIndex > state.ordinal()) {
|
mLoopDetectCount++;
|
}
|
if (mLoopDetectCount > MAX_SUPPLICANT_LOOP_ITERATIONS) {
|
Log.d(TAG, "Supplicant loop detected, disabling network " +
|
stateChangeResult.networkId);
|
handleNetworkConnectionFailure(stateChangeResult.networkId,
|
WifiConfiguration.NetworkSelectionStatus
|
.DISABLED_AUTHENTICATION_FAILURE);
|
}
|
mLoopDetectIndex = state.ordinal();
|
sendSupplicantStateChangedBroadcast(state,
|
mAuthFailureInSupplicantBroadcast, mAuthFailureReason);
|
} else {
|
//Have the DefaultState handle the transition
|
return NOT_HANDLED;
|
}
|
break;
|
default:
|
return NOT_HANDLED;
|
}
|
return HANDLED;
|
}
|
}
|
|
class CompletedState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
/* Reset authentication failure count */
|
if (mNetworksDisabledDuringConnect) {
|
mNetworksDisabledDuringConnect = false;
|
}
|
}
|
@Override
|
public boolean processMessage(Message message) {
|
if (DBG) Log.d(TAG, getName() + message.toString() + "\n");
|
switch(message.what) {
|
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
|
StateChangeResult stateChangeResult = (StateChangeResult) message.obj;
|
SupplicantState state = stateChangeResult.state;
|
sendSupplicantStateChangedBroadcast(state, mAuthFailureInSupplicantBroadcast,
|
mAuthFailureReason);
|
/* Ignore any connecting state in completed state. Group re-keying
|
* events and other auth events that do not affect connectivity are
|
* ignored
|
*/
|
if (SupplicantState.isConnecting(state)) {
|
break;
|
}
|
transitionOnSupplicantStateChange(stateChangeResult);
|
break;
|
default:
|
return NOT_HANDLED;
|
}
|
return HANDLED;
|
}
|
}
|
|
//TODO: remove after getting rid of the state in supplicant
|
class DormantState extends State {
|
@Override
|
public void enter() {
|
if (DBG) Log.d(TAG, getName() + "\n");
|
}
|
}
|
|
@Override
|
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
super.dump(fd, pw, args);
|
pw.println("mAuthFailureInSupplicantBroadcast " + mAuthFailureInSupplicantBroadcast);
|
pw.println("mAuthFailureReason " + mAuthFailureReason);
|
pw.println("mNetworksDisabledDuringConnect " + mNetworksDisabledDuringConnect);
|
pw.println();
|
}
|
}
|