blob: 4fb4b870d556aa44de89f041e05cd8a46cc57583 [file] [log] [blame]
/*
* 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 android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.UserHandle;
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.Preconditions;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.wifi.WifiNative.InterfaceCallback;
import com.android.server.wifi.util.WifiHandler;
import com.android.wifi.resources.R;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
/**
* Manager WiFi in Client Mode where we connect to configured networks.
*/
public class ClientModeManager implements ActiveModeManager {
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 SarManager mSarManager;
private final WakeupController mWakeupController;
private final Listener mModeListener;
private final ClientModeImpl mClientModeImpl;
private String mClientInterfaceName;
private boolean mIfaceIsUp = false;
private DeferStopHandler mDeferStopHandler;
private @Role int mRole = ROLE_UNSPECIFIED;
private @Role int mTargetRole = ROLE_UNSPECIFIED;
private int mActiveSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
ClientModeManager(Context context, @NonNull Looper looper, Clock clock, WifiNative wifiNative,
Listener listener, WifiMetrics wifiMetrics, SarManager sarManager,
WakeupController wakeupController, ClientModeImpl clientModeImpl) {
mContext = context;
mClock = clock;
mWifiNative = wifiNative;
mModeListener = listener;
mWifiMetrics = wifiMetrics;
mSarManager = sarManager;
mWakeupController = wakeupController;
mClientModeImpl = clientModeImpl;
mStateMachine = new ClientModeStateMachine(looper);
mDeferStopHandler = new DeferStopHandler(TAG, looper);
}
/**
* Start client mode.
*/
@Override
public void start() {
mTargetRole = ROLE_CLIENT_SCAN_ONLY;
mStateMachine.sendMessage(ClientModeStateMachine.CMD_START);
}
/**
* Disconnect from any currently connected networks and stop client mode.
*/
@Override
public void stop() {
Log.d(TAG, " currentstate: " + getCurrentStateName());
mTargetRole = ROLE_UNSPECIFIED;
if (mIfaceIsUp) {
updateConnectModeState(WifiManager.WIFI_STATE_DISABLING,
WifiManager.WIFI_STATE_ENABLED);
} else {
updateConnectModeState(WifiManager.WIFI_STATE_DISABLING,
WifiManager.WIFI_STATE_ENABLING);
}
mDeferStopHandler.start(getWifiOffDeferringTimeMs());
}
@Override
public boolean isStopping() {
return mTargetRole == ROLE_UNSPECIFIED && mRole != ROLE_UNSPECIFIED;
}
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 NetworkRequest mImsRequest = null;
private ConnectivityManager mConnectivityManager = null;
private RegistrationManager.RegistrationCallback mImsRegistrationCallback =
new RegistrationManager.RegistrationCallback() {
@Override
public void onRegistered(int imsRadioTech) {
Log.d(TAG, "on IMS registered on type " + imsRadioTech);
if (!mIsDeferring) return;
if (imsRadioTech != AccessNetworkConstants.TRANSPORT_TYPE_WLAN) {
continueToStopWifi();
}
}
@Override
public void onUnregistered(ImsReasonInfo imsReasonInfo) {
Log.d(TAG, "on IMS unregistered");
// Wait for onLost in NetworkCallback
}
};
private NetworkCallback mImsNetworkCallback = new NetworkCallback() {
private int mRegisteredImsNetworkCount = 0;
@Override
public void onAvailable(Network network) {
synchronized (this) {
Log.d(TAG, "IMS network available: " + network);
mRegisteredImsNetworkCount++;
}
}
@Override
public void onLost(Network network) {
synchronized (this) {
Log.d(TAG, "IMS network lost: " + network
+ " ,isDeferring: " + mIsDeferring
+ " ,registered IMS network count: " + mRegisteredImsNetworkCount);
mRegisteredImsNetworkCount--;
if (mIsDeferring && mRegisteredImsNetworkCount <= 0) {
mRegisteredImsNetworkCount = 0;
// 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();
}
}
}
}
};
DeferStopHandler(String tag, 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(TAG, "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(TAG, "registerImsRegistrationCallback failed", e);
continueToStopWifi();
return;
}
mImsRequest = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build();
mConnectivityManager =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
mConnectivityManager.registerNetworkCallback(mImsRequest, mImsNetworkCallback,
new Handler(mLooper));
}
private void continueToStopWifi() {
Log.d(TAG, "The target role " + mTargetRole);
int deferringDurationMillis =
(int) (mClock.getElapsedSinceBootMillis() - mDeferringStartTimeMillis);
boolean isTimedOut = mMaximumDeferringTimeMillis > 0
&& deferringDurationMillis >= mMaximumDeferringTimeMillis;
if (mTargetRole == ROLE_UNSPECIFIED) {
Log.d(TAG, "Continue to stop wifi");
mStateMachine.quitNow();
mWifiMetrics.noteWifiOff(mIsDeferring, isTimedOut, deferringDurationMillis);
} else if (mTargetRole == ROLE_CLIENT_SCAN_ONLY) {
if (!mWifiNative.switchClientInterfaceToScanMode(mClientInterfaceName)) {
mModeListener.onStartFailure();
} else {
mStateMachine.sendMessage(
ClientModeStateMachine.CMD_SWITCH_TO_SCAN_ONLY_MODE_CONTINUE);
mWifiMetrics.noteWifiOff(mIsDeferring, isTimedOut, deferringDurationMillis);
}
} else {
updateConnectModeState(WifiManager.WIFI_STATE_ENABLED,
WifiManager.WIFI_STATE_DISABLING);
}
if (!mIsDeferring) return;
Log.d(TAG, "Stop DeferWifiOff handler.");
removeCallbacks(mRunnable);
if (mImsMmTelManager != null) {
try {
mImsMmTelManager.unregisterImsRegistrationCallback(mImsRegistrationCallback);
} catch (RuntimeException e) {
Log.e(TAG, "unregisterImsRegistrationCallback failed", e);
}
}
if (mConnectivityManager != null) {
mConnectivityManager.unregisterNetworkCallback(mImsNetworkCallback);
}
mIsDeferring = 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(TAG, "SubscriptionManager not found");
return 0;
}
List<SubscriptionInfo> subInfoList = subscriptionManager.getActiveSubscriptionInfoList();
if (subInfoList == null) {
Log.d(TAG, "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(TAG, "Invalid Subscription ID: " + subId);
return 0;
}
ImsMmTelManager imsMmTelManager = ImsMmTelManager.createForSubscriptionId(subId);
// If no wifi calling, no delay
if (!imsMmTelManager.isAvailable(
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN)) {
Log.d(TAG, "IMS not registered over IWLAN for subId: " + subId);
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
public @Role int getRole() {
return mRole;
}
@Override
public void setRole(@Role int role) {
Preconditions.checkState(CLIENT_ROLES.contains(role));
if (role == ROLE_CLIENT_SCAN_ONLY) {
mTargetRole = role;
// Switch client mode manager to scan only mode.
mStateMachine.sendMessage(ClientModeStateMachine.CMD_SWITCH_TO_SCAN_ONLY_MODE);
} else if (CLIENT_CONNECTIVITY_ROLES.contains(role)) {
mTargetRole = role;
// Switch client mode manager to connect mode.
mStateMachine.sendMessage(ClientModeStateMachine.CMD_SWITCH_TO_CONNECT_MODE, role);
}
}
/**
* Dump info about this ClientMode manager.
*/
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("--Dump of ClientModeManager--");
pw.println("current StateMachine mode: " + getCurrentStateName());
pw.println("mRole: " + mRole);
pw.println("mTargetRole: " + mTargetRole);
pw.println("mClientInterfaceName: " + mClientInterfaceName);
pw.println("mIfaceIsUp: " + mIfaceIsUp);
mStateMachine.dump(fd, pw, args);
}
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 newState new Wifi state
* @param currentState current wifi state
*/
private void updateConnectModeState(int newState, int currentState) {
if (newState == WifiManager.WIFI_STATE_UNKNOWN) {
// do not need to broadcast failure to system
return;
}
if (mRole != ROLE_CLIENT_PRIMARY) {
// do not raise public broadcast unless this is the primary client mode manager
return;
}
mClientModeImpl.setWifiStateForApiCalls(newState);
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);
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
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();
private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
@Override
public void onDestroyed(String ifaceName) {
if (mClientInterfaceName != null && mClientInterfaceName.equals(ifaceName)) {
Log.d(TAG, "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
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);
addState(mScanOnlyModeState, mStartedState);
addState(mConnectModeState, mStartedState);
// CHECKSTYLE:ON IndentationCheck
setInitialState(mIdleState);
start();
}
private class IdleState extends State {
@Override
public void enter() {
Log.d(TAG, "entering IdleState");
mClientInterfaceName = null;
mIfaceIsUp = false;
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_START:
// Always start in scan mode first.
mClientInterfaceName =
mWifiNative.setupInterfaceForClientInScanMode(
mWifiNativeInterfaceCallback);
if (TextUtils.isEmpty(mClientInterfaceName)) {
Log.e(TAG, "Failed to create ClientInterface. Sit in Idle");
mModeListener.onStartFailure();
break;
}
transitionTo(mScanOnlyModeState);
break;
default:
Log.d(TAG, "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(TAG, "interface down!");
mStateMachine.sendMessage(CMD_INTERFACE_DOWN);
}
}
@Override
public void enter() {
Log.d(TAG, "entering StartedState");
mIfaceIsUp = 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:
mRole = message.arg1; // could be any one of possible connect mode roles.
updateConnectModeState(WifiManager.WIFI_STATE_ENABLING,
WifiManager.WIFI_STATE_DISABLED);
if (!mWifiNative.switchClientInterfaceToConnectivityMode(
mClientInterfaceName)) {
updateConnectModeState(WifiManager.WIFI_STATE_UNKNOWN,
WifiManager.WIFI_STATE_ENABLING);
updateConnectModeState(WifiManager.WIFI_STATE_DISABLED,
WifiManager.WIFI_STATE_UNKNOWN);
mModeListener.onStartFailure();
break;
}
transitionTo(mConnectModeState);
break;
case CMD_SWITCH_TO_SCAN_ONLY_MODE:
updateConnectModeState(WifiManager.WIFI_STATE_DISABLING,
WifiManager.WIFI_STATE_ENABLED);
mDeferStopHandler.start(getWifiOffDeferringTimeMs());
break;
case CMD_SWITCH_TO_SCAN_ONLY_MODE_CONTINUE:
transitionTo(mScanOnlyModeState);
break;
case CMD_INTERFACE_DOWN:
Log.e(TAG, "Detected an interface down, reporting failure to "
+ "SelfRecovery");
mClientModeImpl.failureDetected(SelfRecovery.REASON_STA_IFACE_DOWN);
transitionTo(mIdleState);
break;
case CMD_INTERFACE_STATUS_CHANGED:
boolean isUp = message.arg1 == 1;
onUpChanged(isUp);
break;
case CMD_INTERFACE_DESTROYED:
Log.d(TAG, "interface destroyed - client mode stopping");
mClientInterfaceName = null;
transitionTo(mIdleState);
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
/**
* Clean up state, unregister listeners and update wifi state.
*/
@Override
public void exit() {
mClientModeImpl.setOperationalMode(ClientModeImpl.DISABLED_MODE, null);
if (mClientInterfaceName != null) {
mWifiNative.teardownInterface(mClientInterfaceName);
mClientInterfaceName = null;
mIfaceIsUp = false;
}
// once we leave started, nothing else to do... stop the state machine
mRole = ROLE_UNSPECIFIED;
mStateMachine.quitNow();
mModeListener.onStopped();
}
}
private class ScanOnlyModeState extends State {
@Override
public void enter() {
Log.d(TAG, "entering ScanOnlyModeState");
mClientModeImpl.setOperationalMode(ClientModeImpl.SCAN_ONLY_MODE,
mClientInterfaceName);
mRole = ROLE_CLIENT_SCAN_ONLY;
mModeListener.onStarted();
// Inform sar manager that scan only is being enabled
mSarManager.setScanOnlyWifiState(WifiManager.WIFI_STATE_ENABLED);
mWakeupController.start();
}
@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() {
// Inform sar manager that scan only is being disabled
mSarManager.setScanOnlyWifiState(WifiManager.WIFI_STATE_DISABLED);
mWakeupController.stop();
}
}
private class ConnectModeState extends State {
@Override
public void enter() {
Log.d(TAG, "entering ConnectModeState");
mClientModeImpl.setOperationalMode(ClientModeImpl.CONNECT_MODE,
mClientInterfaceName);
mModeListener.onStarted();
updateConnectModeState(WifiManager.WIFI_STATE_ENABLED,
WifiManager.WIFI_STATE_ENABLING);
// Inform sar manager that wifi is Enabled
mSarManager.setClientWifiState(WifiManager.WIFI_STATE_ENABLED);
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_SWITCH_TO_CONNECT_MODE:
int newRole = message.arg1;
// Already in connect mode, only switching the connectivity roles.
if (newRole != mRole) {
mRole = newRole;
mModeListener.onStarted();
}
break;
case CMD_SWITCH_TO_SCAN_ONLY_MODE:
updateConnectModeState(WifiManager.WIFI_STATE_DISABLING,
WifiManager.WIFI_STATE_ENABLED);
return NOT_HANDLED; // Handled in StartedState.
case CMD_INTERFACE_DOWN:
updateConnectModeState(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 (!mClientModeImpl.isConnectedMacRandomizationEnabled()) {
// 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(WifiManager.WIFI_STATE_UNKNOWN,
WifiManager.WIFI_STATE_ENABLED);
} else {
return HANDLED; // For MAC randomization, ignore...
}
}
return NOT_HANDLED; // Handled in StartedState.
case CMD_INTERFACE_DESTROYED:
updateConnectModeState(WifiManager.WIFI_STATE_DISABLING,
WifiManager.WIFI_STATE_ENABLED);
return NOT_HANDLED; // Handled in StartedState.
default:
return NOT_HANDLED;
}
return HANDLED;
}
@Override
public void exit() {
updateConnectModeState(WifiManager.WIFI_STATE_DISABLED,
WifiManager.WIFI_STATE_DISABLING);
// Inform sar manager that wifi is being disabled
mSarManager.setClientWifiState(WifiManager.WIFI_STATE_DISABLED);
}
}
}
}