Merge changes from topic 'tethering-ownership' into nyc-mr1-dev

* changes:
  Tethering: Own WiFi tethering state and lifetime
  Use an ArrayMap instead of HashMap to track tether interfaces
  Consolidate cleanup logic in TetherInterfaceSM.TetheredState
  Annotate TetherInterfaceStateMachineTest for APCT
  Rely on Tethering mutex for TetherInterfaceSM
  Rename TetherInterfaceSM to TetherInterfaceStateMachine
  Remove dead code from TetherInterfaceSM
  Remove transient StartingState from TetherInterfaceSM
  Expand test coverage of TetherInterfaceSM
  Add demonstration unittest for TetherInterfaceSM
  Extract TetherInterfaceSM to its own class.
  Make Tethering.TetherInterfaceSM more self contained
  Clean up class members in Tethering.TetherInterfaceSM
  Fix trivial warnings in Tethering.java
This commit is contained in:
Christopher Wiley
2016-06-08 20:55:10 +00:00
committed by Android (Google) Code Review
5 changed files with 886 additions and 589 deletions

View File

@@ -0,0 +1,27 @@
/*
* 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.connectivity.tethering;
/**
* @hide
*
* Interface with methods necessary to notify that a given interface is ready for tethering.
*/
public interface IControlsTethering {
void sendTetherStateChangedBroadcast();
void notifyInterfaceTetheringReadiness(boolean isReady, TetherInterfaceStateMachine who);
}

View File

