/* * Copyright (C) 2016 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 static android.net.wifi.WifiManager.WIFI_STATE_DISABLED; import static android.net.wifi.WifiManager.WIFI_STATE_DISABLING; import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED; import static android.net.wifi.WifiManager.WIFI_STATE_ENABLING; import static android.net.wifi.WifiManager.WIFI_STATE_UNKNOWN; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.DhcpResultsParcelable; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.wifi.IWifiConnectedNetworkScorer; import android.net.wifi.WifiAnnotations; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.hotspot2.IProvisioningCallback; import android.net.wifi.hotspot2.OsuProvider; import android.net.wifi.nl80211.DeviceWiphyCapabilities; import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PersistableBundle; import android.os.UserHandle; import android.os.WorkSource; import android.telephony.AccessNetworkConstants; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.ims.ImsException; import android.telephony.ims.ImsMmTelManager; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.RegistrationManager; import android.telephony.ims.feature.MmTelFeature; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.text.TextUtils; import android.util.Log; import com.android.internal.util.IState; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.wifi.WifiNative.InterfaceCallback; import com.android.server.wifi.WifiNative.RxFateReport; import com.android.server.wifi.WifiNative.TxFateReport; import com.android.server.wifi.util.ActionListenerWrapper; import com.android.server.wifi.util.StateMachineObituary; import com.android.server.wifi.util.WifiHandler; import com.android.wifi.resources.R; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayDeque; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** * Manage WiFi in Client Mode where we connect to configured networks and in Scan Only Mode where * we do not connect to configured networks but do perform scanning. * * An instance of this class is active to manage each client interface. This is in contrast to * {@link DefaultClientModeManager} which handles calls when no client interfaces are active. * * This class will dynamically instantiate {@link ClientModeImpl} when it enters client mode, and * tear it down when it exits client mode. No instance of ClientModeImpl will be active in * scan-only mode, instead {@link ScanOnlyModeImpl} will be used to respond to calls. * *
 *                                           ActiveModeWarden
 *                                      /                        \
 *                                     /                          \
 *                        ConcreteClientModeManager         DefaultClientModeManager
 *                      (Client Mode + Scan Only Mode)            (Wifi off)
 *                             /            \
 *                           /               \
 *                     ClientModeImpl       ScanOnlyModeImpl
 * 
*/ public class ConcreteClientModeManager implements ClientModeManager { private static final String TAG = "WifiClientModeManager"; private final ClientModeStateMachine mStateMachine; private final Context mContext; private final Clock mClock; private final WifiNative mWifiNative; private final WifiMetrics mWifiMetrics; private final WakeupController mWakeupController; private final WifiInjector mWifiInjector; private final SelfRecovery mSelfRecovery; private final WifiGlobals mWifiGlobals; private final DefaultClientModeManager mDefaultClientModeManager; private final ClientModeManagerBroadcastQueue mBroadcastQueue; private final long mId; private final Graveyard mGraveyard = new Graveyard(); private String mClientInterfaceName; private boolean mIfaceIsUp = false; private boolean mShouldReduceNetworkScore = false; private final DeferStopHandler mDeferStopHandler; @Nullable private ClientRole mRole = null; @Nullable private ClientRole mPreviousRole = null; private long mLastRoleChangeSinceBootMs = 0; @Nullable private WorkSource mRequestorWs = null; @NonNull private Listener mModeListener; /** Caches the latest role change request. This is needed for the IMS dereg delay */ @Nullable private RoleChangeInfo mTargetRoleChangeInfo; private boolean mVerboseLoggingEnabled = false; private int mActiveSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private boolean mWifiStateChangeBroadcastEnabled = true; /** * mClientModeImpl is only non-null when in {@link ClientModeStateMachine.ConnectModeState} - * it will be null in all other states */ @Nullable private ClientModeImpl mClientModeImpl = null; @Nullable private ScanOnlyModeImpl mScanOnlyModeImpl = null; /** * One of {@link WifiManager#WIFI_STATE_DISABLED}, * {@link WifiManager#WIFI_STATE_DISABLING}, * {@link WifiManager#WIFI_STATE_ENABLED}, * {@link WifiManager#WIFI_STATE_ENABLING}, * {@link WifiManager#WIFI_STATE_UNKNOWN} */ private final AtomicInteger mWifiState = new AtomicInteger(WIFI_STATE_DISABLED); private boolean mIsStopped = true; ConcreteClientModeManager( Context context, @NonNull Looper looper, Clock clock, WifiNative wifiNative, @NonNull Listener listener, WifiMetrics wifiMetrics, WakeupController wakeupController, WifiInjector wifiInjector, SelfRecovery selfRecovery, WifiGlobals wifiGlobals, DefaultClientModeManager defaultClientModeManager, long id, @NonNull WorkSource requestorWs, @NonNull ClientRole role, @NonNull ClientModeManagerBroadcastQueue broadcastQueue, boolean verboseLoggingEnabled) { mContext = context; mClock = clock; mWifiNative = wifiNative; mModeListener = listener; mWifiMetrics = wifiMetrics; mWakeupController = wakeupController; mWifiInjector = wifiInjector; mStateMachine = new ClientModeStateMachine(looper); mDeferStopHandler = new DeferStopHandler(looper); mSelfRecovery = selfRecovery; mWifiGlobals = wifiGlobals; mDefaultClientModeManager = defaultClientModeManager; mId = id; mTargetRoleChangeInfo = new RoleChangeInfo(role, requestorWs, listener); mBroadcastQueue = broadcastQueue; enableVerboseLogging(verboseLoggingEnabled); mStateMachine.sendMessage(ClientModeStateMachine.CMD_START, mTargetRoleChangeInfo); } private String getTag() { return TAG + "[" + (mClientInterfaceName == null ? "unknown" : mClientInterfaceName) + "]"; } /** * Sets whether to send WIFI_STATE_CHANGED broadcast for this ClientModeManager. * @param enabled */ public void setWifiStateChangeBroadcastEnabled(boolean enabled) { mWifiStateChangeBroadcastEnabled = enabled; } /** * Disconnect from any currently connected networks and stop client mode. */ @Override public void stop() { Log.d(getTag(), " currentstate: " + getCurrentStateName()); mTargetRoleChangeInfo = null; if (mIfaceIsUp) { updateConnectModeState(mRole, WifiManager.WIFI_STATE_DISABLING, WifiManager.WIFI_STATE_ENABLED); } else { updateConnectModeState(mRole, WifiManager.WIFI_STATE_DISABLING, WifiManager.WIFI_STATE_ENABLING); } mDeferStopHandler.start(getWifiOffDeferringTimeMs()); } private class DeferStopHandler extends WifiHandler { private boolean mIsDeferring = false; private ImsMmTelManager mImsMmTelManager = null; private Looper mLooper = null; private final Runnable mRunnable = () -> continueToStopWifi(); private int mMaximumDeferringTimeMillis = 0; private long mDeferringStartTimeMillis = 0; private ConnectivityManager mConnectivityManager = null; private boolean mIsImsNetworkLost = false; private boolean mIsImsNetworkUnregistered = false; private final RegistrationManager.RegistrationCallback mImsRegistrationCallback = new RegistrationManager.RegistrationCallback() { @Override public void onRegistered(int imsRadioTech) { Log.d(getTag(), "on IMS registered on type " + imsRadioTech); if (!mIsDeferring) return; if (imsRadioTech != AccessNetworkConstants.TRANSPORT_TYPE_WLAN) { continueToStopWifi(); } } @Override public void onUnregistered(ImsReasonInfo imsReasonInfo) { Log.d(getTag(), "on IMS unregistered"); mIsImsNetworkUnregistered = true; checkAndContinueToStopWifi(); } }; private final class ImsNetworkCallback extends NetworkCallback { private int mRegisteredImsNetworkCount = 0; @Override public void onAvailable(Network network) { synchronized (this) { Log.d(getTag(), "IMS network available: " + network); mRegisteredImsNetworkCount++; } } @Override public void onLost(Network network) { synchronized (this) { Log.d(getTag(), "IMS network lost: " + network + " ,isDeferring: " + mIsDeferring + " ,registered IMS network count: " + mRegisteredImsNetworkCount); mRegisteredImsNetworkCount--; if (mIsDeferring && mRegisteredImsNetworkCount <= 0) { mRegisteredImsNetworkCount = 0; mIsImsNetworkLost = true; checkAndContinueToStopWifi(); } } } } private NetworkCallback mImsNetworkCallback = null; DeferStopHandler(Looper looper) { super(TAG, looper); mLooper = looper; } public void start(int delayMs) { if (mIsDeferring) return; mMaximumDeferringTimeMillis = delayMs; mDeferringStartTimeMillis = mClock.getElapsedSinceBootMillis(); // Most cases don't need delay, check it first to avoid unnecessary work. if (delayMs == 0) { continueToStopWifi(); return; } mImsMmTelManager = ImsMmTelManager.createForSubscriptionId(mActiveSubId); if (mImsMmTelManager == null || !postDelayed(mRunnable, delayMs)) { // if no delay or failed to add runnable, stop Wifi immediately. continueToStopWifi(); return; } mIsDeferring = true; Log.d(getTag(), "Start DeferWifiOff handler with deferring time " + delayMs + " ms for subId: " + mActiveSubId); try { mImsMmTelManager.registerImsRegistrationCallback( new HandlerExecutor(new Handler(mLooper)), mImsRegistrationCallback); } catch (RuntimeException | ImsException e) { Log.e(getTag(), "registerImsRegistrationCallback failed", e); continueToStopWifi(); return; } NetworkRequest imsRequest = new NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_IMS) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .build(); mConnectivityManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); mImsNetworkCallback = new ImsNetworkCallback(); mConnectivityManager.registerNetworkCallback(imsRequest, mImsNetworkCallback, new Handler(mLooper)); } private void checkAndContinueToStopWifi() { if (mIsImsNetworkLost && mIsImsNetworkUnregistered) { // Add delay for targets where IMS PDN down at modem takes additional delay. int delay = mContext.getResources() .getInteger(R.integer.config_wifiDelayDisconnectOnImsLostMs); if (delay == 0 || !postDelayed(mRunnable, delay)) { continueToStopWifi(); } } } private void continueToStopWifi() { Log.d(getTag(), "The target role change info " + mTargetRoleChangeInfo); int deferringDurationMillis = (int) (mClock.getElapsedSinceBootMillis() - mDeferringStartTimeMillis); boolean isTimedOut = mMaximumDeferringTimeMillis > 0 && deferringDurationMillis >= mMaximumDeferringTimeMillis; if (mTargetRoleChangeInfo == null) { Log.d(getTag(), "Continue to stop wifi"); mStateMachine.captureObituaryAndQuitNow(); mWifiMetrics.noteWifiOff(mIsDeferring, isTimedOut, deferringDurationMillis); } else if (mTargetRoleChangeInfo.role == ROLE_CLIENT_SCAN_ONLY) { if (!mWifiNative.switchClientInterfaceToScanMode( mClientInterfaceName, mTargetRoleChangeInfo.requestorWs)) { mModeListener.onStartFailure(ConcreteClientModeManager.this); } else { mStateMachine.sendMessage( ClientModeStateMachine.CMD_SWITCH_TO_SCAN_ONLY_MODE_CONTINUE, mTargetRoleChangeInfo); mWifiMetrics.noteWifiOff(mIsDeferring, isTimedOut, deferringDurationMillis); } } else { updateConnectModeState(mRole, WifiManager.WIFI_STATE_ENABLED, WifiManager.WIFI_STATE_DISABLING); } if (!mIsDeferring) return; Log.d(getTag(), "Stop DeferWifiOff handler."); removeCallbacks(mRunnable); if (mImsMmTelManager != null) { try { mImsMmTelManager.unregisterImsRegistrationCallback(mImsRegistrationCallback); } catch (RuntimeException e) { Log.e(getTag(), "unregisterImsRegistrationCallback failed", e); } } if (mConnectivityManager != null && mImsNetworkCallback != null) { mConnectivityManager.unregisterNetworkCallback(mImsNetworkCallback); mImsNetworkCallback = null; } mIsDeferring = false; mIsImsNetworkLost = false; mIsImsNetworkUnregistered = false; } } /** * Get deferring time before turning off WiFi. */ private int getWifiOffDeferringTimeMs() { SubscriptionManager subscriptionManager = (SubscriptionManager) mContext.getSystemService( Context.TELEPHONY_SUBSCRIPTION_SERVICE); if (subscriptionManager == null) { Log.d(getTag(), "SubscriptionManager not found"); return 0; } List subInfoList = subscriptionManager.getActiveSubscriptionInfoList(); if (subInfoList == null) { Log.d(getTag(), "Active SubscriptionInfo list not found"); return 0; } // Get the maximum delay for the active subscription latched on IWLAN. int maxDelay = 0; for (SubscriptionInfo subInfo : subInfoList) { int curDelay = getWifiOffDeferringTimeMs(subInfo.getSubscriptionId()); if (curDelay > maxDelay) { maxDelay = curDelay; mActiveSubId = subInfo.getSubscriptionId(); } } return maxDelay; } private int getWifiOffDeferringTimeMs(int subId) { if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.d(getTag(), "Invalid Subscription ID: " + subId); return 0; } ImsMmTelManager imsMmTelManager = ImsMmTelManager.createForSubscriptionId(subId); // If no wifi calling, no delay try { if (!imsMmTelManager.isAvailable( MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE, ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN)) { Log.d(getTag(), "IMS not registered over IWLAN for subId: " + subId); return 0; } } catch (RuntimeException ex) { Log.e(TAG, "IMS Manager is not available.", ex); return 0; } CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); PersistableBundle config = configManager.getConfigForSubId(subId); return (config != null) ? config.getInt(CarrierConfigManager.Ims.KEY_WIFI_OFF_DEFERRING_TIME_MILLIS_INT) : 0; } @Override @Nullable public ClientRole getRole() { return mRole; } @Override @Nullable public ClientRole getPreviousRole() { return mPreviousRole; } @Override public long getLastRoleChangeSinceBootMs() { return mLastRoleChangeSinceBootMs; } /** * Class to hold info needed for role change. */ private static class RoleChangeInfo { @Nullable public final ClientRole role; @Nullable public final WorkSource requestorWs; @Nullable public final Listener modeListener; RoleChangeInfo(@Nullable ClientRole role) { this(role, null, null); } RoleChangeInfo(@Nullable ClientRole role, @Nullable WorkSource requestorWs, @Nullable Listener modeListener) { this.role = role; this.requestorWs = requestorWs; this.modeListener = modeListener; } @Override public String toString() { return "Role: " + role + ", RequestorWs: " + requestorWs + ", ModeListener: " + modeListener; } } /** Set the role of this ClientModeManager */ public void setRole(@NonNull ClientRole role, @NonNull WorkSource requestorWs) { setRole(role, requestorWs, null); } /** Set the role of this ClientModeManager */ public void setRole(@NonNull ClientRole role, @NonNull WorkSource requestorWs, @Nullable Listener modeListener) { mTargetRoleChangeInfo = new RoleChangeInfo(role, requestorWs, modeListener); if (role == ROLE_CLIENT_SCAN_ONLY) { // Switch client mode manager to scan only mode. mStateMachine.sendMessage( ClientModeStateMachine.CMD_SWITCH_TO_SCAN_ONLY_MODE); } else { // Switch client mode manager to connect mode. mStateMachine.sendMessage( ClientModeStateMachine.CMD_SWITCH_TO_CONNECT_MODE, mTargetRoleChangeInfo); } } @Override public String getInterfaceName() { return mClientInterfaceName; } @Override public WorkSource getRequestorWs() { return mRequestorWs; } /** * Keep stopped {@link ClientModeImpl} instances so that they can be dumped to aid debugging. * * TODO(b/160283853): Find a smarter way to evict old ClientModeImpls */ private static class Graveyard { private static final int INSTANCES_TO_KEEP = 3; private final ArrayDeque mClientModeImpls = new ArrayDeque<>(); /** * Add this stopped {@link ClientModeImpl} to the graveyard, and evict the oldest * ClientModeImpl if the graveyard is full. */ void inter(ClientModeImpl clientModeImpl) { if (mClientModeImpls.size() == INSTANCES_TO_KEEP) { mClientModeImpls.removeFirst(); } mClientModeImpls.addLast(clientModeImpl); } /** Dump the contents of the graveyard. */ void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Dump of ConcreteClientModeManager.Graveyard"); pw.println("Stopped ClientModeImpls: " + mClientModeImpls.size() + " total"); for (ClientModeImpl clientModeImpl : mClientModeImpls) { clientModeImpl.dump(fd, pw, args); } pw.println(); } boolean hasAllClientModeImplsQuit() { for (ClientModeImpl cmi : mClientModeImpls) { if (!cmi.hasQuit()) return false; } return true; } } /** * Dump info about this ClientMode manager. */ @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Dump of ClientModeManager id=" + mId); pw.println("current StateMachine mode: " + getCurrentStateName()); pw.println("mRole: " + mRole); pw.println("mTargetRoleChangeInfo: " + mTargetRoleChangeInfo); pw.println("mClientInterfaceName: " + mClientInterfaceName); pw.println("mIfaceIsUp: " + mIfaceIsUp); mStateMachine.dump(fd, pw, args); pw.println(); pw.println("Wi-Fi is " + syncGetWifiStateByName()); if (mClientModeImpl == null) { pw.println("No active ClientModeImpl instance"); } else { mClientModeImpl.dump(fd, pw, args); } mGraveyard.dump(fd, pw, args); pw.println(); } private String getCurrentStateName() { IState currentState = mStateMachine.getCurrentState(); if (currentState != null) { return currentState.getName(); } return "StateMachine not active"; } /** * Update Wifi state and send the broadcast. * * @param role Target/Set role for this client mode manager instance. * @param newState new Wifi state * @param currentState current wifi state */ private void updateConnectModeState(ClientRole role, int newState, int currentState) { setWifiStateForApiCalls(newState); if (newState == WifiManager.WIFI_STATE_UNKNOWN) { // do not need to broadcast failure to system return; } if (role != ROLE_CLIENT_PRIMARY || !mWifiStateChangeBroadcastEnabled) { // do not raise public broadcast unless this is the primary client mode manager return; } // TODO(b/175839153): this broadcast should only be sent out when wifi is toggled on/off, // not per CMM final Intent intent = new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); intent.putExtra(WifiManager.EXTRA_WIFI_STATE, newState); intent.putExtra(WifiManager.EXTRA_PREVIOUS_WIFI_STATE, currentState); String summary = "broadcast=WIFI_STATE_CHANGED_ACTION" + " EXTRA_WIFI_STATE=" + newState + " EXTRA_PREVIOUS_WIFI_STATE=" + currentState; if (mVerboseLoggingEnabled) Log.d(getTag(), "Queuing " + summary); ClientModeManagerBroadcastQueue.QueuedBroadcast broadcast = () -> { if (mVerboseLoggingEnabled) Log.d(getTag(), "Sending " + summary); mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); }; if (mRole == null && role == ROLE_CLIENT_PRIMARY) { // This CMM is intended to be the primary, but has not completed the mode transition // yet. Need to force broadcast to be sent. broadcast.send(); } else { mBroadcastQueue.queueOrSendBroadcast(this, broadcast); } } private void setWifiStateForApiCalls(int newState) { switch (newState) { case WIFI_STATE_DISABLING: case WIFI_STATE_DISABLED: case WIFI_STATE_ENABLING: case WIFI_STATE_ENABLED: case WIFI_STATE_UNKNOWN: if (mVerboseLoggingEnabled) { Log.d(getTag(), "setting wifi state to: " + newState); } mWifiState.set(newState); break; default: Log.d(getTag(), "attempted to set an invalid state: " + newState); break; } } private String syncGetWifiStateByName() { switch (mWifiState.get()) { case WIFI_STATE_DISABLING: return "disabling"; case WIFI_STATE_DISABLED: return "disabled"; case WIFI_STATE_ENABLING: return "enabling"; case WIFI_STATE_ENABLED: return "enabled"; case WIFI_STATE_UNKNOWN: return "unknown state"; default: return "[invalid state]"; } } private class ClientModeStateMachine extends StateMachine { // Commands for the state machine. public static final int CMD_START = 0; public static final int CMD_SWITCH_TO_SCAN_ONLY_MODE = 1; public static final int CMD_SWITCH_TO_CONNECT_MODE = 2; public static final int CMD_INTERFACE_STATUS_CHANGED = 3; public static final int CMD_INTERFACE_DESTROYED = 4; public static final int CMD_INTERFACE_DOWN = 5; public static final int CMD_SWITCH_TO_SCAN_ONLY_MODE_CONTINUE = 6; private final State mIdleState = new IdleState(); private final State mStartedState = new StartedState(); private final State mScanOnlyModeState = new ScanOnlyModeState(); private final State mConnectModeState = new ConnectModeState(); // Workaround since we cannot use transitionTo(mScanOnlyModeState, RoleChangeInfo) private RoleChangeInfo mScanRoleChangeInfoToSetOnTransition = null; // Workaround since we cannot use transitionTo(mConnectModeState, RoleChangeInfo) private RoleChangeInfo mConnectRoleChangeInfoToSetOnTransition = null; @Nullable private StateMachineObituary mObituary = null; private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() { @Override public void onDestroyed(String ifaceName) { if (mClientInterfaceName != null && mClientInterfaceName.equals(ifaceName)) { Log.d(getTag(), "STA iface " + ifaceName + " was destroyed, " + "stopping client mode"); // we must immediately clean up state in ClientModeImpl to unregister // all client mode related objects // Note: onDestroyed is only called from the main Wifi thread if (mClientModeImpl == null) { Log.w(getTag(), "Received mWifiNativeInterfaceCallback.onDestroyed " + "callback when no ClientModeImpl instance is active."); } else { mClientModeImpl.handleIfaceDestroyed(); } sendMessage(CMD_INTERFACE_DESTROYED); } } @Override public void onUp(String ifaceName) { if (mClientInterfaceName != null && mClientInterfaceName.equals(ifaceName)) { sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1); } } @Override public void onDown(String ifaceName) { if (mClientInterfaceName != null && mClientInterfaceName.equals(ifaceName)) { sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0); } } }; ClientModeStateMachine(Looper looper) { super(TAG, looper); // CHECKSTYLE:OFF IndentationCheck addState(mIdleState); addState(mStartedState, mIdleState); addState(mScanOnlyModeState, mStartedState); addState(mConnectModeState, mStartedState); // CHECKSTYLE:ON IndentationCheck setInitialState(mIdleState); start(); } void captureObituaryAndQuitNow() { // capture StateMachine LogRecs since we will lose them after we call quitNow() // This is used for debugging. mObituary = new StateMachineObituary(this); quitNow(); } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mObituary == null) { // StateMachine hasn't quit yet, dump `this` via StateMachineObituary's dump() // method for consistency with `else` branch. new StateMachineObituary(this).dump(fd, pw, args); } else { // StateMachine has quit and cleared all LogRecs. // Get them from the obituary instead. mObituary.dump(fd, pw, args); } } /** * Reset this ConcreteClientModeManager when its role changes, so that it can be reused for * another purpose. */ private void reset() { // Therefore, the caller must ensure that the role change has been completed and these // settings have already reset before setting them, otherwise the new setting would be // lost. setShouldReduceNetworkScore(false); } private void setRoleInternal(@NonNull RoleChangeInfo roleChangeInfo) { mPreviousRole = mRole; mLastRoleChangeSinceBootMs = mClock.getElapsedSinceBootMillis(); mRole = roleChangeInfo.role; if (roleChangeInfo.requestorWs != null) { mRequestorWs = roleChangeInfo.requestorWs; } if (roleChangeInfo.modeListener != null) { mModeListener = roleChangeInfo.modeListener; } } private void setRoleInternalAndInvokeCallback(@NonNull RoleChangeInfo roleChangeInfo) { if (roleChangeInfo.role == mRole) return; if (mRole == null) { Log.v(getTag(), "ClientModeManager started in role: " + roleChangeInfo); setRoleInternal(roleChangeInfo); mModeListener.onStarted(ConcreteClientModeManager.this); } else { Log.v(getTag(), "ClientModeManager role changed: " + roleChangeInfo); setRoleInternal(roleChangeInfo); reset(); mModeListener.onRoleChanged(ConcreteClientModeManager.this); } if (mClientModeImpl != null) { mClientModeImpl.onRoleChanged(); } } private class IdleState extends State { @Override public void enter() { Log.d(getTag(), "entering IdleState"); mClientInterfaceName = null; mIfaceIsUp = false; } @Override public void exit() { // Sometimes the wifi handler thread may become blocked that the statemachine // will exit in the IdleState without first entering StartedState. Trigger a // cleanup here in case the above sequence happens. This the statemachine was // started normally this will will not send a duplicate broadcast since mIsStopped // will get set to false the first time the exit happens. cleanupOnQuitIfApplicable(); } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START: // Always start in scan mode first. RoleChangeInfo roleChangeInfo = (RoleChangeInfo) message.obj; mClientInterfaceName = mWifiNative.setupInterfaceForClientInScanMode( mWifiNativeInterfaceCallback, roleChangeInfo.requestorWs); if (TextUtils.isEmpty(mClientInterfaceName)) { Log.e(getTag(), "Failed to create ClientInterface. Sit in Idle"); mModeListener.onStartFailure(ConcreteClientModeManager.this); break; } if (roleChangeInfo.role instanceof ClientConnectivityRole) { sendMessage(CMD_SWITCH_TO_CONNECT_MODE, roleChangeInfo); transitionTo(mStartedState); } else { mScanRoleChangeInfoToSetOnTransition = roleChangeInfo; transitionTo(mScanOnlyModeState); } break; default: Log.d(getTag(), "received an invalid message: " + message); return NOT_HANDLED; } return HANDLED; } } private class StartedState extends State { private void onUpChanged(boolean isUp) { if (isUp == mIfaceIsUp) { return; // no change } mIfaceIsUp = isUp; if (!isUp) { // if the interface goes down we should exit and go back to idle state. Log.d(getTag(), "interface down!"); mStateMachine.sendMessage(CMD_INTERFACE_DOWN); } } @Override public void enter() { Log.d(getTag(), "entering StartedState"); mIfaceIsUp = false; mIsStopped = false; onUpChanged(mWifiNative.isInterfaceUp(mClientInterfaceName)); } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START: // Already started, ignore this command. break; case CMD_SWITCH_TO_CONNECT_MODE: { RoleChangeInfo roleChangeInfo = (RoleChangeInfo) message.obj; updateConnectModeState(roleChangeInfo.role, WifiManager.WIFI_STATE_ENABLING, WifiManager.WIFI_STATE_DISABLED); if (!mWifiNative.switchClientInterfaceToConnectivityMode( mClientInterfaceName, roleChangeInfo.requestorWs)) { updateConnectModeState(roleChangeInfo.role, WifiManager.WIFI_STATE_UNKNOWN, WifiManager.WIFI_STATE_ENABLING); updateConnectModeState(roleChangeInfo.role, WifiManager.WIFI_STATE_DISABLED, WifiManager.WIFI_STATE_UNKNOWN); mModeListener.onStartFailure(ConcreteClientModeManager.this); break; } // Role set in the enter of ConnectModeState. mConnectRoleChangeInfoToSetOnTransition = roleChangeInfo; transitionTo(mConnectModeState); break; } case CMD_SWITCH_TO_SCAN_ONLY_MODE: updateConnectModeState(mRole, WifiManager.WIFI_STATE_DISABLING, WifiManager.WIFI_STATE_ENABLED); mDeferStopHandler.start(getWifiOffDeferringTimeMs()); break; case CMD_SWITCH_TO_SCAN_ONLY_MODE_CONTINUE: { RoleChangeInfo roleChangeInfo = (RoleChangeInfo) message.obj; mScanRoleChangeInfoToSetOnTransition = roleChangeInfo; transitionTo(mScanOnlyModeState); break; } case CMD_INTERFACE_DOWN: Log.e(getTag(), "Detected an interface down, reporting failure to " + "SelfRecovery"); mSelfRecovery.trigger(SelfRecovery.REASON_STA_IFACE_DOWN); // once interface down, nothing else to do... stop the state machine captureObituaryAndQuitNow(); break; case CMD_INTERFACE_STATUS_CHANGED: boolean isUp = message.arg1 == 1; onUpChanged(isUp); break; case CMD_INTERFACE_DESTROYED: Log.e(getTag(), "interface destroyed - client mode stopping"); mClientInterfaceName = null; // once interface destroyed, nothing else to do... stop the state machine captureObituaryAndQuitNow(); break; default: return NOT_HANDLED; } return HANDLED; } /** * Clean up state, unregister listeners and update wifi state. */ @Override public void exit() { if (mClientInterfaceName != null) { mWifiNative.teardownInterface(mClientInterfaceName); mClientInterfaceName = null; mIfaceIsUp = false; } Log.i(getTag(), "StartedState#exit(), setting mRole = null"); mIsStopped = true; cleanupOnQuitIfApplicable(); } } private class ScanOnlyModeState extends State { @Override public void enter() { Log.d(getTag(), "entering ScanOnlyModeState"); if (mClientInterfaceName != null) { mScanOnlyModeImpl = mWifiInjector.makeScanOnlyModeImpl( mClientInterfaceName); } else { Log.e(getTag(), "Entered ScanOnlyModeState with a null interface name!"); } if (mScanRoleChangeInfoToSetOnTransition == null || (mScanRoleChangeInfoToSetOnTransition.role != ROLE_CLIENT_SCAN_ONLY)) { Log.wtf(TAG, "Unexpected mScanRoleChangeInfoToSetOnTransition: " + mScanRoleChangeInfoToSetOnTransition); // Should never happen, but fallback to scan only to avoid a crash. mScanRoleChangeInfoToSetOnTransition = new RoleChangeInfo(ROLE_CLIENT_SCAN_ONLY); } setRoleInternalAndInvokeCallback(mScanRoleChangeInfoToSetOnTransition); // If we're in ScanOnlyModeState, there is only 1 CMM. So it's ok to call // WakeupController directly, there won't be multiple CMMs trampling over each other mWakeupController.start(); mWifiNative.setScanMode(mClientInterfaceName, true); } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_SWITCH_TO_SCAN_ONLY_MODE: // Already in scan only mode, ignore this command. break; default: return NOT_HANDLED; } return HANDLED; } @Override public void exit() { mScanOnlyModeImpl = null; mScanRoleChangeInfoToSetOnTransition = null; // If we're in ScanOnlyModeState, there is only 1 CMM. So it's ok to call // WakeupController directly, there won't be multiple CMMs trampling over each other mWakeupController.stop(); mWifiNative.setScanMode(mClientInterfaceName, false); } } private class ConnectModeState extends State { @Override public void enter() { Log.d(getTag(), "entering ConnectModeState, starting ClientModeImpl"); if (mClientInterfaceName == null) { Log.e(getTag(), "Supposed to start ClientModeImpl, but iface is null!"); } else { if (mClientModeImpl != null) { Log.e(getTag(), "ConnectModeState.enter(): mClientModeImpl is already " + "instantiated?!"); } mClientModeImpl = mWifiInjector.makeClientModeImpl( mClientInterfaceName, ConcreteClientModeManager.this, mVerboseLoggingEnabled); mClientModeImpl.setShouldReduceNetworkScore(mShouldReduceNetworkScore); } if (mConnectRoleChangeInfoToSetOnTransition == null || !(mConnectRoleChangeInfoToSetOnTransition.role instanceof ClientConnectivityRole)) { Log.wtf(TAG, "Unexpected mConnectRoleChangeInfoToSetOnTransition: " + mConnectRoleChangeInfoToSetOnTransition); // Should never happen, but fallback to primary to avoid a crash. mConnectRoleChangeInfoToSetOnTransition = new RoleChangeInfo(ROLE_CLIENT_PRIMARY); } // Could be any one of possible connect mode roles. setRoleInternalAndInvokeCallback(mConnectRoleChangeInfoToSetOnTransition); updateConnectModeState(mConnectRoleChangeInfoToSetOnTransition.role, WIFI_STATE_ENABLED, WIFI_STATE_ENABLING); } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_SWITCH_TO_CONNECT_MODE: RoleChangeInfo roleChangeInfo = (RoleChangeInfo) message.obj; // switching to connect mode when already in connect mode, just update the // requestor WorkSource. boolean success = mWifiNative.replaceStaIfaceRequestorWs( mClientInterfaceName, roleChangeInfo.requestorWs); if (success) { setRoleInternalAndInvokeCallback(roleChangeInfo); } else { // If this call failed, the iface would be torn down. // Thus, simply abort and let the iface down handling take care of the // rest. Log.e(getTag(), "Failed to switch ClientModeManager=" + ConcreteClientModeManager.this + "'s requestorWs"); } break; case CMD_SWITCH_TO_SCAN_ONLY_MODE: case CMD_INTERFACE_DESTROYED: updateConnectModeState(mRole, WifiManager.WIFI_STATE_DISABLING, WifiManager.WIFI_STATE_ENABLED); return NOT_HANDLED; // Handled in StartedState. case CMD_INTERFACE_DOWN: updateConnectModeState(mRole, WifiManager.WIFI_STATE_DISABLING, WifiManager.WIFI_STATE_UNKNOWN); return NOT_HANDLED; // Handled in StartedState. case CMD_INTERFACE_STATUS_CHANGED: boolean isUp = message.arg1 == 1; if (isUp == mIfaceIsUp) { break; // no change } if (!isUp) { if (mWifiGlobals.isConnectedMacRandomizationEnabled() && getClientMode().isConnecting()) { return HANDLED; // For MAC randomization, ignore... } else { // Handle the error case where our underlying interface went down if // we do not have mac randomization enabled (b/72459123). // if the interface goes down we should exit and go back to idle // state. updateConnectModeState(mRole, WifiManager.WIFI_STATE_UNKNOWN, WifiManager.WIFI_STATE_ENABLED); } } return NOT_HANDLED; // Handled in StartedState. default: return NOT_HANDLED; } return HANDLED; } @Override public void exit() { updateConnectModeState(mRole, WifiManager.WIFI_STATE_DISABLED, WifiManager.WIFI_STATE_DISABLING); if (mClientModeImpl == null) { Log.w(getTag(), "ConnectModeState.exit(): mClientModeImpl is already null?!"); } else { Log.d(getTag(), "Stopping ClientModeImpl"); mClientModeImpl.stop(); mGraveyard.inter(mClientModeImpl); mClientModeImpl = null; } mConnectRoleChangeInfoToSetOnTransition = null; } } } /** Called by a ClientModeImpl owned by this CMM informing it has fully stopped. */ public void onClientModeImplQuit() { cleanupOnQuitIfApplicable(); } /** * Only clean up this CMM once the CMM and all associated ClientModeImpls have been stopped. * This is necessary because ClientModeImpl sends broadcasts during stop, and the role must * remain primary for {@link ClientModeManagerBroadcastQueue} to send them out. */ private void cleanupOnQuitIfApplicable() { if (mIsStopped && mGraveyard.hasAllClientModeImplsQuit()) { mPreviousRole = mRole; mLastRoleChangeSinceBootMs = mClock.getElapsedSinceBootMillis(); mRole = null; // only call onStopped() after role has been reset to null since ActiveModeWarden // expects the CMM to be fully stopped before onStopped(). mModeListener.onStopped(ConcreteClientModeManager.this); // reset to false so that onStopped() won't be triggered again. mIsStopped = false; } } @Override public int syncGetWifiState() { return mWifiState.get(); } @NonNull private ClientMode getClientMode() { if (mClientModeImpl != null) { return mClientModeImpl; } if (mScanOnlyModeImpl != null) { return mScanOnlyModeImpl; } return mDefaultClientModeManager; } /* * Note: These are simple wrappers over methods to {@link ClientModeImpl}. */ @Override public void connectNetwork(NetworkUpdateResult result, ActionListenerWrapper wrapper, int callingUid) { getClientMode().connectNetwork(result, wrapper, callingUid); } @Override public void saveNetwork(NetworkUpdateResult result, ActionListenerWrapper wrapper, int callingUid) { getClientMode().saveNetwork(result, wrapper, callingUid); } @Override public void disconnect() { getClientMode().disconnect(); } @Override public void reconnect(WorkSource ws) { getClientMode().reconnect(ws); } @Override public void reassociate() { getClientMode().reassociate(); } @Override public void startConnectToNetwork(int networkId, int uid, String bssid) { getClientMode().startConnectToNetwork(networkId, uid, bssid); } @Override public void startRoamToNetwork(int networkId, String bssid) { getClientMode().startRoamToNetwork(networkId, bssid); } @Override public boolean setWifiConnectedNetworkScorer( IBinder binder, IWifiConnectedNetworkScorer scorer) { return getClientMode().setWifiConnectedNetworkScorer(binder, scorer); } @Override public void clearWifiConnectedNetworkScorer() { getClientMode().clearWifiConnectedNetworkScorer(); } @Override public void resetSimAuthNetworks(@ClientModeImpl.ResetSimReason int resetReason) { getClientMode().resetSimAuthNetworks(resetReason); } @Override public void onBluetoothConnectionStateChanged() { getClientMode().onBluetoothConnectionStateChanged(); } @Override public WifiInfo syncRequestConnectionInfo() { return getClientMode().syncRequestConnectionInfo(); } @Override public boolean syncQueryPasspointIcon(long bssid, String fileName) { return getClientMode().syncQueryPasspointIcon(bssid, fileName); } @Override public Network syncGetCurrentNetwork() { return getClientMode().syncGetCurrentNetwork(); } @Override public DhcpResultsParcelable syncGetDhcpResultsParcelable() { return getClientMode().syncGetDhcpResultsParcelable(); } @Override public long getSupportedFeatures() { return getClientMode().getSupportedFeatures(); } @Override public boolean syncStartSubscriptionProvisioning(int callingUid, OsuProvider provider, IProvisioningCallback callback) { return getClientMode().syncStartSubscriptionProvisioning( callingUid, provider, callback); } @Override public boolean isWifiStandardSupported(@WifiAnnotations.WifiStandard int standard) { return getClientMode().isWifiStandardSupported(standard); } @Override public void enableTdls(String remoteMacAddress, boolean enable) { getClientMode().enableTdls(remoteMacAddress, enable); } @Override public void dumpIpClient(FileDescriptor fd, PrintWriter pw, String[] args) { getClientMode().dumpIpClient(fd, pw, args); } @Override public void dumpWifiScoreReport(FileDescriptor fd, PrintWriter pw, String[] args) { getClientMode().dumpWifiScoreReport(fd, pw, args); } @Override public void enableVerboseLogging(boolean verbose) { mVerboseLoggingEnabled = verbose; getClientMode().enableVerboseLogging(verbose); } @Override public String getFactoryMacAddress() { return getClientMode().getFactoryMacAddress(); } @Override public WifiConfiguration getConnectedWifiConfiguration() { return getClientMode().getConnectedWifiConfiguration(); } @Override public WifiConfiguration getConnectingWifiConfiguration() { return getClientMode().getConnectingWifiConfiguration(); } @Override public String getConnectedBssid() { return getClientMode().getConnectedBssid(); } @Override public String getConnectingBssid() { return getClientMode().getConnectingBssid(); } @Override public WifiLinkLayerStats getWifiLinkLayerStats() { return getClientMode().getWifiLinkLayerStats(); } @Override public boolean setPowerSave(boolean ps) { return getClientMode().setPowerSave(ps); } @Override public boolean setLowLatencyMode(boolean enabled) { return getClientMode().setLowLatencyMode(enabled); } @Override public WifiMulticastLockManager.FilterController getMcastLockManagerFilterController() { return getClientMode().getMcastLockManagerFilterController(); } @Override public boolean isConnected() { return getClientMode().isConnected(); } @Override public boolean isConnecting() { return getClientMode().isConnecting(); } @Override public boolean isRoaming() { return getClientMode().isRoaming(); } @Override public boolean isDisconnected() { return getClientMode().isDisconnected(); } @Override public boolean isSupplicantTransientState() { return getClientMode().isSupplicantTransientState(); } @Override public void onCellularConnectivityChanged(@WifiDataStall.CellularDataStatusCode int status) { getClientMode().onCellularConnectivityChanged(status); } @Override public void probeLink(LinkProbeCallback callback, int mcs) { getClientMode().probeLink(callback, mcs); } @Override public void sendMessageToClientModeImpl(Message msg) { getClientMode().sendMessageToClientModeImpl(msg); } @Override public long getId() { return mId; } @Override public void setMboCellularDataStatus(boolean available) { getClientMode().setMboCellularDataStatus(available); } @Override public WifiNative.RoamingCapabilities getRoamingCapabilities() { return getClientMode().getRoamingCapabilities(); } @Override public boolean configureRoaming(WifiNative.RoamingConfig config) { return getClientMode().configureRoaming(config); } @Override public boolean enableRoaming(boolean enabled) { return getClientMode().enableRoaming(enabled); } @Override public boolean setCountryCode(String countryCode) { return getClientMode().setCountryCode(countryCode); } @Override public List getTxPktFates() { return getClientMode().getTxPktFates(); } @Override public List getRxPktFates() { return getClientMode().getRxPktFates(); } @Override public DeviceWiphyCapabilities getDeviceWiphyCapabilities() { return getClientMode().getDeviceWiphyCapabilities(); } @Override public boolean requestAnqp(String bssid, Set anqpIds, Set hs20Subtypes) { return getClientMode().requestAnqp(bssid, anqpIds, hs20Subtypes); } @Override public boolean requestVenueUrlAnqp(String bssid) { return getClientMode().requestVenueUrlAnqp(bssid); } @Override public boolean requestIcon(String bssid, String fileName) { return getClientMode().requestIcon(bssid, fileName); } @Override public void setShouldReduceNetworkScore(boolean shouldReduceNetworkScore) { mShouldReduceNetworkScore = shouldReduceNetworkScore; getClientMode().setShouldReduceNetworkScore(shouldReduceNetworkScore); } @Override public String toString() { return "ConcreteClientModeManager{id=" + getId() + " iface=" + getInterfaceName() + " role=" + getRole() + "}"; } }