/* * 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 com.android.server.wifi.util.ApConfigUtil.ERROR_GENERIC; import static com.android.server.wifi.util.ApConfigUtil.ERROR_NO_CHANNEL; import static com.android.server.wifi.util.ApConfigUtil.SUCCESS; import android.annotation.NonNull; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IState; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.WakeupMessage; import com.android.server.wifi.WifiNative.InterfaceCallback; import com.android.server.wifi.WifiNative.SoftApListener; import com.android.server.wifi.util.ApConfigUtil; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Locale; /** * Manage WiFi in AP mode. * The internal state machine runs under the ClientModeImpl handler thread context. */ public class SoftApManager implements ActiveModeManager { private static final String TAG = "SoftApManager"; // Minimum limit to use for timeout delay if the value from overlay setting is too small. private static final int MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000; // 10 minutes @VisibleForTesting public static final String SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG = TAG + " Soft AP Send Message Timeout"; private final Context mContext; private final FrameworkFacade mFrameworkFacade; private final WifiNative mWifiNative; private final String mCountryCode; private final SoftApStateMachine mStateMachine; private final WifiManager.SoftApCallback mCallback; private String mApInterfaceName; private boolean mIfaceIsUp; private boolean mIfaceIsDestroyed; private final WifiApConfigStore mWifiApConfigStore; private final WifiMetrics mWifiMetrics; private final int mMode; private WifiConfiguration mApConfig; private int mReportedFrequency = -1; private int mReportedBandwidth = -1; private int mNumAssociatedStations = 0; private boolean mTimeoutEnabled = false; private final SarManager mSarManager; private long mStartTimestamp = -1; /** * Listener for soft AP events. */ private final SoftApListener mSoftApListener = new SoftApListener() { @Override public void onFailure() { mStateMachine.sendMessage(SoftApStateMachine.CMD_FAILURE); } @Override public void onNumAssociatedStationsChanged(int numStations) { mStateMachine.sendMessage( SoftApStateMachine.CMD_NUM_ASSOCIATED_STATIONS_CHANGED, numStations); } @Override public void onSoftApChannelSwitched(int frequency, int bandwidth) { mStateMachine.sendMessage( SoftApStateMachine.CMD_SOFT_AP_CHANNEL_SWITCHED, frequency, bandwidth); } }; public SoftApManager(@NonNull Context context, @NonNull Looper looper, @NonNull FrameworkFacade framework, @NonNull WifiNative wifiNative, String countryCode, @NonNull WifiManager.SoftApCallback callback, @NonNull WifiApConfigStore wifiApConfigStore, @NonNull SoftApModeConfiguration apConfig, @NonNull WifiMetrics wifiMetrics, @NonNull SarManager sarManager) { mContext = context; mFrameworkFacade = framework; mWifiNative = wifiNative; mCountryCode = countryCode; mCallback = callback; mWifiApConfigStore = wifiApConfigStore; mMode = apConfig.getTargetMode(); WifiConfiguration config = apConfig.getWifiConfiguration(); if (config == null) { mApConfig = mWifiApConfigStore.getApConfiguration(); } else { mApConfig = config; } mWifiMetrics = wifiMetrics; mSarManager = sarManager; mStateMachine = new SoftApStateMachine(looper); } /** * Start soft AP with the supplied config. */ public void start() { mStateMachine.sendMessage(SoftApStateMachine.CMD_START, mApConfig); } /** * Stop soft AP. */ public void stop() { Log.d(TAG, " currentstate: " + getCurrentStateName()); if (mApInterfaceName != null) { if (mIfaceIsUp) { updateApState(WifiManager.WIFI_AP_STATE_DISABLING, WifiManager.WIFI_AP_STATE_ENABLED, 0); } else { updateApState(WifiManager.WIFI_AP_STATE_DISABLING, WifiManager.WIFI_AP_STATE_ENABLING, 0); } } mStateMachine.quitNow(); } public @ScanMode int getScanMode() { return SCAN_NONE; } public int getIpMode() { return mMode; } /** * Dump info about this softap manager. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("--Dump of SoftApManager--"); pw.println("current StateMachine mode: " + getCurrentStateName()); pw.println("mApInterfaceName: " + mApInterfaceName); pw.println("mIfaceIsUp: " + mIfaceIsUp); pw.println("mMode: " + mMode); pw.println("mCountryCode: " + mCountryCode); if (mApConfig != null) { pw.println("mApConfig.SSID: " + mApConfig.SSID); pw.println("mApConfig.apBand: " + mApConfig.apBand); pw.println("mApConfig.hiddenSSID: " + mApConfig.hiddenSSID); } else { pw.println("mApConfig: null"); } pw.println("mNumAssociatedStations: " + mNumAssociatedStations); pw.println("mTimeoutEnabled: " + mTimeoutEnabled); pw.println("mReportedFrequency: " + mReportedFrequency); pw.println("mReportedBandwidth: " + mReportedBandwidth); pw.println("mStartTimestamp: " + mStartTimestamp); } private String getCurrentStateName() { IState currentState = mStateMachine.getCurrentState(); if (currentState != null) { return currentState.getName(); } return "StateMachine not active"; } /** * Update AP state. * @param newState new AP state * @param currentState current AP state * @param reason Failure reason if the new AP state is in failure state */ private void updateApState(int newState, int currentState, int reason) { mCallback.onStateChanged(newState, reason); //send the AP state change broadcast final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); intent.putExtra(WifiManager.EXTRA_WIFI_AP_STATE, newState); intent.putExtra(WifiManager.EXTRA_PREVIOUS_WIFI_AP_STATE, currentState); if (newState == WifiManager.WIFI_AP_STATE_FAILED) { //only set reason number when softAP start failed intent.putExtra(WifiManager.EXTRA_WIFI_AP_FAILURE_REASON, reason); } intent.putExtra(WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME, mApInterfaceName); intent.putExtra(WifiManager.EXTRA_WIFI_AP_MODE, mMode); mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); } /** * Start a soft AP instance with the given configuration. * @param config AP configuration * @return integer result code */ private int startSoftAp(WifiConfiguration config) { if (config == null || config.SSID == null) { Log.e(TAG, "Unable to start soft AP without valid configuration"); return ERROR_GENERIC; } // Setup country code if (TextUtils.isEmpty(mCountryCode)) { if (config.apBand == WifiConfiguration.AP_BAND_5GHZ) { // Country code is mandatory for 5GHz band. Log.e(TAG, "Invalid country code, required for setting up " + "soft ap in 5GHz"); return ERROR_GENERIC; } // Absence of country code is not fatal for 2Ghz & Any band options. } else if (!mWifiNative.setCountryCodeHal( mApInterfaceName, mCountryCode.toUpperCase(Locale.ROOT))) { if (config.apBand == WifiConfiguration.AP_BAND_5GHZ) { // Return an error if failed to set country code when AP is configured for // 5GHz band. Log.e(TAG, "Failed to set country code, required for setting up " + "soft ap in 5GHz"); return ERROR_GENERIC; } // Failure to set country code is not fatal for 2Ghz & Any band options. } // Make a copy of configuration for updating AP band and channel. WifiConfiguration localConfig = new WifiConfiguration(config); int result = ApConfigUtil.updateApChannelConfig( mWifiNative, mCountryCode, mWifiApConfigStore.getAllowed2GChannel(), localConfig); if (result != SUCCESS) { Log.e(TAG, "Failed to update AP band and channel"); return result; } if (localConfig.hiddenSSID) { Log.d(TAG, "SoftAP is a hidden network"); } if (!mWifiNative.startSoftAp(mApInterfaceName, localConfig, mSoftApListener)) { Log.e(TAG, "Soft AP start failed"); return ERROR_GENERIC; } mStartTimestamp = SystemClock.elapsedRealtime(); Log.d(TAG, "Soft AP is started"); return SUCCESS; } /** * Teardown soft AP and teardown the interface. */ private void stopSoftAp() { mWifiNative.teardownInterface(mApInterfaceName); Log.d(TAG, "Soft AP is stopped"); } private class SoftApStateMachine extends StateMachine { // Commands for the state machine. public static final int CMD_START = 0; public static final int CMD_FAILURE = 2; public static final int CMD_INTERFACE_STATUS_CHANGED = 3; public static final int CMD_NUM_ASSOCIATED_STATIONS_CHANGED = 4; public static final int CMD_NO_ASSOCIATED_STATIONS_TIMEOUT = 5; public static final int CMD_TIMEOUT_TOGGLE_CHANGED = 6; public static final int CMD_INTERFACE_DESTROYED = 7; public static final int CMD_INTERFACE_DOWN = 8; public static final int CMD_SOFT_AP_CHANNEL_SWITCHED = 9; private final State mIdleState = new IdleState(); private final State mStartedState = new StartedState(); private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() { @Override public void onDestroyed(String ifaceName) { if (mApInterfaceName != null && mApInterfaceName.equals(ifaceName)) { sendMessage(CMD_INTERFACE_DESTROYED); } } @Override public void onUp(String ifaceName) { if (mApInterfaceName != null && mApInterfaceName.equals(ifaceName)) { sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1); } } @Override public void onDown(String ifaceName) { if (mApInterfaceName != null && mApInterfaceName.equals(ifaceName)) { sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0); } } }; SoftApStateMachine(Looper looper) { super(TAG, looper); addState(mIdleState); addState(mStartedState); setInitialState(mIdleState); start(); } private class IdleState extends State { @Override public void enter() { mApInterfaceName = null; mIfaceIsUp = false; mIfaceIsDestroyed = false; } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START: mApInterfaceName = mWifiNative.setupInterfaceForSoftApMode( mWifiNativeInterfaceCallback); if (TextUtils.isEmpty(mApInterfaceName)) { Log.e(TAG, "setup failure when creating ap interface."); updateApState(WifiManager.WIFI_AP_STATE_FAILED, WifiManager.WIFI_AP_STATE_DISABLED, WifiManager.SAP_START_FAILURE_GENERAL); mWifiMetrics.incrementSoftApStartResult( false, WifiManager.SAP_START_FAILURE_GENERAL); break; } updateApState(WifiManager.WIFI_AP_STATE_ENABLING, WifiManager.WIFI_AP_STATE_DISABLED, 0); int result = startSoftAp((WifiConfiguration) message.obj); if (result != SUCCESS) { int failureReason = WifiManager.SAP_START_FAILURE_GENERAL; if (result == ERROR_NO_CHANNEL) { failureReason = WifiManager.SAP_START_FAILURE_NO_CHANNEL; } updateApState(WifiManager.WIFI_AP_STATE_FAILED, WifiManager.WIFI_AP_STATE_ENABLING, failureReason); stopSoftAp(); mWifiMetrics.incrementSoftApStartResult(false, failureReason); break; } transitionTo(mStartedState); break; default: // Ignore all other commands. break; } return HANDLED; } } private class StartedState extends State { private int mTimeoutDelay; private WakeupMessage mSoftApTimeoutMessage; private SoftApTimeoutEnabledSettingObserver mSettingObserver; /** * Observer for timeout settings changes. */ private class SoftApTimeoutEnabledSettingObserver extends ContentObserver { SoftApTimeoutEnabledSettingObserver(Handler handler) { super(handler); } public void register() { mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(Settings.Global.SOFT_AP_TIMEOUT_ENABLED), true, this); mTimeoutEnabled = getValue(); } public void unregister() { mFrameworkFacade.unregisterContentObserver(mContext, this); } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); mStateMachine.sendMessage(SoftApStateMachine.CMD_TIMEOUT_TOGGLE_CHANGED, getValue() ? 1 : 0); } private boolean getValue() { boolean enabled = mFrameworkFacade.getIntegerSetting(mContext, Settings.Global.SOFT_AP_TIMEOUT_ENABLED, 1) == 1; return enabled; } } private int getConfigSoftApTimeoutDelay() { int delay = mContext.getResources().getInteger( R.integer.config_wifi_framework_soft_ap_timeout_delay); if (delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) { delay = MIN_SOFT_AP_TIMEOUT_DELAY_MS; Log.w(TAG, "Overriding timeout delay with minimum limit value"); } Log.d(TAG, "Timeout delay: " + delay); return delay; } private void scheduleTimeoutMessage() { if (!mTimeoutEnabled) { return; } mSoftApTimeoutMessage.schedule(SystemClock.elapsedRealtime() + mTimeoutDelay); Log.d(TAG, "Timeout message scheduled"); } private void cancelTimeoutMessage() { mSoftApTimeoutMessage.cancel(); Log.d(TAG, "Timeout message canceled"); } /** * Set number of stations associated with this soft AP * @param numStations Number of connected stations */ private void setNumAssociatedStations(int numStations) { if (mNumAssociatedStations == numStations) { return; } mNumAssociatedStations = numStations; Log.d(TAG, "Number of associated stations changed: " + mNumAssociatedStations); if (mCallback != null) { mCallback.onNumClientsChanged(mNumAssociatedStations); } else { Log.e(TAG, "SoftApCallback is null. Dropping NumClientsChanged event."); } mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(mNumAssociatedStations, mMode); if (mNumAssociatedStations == 0) { scheduleTimeoutMessage(); } else { cancelTimeoutMessage(); } } private void onUpChanged(boolean isUp) { if (isUp == mIfaceIsUp) { return; // no change } mIfaceIsUp = isUp; if (isUp) { Log.d(TAG, "SoftAp is ready for use"); updateApState(WifiManager.WIFI_AP_STATE_ENABLED, WifiManager.WIFI_AP_STATE_ENABLING, 0); mWifiMetrics.incrementSoftApStartResult(true, 0); if (mCallback != null) { mCallback.onNumClientsChanged(mNumAssociatedStations); } } else { // the interface was up, but goes down sendMessage(CMD_INTERFACE_DOWN); } mWifiMetrics.addSoftApUpChangedEvent(isUp, mMode); } @Override public void enter() { mIfaceIsUp = false; mIfaceIsDestroyed = false; onUpChanged(mWifiNative.isInterfaceUp(mApInterfaceName)); mTimeoutDelay = getConfigSoftApTimeoutDelay(); Handler handler = mStateMachine.getHandler(); mSoftApTimeoutMessage = new WakeupMessage(mContext, handler, SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG, SoftApStateMachine.CMD_NO_ASSOCIATED_STATIONS_TIMEOUT); mSettingObserver = new SoftApTimeoutEnabledSettingObserver(handler); if (mSettingObserver != null) { mSettingObserver.register(); } mSarManager.setSapWifiState(WifiManager.WIFI_AP_STATE_ENABLED); Log.d(TAG, "Resetting num stations on start"); mNumAssociatedStations = 0; scheduleTimeoutMessage(); } @Override public void exit() { if (!mIfaceIsDestroyed) { stopSoftAp(); } if (mSettingObserver != null) { mSettingObserver.unregister(); } Log.d(TAG, "Resetting num stations on stop"); setNumAssociatedStations(0); cancelTimeoutMessage(); // Need this here since we are exiting |Started| state and won't handle any // future CMD_INTERFACE_STATUS_CHANGED events after this point mWifiMetrics.addSoftApUpChangedEvent(false, mMode); updateApState(WifiManager.WIFI_AP_STATE_DISABLED, WifiManager.WIFI_AP_STATE_DISABLING, 0); mSarManager.setSapWifiState(WifiManager.WIFI_AP_STATE_DISABLED); mApInterfaceName = null; mIfaceIsUp = false; mIfaceIsDestroyed = false; mStateMachine.quitNow(); } private void updateUserBandPreferenceViolationMetricsIfNeeded() { boolean bandPreferenceViolated = false; if (mApConfig.apBand == WifiConfiguration.AP_BAND_2GHZ && ScanResult.is5GHz(mReportedFrequency)) { bandPreferenceViolated = true; } else if (mApConfig.apBand == WifiConfiguration.AP_BAND_5GHZ && ScanResult.is24GHz(mReportedFrequency)) { bandPreferenceViolated = true; } if (bandPreferenceViolated) { Log.e(TAG, "Channel does not satisfy user band preference: " + mReportedFrequency); mWifiMetrics.incrementNumSoftApUserBandPreferenceUnsatisfied(); } } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_NUM_ASSOCIATED_STATIONS_CHANGED: if (message.arg1 < 0) { Log.e(TAG, "Invalid number of associated stations: " + message.arg1); break; } Log.d(TAG, "Setting num stations on CMD_NUM_ASSOCIATED_STATIONS_CHANGED"); setNumAssociatedStations(message.arg1); break; case CMD_SOFT_AP_CHANNEL_SWITCHED: mReportedFrequency = message.arg1; mReportedBandwidth = message.arg2; Log.d(TAG, "Channel switched. Frequency: " + mReportedFrequency + " Bandwidth: " + mReportedBandwidth); mWifiMetrics.addSoftApChannelSwitchedEvent(mReportedFrequency, mReportedBandwidth, mMode); updateUserBandPreferenceViolationMetricsIfNeeded(); break; case CMD_TIMEOUT_TOGGLE_CHANGED: boolean isEnabled = (message.arg1 == 1); if (mTimeoutEnabled == isEnabled) { break; } mTimeoutEnabled = isEnabled; if (!mTimeoutEnabled) { cancelTimeoutMessage(); } if (mTimeoutEnabled && mNumAssociatedStations == 0) { scheduleTimeoutMessage(); } break; case CMD_INTERFACE_STATUS_CHANGED: boolean isUp = message.arg1 == 1; onUpChanged(isUp); break; case CMD_START: // Already started, ignore this command. break; case CMD_NO_ASSOCIATED_STATIONS_TIMEOUT: if (!mTimeoutEnabled) { Log.wtf(TAG, "Timeout message received while timeout is disabled." + " Dropping."); break; } if (mNumAssociatedStations != 0) { Log.wtf(TAG, "Timeout message received but has clients. Dropping."); break; } Log.i(TAG, "Timeout message received. Stopping soft AP."); updateApState(WifiManager.WIFI_AP_STATE_DISABLING, WifiManager.WIFI_AP_STATE_ENABLED, 0); transitionTo(mIdleState); break; case CMD_INTERFACE_DESTROYED: Log.d(TAG, "Interface was cleanly destroyed."); updateApState(WifiManager.WIFI_AP_STATE_DISABLING, WifiManager.WIFI_AP_STATE_ENABLED, 0); mIfaceIsDestroyed = true; transitionTo(mIdleState); break; case CMD_FAILURE: Log.w(TAG, "hostapd failure, stop and report failure"); /* fall through */ case CMD_INTERFACE_DOWN: Log.w(TAG, "interface error, stop and report failure"); updateApState(WifiManager.WIFI_AP_STATE_FAILED, WifiManager.WIFI_AP_STATE_ENABLED, WifiManager.SAP_START_FAILURE_GENERAL); updateApState(WifiManager.WIFI_AP_STATE_DISABLING, WifiManager.WIFI_AP_STATE_FAILED, 0); transitionTo(mIdleState); break; default: return NOT_HANDLED; } return HANDLED; } } } }