content: /*
   * 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();
      }
  }
  