/* * 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.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.InterfaceConfiguration; import android.net.LinkAddress; import android.net.NetworkUtils; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.INetworkManagementService; import android.os.Looper; import android.os.Message; import android.util.Log; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.wifi.util.ApConfigUtil; import java.util.ArrayList; import java.util.Locale; /** * Manage WiFi in AP mode. * The internal state machine runs under "WifiStateMachine" thread context. */ public class SoftApManager { private static final String TAG = "SoftApManager"; private final Context mContext; private final INetworkManagementService mNmService; private final WifiNative mWifiNative; private final ConnectivityManager mConnectivityManager; private final ArrayList mAllowed2GChannels; private final String mCountryCode; private final String mInterfaceName; private String mTetherInterfaceName; private final SoftApStateMachine mStateMachine; private final Listener mListener; private static class TetherStateChange { public ArrayList available; public ArrayList active; TetherStateChange(ArrayList av, ArrayList ac) { available = av; active = ac; } } /** * Listener for soft AP state changes. */ public interface Listener { /** * Invoke when AP state changed. * @param state new AP state * @param failureReason reason when in failed state */ void onStateChanged(int state, int failureReason); } public SoftApManager(Context context, Looper looper, WifiNative wifiNative, INetworkManagementService nmService, ConnectivityManager connectivityManager, String countryCode, ArrayList allowed2GChannels, Listener listener) { mStateMachine = new SoftApStateMachine(looper); mContext = context; mNmService = nmService; mWifiNative = wifiNative; mConnectivityManager = connectivityManager; mCountryCode = countryCode; mAllowed2GChannels = allowed2GChannels; mListener = listener; mInterfaceName = mWifiNative.getInterfaceName(); /* Register receiver for tether state changes. */ mContext.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { ArrayList available = intent.getStringArrayListExtra( ConnectivityManager.EXTRA_AVAILABLE_TETHER); ArrayList active = intent.getStringArrayListExtra( ConnectivityManager.EXTRA_ACTIVE_TETHER); mStateMachine.sendMessage( SoftApStateMachine.CMD_TETHER_STATE_CHANGE, new TetherStateChange(available, active)); } }, new IntentFilter(ConnectivityManager.ACTION_TETHER_STATE_CHANGED)); } /** * Start soft AP with given configuration. * @param config AP configuration */ public void start(WifiConfiguration config) { mStateMachine.sendMessage(SoftApStateMachine.CMD_START, config); } /** * Stop soft AP. */ public void stop() { mStateMachine.sendMessage(SoftApStateMachine.CMD_STOP); } /** * Update AP state. * @param state new AP state * @param reason Failure reason if the new AP state is in failure state */ private void updateApState(int state, int reason) { if (mListener != null) { mListener.onStateChanged(state, reason); } } /** * 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) { Log.e(TAG, "Unable to start soft AP without configuration"); return ERROR_GENERIC; } /* Make a copy of configuration for updating AP band and channel. */ WifiConfiguration localConfig = new WifiConfiguration(config); int result = ApConfigUtil.updateApChannelConfig( mWifiNative, mCountryCode, mAllowed2GChannels, localConfig); if (result != SUCCESS) { Log.e(TAG, "Failed to update AP band and channel"); return result; } /* Setup country code if it is provide. */ if (mCountryCode != null) { /** * Country code is mandatory for 5GHz band, return an error if failed to set * country code when AP is configured for 5GHz band. */ if (!mWifiNative.setCountryCodeHal(mCountryCode.toUpperCase(Locale.ROOT)) && config.apBand == WifiConfiguration.AP_BAND_5GHZ) { Log.e(TAG, "Failed to set country code, required for setting up " + "soft ap in 5GHz"); return ERROR_GENERIC; } } try { mNmService.startAccessPoint(localConfig, mInterfaceName); } catch (Exception e) { Log.e(TAG, "Exception in starting soft AP: " + e); return ERROR_GENERIC; } Log.d(TAG, "Soft AP is started"); return SUCCESS; } /** * Teardown soft AP. */ private void stopSoftAp() { try { mNmService.stopAccessPoint(mInterfaceName); } catch (Exception e) { Log.e(TAG, "Exception in stopping soft AP: " + e); return; } Log.d(TAG, "Soft AP is stopped"); } private boolean startTethering(ArrayList available) { String[] wifiRegexs = mConnectivityManager.getTetherableWifiRegexs(); for (String intf : available) { for (String regex : wifiRegexs) { if (intf.matches(regex)) { try { InterfaceConfiguration ifcg = mNmService.getInterfaceConfig(intf); if (ifcg != null) { /* IP/netmask: 192.168.43.1/255.255.255.0 */ ifcg.setLinkAddress(new LinkAddress( NetworkUtils.numericToInetAddress("192.168.43.1"), 24)); ifcg.setInterfaceUp(); mNmService.setInterfaceConfig(intf, ifcg); } } catch (Exception e) { Log.e(TAG, "Error configuring interface " + intf + ", :" + e); return false; } if (mConnectivityManager.tether(intf) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { Log.e(TAG, "Error tethering on " + intf); return false; } mTetherInterfaceName = intf; return true; } } } /* We found no interfaces to tether. */ return false; } private void stopTethering() { try { /* Clear the interface address. */ InterfaceConfiguration ifcg = mNmService.getInterfaceConfig(mTetherInterfaceName); if (ifcg != null) { ifcg.setLinkAddress( new LinkAddress( NetworkUtils.numericToInetAddress("0.0.0.0"), 0)); mNmService.setInterfaceConfig(mTetherInterfaceName, ifcg); } } catch (Exception e) { Log.e(TAG, "Error resetting interface " + mTetherInterfaceName + ", :" + e); } if (mConnectivityManager.untether(mTetherInterfaceName) != ConnectivityManager.TETHER_ERROR_NO_ERROR) { Log.e(TAG, "Untether initiate failed!"); } } private boolean isWifiTethered(ArrayList active) { String[] wifiRegexs = mConnectivityManager.getTetherableWifiRegexs(); for (String intf : active) { for (String regex : wifiRegexs) { if (intf.matches(regex)) { return true; } } } /* No tethered interface. */ return false; } 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_TETHER_STATE_CHANGE = 2; public static final int CMD_TETHER_NOTIFICATION_TIMEOUT = 3; private static final int TETHER_NOTIFICATION_TIME_OUT_MSECS = 5000; /* Sequence number used to track tether notification timeout. */ private int mTetherToken = 0; private final State mIdleState = new IdleState(); private final State mStartedState = new StartedState(); private final State mTetheringState = new TetheringState(); private final State mTetheredState = new TetheredState(); private final State mUntetheringState = new UntetheringState(); SoftApStateMachine(Looper looper) { super(TAG, looper); // CHECKSTYLE:OFF IndentationCheck addState(mIdleState); addState(mStartedState, mIdleState); addState(mTetheringState, mStartedState); addState(mTetheredState, mStartedState); addState(mUntetheringState, mStartedState); // CHECKSTYLE:ON IndentationCheck setInitialState(mIdleState); start(); } private class IdleState extends State { @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START: updateApState(WifiManager.WIFI_AP_STATE_ENABLING, 0); int result = startSoftAp((WifiConfiguration) message.obj); if (result == SUCCESS) { updateApState(WifiManager.WIFI_AP_STATE_ENABLED, 0); transitionTo(mStartedState); } else { int reason = WifiManager.SAP_START_FAILURE_GENERAL; if (result == ERROR_NO_CHANNEL) { reason = WifiManager.SAP_START_FAILURE_NO_CHANNEL; } updateApState(WifiManager.WIFI_AP_STATE_FAILED, reason); } break; default: /* Ignore all other commands. */ break; } return HANDLED; } } private class StartedState extends State { @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_START: /* Already started, ignore this command. */ break; case CMD_STOP: updateApState(WifiManager.WIFI_AP_STATE_DISABLING, 0); stopSoftAp(); updateApState(WifiManager.WIFI_AP_STATE_DISABLED, 0); transitionTo(mIdleState); break; case CMD_TETHER_STATE_CHANGE: TetherStateChange stateChange = (TetherStateChange) message.obj; if (startTethering(stateChange.available)) { transitionTo(mTetheringState); } break; default: return NOT_HANDLED; } return HANDLED; } } /** * This is a transient state. We will transition out of this state when * we receive a notification that WiFi is tethered (TetheredState) or * we timed out waiting for that notification (StartedState). */ private class TetheringState extends State { @Override public void enter() { /* Send a delayed message to terminate if tethering fails to notify. */ sendMessageDelayed( obtainMessage(CMD_TETHER_NOTIFICATION_TIMEOUT, ++mTetherToken), TETHER_NOTIFICATION_TIME_OUT_MSECS); } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_TETHER_STATE_CHANGE: TetherStateChange stateChange = (TetherStateChange) message.obj; if (isWifiTethered(stateChange.active)) { transitionTo(mTetheredState); } break; case CMD_TETHER_NOTIFICATION_TIMEOUT: if (message.arg1 == mTetherToken) { Log.e(TAG, "Failed to get tether update, " + "shutdown soft access point"); transitionTo(mStartedState); /* Needs to be first thing handled. */ sendMessageAtFrontOfQueue(CMD_STOP); } break; default: return NOT_HANDLED; } return HANDLED; } } private class TetheredState extends State { @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_TETHER_STATE_CHANGE: TetherStateChange stateChange = (TetherStateChange) message.obj; if (!isWifiTethered(stateChange.active)) { Log.e(TAG, "Tethering reports wifi as untethered!, " + "shut down soft Ap"); sendMessage(CMD_STOP); } break; case CMD_STOP: Log.d(TAG, "Untethering before stopping AP"); stopTethering(); transitionTo(mUntetheringState); break; default: return NOT_HANDLED; } return HANDLED; } } /** * This is a transient state, will transition out of this state to StartedState * when we receive a notification that WiFi is untethered or we timed out waiting * for that notification. */ private class UntetheringState extends State { @Override public void enter() { /* Send a delayed message to terminate if tethering fails to notify. */ sendMessageDelayed( obtainMessage(CMD_TETHER_NOTIFICATION_TIMEOUT, ++mTetherToken), TETHER_NOTIFICATION_TIME_OUT_MSECS); } @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_TETHER_STATE_CHANGE: TetherStateChange stateChange = (TetherStateChange) message.obj; /* Transition back to StartedState when WiFi is untethered. */ if (!isWifiTethered(stateChange.active)) { transitionTo(mStartedState); /* Needs to be first thing handled */ sendMessageAtFrontOfQueue(CMD_STOP); } break; case CMD_TETHER_NOTIFICATION_TIMEOUT: if (message.arg1 == mTetherToken) { Log.e(TAG, "Failed to get tether update, " + "force stop access point"); transitionTo(mStartedState); /* Needs to be first thing handled. */ sendMessageAtFrontOfQueue(CMD_STOP); } break; default: /* Defer handling of this message until untethering is completed. */ deferMessage(message); break; } return HANDLED; } } } }