@@ -0,0 +1,360 @@
/*
* 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.connectivity.tethering;
import android.net.ConnectivityManager;
import android.net.INetworkStatsService;
import android.net.InterfaceConfiguration;
import android.net.LinkAddress;
import android.net.NetworkUtils;
import android.os.INetworkManagementService;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.util.IState;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import java.net.InetAddress;
/**
* @hide
*
* Tracks the eligibility of a given network interface for tethering.
*/
public class TetherInterfaceStateMachine extends StateMachine {
private static final String USB_NEAR_IFACE_ADDR = "192.168.42.129";
private static final int USB_PREFIX_LENGTH = 24;
private static final String WIFI_HOST_IFACE_ADDR = "192.168.43.1";
private static final int WIFI_HOST_IFACE_PREFIX_LENGTH = 24;
private final static String TAG = "TetherInterfaceSM";
private final static boolean DBG = false;
private final static boolean VDBG = false;
private static final Class[] messageClasses = {
TetherInterfaceStateMachine.class
};
private static final SparseArray<String> sMagicDecoderRing =
MessageUtils.findMessageNames(messageClasses);
private static final int BASE_IFACE = Protocol.BASE_TETHERING + 100;
// request from the user that it wants to tether
public static final int CMD_TETHER_REQUESTED = BASE_IFACE + 2;
// request from the user that it wants to untether
public static final int CMD_TETHER_UNREQUESTED = BASE_IFACE + 3;
// notification that this interface is down
public static final int CMD_INTERFACE_DOWN = BASE_IFACE + 4;
// notification from the master SM that it had trouble enabling IP Forwarding
public static final int CMD_IP_FORWARDING_ENABLE_ERROR = BASE_IFACE + 7;
// notification from the master SM that it had trouble disabling IP Forwarding
public static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IFACE + 8;
// notification from the master SM that it had trouble starting tethering
public static final int CMD_START_TETHERING_ERROR = BASE_IFACE + 9;
// notification from the master SM that it had trouble stopping tethering
public static final int CMD_STOP_TETHERING_ERROR = BASE_IFACE + 10;
// notification from the master SM that it had trouble setting the DNS forwarders
public static final int CMD_SET_DNS_FORWARDERS_ERROR = BASE_IFACE + 11;
// the upstream connection has changed
public static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IFACE + 12;
private final State mInitialState;
private final State mTetheredState;
private final State mUnavailableState;
private final INetworkManagementService mNMService;
private final INetworkStatsService mStatsService;
private final IControlsTethering mTetherController;
private final String mIfaceName;
private final int mInterfaceType;
private int mLastError;
private String mMyUpstreamIfaceName; // may change over time
public TetherInterfaceStateMachine(String ifaceName, Looper looper, int interfaceType,
INetworkManagementService nMService, INetworkStatsService statsService,
IControlsTethering tetherController) {
super(ifaceName, looper);
mNMService = nMService;
mStatsService = statsService;
mTetherController = tetherController;
mIfaceName = ifaceName;
mInterfaceType = interfaceType;
setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR);
mInitialState = new InitialState();
addState(mInitialState);
mTetheredState = new TetheredState();
addState(mTetheredState);
mUnavailableState = new UnavailableState();
addState(mUnavailableState);
setInitialState(mInitialState);
}
@Override
public String toString() {
String res = new String();
res += mIfaceName + " - ";
IState current = getCurrentState();
if (current == mInitialState) res += "InitialState";
if (current == mTetheredState) res += "TetheredState";
if (current == mUnavailableState) res += "UnavailableState";
if (isAvailable()) res += " - Available";
if (isTethered()) res += " - Tethered";
res += " - lastError =" + getLastError();
return res;
}
public int getLastError() {
return mLastError;
}
private void setLastError(int error) {
mLastError = error;
}
public boolean isAvailable() {
return getCurrentState() == mInitialState;
}
public boolean isTethered() {
return getCurrentState() == mTetheredState;
}
public boolean isErrored() {
return (mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR);
}
// configured when we start tethering and unconfig'd on error or conclusion
private boolean configureIfaceIp(boolean enabled) {
if (VDBG) Log.d(TAG, "configureIfaceIp(" + enabled + ")");
String ipAsString = null;
int prefixLen = 0;
if (mInterfaceType == ConnectivityManager.TETHERING_USB) {
ipAsString = USB_NEAR_IFACE_ADDR;
prefixLen = USB_PREFIX_LENGTH;
} else if (mInterfaceType == ConnectivityManager.TETHERING_WIFI) {
ipAsString = WIFI_HOST_IFACE_ADDR;
prefixLen = WIFI_HOST_IFACE_PREFIX_LENGTH;
} else {
// Nothing to do, BT does this elsewhere.
return true;
}
InterfaceConfiguration ifcg = null;
try {
ifcg = mNMService.getInterfaceConfig(mIfaceName);
if (ifcg != null) {
InetAddress addr = NetworkUtils.numericToInetAddress(ipAsString);
ifcg.setLinkAddress(new LinkAddress(addr, prefixLen));
if (enabled) {
ifcg.setInterfaceUp();
} else {
ifcg.setInterfaceDown();
}
ifcg.clearFlag("running");
mNMService.setInterfaceConfig(mIfaceName, ifcg);
}
} catch (Exception e) {
Log.e(TAG, "Error configuring interface " + mIfaceName, e);
return false;
}
return true;
}
private void maybeLogMessage(State state, int what) {
if (DBG) {
Log.d(TAG, state.getName() + " got " +
sMagicDecoderRing.get(what, Integer.toString(what)));
}
}
class InitialState extends State {
@Override
public void enter() {
mTetherController.sendTetherStateChangedBroadcast();
}
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_REQUESTED:
setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR);
mTetherController.notifyInterfaceTetheringReadiness(true, TetherInterfaceStateMachine.this);
transitionTo(mTetheredState);
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
break;
default:
retValue = false;
break;
}
return retValue;
}
}
class TetheredState extends State {
@Override
public void enter() {
if (!configureIfaceIp(true)) {
setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR);
transitionTo(mInitialState);
return;
}
try {
mNMService.tetherInterface(mIfaceName);
} catch (Exception e) {
Log.e(TAG, "Error Tethering: " + e.toString());
setLastError(ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR);
transitionTo(mInitialState);
return;
}
if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
mTetherController.sendTetherStateChangedBroadcast();
}
@Override
public void exit() {
mTetherController.notifyInterfaceTetheringReadiness(false,
TetherInterfaceStateMachine.this);
// Note that at this point, we're leaving the tethered state. We can fail any
// of these operations, but it doesn't really change that we have to try them
// all in sequence.
cleanupUpstream();
try {
mNMService.untetherInterface(mIfaceName);
} catch (Exception ee) {
setLastError(ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR);
Log.e(TAG, "Failed to untether interface: " + ee.toString());
}
configureIfaceIp(false);
}
private void cleanupUpstream() {
if (mMyUpstreamIfaceName != null) {
// note that we don't care about errors here.
// sometimes interfaces are gone before we get
// to remove their rules, which generates errors.
// just do the best we can.
try {
// about to tear down NAT; gather remaining statistics
mStatsService.forceUpdate();
} catch (Exception e) {
if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
}
try {
mNMService.stopInterfaceForwarding(mIfaceName, mMyUpstreamIfaceName);
} catch (Exception e) {
if (VDBG) Log.e(
TAG, "Exception in removeInterfaceForward: " + e.toString());
}
try {
mNMService.disableNat(mIfaceName, mMyUpstreamIfaceName);
} catch (Exception e) {
if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
}
mMyUpstreamIfaceName = null;
}
return;
}
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_UNREQUESTED:
transitionTo(mInitialState);
if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
break;
case CMD_TETHER_CONNECTION_CHANGED:
String newUpstreamIfaceName = (String)(message.obj);
if ((mMyUpstreamIfaceName == null && newUpstreamIfaceName == null) ||
(mMyUpstreamIfaceName != null &&
mMyUpstreamIfaceName.equals(newUpstreamIfaceName))) {
if (VDBG) Log.d(TAG, "Connection changed noop - dropping");
break;
}
cleanupUpstream();
if (newUpstreamIfaceName != null) {
try {
mNMService.enableNat(mIfaceName, newUpstreamIfaceName);
mNMService.startInterfaceForwarding(mIfaceName,
newUpstreamIfaceName);
} catch (Exception e) {
Log.e(TAG, "Exception enabling Nat: " + e.toString());
setLastError(ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR);
transitionTo(mInitialState);
return true;
}
}
mMyUpstreamIfaceName = newUpstreamIfaceName;
break;
case CMD_IP_FORWARDING_ENABLE_ERROR:
case CMD_IP_FORWARDING_DISABLE_ERROR:
case CMD_START_TETHERING_ERROR:
case CMD_STOP_TETHERING_ERROR:
case CMD_SET_DNS_FORWARDERS_ERROR:
setLastErrorAndTransitionToInitialState(
ConnectivityManager.TETHER_ERROR_MASTER_ERROR);
break;
default:
retValue = false;
break;
}
return retValue;
}
}
/**
* This state is terminal for the per interface state machine. At this
* point, the master state machine should have removed this interface
* specific state machine from its list of possible recipients of
* tethering requests. The state machine itself will hang around until
* the garbage collector finds it.
*/
class UnavailableState extends State {
@Override
public void enter() {
setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR);
mTetherController.sendTetherStateChangedBroadcast();
}
}
void setLastErrorAndTransitionToInitialState(int error) {
setLastError(error);
transitionTo(mInitialState);
}
}

