blob: bfa2ae033100a6aa19b52482df5ca6ba380bf6e4 [file] [log] [blame]
/*
* Copyright (C) 2013 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.LocationManager;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import com.android.internal.R;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.wifi.util.WifiPermissionsUtil;
/**
* WifiController is the class used to manage wifi state for various operating
* modes (normal, airplane, wifi hotspot, etc.).
*/
public class WifiController extends StateMachine {
private static final String TAG = "WifiController";
private static final boolean DBG = false;
private Context mContext;
private boolean mFirstUserSignOnSeen = false;
/**
* See {@link Settings.Global#WIFI_REENABLE_DELAY_MS}. This is the default value if a
* Settings.Global value is not present. This is the minimum time after wifi is disabled
* we'll act on an enable. Enable requests received before this delay will be deferred.
*/
private static final long DEFAULT_REENABLE_DELAY_MS = 500;
// Maximum limit to use for timeout delay if the value from overlay setting is too large.
private static final int MAX_RECOVERY_TIMEOUT_DELAY_MS = 4000;
// finding that delayed messages can sometimes be delivered earlier than expected
// probably rounding errors. add a margin to prevent problems
private static final long DEFER_MARGIN_MS = 5;
/* References to values tracked in WifiService */
private final ClientModeImpl mClientModeImpl;
private final Looper mClientModeImplLooper;
private final ActiveModeWarden mActiveModeWarden;
private final WifiSettingsStore mSettingsStore;
private final FrameworkFacade mFacade;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private long mReEnableDelayMillis;
private int mRecoveryDelayMillis;
private static final int BASE = Protocol.BASE_WIFI_CONTROLLER;
static final int CMD_EMERGENCY_MODE_CHANGED = BASE + 1;
static final int CMD_SCAN_ALWAYS_MODE_CHANGED = BASE + 7;
static final int CMD_WIFI_TOGGLED = BASE + 8;
static final int CMD_AIRPLANE_TOGGLED = BASE + 9;
static final int CMD_SET_AP = BASE + 10;
static final int CMD_DEFERRED_TOGGLE = BASE + 11;
static final int CMD_AP_START_FAILURE = BASE + 13;
static final int CMD_EMERGENCY_CALL_STATE_CHANGED = BASE + 14;
static final int CMD_AP_STOPPED = BASE + 15;
static final int CMD_STA_START_FAILURE = BASE + 16;
// Command used to trigger a wifi stack restart when in active mode
static final int CMD_RECOVERY_RESTART_WIFI = BASE + 17;
// Internal command used to complete wifi stack restart
private static final int CMD_RECOVERY_RESTART_WIFI_CONTINUE = BASE + 18;
// Command to disable wifi when SelfRecovery is throttled or otherwise not doing full recovery
static final int CMD_RECOVERY_DISABLE_WIFI = BASE + 19;
static final int CMD_STA_STOPPED = BASE + 20;
static final int CMD_SCANNING_STOPPED = BASE + 21;
static final int CMD_DEFERRED_RECOVERY_RESTART_WIFI = BASE + 22;
private DefaultState mDefaultState = new DefaultState();
private StaEnabledState mStaEnabledState = new StaEnabledState();
private StaDisabledState mStaDisabledState = new StaDisabledState();
private StaDisabledWithScanState mStaDisabledWithScanState = new StaDisabledWithScanState();
private EcmState mEcmState = new EcmState();
private ScanOnlyModeManager.Listener mScanOnlyModeCallback = new ScanOnlyCallback();
private ClientModeManager.Listener mClientModeCallback = new ClientModeCallback();
WifiController(Context context, ClientModeImpl clientModeImpl, Looper clientModeImplLooper,
WifiSettingsStore wss, Looper wifiServiceLooper, FrameworkFacade f,
ActiveModeWarden amw, WifiPermissionsUtil wifiPermissionsUtil) {
super(TAG, wifiServiceLooper);
mFacade = f;
mContext = context;
mClientModeImpl = clientModeImpl;
mClientModeImplLooper = clientModeImplLooper;
mActiveModeWarden = amw;
mSettingsStore = wss;
mWifiPermissionsUtil = wifiPermissionsUtil;
// CHECKSTYLE:OFF IndentationCheck
addState(mDefaultState);
addState(mStaDisabledState, mDefaultState);
addState(mStaEnabledState, mDefaultState);
addState(mStaDisabledWithScanState, mDefaultState);
addState(mEcmState, mDefaultState);
// CHECKSTYLE:ON IndentationCheck
setLogRecSize(100);
setLogOnlyTransitions(false);
// register for state updates via callbacks (vs the intents registered below)
mActiveModeWarden.registerScanOnlyCallback(mScanOnlyModeCallback);
mActiveModeWarden.registerClientModeCallback(mClientModeCallback);
readWifiReEnableDelay();
readWifiRecoveryDelay();
}
@Override
public void start() {
boolean isAirplaneModeOn = mSettingsStore.isAirplaneModeOn();
boolean isWifiEnabled = mSettingsStore.isWifiToggleEnabled();
boolean isScanningAlwaysAvailable = mSettingsStore.isScanAlwaysAvailable();
boolean isLocationModeActive = mWifiPermissionsUtil.isLocationModeEnabled();
log("isAirplaneModeOn = " + isAirplaneModeOn
+ ", isWifiEnabled = " + isWifiEnabled
+ ", isScanningAvailable = " + isScanningAlwaysAvailable
+ ", isLocationModeActive = " + isLocationModeActive);
if (checkScanOnlyModeAvailable()) {
setInitialState(mStaDisabledWithScanState);
} else {
setInitialState(mStaDisabledState);
}
IntentFilter filter = new IntentFilter();
filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
filter.addAction(LocationManager.MODE_CHANGED_ACTION);
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
int state = intent.getIntExtra(
WifiManager.EXTRA_WIFI_AP_STATE,
WifiManager.WIFI_AP_STATE_FAILED);
if (state == WifiManager.WIFI_AP_STATE_FAILED) {
Log.e(TAG, "SoftAP start failed");
sendMessage(CMD_AP_START_FAILURE);
} else if (state == WifiManager.WIFI_AP_STATE_DISABLED) {
sendMessage(CMD_AP_STOPPED);
}
} else if (action.equals(LocationManager.MODE_CHANGED_ACTION)) {
// Location mode has been toggled... trigger with the scan change
// update to make sure we are in the correct mode
sendMessage(CMD_SCAN_ALWAYS_MODE_CHANGED);
}
}
},
new IntentFilter(filter));
super.start();
}
private boolean checkScanOnlyModeAvailable() {
// first check if Location service is disabled, if so return false
if (!mWifiPermissionsUtil.isLocationModeEnabled()) {
return false;
}
return mSettingsStore.isScanAlwaysAvailable();
}
/**
* Listener used to receive scan mode updates - really needed for disabled updates to trigger
* mode changes.
*/
private class ScanOnlyCallback implements ScanOnlyModeManager.Listener {
@Override
public void onStateChanged(int state) {
if (state == WifiManager.WIFI_STATE_UNKNOWN) {
Log.d(TAG, "ScanOnlyMode unexpected failure: state unknown");
} else if (state == WifiManager.WIFI_STATE_DISABLED) {
Log.d(TAG, "ScanOnlyMode stopped");
sendMessage(CMD_SCANNING_STOPPED);
} else if (state == WifiManager.WIFI_STATE_ENABLED) {
// scan mode is ready to go
Log.d(TAG, "scan mode active");
} else {
Log.d(TAG, "unexpected state update: " + state);
}
}
}
/**
* Listener used to receive client mode updates
*/
private class ClientModeCallback implements ClientModeManager.Listener {
@Override
public void onStateChanged(int state) {
if (state == WifiManager.WIFI_STATE_UNKNOWN) {
logd("ClientMode unexpected failure: state unknown");
sendMessage(CMD_STA_START_FAILURE);
} else if (state == WifiManager.WIFI_STATE_DISABLED) {
logd("ClientMode stopped");
sendMessage(CMD_STA_STOPPED);
} else if (state == WifiManager.WIFI_STATE_ENABLED) {
// scan mode is ready to go
logd("client mode active");
} else {
logd("unexpected state update: " + state);
}
}
}
private void readWifiReEnableDelay() {
mReEnableDelayMillis = mFacade.getLongSetting(mContext,
Settings.Global.WIFI_REENABLE_DELAY_MS, DEFAULT_REENABLE_DELAY_MS);
}
private void readWifiRecoveryDelay() {
mRecoveryDelayMillis = mContext.getResources().getInteger(
R.integer.config_wifi_framework_recovery_timeout_delay);
if (mRecoveryDelayMillis > MAX_RECOVERY_TIMEOUT_DELAY_MS) {
mRecoveryDelayMillis = MAX_RECOVERY_TIMEOUT_DELAY_MS;
Log.w(TAG, "Overriding timeout delay with maximum limit value");
}
}
class DefaultState extends State {
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_SCAN_ALWAYS_MODE_CHANGED:
case CMD_WIFI_TOGGLED:
case CMD_AP_START_FAILURE:
case CMD_SCANNING_STOPPED:
case CMD_STA_STOPPED:
case CMD_STA_START_FAILURE:
case CMD_RECOVERY_RESTART_WIFI_CONTINUE:
case CMD_DEFERRED_RECOVERY_RESTART_WIFI:
break;
case CMD_RECOVERY_DISABLE_WIFI:
log("Recovery has been throttled, disable wifi");
mActiveModeWarden.shutdownWifi();
transitionTo(mStaDisabledState);
break;
case CMD_RECOVERY_RESTART_WIFI:
deferMessage(obtainMessage(CMD_DEFERRED_RECOVERY_RESTART_WIFI));
mActiveModeWarden.shutdownWifi();
transitionTo(mStaDisabledState);
break;
case CMD_DEFERRED_TOGGLE:
log("DEFERRED_TOGGLE ignored due to state change");
break;
case CMD_SET_AP:
// note: CMD_SET_AP is handled/dropped in ECM mode - will not start here
if (msg.arg1 == 1) {
SoftApModeConfiguration config = (SoftApModeConfiguration) msg.obj;
mActiveModeWarden.enterSoftAPMode((SoftApModeConfiguration) msg.obj);
} else {
mActiveModeWarden.stopSoftAPMode(msg.arg2);
}
break;
case CMD_AIRPLANE_TOGGLED:
if (mSettingsStore.isAirplaneModeOn()) {
log("Airplane mode toggled, shutdown all modes");
mActiveModeWarden.shutdownWifi();
transitionTo(mStaDisabledState);
} else {
log("Airplane mode disabled, determine next state");
if (mSettingsStore.isWifiToggleEnabled()) {
transitionTo(mStaEnabledState);
} else if (checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledWithScanState);
}
// wifi should remain disabled, do not need to transition
}
break;
case CMD_EMERGENCY_CALL_STATE_CHANGED:
case CMD_EMERGENCY_MODE_CHANGED:
if (msg.arg1 == 1) {
transitionTo(mEcmState);
}
break;
case CMD_AP_STOPPED:
log("SoftAp mode disabled, determine next state");
if (mSettingsStore.isWifiToggleEnabled()) {
transitionTo(mStaEnabledState);
} else if (checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledWithScanState);
}
// wifi should remain disabled, do not need to transition
break;
default:
throw new RuntimeException("WifiController.handleMessage " + msg.what);
}
return HANDLED;
}
}
class StaDisabledState extends State {
private int mDeferredEnableSerialNumber = 0;
private boolean mHaveDeferredEnable = false;
private long mDisabledTimestamp;
@Override
public void enter() {
mActiveModeWarden.disableWifi();
// Supplicant can't restart right away, so note the time we switched off
mDisabledTimestamp = SystemClock.elapsedRealtime();
mDeferredEnableSerialNumber++;
mHaveDeferredEnable = false;
}
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_WIFI_TOGGLED:
if (mSettingsStore.isWifiToggleEnabled()) {
if (doDeferEnable(msg)) {
if (mHaveDeferredEnable) {
// have 2 toggles now, inc serial number and ignore both
mDeferredEnableSerialNumber++;
}
mHaveDeferredEnable = !mHaveDeferredEnable;
break;
}
transitionTo(mStaEnabledState);
} else if (checkScanOnlyModeAvailable()) {
// only go to scan mode if we aren't in airplane mode
if (mSettingsStore.isAirplaneModeOn()) {
transitionTo(mStaDisabledWithScanState);
}
}
break;
case CMD_SCAN_ALWAYS_MODE_CHANGED:
if (checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledWithScanState);
break;
}
break;
case CMD_SET_AP:
if (msg.arg1 == 1) {
// remember that we were disabled, but pass the command up to start softap
mSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_DISABLED);
}
return NOT_HANDLED;
case CMD_DEFERRED_TOGGLE:
if (msg.arg1 != mDeferredEnableSerialNumber) {
log("DEFERRED_TOGGLE ignored due to serial mismatch");
break;
}
log("DEFERRED_TOGGLE handled");
sendMessage((Message)(msg.obj));
break;
case CMD_DEFERRED_RECOVERY_RESTART_WIFI:
// wait mRecoveryDelayMillis for letting driver clean reset.
sendMessageDelayed(CMD_RECOVERY_RESTART_WIFI_CONTINUE, mRecoveryDelayMillis);
break;
case CMD_RECOVERY_RESTART_WIFI_CONTINUE:
if (mSettingsStore.isWifiToggleEnabled()) {
// wifi is currently disabled but the toggle is on, must have had an
// interface down before the recovery triggered
transitionTo(mStaEnabledState);
break;
} else if (checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledWithScanState);
break;
}
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
private boolean doDeferEnable(Message msg) {
long delaySoFar = SystemClock.elapsedRealtime() - mDisabledTimestamp;
if (delaySoFar >= mReEnableDelayMillis) {
return false;
}
log("WifiController msg " + msg + " deferred for " +
(mReEnableDelayMillis - delaySoFar) + "ms");
// need to defer this action.
Message deferredMsg = obtainMessage(CMD_DEFERRED_TOGGLE);
deferredMsg.obj = Message.obtain(msg);
deferredMsg.arg1 = ++mDeferredEnableSerialNumber;
sendMessageDelayed(deferredMsg, mReEnableDelayMillis - delaySoFar + DEFER_MARGIN_MS);
return true;
}
}
class StaEnabledState extends State {
@Override
public void enter() {
log("StaEnabledState.enter()");
mActiveModeWarden.enterClientMode();
}
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_WIFI_TOGGLED:
if (! mSettingsStore.isWifiToggleEnabled()) {
if (checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledWithScanState);
} else {
transitionTo(mStaDisabledState);
}
}
break;
case CMD_AIRPLANE_TOGGLED:
// airplane mode toggled on is handled in the default state
if (mSettingsStore.isAirplaneModeOn()) {
return NOT_HANDLED;
} else {
// when airplane mode is toggled off, but wifi is on, we can keep it on
log("airplane mode toggled - and airplane mode is off. return handled");
return HANDLED;
}
case CMD_STA_START_FAILURE:
if (!checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledState);
} else {
transitionTo(mStaDisabledWithScanState);
}
break;
case CMD_SET_AP:
if (msg.arg1 == 1) {
// remember that we were enabled, but pass the command up to start softap
mSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_ENABLED);
}
return NOT_HANDLED;
case CMD_AP_START_FAILURE:
case CMD_AP_STOPPED:
// already in a wifi mode, no need to check where we should go with softap
// stopped
break;
case CMD_STA_STOPPED:
// Client mode stopped. head to Disabled to wait for next command
transitionTo(mStaDisabledState);
break;
case CMD_RECOVERY_RESTART_WIFI:
final String bugTitle;
final String bugDetail;
if (msg.arg1 < SelfRecovery.REASON_STRINGS.length && msg.arg1 >= 0) {
bugDetail = SelfRecovery.REASON_STRINGS[msg.arg1];
bugTitle = "Wi-Fi BugReport: " + bugDetail;
} else {
bugDetail = "";
bugTitle = "Wi-Fi BugReport";
}
if (msg.arg1 != SelfRecovery.REASON_LAST_RESORT_WATCHDOG) {
(new Handler(mClientModeImplLooper)).post(() -> {
mClientModeImpl.takeBugReport(bugTitle, bugDetail);
});
}
// after the bug report trigger, more handling needs to be done
return NOT_HANDLED;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
class StaDisabledWithScanState extends State {
private int mDeferredEnableSerialNumber = 0;
private boolean mHaveDeferredEnable = false;
private long mDisabledTimestamp;
@Override
public void enter() {
// now trigger the actual mode switch in ActiveModeWarden
mActiveModeWarden.enterScanOnlyMode();
// TODO b/71559473: remove the defered enable after mode management changes are complete
// Supplicant can't restart right away, so not the time we switched off
mDisabledTimestamp = SystemClock.elapsedRealtime();
mDeferredEnableSerialNumber++;
mHaveDeferredEnable = false;
}
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_WIFI_TOGGLED:
if (mSettingsStore.isWifiToggleEnabled()) {
if (doDeferEnable(msg)) {
if (mHaveDeferredEnable) {
// have 2 toggles now, inc serial number and ignore both
mDeferredEnableSerialNumber++;
}
mHaveDeferredEnable = !mHaveDeferredEnable;
break;
}
transitionTo(mStaEnabledState);
}
break;
case CMD_SCAN_ALWAYS_MODE_CHANGED:
if (!checkScanOnlyModeAvailable()) {
log("StaDisabledWithScanState: scan no longer available");
transitionTo(mStaDisabledState);
}
break;
case CMD_SET_AP:
if (msg.arg1 == 1) {
// remember that we were disabled, but pass the command up to start softap
mSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_DISABLED);
}
return NOT_HANDLED;
case CMD_DEFERRED_TOGGLE:
if (msg.arg1 != mDeferredEnableSerialNumber) {
log("DEFERRED_TOGGLE ignored due to serial mismatch");
break;
}
logd("DEFERRED_TOGGLE handled");
sendMessage((Message)(msg.obj));
break;
case CMD_AP_START_FAILURE:
case CMD_AP_STOPPED:
// already in a wifi mode, no need to check where we should go with softap
// stopped
break;
case CMD_SCANNING_STOPPED:
// stopped due to interface destruction - return to disabled and wait
log("WifiController: SCANNING_STOPPED when in scan mode -> StaDisabled");
transitionTo(mStaDisabledState);
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
private boolean doDeferEnable(Message msg) {
long delaySoFar = SystemClock.elapsedRealtime() - mDisabledTimestamp;
if (delaySoFar >= mReEnableDelayMillis) {
return false;
}
log("WifiController msg " + msg + " deferred for " +
(mReEnableDelayMillis - delaySoFar) + "ms");
// need to defer this action.
Message deferredMsg = obtainMessage(CMD_DEFERRED_TOGGLE);
deferredMsg.obj = Message.obtain(msg);
deferredMsg.arg1 = ++mDeferredEnableSerialNumber;
sendMessageDelayed(deferredMsg, mReEnableDelayMillis - delaySoFar + DEFER_MARGIN_MS);
return true;
}
}
/**
* Determine the next state based on the current settings (e.g. saved
* wifi state).
*/
private State getNextWifiState() {
if (mSettingsStore.getWifiSavedState() == WifiSettingsStore.WIFI_ENABLED) {
return mStaEnabledState;
}
if (checkScanOnlyModeAvailable()) {
return mStaDisabledWithScanState;
}
return mStaDisabledState;
}
class EcmState extends State {
// we can enter EcmState either because an emergency call started or because
// emergency callback mode started. This count keeps track of how many such
// events happened; so we can exit after all are undone
private int mEcmEntryCount;
@Override
public void enter() {
mActiveModeWarden.stopSoftAPMode(WifiManager.IFACE_IP_MODE_UNSPECIFIED);
boolean configWiFiDisableInECBM =
mFacade.getConfigWiFiDisableInECBM(mContext);
log("WifiController msg getConfigWiFiDisableInECBM "
+ configWiFiDisableInECBM);
if (configWiFiDisableInECBM) {
mActiveModeWarden.shutdownWifi();
}
mEcmEntryCount = 1;
}
/**
* Handles messages received while in EcmMode.
*/
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_EMERGENCY_CALL_STATE_CHANGED:
if (msg.arg1 == 1) {
// nothing to do - just says emergency call started
mEcmEntryCount++;
} else if (msg.arg1 == 0) {
// emergency call ended
decrementCountAndReturnToAppropriateState();
}
return HANDLED;
case CMD_EMERGENCY_MODE_CHANGED:
if (msg.arg1 == 1) {
// Transitioned into emergency callback mode
mEcmEntryCount++;
} else if (msg.arg1 == 0) {
// out of emergency callback mode
decrementCountAndReturnToAppropriateState();
}
return HANDLED;
case CMD_RECOVERY_RESTART_WIFI:
case CMD_RECOVERY_DISABLE_WIFI:
// do not want to restart wifi if we are in emergency mode
return HANDLED;
case CMD_AP_STOPPED:
case CMD_SCANNING_STOPPED:
case CMD_STA_STOPPED:
// do not want to trigger a mode switch if we are in emergency mode
return HANDLED;
case CMD_SET_AP:
// do not want to start softap if we are in emergency mode
return HANDLED;
default:
return NOT_HANDLED;
}
}
private void decrementCountAndReturnToAppropriateState() {
boolean exitEcm = false;
if (mEcmEntryCount == 0) {
loge("mEcmEntryCount is 0; exiting Ecm");
exitEcm = true;
} else if (--mEcmEntryCount == 0) {
exitEcm = true;
}
if (exitEcm) {
if (mSettingsStore.isWifiToggleEnabled()) {
transitionTo(mStaEnabledState);
} else if (checkScanOnlyModeAvailable()) {
transitionTo(mStaDisabledWithScanState);
} else {
transitionTo(mStaDisabledState);
}
}
}
}
}