blob: 0f0299fd20da3f76a5ec26e19ce4466819bc7364 [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 static android.net.wifi.WifiManager.SAP_CLIENT_DISCONNECT_REASON_CODE_UNSPECIFIED;
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.ERROR_UNSUPPORTED_CONFIGURATION;
import static com.android.server.wifi.util.ApConfigUtil.SUCCESS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Intent;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.SoftApCapability;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.SoftApInfo;
import android.net.wifi.WifiAnnotations;
import android.net.wifi.WifiClient;
import android.net.wifi.WifiInfo;
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.os.WorkSource;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
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.internal.util.WakeupMessage;
import com.android.modules.utils.build.SdkLevel;
import com.android.server.wifi.WifiNative.InterfaceCallback;
import com.android.server.wifi.WifiNative.SoftApListener;
import com.android.server.wifi.coex.CoexManager;
import com.android.server.wifi.coex.CoexManager.CoexListener;
import com.android.server.wifi.util.ApConfigUtil;
import com.android.wifi.resources.R;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* 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";
@VisibleForTesting
public static final String SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG = TAG
+ " Soft AP Send Message Timeout";
@VisibleForTesting
public static final String SOFT_AP_SEND_MESSAGE_IDLE_IN_BRIDGED_MODE_TIMEOUT_TAG = TAG
+ " Soft AP Send Message Bridged Mode Idle Timeout";
private final WifiContext mContext;
private final FrameworkFacade mFrameworkFacade;
private final WifiNative mWifiNative;
// This will only be null if SdkLevel is not at least S
@Nullable private final CoexManager mCoexManager;
private final ClientModeImplMonitor mCmiMonitor;
private final ActiveModeWarden mActiveModeWarden;
private final SoftApNotifier mSoftApNotifier;
@VisibleForTesting
static final long SOFT_AP_PENDING_DISCONNECTION_CHECK_DELAY_MS = 1000;
private String mCountryCode;
private final SoftApStateMachine mStateMachine;
private final Listener<SoftApManager> mModeListener;
private final WifiServiceImpl.SoftApCallbackInternal mSoftApCallback;
private String mApInterfaceName;
private boolean mIfaceIsUp;
private boolean mIfaceIsDestroyed;
private final WifiApConfigStore mWifiApConfigStore;
private final WifiMetrics mWifiMetrics;
private final long mId;
private boolean mIsUnsetBssid;
private boolean mVerboseLoggingEnabled = false;
/**
* Original configuration, which is the passed configuration when init or
* the user-configured {@code WifiApConfigStore#getApConfiguration}tethering}
* settings when input is null.
*
* Use it when doing configuration update to know if the input configuration was changed.
* For others use case, it should use {@code mCurrentSoftApConfiguration}.
*/
@NonNull
private final SoftApModeConfiguration mOriginalModeConfiguration;
/**
* Current Soft AP configuration which is used to start Soft AP.
* The configuration may be changed because
* 1. bssid is changed because MAC randomization
* 2. bands are changed because fallback to single AP mode mechanism.
*/
@Nullable
private SoftApConfiguration mCurrentSoftApConfiguration;
@NonNull
private Map<String, SoftApInfo> mCurrentSoftApInfoMap = new HashMap<>();
@NonNull
private SoftApCapability mCurrentSoftApCapability;
private Map<String, List<WifiClient>> mConnectedClientWithApInfoMap = new HashMap<>();
@VisibleForTesting
Map<WifiClient, Integer> mPendingDisconnectClients = new HashMap<>();
private boolean mTimeoutEnabled = false;
private boolean mBridgedModeOpportunisticsShutdownTimeoutEnabled = false;
private final SarManager mSarManager;
private String mStartTimestamp;
private long mDefaultShutdownTimeoutMillis;
private long mDefaultShutdownIdleInstanceInBridgedModeTimeoutMillis;
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
private WifiDiagnostics mWifiDiagnostics;
@Nullable
private SoftApRole mRole = null;
@Nullable
private WorkSource mRequestorWs = null;
private boolean mEverReportMetricsForMaxClient = false;
@NonNull
private Set<MacAddress> mBlockedClientList = new HashSet<>();
@NonNull
private Set<MacAddress> mAllowedClientList = new HashSet<>();
@NonNull
private Set<Integer> mSafeChannelFrequencyList = new HashSet<>();
@VisibleForTesting
public WakeupMessage mSoftApTimeoutMessage;
@VisibleForTesting
public WakeupMessage mSoftApBridgedModeIdleInstanceTimeoutMessage;
// Internal flag which is used to avoid the timer re-schedule.
private boolean mIsBridgedModeIdleInstanceTimerActive = false;
/**
* Listener for soft AP events.
*/
private final SoftApListener mSoftApListener = new SoftApListener() {
@Override
public void onFailure() {
mStateMachine.sendMessage(SoftApStateMachine.CMD_FAILURE);
}
@Override
public void onInfoChanged(String apIfaceInstance, int frequency,
@WifiAnnotations.Bandwidth int bandwidth,
@WifiAnnotations.WifiStandard int generation,
MacAddress apIfaceInstanceMacAddress) {
SoftApInfo apInfo = new SoftApInfo();
apInfo.setFrequency(frequency);
apInfo.setBandwidth(bandwidth);
apInfo.setWifiStandard(generation);
if (apIfaceInstanceMacAddress != null) {
apInfo.setBssid(apIfaceInstanceMacAddress);
}
apInfo.setApInstanceIdentifier(apIfaceInstance != null
? apIfaceInstance : mApInterfaceName);
mStateMachine.sendMessage(
SoftApStateMachine.CMD_AP_INFO_CHANGED, 0, 0, apInfo);
}
@Override
public void onConnectedClientsChanged(String apIfaceInstance, MacAddress clientAddress,
boolean isConnected) {
if (clientAddress != null) {
WifiClient client = new WifiClient(clientAddress, apIfaceInstance != null
? apIfaceInstance : mApInterfaceName);
mStateMachine.sendMessage(SoftApStateMachine.CMD_ASSOCIATED_STATIONS_CHANGED,
isConnected ? 1 : 0, 0, client);
} else {
Log.e(getTag(), "onConnectedClientsChanged: Invalid type returned");
}
}
};
// This will only be null if SdkLevel is not at least S
@Nullable private final CoexListener mCoexListener;
private void updateSafeChannelFrequencyList() {
if (!SdkLevel.isAtLeastS() || mCurrentSoftApConfiguration == null) {
return;
}
mSafeChannelFrequencyList.clear();
for (int configuredBand : mCurrentSoftApConfiguration.getBands()) {
for (int band : SoftApConfiguration.BAND_TYPES) {
if ((band & configuredBand) == 0) {
continue;
}
for (int channel : mCurrentSoftApCapability.getSupportedChannelList(band)) {
mSafeChannelFrequencyList.add(
ApConfigUtil.convertChannelToFrequency(channel, band));
}
}
}
if ((mCoexManager.getCoexRestrictions() & WifiManager.COEX_RESTRICTION_SOFTAP) != 0) {
mSafeChannelFrequencyList.removeAll(
ApConfigUtil.getUnsafeChannelFreqsFromCoex(mCoexManager));
}
Log.d(getTag(), "SafeChannelFrequencyList = " + mSafeChannelFrequencyList);
}
private void configureInternalConfiguration() {
if (mCurrentSoftApConfiguration == null) {
return;
}
mBlockedClientList = new HashSet<>(mCurrentSoftApConfiguration.getBlockedClientList());
mAllowedClientList = new HashSet<>(mCurrentSoftApConfiguration.getAllowedClientList());
mTimeoutEnabled = mCurrentSoftApConfiguration.isAutoShutdownEnabled();
mBridgedModeOpportunisticsShutdownTimeoutEnabled =
mCurrentSoftApConfiguration.isBridgedModeOpportunisticShutdownEnabledInternal();
}
private void updateChangeableConfiguration(SoftApConfiguration newConfig) {
if (mCurrentSoftApConfiguration == null || newConfig == null) {
return;
}
/**
* update configurations only which mentioned in WifiManager#setSoftApConfiguration
*/
SoftApConfiguration.Builder newConfigurBuilder =
new SoftApConfiguration.Builder(mCurrentSoftApConfiguration)
.setAllowedClientList(newConfig.getAllowedClientList())
.setBlockedClientList(newConfig.getBlockedClientList())
.setClientControlByUserEnabled(newConfig.isClientControlByUserEnabled())
.setMaxNumberOfClients(newConfig.getMaxNumberOfClients())
.setShutdownTimeoutMillis(newConfig.getShutdownTimeoutMillis())
.setAutoShutdownEnabled(newConfig.isAutoShutdownEnabled());
if (SdkLevel.isAtLeastS()) {
newConfigurBuilder.setBridgedModeOpportunisticShutdownEnabled(
newConfig.isBridgedModeOpportunisticShutdownEnabledInternal());
}
mCurrentSoftApConfiguration = newConfigurBuilder.build();
configureInternalConfiguration();
}
public SoftApManager(
@NonNull WifiContext context,
@NonNull Looper looper,
@NonNull FrameworkFacade framework,
@NonNull WifiNative wifiNative,
@NonNull CoexManager coexManager,
String countryCode,
@NonNull Listener<SoftApManager> listener,
@NonNull WifiServiceImpl.SoftApCallbackInternal callback,
@NonNull WifiApConfigStore wifiApConfigStore,
@NonNull SoftApModeConfiguration apConfig,
@NonNull WifiMetrics wifiMetrics,
@NonNull SarManager sarManager,
@NonNull WifiDiagnostics wifiDiagnostics,
@NonNull SoftApNotifier softApNotifier,
@NonNull ClientModeImplMonitor cmiMonitor,
@NonNull ActiveModeWarden activeModeWarden,
long id,
@NonNull WorkSource requestorWs,
@NonNull SoftApRole role,
boolean verboseLoggingEnabled) {
mContext = context;
mFrameworkFacade = framework;
mSoftApNotifier = softApNotifier;
mWifiNative = wifiNative;
mCoexManager = coexManager;
if (SdkLevel.isAtLeastS()) {
mCoexListener = new CoexListener() {
@Override
public void onCoexUnsafeChannelsChanged() {
if (mCurrentSoftApConfiguration == null) {
return;
}
mStateMachine.sendMessage(
SoftApStateMachine.CMD_SAFE_CHANNEL_FREQUENCY_CHANGED);
}
};
} else {
mCoexListener = null;
}
mCountryCode = countryCode;
mModeListener = listener;
mSoftApCallback = callback;
mWifiApConfigStore = wifiApConfigStore;
mCurrentSoftApConfiguration = apConfig.getSoftApConfiguration();
mCurrentSoftApCapability = apConfig.getCapability();
// null is a valid input and means we use the user-configured tethering settings.
if (mCurrentSoftApConfiguration == null) {
mCurrentSoftApConfiguration = mWifiApConfigStore.getApConfiguration();
// may still be null if we fail to load the default config
}
// Store mode configuration before update the configuration.
mOriginalModeConfiguration = new SoftApModeConfiguration(apConfig.getTargetMode(),
mCurrentSoftApConfiguration, mCurrentSoftApCapability);
if (mCurrentSoftApConfiguration != null) {
mIsUnsetBssid = mCurrentSoftApConfiguration.getBssid() == null;
if (mCurrentSoftApCapability.areFeaturesSupported(
SoftApCapability.SOFTAP_FEATURE_MAC_ADDRESS_CUSTOMIZATION)) {
mCurrentSoftApConfiguration = mWifiApConfigStore.randomizeBssidIfUnset(
mContext, mCurrentSoftApConfiguration);
}
}
mWifiMetrics = wifiMetrics;
mSarManager = sarManager;
mWifiDiagnostics = wifiDiagnostics;
mStateMachine = new SoftApStateMachine(looper);
configureInternalConfiguration();
mDefaultShutdownTimeoutMillis = mContext.getResources().getInteger(
R.integer.config_wifiFrameworkSoftApShutDownTimeoutMilliseconds);
mDefaultShutdownIdleInstanceInBridgedModeTimeoutMillis = mContext.getResources().getInteger(
R.integer
.config_wifiFrameworkSoftApShutDownIdleInstanceInBridgedModeTimeoutMillisecond);
mCmiMonitor = cmiMonitor;
mActiveModeWarden = activeModeWarden;
mCmiMonitor.registerListener(new ClientModeImplListener() {
@Override
public void onL2Connected(@NonNull ConcreteClientModeManager clientModeManager) {
SoftApManager.this.onL2Connected(clientModeManager);
}
});
updateSafeChannelFrequencyList();
mId = id;
mRole = role;
enableVerboseLogging(verboseLoggingEnabled);
mStateMachine.sendMessage(SoftApStateMachine.CMD_START, requestorWs);
}
@Override
public long getId() {
return mId;
}
private String getTag() {
return TAG + "[" + (mApInterfaceName == null ? "unknown" : mApInterfaceName) + "]";
}
/**
* Stop soft AP.
*/
@Override
public void stop() {
Log.d(getTag(), " currentstate: " + getCurrentStateName());
mStateMachine.sendMessage(SoftApStateMachine.CMD_STOP);
}
private boolean isBridgedMode() {
return (SdkLevel.isAtLeastS() && mCurrentSoftApConfiguration != null
&& mCurrentSoftApConfiguration.getBands().length > 1);
}
private long getShutdownTimeoutMillis() {
long timeout = mCurrentSoftApConfiguration.getShutdownTimeoutMillis();
return timeout > 0 ? timeout : mDefaultShutdownTimeoutMillis;
}
@Override
@Nullable public SoftApRole getRole() {
return mRole;
}
@Override
@Nullable public ClientRole getPreviousRole() {
return null;
}
@Override
public long getLastRoleChangeSinceBootMs() {
return 0;
}
/** Set the role of this SoftApManager */
public void setRole(SoftApRole role) {
// softap does not allow in-place switching of roles.
Preconditions.checkState(mRole == null);
mRole = role;
}
@Override
public String getInterfaceName() {
return mApInterfaceName;
}
@Override
public WorkSource getRequestorWs() {
return mRequestorWs;
}
/**
* Update AP capability. Called when carrier config or device resouce config changed.
*
* @param capability new AP capability.
*/
public void updateCapability(@NonNull SoftApCapability capability) {
mStateMachine.sendMessage(SoftApStateMachine.CMD_UPDATE_CAPABILITY, capability);
}
/**
* Update AP configuration. Called when setting update config via
* {@link WifiManager#setSoftApConfiguration(SoftApConfiguration)}
*
* @param config new AP config.
*/
public void updateConfiguration(@NonNull SoftApConfiguration config) {
mStateMachine.sendMessage(SoftApStateMachine.CMD_UPDATE_CONFIG, config);
}
/**
* Retrieve the {@link SoftApModeConfiguration} instance associated with this mode manager.
*/
public SoftApModeConfiguration getSoftApModeConfiguration() {
return new SoftApModeConfiguration(mOriginalModeConfiguration.getTargetMode(),
mCurrentSoftApConfiguration, mCurrentSoftApCapability);
}
/**
* Dump info about this softap manager.
*/
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Dump of SoftApManager id=" + mId);
pw.println("current StateMachine mode: " + getCurrentStateName());
pw.println("mRole: " + mRole);
pw.println("mApInterfaceName: " + mApInterfaceName);
pw.println("mIfaceIsUp: " + mIfaceIsUp);
pw.println("mSoftApCountryCode: " + mCountryCode);
pw.println("mOriginalModeConfiguration.targetMode: "
+ mOriginalModeConfiguration.getTargetMode());
pw.println("mCurrentSoftApConfiguration.SSID: " + mCurrentSoftApConfiguration.getSsid());
pw.println("mCurrentSoftApConfiguration.mBand: " + mCurrentSoftApConfiguration.getBand());
pw.println("mCurrentSoftApConfiguration.hiddenSSID: "
+ mCurrentSoftApConfiguration.isHiddenSsid());
pw.println("getConnectedClientList().size(): " + getConnectedClientList().size());
pw.println("mTimeoutEnabled: " + mTimeoutEnabled);
pw.println("mBridgedModeOpportunisticsShutdownTimeoutEnabled: "
+ mBridgedModeOpportunisticsShutdownTimeoutEnabled);
pw.println("mCurrentSoftApInfoMap " + mCurrentSoftApInfoMap);
pw.println("mStartTimestamp: " + mStartTimestamp);
mStateMachine.dump(fd, pw, args);
}
@Override
public void enableVerboseLogging(boolean verbose) {
mVerboseLoggingEnabled = verbose;
}
@Override
public String toString() {
return "SoftApManager{id=" + getId()
+ " iface=" + getInterfaceName()
+ " role=" + getRole()
+ "}";
}
/**
* A ClientModeImpl instance has been L2 connected.
*
* @param newPrimary the corresponding ConcreteClientModeManager instance for the ClientModeImpl
* that has been L2 connected.
*/
private void onL2Connected(@NonNull ConcreteClientModeManager clientModeManager) {
Log.d(getTag(), "onL2Connected called");
mStateMachine.sendMessage(SoftApStateMachine.CMD_HANDLE_WIFI_CONNECTED,
clientModeManager.syncRequestConnectionInfo());
}
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) {
mSoftApCallback.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, mOriginalModeConfiguration.getTargetMode());
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
private int setMacAddress() {
MacAddress mac = mCurrentSoftApConfiguration.getBssid();
if (mac == null) {
// If no BSSID is explicitly requested, (re-)configure the factory MAC address. Some
// drivers may not support setting the MAC at all, so fail soft in this case.
if (!mWifiNative.resetApMacToFactoryMacAddress(mApInterfaceName)) {
Log.w(getTag(), "failed to reset to factory MAC address; "
+ "continuing with current MAC");
}
} else {
if (mWifiNative.isApSetMacAddressSupported(mApInterfaceName)) {
if (!mWifiNative.setApMacAddress(mApInterfaceName, mac)) {
Log.e(getTag(), "failed to set explicitly requested MAC address");
return ERROR_GENERIC;
}
} else if (!mIsUnsetBssid) {
// If hardware does not support MAC address setter,
// only report the error for non randomization.
return ERROR_UNSUPPORTED_CONFIGURATION;
}
}
return SUCCESS;
}
/**
* Dynamic update the country code when Soft AP enabled.
*
* @param countryCode 2 byte ASCII string. For ex: US, CA.
* @return true if request is sent successfully, false otherwise.
*/
public boolean updateCountryCode(@NonNull String countryCode) {
if (ApConfigUtil.isSoftApDynamicCountryCodeSupported(mContext)
&& mCurrentSoftApCapability.areFeaturesSupported(
SoftApCapability.SOFTAP_FEATURE_ACS_OFFLOAD)) {
mStateMachine.sendMessage(SoftApStateMachine.CMD_UPDATE_COUNTRY_CODE, countryCode);
return true;
}
return false;
}
private int setCountryCode() {
int band = mCurrentSoftApConfiguration.getBand();
if (TextUtils.isEmpty(mCountryCode)) {
if (band == SoftApConfiguration.BAND_5GHZ) {
// Country code is mandatory for 5GHz band.
Log.e(getTag(), "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.
return SUCCESS;
}
if (!mWifiNative.setApCountryCode(
mApInterfaceName, mCountryCode.toUpperCase(Locale.ROOT))) {
if (band == SoftApConfiguration.BAND_5GHZ) {
// Return an error if failed to set country code when AP is configured for
// 5GHz band.
Log.e(getTag(), "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 other band options.
}
return SUCCESS;
}
/**
* Start a soft AP instance as configured.
*
* @return integer result code
*/
private int startSoftAp() {
Log.d(getTag(), "band " + mCurrentSoftApConfiguration.getBand() + " iface "
+ mApInterfaceName + " country " + mCountryCode);
int result = setMacAddress();
if (result != SUCCESS) {
return result;
}
result = setCountryCode();
if (result != SUCCESS) {
return result;
}
// Make a copy of configuration for updating AP band and channel.
SoftApConfiguration.Builder localConfigBuilder =
new SoftApConfiguration.Builder(mCurrentSoftApConfiguration);
boolean acsEnabled = mCurrentSoftApCapability.areFeaturesSupported(
SoftApCapability.SOFTAP_FEATURE_ACS_OFFLOAD);
result = ApConfigUtil.updateApChannelConfig(
mWifiNative, mCoexManager, mContext.getResources(), mCountryCode,
localConfigBuilder, mCurrentSoftApConfiguration, acsEnabled);
if (result != SUCCESS) {
Log.e(getTag(), "Failed to update AP band and channel");
return result;
}
if (mCurrentSoftApConfiguration.isHiddenSsid()) {
Log.d(getTag(), "SoftAP is a hidden network");
}
if (!ApConfigUtil.checkSupportAllConfiguration(
mCurrentSoftApConfiguration, mCurrentSoftApCapability)) {
Log.d(getTag(), "Unsupported Configuration detect! config = "
+ mCurrentSoftApConfiguration);
return ERROR_UNSUPPORTED_CONFIGURATION;
}
if (!mWifiNative.startSoftAp(mApInterfaceName,
localConfigBuilder.build(),
mOriginalModeConfiguration.getTargetMode() == WifiManager.IFACE_IP_MODE_TETHERED,
mSoftApListener)) {
Log.e(getTag(), "Soft AP start failed");
return ERROR_GENERIC;
}
mWifiDiagnostics.startLogging(mApInterfaceName);
mStartTimestamp = FORMATTER.format(new Date(System.currentTimeMillis()));
Log.d(getTag(), "Soft AP is started ");
return SUCCESS;
}
/**
* Disconnect all connected clients on active softap interface(s).
* This is usually done just before stopSoftAp().
*/
private void disconnectAllClients() {
for (WifiClient client : getConnectedClientList()) {
mWifiNative.forceClientDisconnect(mApInterfaceName, client.getMacAddress(),
SAP_CLIENT_DISCONNECT_REASON_CODE_UNSPECIFIED);
}
}
/**
* Teardown soft AP and teardown the interface.
*/
private void stopSoftAp() {
disconnectAllClients();
mWifiDiagnostics.stopLogging(mApInterfaceName);
mWifiNative.teardownInterface(mApInterfaceName);
Log.d(getTag(), "Soft AP is stopped");
}
private void addClientToPendingDisconnectionList(WifiClient client, int reason) {
Log.d(getTag(), "Fail to disconnect client: " + client.getMacAddress()
+ ", add it into pending list");
mPendingDisconnectClients.put(client, reason);
mStateMachine.getHandler().removeMessages(
SoftApStateMachine.CMD_FORCE_DISCONNECT_PENDING_CLIENTS);
mStateMachine.sendMessageDelayed(
SoftApStateMachine.CMD_FORCE_DISCONNECT_PENDING_CLIENTS,
SOFT_AP_PENDING_DISCONNECTION_CHECK_DELAY_MS);
}
private List<WifiClient> getConnectedClientList() {
List<WifiClient> connectedClientList = new ArrayList<>();
for (List<WifiClient> it : mConnectedClientWithApInfoMap.values()) {
connectedClientList.addAll(it);
}
return connectedClientList;
}
private boolean checkSoftApClient(SoftApConfiguration config, WifiClient newClient) {
if (!mCurrentSoftApCapability.areFeaturesSupported(
SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
return true;
}
if (mBlockedClientList.contains(newClient.getMacAddress())) {
Log.d(getTag(), "Force disconnect for client: " + newClient + "in blocked list");
if (!mWifiNative.forceClientDisconnect(
mApInterfaceName, newClient.getMacAddress(),
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER)) {
addClientToPendingDisconnectionList(newClient,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
}
return false;
}
if (config.isClientControlByUserEnabled()
&& !mAllowedClientList.contains(newClient.getMacAddress())) {
mSoftApCallback.onBlockedClientConnecting(newClient,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
Log.d(getTag(), "Force disconnect for unauthorized client: " + newClient);
if (!mWifiNative.forceClientDisconnect(
mApInterfaceName, newClient.getMacAddress(),
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER)) {
addClientToPendingDisconnectionList(newClient,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
}
return false;
}
int maxConfig = mCurrentSoftApCapability.getMaxSupportedClients();
if (config.getMaxNumberOfClients() > 0) {
maxConfig = Math.min(maxConfig, config.getMaxNumberOfClients());
}
if (getConnectedClientList().size() >= maxConfig) {
Log.i(getTag(), "No more room for new client:" + newClient);
if (!mWifiNative.forceClientDisconnect(
mApInterfaceName, newClient.getMacAddress(),
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS)) {
addClientToPendingDisconnectionList(newClient,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
}
mSoftApCallback.onBlockedClientConnecting(newClient,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
// Avoid report the max client blocked in the same settings.
if (!mEverReportMetricsForMaxClient) {
mWifiMetrics.noteSoftApClientBlocked(maxConfig);
mEverReportMetricsForMaxClient = true;
}
return false;
}
return true;
}
private class SoftApStateMachine extends StateMachine {
// Commands for the state machine.
public static final int CMD_START = 0;
public static final int CMD_STOP = 1;
public static final int CMD_FAILURE = 2;
public static final int CMD_INTERFACE_STATUS_CHANGED = 3;
public static final int CMD_ASSOCIATED_STATIONS_CHANGED = 4;
public static final int CMD_NO_ASSOCIATED_STATIONS_TIMEOUT = 5;
public static final int CMD_INTERFACE_DESTROYED = 7;
public static final int CMD_INTERFACE_DOWN = 8;
public static final int CMD_AP_INFO_CHANGED = 9;
public static final int CMD_UPDATE_CAPABILITY = 10;
public static final int CMD_UPDATE_CONFIG = 11;
public static final int CMD_FORCE_DISCONNECT_PENDING_CLIENTS = 12;
public static final int CMD_NO_ASSOCIATED_STATIONS_TIMEOUT_ON_ONE_INSTANCE = 13;
public static final int CMD_SAFE_CHANNEL_FREQUENCY_CHANGED = 14;
public static final int CMD_HANDLE_WIFI_CONNECTED = 15;
public static final int CMD_UPDATE_COUNTRY_CODE = 16;
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);
// CHECKSTYLE:OFF IndentationCheck
addState(mIdleState);
addState(mStartedState, mIdleState);
// CHECKSTYLE:ON IndentationCheck
setInitialState(mIdleState);
start();
}
private class IdleState extends State {
@Override
public void enter() {
mApInterfaceName = null;
mIfaceIsUp = false;
mIfaceIsDestroyed = false;
}
@Override
public void exit() {
mModeListener.onStopped(SoftApManager.this);
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_STOP:
mStateMachine.quitNow();
break;
case CMD_START:
mRequestorWs = (WorkSource) message.obj;
if (mCurrentSoftApConfiguration == null
|| mCurrentSoftApConfiguration.getSsid() == null) {
Log.e(getTag(), "Unable to start soft AP without valid configuration");
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_DISABLED,
WifiManager.SAP_START_FAILURE_GENERAL);
mWifiMetrics.incrementSoftApStartResult(
false, WifiManager.SAP_START_FAILURE_GENERAL);
mModeListener.onStartFailure(SoftApManager.this);
break;
}
if (isBridgedMode()) {
boolean isFallbackToSingleAp = false;
int newSingleApBand = 0;
for (ClientModeManager cmm
: mActiveModeWarden.getClientModeManagers()) {
WifiInfo wifiConnectedInfo = cmm.syncRequestConnectionInfo();
int wifiFrequency = wifiConnectedInfo.getFrequency();
if (wifiFrequency > 0
&& !mSafeChannelFrequencyList.contains(
wifiFrequency)) {
Log.d(getTag(), "Wifi connected to unavailable freq: "
+ wifiFrequency);
isFallbackToSingleAp = true;
break;
}
}
for (int configuredBand : mCurrentSoftApConfiguration.getBands()) {
int availableBand = ApConfigUtil.removeUnavailableBands(
mCurrentSoftApCapability,
configuredBand, mCoexManager);
if (configuredBand != availableBand) {
isFallbackToSingleAp = true;
}
newSingleApBand |= availableBand;
}
if (isFallbackToSingleAp) {
newSingleApBand = ApConfigUtil.append24GToBandIf24GSupported(
newSingleApBand, mContext);
Log.i(getTag(), "Fallback to single AP mode with band "
+ newSingleApBand);
mCurrentSoftApConfiguration =
new SoftApConfiguration.Builder(mCurrentSoftApConfiguration)
.setBand(newSingleApBand)
.build();
}
}
mApInterfaceName = mWifiNative.setupInterfaceForSoftApMode(
mWifiNativeInterfaceCallback, mRequestorWs,
mCurrentSoftApConfiguration.getBand(), isBridgedMode());
if (TextUtils.isEmpty(mApInterfaceName)) {
Log.e(getTag(), "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);
mModeListener.onStartFailure(SoftApManager.this);
break;
}
mSoftApNotifier.dismissSoftApShutdownTimeoutExpiredNotification();
updateApState(WifiManager.WIFI_AP_STATE_ENABLING,
WifiManager.WIFI_AP_STATE_DISABLED, 0);
int result = startSoftAp();
if (result != SUCCESS) {
int failureReason = WifiManager.SAP_START_FAILURE_GENERAL;
if (result == ERROR_NO_CHANNEL) {
failureReason = WifiManager.SAP_START_FAILURE_NO_CHANNEL;
} else if (result == ERROR_UNSUPPORTED_CONFIGURATION) {
failureReason = WifiManager
.SAP_START_FAILURE_UNSUPPORTED_CONFIGURATION;
}
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_ENABLING,
failureReason);
stopSoftAp();
mWifiMetrics.incrementSoftApStartResult(false, failureReason);
mModeListener.onStartFailure(SoftApManager.this);
break;
}
transitionTo(mStartedState);
break;
case CMD_UPDATE_CAPABILITY:
// Capability should only changed by carrier requirement. Only apply to
// Tether Mode
if (mOriginalModeConfiguration.getTargetMode()
== WifiManager.IFACE_IP_MODE_TETHERED) {
SoftApCapability capability = (SoftApCapability) message.obj;
mCurrentSoftApCapability = new SoftApCapability(capability);
}
break;
case CMD_UPDATE_CONFIG:
SoftApConfiguration newConfig = (SoftApConfiguration) message.obj;
Log.d(getTag(), "Configuration changed to " + newConfig);
// Idle mode, update all configurations.
mCurrentSoftApConfiguration = newConfig;
configureInternalConfiguration();
break;
case CMD_UPDATE_COUNTRY_CODE:
String countryCode = (String) message.obj;
if (!TextUtils.isEmpty(countryCode)) {
mCountryCode = countryCode;
}
break;
default:
// Ignore all other commands.
break;
}
return HANDLED;
}
}
private class StartedState extends State {
private void scheduleTimeoutMessages() {
// When SAP started, the mCurrentSoftApInfoMap is 0 because info does not update.
// Don't trigger bridged mode shutdown timeout when only one active instance
// In Dual AP, one instance may already be closed due to LTE coexistence or DFS
// restrictions or due to inactivity. i.e. mCurrentSoftApInfoMap.size() is 1)
final int connectedClients = getConnectedClientList().size();
if (isBridgedMode() && mCurrentSoftApInfoMap.size() != 1) {
if (mBridgedModeOpportunisticsShutdownTimeoutEnabled
&& (connectedClients == 0 || getIdleInstances().size() != 0)) {
if (!mIsBridgedModeIdleInstanceTimerActive) {
mSoftApBridgedModeIdleInstanceTimeoutMessage.schedule(SystemClock
.elapsedRealtime()
+ mDefaultShutdownIdleInstanceInBridgedModeTimeoutMillis);
mIsBridgedModeIdleInstanceTimerActive = true;
Log.d(getTag(), "Bridged mode instance opportunistic timeout message"
+ " scheduled, delay = "
+ mDefaultShutdownIdleInstanceInBridgedModeTimeoutMillis);
}
} else {
cancelBridgedModeIdleInstanceTimeoutMessage();
}
}
if (!mTimeoutEnabled || connectedClients != 0) {
cancelTimeoutMessage();
return;
}
long timeout = getShutdownTimeoutMillis();
mSoftApTimeoutMessage.schedule(SystemClock.elapsedRealtime()
+ timeout);
Log.d(getTag(), "Timeout message scheduled, delay = "
+ timeout);
}
private String getHighestFrequencyInstance(Set<String> candidateInstances) {
int currentHighestFrequencyOnAP = 0;
String highestFrequencyInstance = null;
for (String instance : candidateInstances) {
SoftApInfo info = mCurrentSoftApInfoMap.get(instance);
if (info == null) {
Log.wtf(getTag(), "Invalid instance name, no way to get the frequency");
return "";
}
int frequencyOnInstance = info.getFrequency();
if (frequencyOnInstance > currentHighestFrequencyOnAP) {
currentHighestFrequencyOnAP = frequencyOnInstance;
highestFrequencyInstance = instance;
}
}
return highestFrequencyInstance;
}
private void removeIfaceInstanceFromBridgedApIface(String instanceName) {
if (TextUtils.isEmpty(instanceName)) {
return;
}
if (mCurrentSoftApInfoMap.containsKey(instanceName)) {
Log.i(getTag(), "remove instance " + instanceName + "("
+ mCurrentSoftApInfoMap.get(instanceName).getFrequency()
+ ") from bridged iface " + mApInterfaceName);
mWifiNative.removeIfaceInstanceFromBridgedApIface(mApInterfaceName,
instanceName);
// Remove the info and update it.
updateSoftApInfo(mCurrentSoftApInfoMap.get(instanceName), true);
}
}
private Set<String> getIdleInstances() {
Set<String> idleInstances = new HashSet<String>();
for (String instance : mConnectedClientWithApInfoMap.keySet()) {
if (mConnectedClientWithApInfoMap.getOrDefault(
instance, Collections.emptyList()).size() == 0) {
idleInstances.add(instance);
}
}
return idleInstances;
}
private void cancelTimeoutMessage() {
mSoftApTimeoutMessage.cancel();
Log.d(getTag(), "Timeout message canceled");
}
private void cancelBridgedModeIdleInstanceTimeoutMessage() {
mSoftApBridgedModeIdleInstanceTimeoutMessage.cancel();
mIsBridgedModeIdleInstanceTimerActive = false;
Log.d(getTag(), "Bridged mode idle instance timeout message canceled");
}
/**
* When configuration changed, it need to force some clients disconnect to match the
* configuration.
*/
private void updateClientConnection() {
if (!mCurrentSoftApCapability.areFeaturesSupported(
SoftApCapability.SOFTAP_FEATURE_CLIENT_FORCE_DISCONNECT)) {
return;
}
final int maxAllowedClientsByHardwareAndCarrier =
mCurrentSoftApCapability.getMaxSupportedClients();
final int userApConfigMaxClientCount =
mCurrentSoftApConfiguration.getMaxNumberOfClients();
int finalMaxClientCount = maxAllowedClientsByHardwareAndCarrier;
if (userApConfigMaxClientCount > 0) {
finalMaxClientCount = Math.min(userApConfigMaxClientCount,
maxAllowedClientsByHardwareAndCarrier);
}
List<WifiClient> currentClients = getConnectedClientList();
int targetDisconnectClientNumber = currentClients.size() - finalMaxClientCount;
List<WifiClient> allowedConnectedList = new ArrayList<>();
Iterator<WifiClient> iterator = currentClients.iterator();
while (iterator.hasNext()) {
WifiClient client = iterator.next();
if (mBlockedClientList.contains(client.getMacAddress())
|| (mCurrentSoftApConfiguration.isClientControlByUserEnabled()
&& !mAllowedClientList.contains(client.getMacAddress()))) {
Log.d(getTag(), "Force disconnect for not allowed client: " + client);
if (!mWifiNative.forceClientDisconnect(
mApInterfaceName, client.getMacAddress(),
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER)) {
addClientToPendingDisconnectionList(client,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_BLOCKED_BY_USER);
}
targetDisconnectClientNumber--;
} else {
allowedConnectedList.add(client);
}
}
if (targetDisconnectClientNumber > 0) {
Iterator<WifiClient> allowedClientIterator = allowedConnectedList.iterator();
while (allowedClientIterator.hasNext()) {
if (targetDisconnectClientNumber == 0) break;
WifiClient allowedClient = allowedClientIterator.next();
Log.d(getTag(), "Force disconnect for client due to no more room: "
+ allowedClient);
if (!mWifiNative.forceClientDisconnect(
mApInterfaceName, allowedClient.getMacAddress(),
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS)) {
addClientToPendingDisconnectionList(allowedClient,
WifiManager.SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS);
}
targetDisconnectClientNumber--;
}
}
}
/**
* Set stations associated with this soft AP
* @param client The station for which connection state changed.
* @param isConnected True for the connection changed to connect, otherwise false.
*/
private void updateConnectedClients(WifiClient client, boolean isConnected) {
if (client == null) {
return;
}
if (null != mPendingDisconnectClients.remove(client)) {
Log.d(getTag(), "Remove client: " + client.getMacAddress()
+ "from pending disconnectionlist");
}
String apInstanceIdentifier = client.getApInstanceIdentifier();
List clientList = mConnectedClientWithApInfoMap.computeIfAbsent(
apInstanceIdentifier, k -> new ArrayList<>());
int index = clientList.indexOf(client);
if ((index != -1) == isConnected) {
Log.e(getTag(), "Drop client connection event, client "
+ client + "isConnected: " + isConnected
+ " , duplicate event or client is blocked");
return;
}
if (isConnected) {
boolean isAllow = checkSoftApClient(mCurrentSoftApConfiguration, client);
if (isAllow) {
clientList.add(client);
} else {
return;
}
} else {
if (null == clientList.remove(index)) {
Log.e(getTag(), "client doesn't exist in list, it should NOT happen");
}
}
// Update clients list.
mConnectedClientWithApInfoMap.put(apInstanceIdentifier, clientList);
SoftApInfo currentInfoWithClientsChanged = mCurrentSoftApInfoMap
.get(apInstanceIdentifier);
Log.d(getTag(), "The connected wifi stations have changed with count: "
+ clientList.size() + ": " + clientList + " on the AP which info is "
+ currentInfoWithClientsChanged);
if (mSoftApCallback != null) {
mSoftApCallback.onConnectedClientsOrInfoChanged(mCurrentSoftApInfoMap,
mConnectedClientWithApInfoMap, isBridgedMode());
} else {
Log.e(getTag(),
"SoftApCallback is null. Dropping ConnectedClientsChanged event.");
}
mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(
getConnectedClientList().size(),
mConnectedClientWithApInfoMap.get(apInstanceIdentifier).size(),
mOriginalModeConfiguration.getTargetMode(),
mCurrentSoftApInfoMap.get(apInstanceIdentifier));
scheduleTimeoutMessages();
}
/**
* @param apInfo, the new SoftApInfo changed. Null used to clean up.
*/
private void updateSoftApInfo(@Nullable SoftApInfo apInfo, boolean isRemoved) {
Log.d(getTag(), "SoftApInfo update " + apInfo + ", isRemoved: " + isRemoved);
if (apInfo == null) {
// Clean up
mCurrentSoftApInfoMap.clear();
mConnectedClientWithApInfoMap.clear();
mSoftApCallback.onConnectedClientsOrInfoChanged(mCurrentSoftApInfoMap,
mConnectedClientWithApInfoMap, isBridgedMode());
return;
}
String changedInstance = apInfo.getApInstanceIdentifier();
if (apInfo.equals(mCurrentSoftApInfoMap.get(changedInstance))) {
if (isRemoved) {
boolean isClientConnected =
mConnectedClientWithApInfoMap.get(changedInstance).size() > 0;
mCurrentSoftApInfoMap.remove(changedInstance);
mConnectedClientWithApInfoMap.remove(changedInstance);
mSoftApCallback.onConnectedClientsOrInfoChanged(mCurrentSoftApInfoMap,
mConnectedClientWithApInfoMap, isBridgedMode());
if (isClientConnected) {
mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(
getConnectedClientList().size(), 0,
mOriginalModeConfiguration.getTargetMode(), apInfo);
}
if (isBridgedMode()) {
mWifiMetrics.addSoftApInstanceDownEventInDualMode(
mOriginalModeConfiguration.getTargetMode(), apInfo);
}
}
return;
}
// Make sure an empty client list is created when info updated
List clientList = mConnectedClientWithApInfoMap.computeIfAbsent(
changedInstance, k -> new ArrayList<>());
if (clientList.size() != 0) {
Log.e(getTag(), "The info: " + apInfo
+ " changed when client connected, it should NOT happen!!");
}
// Update the info when getting two infos in bridged mode.
// TODO: b/173999527. It may only one instance come up when starting bridged AP.
// Consider the handling with co-ex mechanism in bridged mode.
boolean waitForAnotherSoftApInfoInBridgedMode =
isBridgedMode() && mCurrentSoftApInfoMap.size() == 0;
mCurrentSoftApInfoMap.put(changedInstance, new SoftApInfo(apInfo));
if (!waitForAnotherSoftApInfoInBridgedMode) {
mSoftApCallback.onConnectedClientsOrInfoChanged(mCurrentSoftApInfoMap,
mConnectedClientWithApInfoMap, isBridgedMode());
}
// ignore invalid freq and softap disable case for metrics
if (apInfo.getFrequency() > 0
&& apInfo.getBandwidth() != SoftApInfo.CHANNEL_WIDTH_INVALID) {
mWifiMetrics.addSoftApChannelSwitchedEvent(
new ArrayList<>(mCurrentSoftApInfoMap.values()),
mOriginalModeConfiguration.getTargetMode(), isBridgedMode());
updateUserBandPreferenceViolationMetricsIfNeeded(apInfo);
}
}
private void onUpChanged(boolean isUp) {
if (isUp == mIfaceIsUp) {
return; // no change
}
mIfaceIsUp = isUp;
if (isUp) {
Log.d(getTag(), "SoftAp is ready for use");
updateApState(WifiManager.WIFI_AP_STATE_ENABLED,
WifiManager.WIFI_AP_STATE_ENABLING, 0);
mModeListener.onStarted(SoftApManager.this);
mWifiMetrics.incrementSoftApStartResult(true, 0);
mCurrentSoftApInfoMap.clear();
mConnectedClientWithApInfoMap.clear();
if (mSoftApCallback != null) {
mSoftApCallback.onConnectedClientsOrInfoChanged(mCurrentSoftApInfoMap,
mConnectedClientWithApInfoMap, isBridgedMode());
}
} else {
// the interface was up, but goes down
sendMessage(CMD_INTERFACE_DOWN);
}
mWifiMetrics.addSoftApUpChangedEvent(isUp,
mOriginalModeConfiguration.getTargetMode(),
mDefaultShutdownTimeoutMillis, isBridgedMode());
if (isUp) {
mWifiMetrics.updateSoftApConfiguration(mCurrentSoftApConfiguration,
mOriginalModeConfiguration.getTargetMode(), isBridgedMode());
mWifiMetrics.updateSoftApCapability(mCurrentSoftApCapability,
mOriginalModeConfiguration.getTargetMode(), isBridgedMode());
}
}
@Override
public void enter() {
mIfaceIsUp = false;
mIfaceIsDestroyed = false;
onUpChanged(mWifiNative.isInterfaceUp(mApInterfaceName));
Handler handler = mStateMachine.getHandler();
mSoftApTimeoutMessage = new WakeupMessage(mContext, handler,
SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG,
SoftApStateMachine.CMD_NO_ASSOCIATED_STATIONS_TIMEOUT);
mSoftApBridgedModeIdleInstanceTimeoutMessage = new WakeupMessage(mContext, handler,
SOFT_AP_SEND_MESSAGE_IDLE_IN_BRIDGED_MODE_TIMEOUT_TAG,
SoftApStateMachine.CMD_NO_ASSOCIATED_STATIONS_TIMEOUT_ON_ONE_INSTANCE);
if (SdkLevel.isAtLeastS()) {
mCoexManager.registerCoexListener(mCoexListener);
}
mSarManager.setSapWifiState(WifiManager.WIFI_AP_STATE_ENABLED);
Log.d(getTag(), "Resetting connected clients on start");
mConnectedClientWithApInfoMap.clear();
mPendingDisconnectClients.clear();
mEverReportMetricsForMaxClient = false;
scheduleTimeoutMessages();
}
@Override
public void exit() {
if (!mIfaceIsDestroyed) {
stopSoftAp();
}
if (SdkLevel.isAtLeastS()) {
mCoexManager.unregisterCoexListener(mCoexListener);
}
if (getConnectedClientList().size() != 0) {
Log.d(getTag(), "Resetting num stations on stop");
for (List<WifiClient> it : mConnectedClientWithApInfoMap.values()) {
if (it.size() != 0) {
mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(
0, 0, mOriginalModeConfiguration.getTargetMode(),
mCurrentSoftApInfoMap
.get(it.get(0).getApInstanceIdentifier()));
}
}
mConnectedClientWithApInfoMap.clear();
if (mSoftApCallback != null) {
mSoftApCallback.onConnectedClientsOrInfoChanged(mCurrentSoftApInfoMap,
mConnectedClientWithApInfoMap, isBridgedMode());
}
}
mPendingDisconnectClients.clear();
cancelTimeoutMessage();
cancelBridgedModeIdleInstanceTimeoutMessage();
// 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,
mOriginalModeConfiguration.getTargetMode(),
mDefaultShutdownTimeoutMillis, isBridgedMode());
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;
mRole = null;
updateSoftApInfo(null, false);
}
private void updateUserBandPreferenceViolationMetricsIfNeeded(SoftApInfo apInfo) {
// The band preference violation only need to detect in single AP mode.
if (isBridgedMode()) return;
int band = mCurrentSoftApConfiguration.getBand();
boolean bandPreferenceViolated =
(ScanResult.is24GHz(apInfo.getFrequency())
&& !ApConfigUtil.containsBand(band,
SoftApConfiguration.BAND_2GHZ))
|| (ScanResult.is5GHz(apInfo.getFrequency())
&& !ApConfigUtil.containsBand(band,
SoftApConfiguration.BAND_5GHZ))
|| (ScanResult.is6GHz(apInfo.getFrequency())
&& !ApConfigUtil.containsBand(band,
SoftApConfiguration.BAND_6GHZ));
if (bandPreferenceViolated) {
Log.e(getTag(), "Channel does not satisfy user band preference: "
+ apInfo.getFrequency());
mWifiMetrics.incrementNumSoftApUserBandPreferenceUnsatisfied();
}
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_ASSOCIATED_STATIONS_CHANGED:
if (!(message.obj instanceof WifiClient)) {
Log.e(getTag(), "Invalid type returned for"
+ " CMD_ASSOCIATED_STATIONS_CHANGED");
break;
}
boolean isConnected = (message.arg1 == 1);
WifiClient client = (WifiClient) message.obj;
Log.d(getTag(), "CMD_ASSOCIATED_STATIONS_CHANGED, Client: "
+ client.getMacAddress().toString() + " isConnected: "
+ isConnected);
updateConnectedClients(client, isConnected);
break;
case CMD_AP_INFO_CHANGED:
if (!(message.obj instanceof SoftApInfo)) {
Log.e(getTag(), "Invalid type returned for"
+ " CMD_AP_INFO_CHANGED");
break;
}
SoftApInfo apInfo = (SoftApInfo) message.obj;
if (apInfo.getFrequency() < 0) {
Log.e(getTag(), "Invalid ap channel frequency: "
+ apInfo.getFrequency());
break;
}
// Update shutdown timeout
apInfo.setAutoShutdownTimeoutMillis(mTimeoutEnabled
? getShutdownTimeoutMillis() : 0);
updateSoftApInfo(apInfo, false);
break;
case CMD_INTERFACE_STATUS_CHANGED:
boolean isUp = message.arg1 == 1;
onUpChanged(isUp);
break;
case CMD_STOP:
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);
}
quitNow();
break;
case CMD_START:
// Already started, ignore this command.
break;
case CMD_NO_ASSOCIATED_STATIONS_TIMEOUT:
if (!mTimeoutEnabled) {
Log.wtf(getTag(), "Timeout message received while timeout is disabled."
+ " Dropping.");
break;
}
if (getConnectedClientList().size() != 0) {
Log.wtf(getTag(), "Timeout message received but has clients. "
+ "Dropping.");
break;
}
mSoftApNotifier.showSoftApShutdownTimeoutExpiredNotification();
Log.i(getTag(), "Timeout message received. Stopping soft AP.");
updateApState(WifiManager.WIFI_AP_STATE_DISABLING,
WifiManager.WIFI_AP_STATE_ENABLED, 0);
quitNow();
break;
case CMD_NO_ASSOCIATED_STATIONS_TIMEOUT_ON_ONE_INSTANCE:
if (!isBridgedMode() || mCurrentSoftApInfoMap.size() != 2) {
Log.wtf(getTag(), "Ignore Bridged Mode Timeout message received"
+ " in single AP state. Dropping");
break;
}
if (!mBridgedModeOpportunisticsShutdownTimeoutEnabled) {
Log.wtf(getTag(), "Bridged Mode Timeout message received"
+ " while timeout is disabled. Dropping.");
break;
}
Set<String> idleInstances = getIdleInstances();
if (idleInstances.size() == 0) {
break;
}
Log.d(getTag(), "Instance idle timout, the number of the idle instances is "
+ idleInstances.size());
removeIfaceInstanceFromBridgedApIface(
getHighestFrequencyInstance(idleInstances));
break;
case CMD_INTERFACE_DESTROYED:
Log.d(getTag(), "Interface was cleanly destroyed.");
updateApState(WifiManager.WIFI_AP_STATE_DISABLING,
WifiManager.WIFI_AP_STATE_ENABLED, 0);
mIfaceIsDestroyed = true;
quitNow();
break;
case CMD_FAILURE:
Log.w(getTag(), "hostapd failure, stop and report failure");
/* fall through */
case CMD_INTERFACE_DOWN:
Log.w(getTag(), "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);
quitNow();
break;
case CMD_UPDATE_CAPABILITY:
// Capability should only changed by carrier requirement. Only apply to
// Tether Mode
if (mOriginalModeConfiguration.getTargetMode()
== WifiManager.IFACE_IP_MODE_TETHERED) {
SoftApCapability capability = (SoftApCapability) message.obj;
mCurrentSoftApCapability = new SoftApCapability(capability);
mWifiMetrics.updateSoftApCapability(mCurrentSoftApCapability,
mOriginalModeConfiguration.getTargetMode(), isBridgedMode());
updateClientConnection();
}
break;
case CMD_UPDATE_CONFIG:
SoftApConfiguration newConfig = (SoftApConfiguration) message.obj;
SoftApConfiguration originalConfig =
mOriginalModeConfiguration.getSoftApConfiguration();
if (!ApConfigUtil.checkConfigurationChangeNeedToRestart(
originalConfig, newConfig)) {
Log.d(getTag(), "Configuration changed to " + newConfig);
if (mCurrentSoftApConfiguration.getMaxNumberOfClients()
!= newConfig.getMaxNumberOfClients()) {
Log.d(getTag(), "Max Client changed, reset to record the metrics");
mEverReportMetricsForMaxClient = false;
}
boolean needRescheduleTimer =
mCurrentSoftApConfiguration.getShutdownTimeoutMillis()
!= newConfig.getShutdownTimeoutMillis()
|| mTimeoutEnabled != newConfig.isAutoShutdownEnabled()
|| mBridgedModeOpportunisticsShutdownTimeoutEnabled
!= newConfig
.isBridgedModeOpportunisticShutdownEnabledInternal();
updateChangeableConfiguration(newConfig);
updateClientConnection();
if (needRescheduleTimer) {
cancelTimeoutMessage();
cancelBridgedModeIdleInstanceTimeoutMessage();
scheduleTimeoutMessages();
// Update SoftApInfo
for (SoftApInfo info : mCurrentSoftApInfoMap.values()) {
SoftApInfo newInfo = new SoftApInfo(info);
newInfo.setAutoShutdownTimeoutMillis(mTimeoutEnabled
? getShutdownTimeoutMillis() : 0);
updateSoftApInfo(newInfo, false);
}
}
mWifiMetrics.updateSoftApConfiguration(
mCurrentSoftApConfiguration,
mOriginalModeConfiguration.getTargetMode(), isBridgedMode());
} else {
Log.d(getTag(), "Ignore the config: " + newConfig
+ " update since it requires restart");
}
break;
case CMD_UPDATE_COUNTRY_CODE:
String countryCode = (String) message.obj;
if (!TextUtils.isEmpty(countryCode)
&& !TextUtils.equals(mCountryCode, countryCode)
&& mWifiNative.setApCountryCode(
mApInterfaceName, countryCode.toUpperCase(Locale.ROOT))) {
Log.i(getTag(), "Update country code when Soft AP enabled from "
+ mCountryCode + " to " + countryCode);
mCountryCode = countryCode;
}
break;
case CMD_FORCE_DISCONNECT_PENDING_CLIENTS:
if (mPendingDisconnectClients.size() != 0) {
Log.d(getTag(), "Disconnect pending list is NOT empty");
mPendingDisconnectClients.forEach((pendingClient, reason)->
mWifiNative.forceClientDisconnect(mApInterfaceName,
pendingClient.getMacAddress(), reason));
sendMessageDelayed(
SoftApStateMachine.CMD_FORCE_DISCONNECT_PENDING_CLIENTS,
SOFT_AP_PENDING_DISCONNECTION_CHECK_DELAY_MS);
}
break;
case CMD_SAFE_CHANNEL_FREQUENCY_CHANGED:
updateSafeChannelFrequencyList();
if (!isBridgedMode() || mCurrentSoftApInfoMap.size() != 2) {
Log.d(getTag(), "Ignore safe channel changed in single AP state");
break;
}
Set<String> unavailableInstances = new HashSet<>();
for (SoftApInfo currentInfo : mCurrentSoftApInfoMap.values()) {
int sapFreq = currentInfo.getFrequency();
if (!mSafeChannelFrequencyList.contains(sapFreq)) {
int sapBand = ApConfigUtil.convertFrequencyToBand(sapFreq);
if (sapBand != ApConfigUtil.removeUnavailableBands(
mCurrentSoftApCapability,
sapBand, mCoexManager)) {
unavailableInstances.add(currentInfo.getApInstanceIdentifier());
}
}
}
removeIfaceInstanceFromBridgedApIface(
getHighestFrequencyInstance(unavailableInstances));
break;
case CMD_HANDLE_WIFI_CONNECTED:
if (!isBridgedMode() || mCurrentSoftApInfoMap.size() != 2) {
Log.d(getTag(), "Ignore wifi connected in single AP state");
break;
}
WifiInfo wifiInfo = (WifiInfo) message.obj;
int wifiFreq = wifiInfo.getFrequency();
String targetShutDownInstance = "";
if (!mSafeChannelFrequencyList.contains(wifiFreq)) {
Log.i(getTag(), "Wifi connected to freq:" + wifiFreq
+ " which is unavailable for SAP");
for (SoftApInfo sapInfo : mCurrentSoftApInfoMap.values()) {
if (ApConfigUtil.convertFrequencyToBand(sapInfo.getFrequency())
== ApConfigUtil.convertFrequencyToBand(wifiFreq)) {
targetShutDownInstance = sapInfo.getApInstanceIdentifier();
Log.d(getTag(), "Remove the " + targetShutDownInstance
+ " instance which is running on the same band as "
+ "the wifi connection on an unsafe channel");
break;
}
}
// Wifi may connect to different band as the SAP. For instances:
// Wifi connect to 6Ghz but bridged AP is running on 2.4Ghz + 5Ghz.
// In this case, targetShutDownInstance will be empty, shutdown the
// highest frequency instance.
removeIfaceInstanceFromBridgedApIface(
TextUtils.isEmpty(targetShutDownInstance)
? getHighestFrequencyInstance(mCurrentSoftApInfoMap.keySet())
: targetShutDownInstance);
}
break;
default:
return NOT_HANDLED;
}
return HANDLED;
}
}
}
}