View File

@@ -12,6 +12,7 @@ LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
frameworks-base-testutils \
services.core \
services.devicepolicy \
services.net \

View File

@@ -0,0 +1,298 @@
/*
* 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.connectivity.tethering;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.net.ConnectivityManager;
import android.net.INetworkStatsService;
import android.net.InterfaceConfiguration;
import android.os.INetworkManagementService;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TetherInterfaceStateMachineTest {
private static final String IFACE_NAME = "testnet1";
private static final String UPSTREAM_IFACE = "upstream0";
private static final String UPSTREAM_IFACE2 = "upstream1";
@Mock private INetworkManagementService mNMService;
@Mock private INetworkStatsService mStatsService;
@Mock private IControlsTethering mTetherHelper;
@Mock private InterfaceConfiguration mInterfaceConfiguration;
private final TestLooper mLooper = new TestLooper();
private TetherInterfaceStateMachine mTestedSm;
private void initStateMachine(int interfaceType) throws Exception {
mTestedSm = new TetherInterfaceStateMachine(IFACE_NAME, mLooper.getLooper(), interfaceType,
mNMService, mStatsService, mTetherHelper);
mTestedSm.start();
// Starting the state machine always puts us in a consistent state and notifies
// the test of the world that we've changed from an unknown to available state.
mLooper.dispatchAll();
reset(mNMService, mStatsService, mTetherHelper);
when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration);
}
private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception {
initStateMachine(interfaceType);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
if (upstreamIface != null) {
dispatchTetherConnectionChanged(upstreamIface);
}
reset(mNMService, mStatsService, mTetherHelper);
when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration);
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public void startsOutAvailable() {
mTestedSm = new TetherInterfaceStateMachine(IFACE_NAME, mLooper.getLooper(),
ConnectivityManager.TETHERING_BLUETOOTH, mNMService, mStatsService, mTetherHelper);
mTestedSm.start();
mLooper.dispatchAll();
assertTrue("Should start out available for tethering", mTestedSm.isAvailable());
assertFalse("Should not be tethered initially", mTestedSm.isTethered());
assertFalse("Should have no errors initially", mTestedSm.isErrored());
verify(mTetherHelper).sendTetherStateChangedBroadcast();
verifyNoMoreInteractions(mTetherHelper, mNMService, mStatsService);
}
@Test
public void shouldDoNothingUntilRequested() throws Exception {
initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH);
final int [] NOOP_COMMANDS = {
TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED,
TetherInterfaceStateMachine.CMD_IP_FORWARDING_ENABLE_ERROR,
TetherInterfaceStateMachine.CMD_IP_FORWARDING_DISABLE_ERROR,
TetherInterfaceStateMachine.CMD_START_TETHERING_ERROR,
TetherInterfaceStateMachine.CMD_STOP_TETHERING_ERROR,
TetherInterfaceStateMachine.CMD_SET_DNS_FORWARDERS_ERROR,
TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED
};
for (int command : NOOP_COMMANDS) {
// None of these commands should trigger us to request action from
// the rest of the system.
dispatchCommand(command);
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
}
}
@Test
public void handlesImmediateInterfaceDown() throws Exception {
initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH);
dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
verify(mTetherHelper).sendTetherStateChangedBroadcast();
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertFalse("Should not be tetherable when the interface is down", mTestedSm.isAvailable());
assertFalse("Should not be tethered when the interface is down", mTestedSm.isTethered());
assertFalse("Should have no errors when the interface goes immediately down",
mTestedSm.isErrored());
}
@Test
public void canBeTethered() throws Exception {
initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
InOrder inOrder = inOrder(mTetherHelper, mNMService);
inOrder.verify(mTetherHelper).notifyInterfaceTetheringReadiness(true, mTestedSm);
inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
inOrder.verify(mTetherHelper).sendTetherStateChangedBroadcast();
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertFalse("Should not be tetherable when tethered", mTestedSm.isAvailable());
assertTrue("Should be in a tethered state", mTestedSm.isTethered());
assertFalse("Should have no errors when tethered", mTestedSm.isErrored());
}
@Test
public void canUnrequestTethering() throws Exception {
initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, null);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED);
InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper);
inOrder.verify(mTetherHelper).notifyInterfaceTetheringReadiness(false, mTestedSm);
inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
inOrder.verify(mTetherHelper).sendTetherStateChangedBroadcast();
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertTrue("Should be ready for tethering again", mTestedSm.isAvailable());
assertFalse("Should not be tethered", mTestedSm.isTethered());
assertFalse("Should have no errors", mTestedSm.isErrored());
}
@Test
public void canBeTetheredAsUsb() throws Exception {
initStateMachine(ConnectivityManager.TETHERING_USB);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
InOrder inOrder = inOrder(mTetherHelper, mNMService);
inOrder.verify(mTetherHelper).notifyInterfaceTetheringReadiness(true, mTestedSm);
inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
inOrder.verify(mTetherHelper).sendTetherStateChangedBroadcast();
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertFalse("Should not be tetherable when tethered", mTestedSm.isAvailable());
assertTrue("Should be in a tethered state", mTestedSm.isTethered());
assertFalse("Should have no errors when tethered", mTestedSm.isErrored());
}
@Test
public void handlesFirstUpstreamChange() throws Exception {
initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, null);
// Telling the state machine about its upstream interface triggers a little more configuration.
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
InOrder inOrder = inOrder(mNMService);
inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertFalse("Should not be tetherable when tethered", mTestedSm.isAvailable());
assertTrue("Should be in a tethered state", mTestedSm.isTethered());
assertFalse("Should have no errors when tethered", mTestedSm.isErrored());
}
@Test
public void handlesChangingUpstream() throws Exception {
initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
InOrder inOrder = inOrder(mNMService, mStatsService);
inOrder.verify(mStatsService).forceUpdate();
inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertFalse("Should not be tetherable when tethered", mTestedSm.isAvailable());
assertTrue("Should be in a tethered state", mTestedSm.isTethered());
assertFalse("Should have no errors when tethered", mTestedSm.isErrored());
}
@Test
public void canUnrequestTetheringWithUpstream() throws Exception {
initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, UPSTREAM_IFACE);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED);
InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper);
inOrder.verify(mTetherHelper).notifyInterfaceTetheringReadiness(false, mTestedSm);
inOrder.verify(mStatsService).forceUpdate();
inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
inOrder.verify(mTetherHelper).sendTetherStateChangedBroadcast();
verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper);
assertTrue("Should be ready for tethering again", mTestedSm.isAvailable());
assertFalse("Should not be tethered", mTestedSm.isTethered());
assertFalse("Should have no errors", mTestedSm.isErrored());
}
@Test
public void interfaceDownLeadsToUnavailable() throws Exception {
for (boolean shouldThrow : new boolean[]{true, false}) {
initTetheredStateMachine(ConnectivityManager.TETHERING_USB, null);
if (shouldThrow) {
doThrow(RemoteException.class).when(mNMService).untetherInterface(IFACE_NAME);
}
dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN);
InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration);
usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
usbTeardownOrder.verify(mNMService).setInterfaceConfig(
IFACE_NAME, mInterfaceConfiguration);
verify(mTetherHelper).notifyInterfaceTetheringReadiness(false, mTestedSm);
assertFalse("Should not be available", mTestedSm.isAvailable());
assertFalse("Should not be tethered", mTestedSm.isTethered());
}
}
@Test
public void usbShouldBeTornDownOnTetherError() throws Exception {
initStateMachine(ConnectivityManager.TETHERING_USB);
doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration);
usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
usbTeardownOrder.verify(mNMService).setInterfaceConfig(
IFACE_NAME, mInterfaceConfiguration);
// Initial call is when we transition to the tethered state on request.
verify(mTetherHelper).notifyInterfaceTetheringReadiness(true, mTestedSm);
// And this call is to notify that we really aren't requested tethering.
verify(mTetherHelper).notifyInterfaceTetheringReadiness(false, mTestedSm);
assertTrue("Expected to see an error reported", mTestedSm.isErrored());
}
@Test
public void shouldTearDownUsbOnUpstreamError() throws Exception {
initTetheredStateMachine(ConnectivityManager.TETHERING_USB, null);
doThrow(RemoteException.class).when(mNMService).enableNat(anyString(), anyString());
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration);
usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
usbTeardownOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
verify(mTetherHelper).notifyInterfaceTetheringReadiness(false, mTestedSm);
}
/**
* Send a command to the state machine under test, and run the event loop to idle.
*
* @param command One of the TetherInterfaceStateMachine.CMD_* constants.
*/
private void dispatchCommand(int command) {
mTestedSm.sendMessage(command);
mLooper.dispatchAll();
}
/**
* Special override to tell the state machine that the upstream interface has changed.
*
* @see #dispatchCommand(int)
* @param upstreamIface String name of upstream interface (or null)
*/
private void dispatchTetherConnectionChanged(String upstreamIface) {
mTestedSm.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
upstreamIface);
mLooper.dispatchAll();
}
}