Merge "Tethering: support Local-only Hotspot mode for downstreams" am: d3128d4b55

am: 908ce167eb

Change-Id: I6e72a9b90e59ea5abd1dfa11c6c3d16a5c98751c
This commit is contained in:
Erik Kline
2017-04-10 12:12:17 +00:00
committed by android-build-merger
10 changed files with 605 additions and 107 deletions

View File

@@ -90,7 +90,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@@ -122,12 +124,23 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
public final TetherInterfaceStateMachine stateMachine;
public int lastState;
public int lastError;
public TetherState(TetherInterfaceStateMachine sm) {
stateMachine = sm;
// Assume all state machines start out available and with no errors.
lastState = IControlsTethering.STATE_AVAILABLE;
lastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
public boolean isCurrentlyServing() {
switch (lastState) {
case IControlsTethering.STATE_TETHERED:
case IControlsTethering.STATE_LOCAL_HOTSPOT:
return true;
default:
return false;
}
}
}
// used to synchronize public access to members
@@ -143,11 +156,13 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
private final StateMachine mTetherMasterSM;
private final OffloadController mOffloadController;
private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
private final HashSet<TetherInterfaceStateMachine> mForwardedDownstreams;
private volatile TetheringConfiguration mConfig;
private String mCurrentUpstreamIface;
private Notification.Builder mTetheredNotificationBuilder;
private int mLastNotificationId;
private boolean mRndisEnabled; // track the RNDIS function enabled state
private boolean mUsbTetherRequested; // true if USB tethering should be started
// when RNDIS is enabled
@@ -174,6 +189,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
mOffloadController = new OffloadController(mTetherMasterSM.getHandler());
mUpstreamNetworkMonitor = new UpstreamNetworkMonitor(
mContext, mTetherMasterSM, TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
mForwardedDownstreams = new HashSet<>();
mStateReceiver = new StateReceiver();
IntentFilter filter = new IntentFilter();
@@ -511,6 +527,10 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
}
public int tether(String iface) {
return tether(iface, IControlsTethering.STATE_TETHERED);
}
private int tether(String iface, int requestedState) {
if (DBG) Log.d(TAG, "Tethering " + iface);
synchronized (mPublicSync) {
TetherState tetherState = mTetherStates.get(iface);
@@ -524,7 +544,13 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
}
tetherState.stateMachine.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
// NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's
// queue but not yet processed, this will be a no-op and it will not
// return an error.
//
// TODO: reexamine the threading and messaging model.
tetherState.stateMachine.sendMessage(
TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, requestedState);
return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
}
@@ -537,8 +563,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
}
if (tetherState.lastState != IControlsTethering.STATE_TETHERED) {
Log.e(TAG, "Tried to untether an untethered iface :" + iface + ", ignoring");
if (!tetherState.isCurrentlyServing()) {
Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
}
tetherState.stateMachine.sendMessage(
@@ -565,6 +591,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
}
}
// TODO: Figure out how to update for local hotspot mode interfaces.
private void sendTetherStateChangedBroadcast() {
if (!getConnectivityManager().isTetheringSupported()) return;
@@ -728,7 +755,9 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
mRndisEnabled = rndisEnabled;
// start tethering if we have a request pending
if (usbConnected && mRndisEnabled && mUsbTetherRequested) {
tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
tetherMatchingInterfaces(
IControlsTethering.STATE_TETHERED,
ConnectivityManager.TETHERING_USB);
}
mUsbTetherRequested = false;
}
@@ -743,9 +772,11 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
break;
case WifiManager.WIFI_AP_STATE_ENABLED:
// When the AP comes up and we've been requested to tether it, do so.
if (mWifiTetherRequested) {
tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_WIFI);
}
// Otherwise, assume it's a local-only hotspot request.
final int state = mWifiTetherRequested
? IControlsTethering.STATE_TETHERED
: IControlsTethering.STATE_LOCAL_HOTSPOT;
tetherMatchingInterfaces(state, ConnectivityManager.TETHERING_WIFI);
break;
case WifiManager.WIFI_AP_STATE_DISABLED:
case WifiManager.WIFI_AP_STATE_DISABLING:
@@ -775,8 +806,16 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
}
}
private void tetherMatchingInterfaces(boolean enable, int interfaceType) {
if (VDBG) Log.d(TAG, "tetherMatchingInterfaces(" + enable + ", " + interfaceType + ")");
// TODO: Consider renaming to something more accurate in its description.
// This method:
// - allows requesting either tethering or local hotspot serving states
// - handles both enabling and disabling serving states
// - only tethers the first matching interface in listInterfaces()
// order of a given type
private void tetherMatchingInterfaces(int requestedState, int interfaceType) {
if (VDBG) {
Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")");
}
String[] ifaces = null;
try {
@@ -799,7 +838,20 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
return;
}
int result = (enable ? tether(chosenIface) : untether(chosenIface));
final int result;
switch (requestedState) {
case IControlsTethering.STATE_UNAVAILABLE:
case IControlsTethering.STATE_AVAILABLE:
result = untether(chosenIface);
break;
case IControlsTethering.STATE_TETHERED:
case IControlsTethering.STATE_LOCAL_HOTSPOT:
result = tether(chosenIface, requestedState);
break;
default:
Log.wtf(TAG, "Unknown interface state: " + requestedState);
return;
}
if (result != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
Log.e(TAG, "unable start or stop tethering on iface " + chosenIface);
return;
@@ -844,7 +896,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
if (mRndisEnabled) {
final long ident = Binder.clearCallingIdentity();
try {
tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
tetherMatchingInterfaces(IControlsTethering.STATE_TETHERED,
ConnectivityManager.TETHERING_USB);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -855,7 +908,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
} else {
final long ident = Binder.clearCallingIdentity();
try {
tetherMatchingInterfaces(false, ConnectivityManager.TETHERING_USB);
tetherMatchingInterfaces(IControlsTethering.STATE_AVAILABLE,
ConnectivityManager.TETHERING_USB);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -919,6 +973,14 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
}
}
private boolean upstreamWanted() {
if (!mForwardedDownstreams.isEmpty()) return true;
synchronized (mPublicSync) {
return mUsbTetherRequested || mWifiTetherRequested;
}
}
// Needed because the canonical source of upstream truth is just the
// upstream interface name, |mCurrentUpstreamIface|. This is ripe for
// future simplification, once the upstream Network is canonical.
@@ -935,10 +997,10 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
class TetherMasterSM extends StateMachine {
private static final int BASE_MASTER = Protocol.BASE_TETHERING;
// an interface SM has requested Tethering
static final int CMD_TETHER_MODE_REQUESTED = BASE_MASTER + 1;
// an interface SM has unrequested Tethering
static final int CMD_TETHER_MODE_UNREQUESTED = BASE_MASTER + 2;
// an interface SM has requested Tethering/Local Hotspot
static final int EVENT_IFACE_SERVING_STATE_ACTIVE = BASE_MASTER + 1;
// an interface SM has unrequested Tethering/Local Hotspot
static final int EVENT_IFACE_SERVING_STATE_INACTIVE = BASE_MASTER + 2;
// upstream connection change - do the right thing
static final int CMD_UPSTREAM_CHANGED = BASE_MASTER + 3;
// we don't have a valid upstream conn, check again after a delay
@@ -1023,7 +1085,9 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
transitionTo(mSetIpForwardingEnabledErrorState);
return false;
}
// TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
try {
// TODO: Find a more accurate method name (startDHCPv4()?).
mNMService.startTethering(cfg.dhcpRanges);
} catch (Exception e) {
try {
@@ -1325,26 +1389,41 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
}
}
private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
if (mNotifyList.indexOf(who) < 0) {
mNotifyList.add(who);
mIPv6TetheringCoordinator.addActiveDownstream(who, mode);
}
if (mode == IControlsTethering.STATE_TETHERED) {
mForwardedDownstreams.add(who);
} else {
mForwardedDownstreams.remove(who);
}
}
private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) {
mNotifyList.remove(who);
mIPv6TetheringCoordinator.removeActiveDownstream(who);
mForwardedDownstreams.remove(who);
}
class InitialState extends TetherMasterUtilState {
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_MODE_REQUESTED:
case EVENT_IFACE_SERVING_STATE_ACTIVE:
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
if (mNotifyList.indexOf(who) < 0) {
mNotifyList.add(who);
mIPv6TetheringCoordinator.addActiveDownstream(who);
}
handleInterfaceServingStateActive(message.arg1, who);
transitionTo(mTetherModeAliveState);
break;
case CMD_TETHER_MODE_UNREQUESTED:
case EVENT_IFACE_SERVING_STATE_INACTIVE:
who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
mNotifyList.remove(who);
mIPv6TetheringCoordinator.removeActiveDownstream(who);
handleInterfaceServingStateInactive(who);
break;
default:
retValue = false;
@@ -1356,6 +1435,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
class TetherModeAliveState extends TetherMasterUtilState {
final SimChangeListener simChange = new SimChangeListener(mContext);
boolean mUpstreamWanted = false;
boolean mTryCell = true;
@Override
@@ -1366,9 +1446,11 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
mUpstreamNetworkMonitor.start();
mOffloadController.start();
// Better try something first pass or crazy tests cases will fail.
chooseUpstreamType(true);
mTryCell = false;
if (upstreamWanted()) {
mUpstreamWanted = true;
chooseUpstreamType(true);
mTryCell = false;
}
}
@Override
@@ -1381,54 +1463,74 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
handleNewUpstreamNetworkState(null);
}
private boolean updateUpstreamWanted() {
final boolean previousUpstreamWanted = mUpstreamWanted;
mUpstreamWanted = upstreamWanted();
return previousUpstreamWanted;
}
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_MODE_REQUESTED: {
case EVENT_IFACE_SERVING_STATE_ACTIVE: {
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
if (mNotifyList.indexOf(who) < 0) {
mNotifyList.add(who);
mIPv6TetheringCoordinator.addActiveDownstream(who);
}
handleInterfaceServingStateActive(message.arg1, who);
who.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
mCurrentUpstreamIface);
// If there has been a change and an upstream is now
// desired, kick off the selection process.
final boolean previousUpstreamWanted = updateUpstreamWanted();
if (!previousUpstreamWanted && mUpstreamWanted) {
chooseUpstreamType(true);
}
break;
}
case CMD_TETHER_MODE_UNREQUESTED: {
case EVENT_IFACE_SERVING_STATE_INACTIVE: {
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
if (mNotifyList.remove(who)) {
if (DBG) Log.d(TAG, "TetherModeAlive removing notifyee " + who);
if (mNotifyList.isEmpty()) {
turnOffMasterTetherSettings(); // transitions appropriately
} else {
if (DBG) {
Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
" live requests:");
for (TetherInterfaceStateMachine o : mNotifyList) {
Log.d(TAG, " " + o);
}
handleInterfaceServingStateInactive(who);
if (mNotifyList.isEmpty()) {
turnOffMasterTetherSettings(); // transitions appropriately
} else {
if (DBG) {
Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
" live requests:");
for (TetherInterfaceStateMachine o : mNotifyList) {
Log.d(TAG, " " + o);
}
}
} else {
Log.e(TAG, "TetherModeAliveState UNREQUESTED has unknown who: " + who);
}
mIPv6TetheringCoordinator.removeActiveDownstream(who);
// If there has been a change and an upstream is no
// longer desired, release any mobile requests.
final boolean previousUpstreamWanted = updateUpstreamWanted();
if (previousUpstreamWanted && !mUpstreamWanted) {
mUpstreamNetworkMonitor.releaseMobileNetworkRequest();
}
break;
}
case CMD_UPSTREAM_CHANGED:
updateUpstreamWanted();
if (!mUpstreamWanted) break;
// Need to try DUN immediately if Wi-Fi goes down.
chooseUpstreamType(true);
mTryCell = false;
break;
case CMD_RETRY_UPSTREAM:
updateUpstreamWanted();
if (!mUpstreamWanted) break;
chooseUpstreamType(mTryCell);
mTryCell = !mTryCell;
break;
case EVENT_UPSTREAM_CALLBACK: {
updateUpstreamWanted();
if (!mUpstreamWanted) break;
final NetworkState ns = (NetworkState) message.obj;
if (ns == null || !pertainsToCurrentUpstream(ns)) {
@@ -1490,7 +1592,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
public boolean processMessage(Message message) {
boolean retValue = true;
switch (message.what) {
case CMD_TETHER_MODE_REQUESTED:
case EVENT_IFACE_SERVING_STATE_ACTIVE:
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
who.sendMessage(mErrorNotification);
break;
@@ -1604,12 +1706,16 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
case IControlsTethering.STATE_TETHERED:
pw.print("TetheredState");
break;
case IControlsTethering.STATE_LOCAL_HOTSPOT:
pw.print("LocalHotspotState");
break;
default:
pw.print("UnknownState");
break;
}
pw.println(" - lastError = " + tetherState.lastError);
}
pw.println("Upstream wanted: " + upstreamWanted());
pw.decreaseIndent();
}
pw.decreaseIndent();
@@ -1648,15 +1754,21 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
if (error == ConnectivityManager.TETHER_ERROR_MASTER_ERROR) {
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_CLEAR_ERROR, who);
}
int which;
switch (state) {
case IControlsTethering.STATE_UNAVAILABLE:
case IControlsTethering.STATE_AVAILABLE:
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who);
which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
break;
case IControlsTethering.STATE_TETHERED:
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, who);
case IControlsTethering.STATE_LOCAL_HOTSPOT:
which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
break;
default:
Log.wtf(TAG, "Unknown interface state: " + state);
return;
}
mTetherMasterSM.sendMessage(which, state, 0, who);
sendTetherStateChangedBroadcast();
}

View File

@@ -25,6 +25,7 @@ public interface IControlsTethering {
public final int STATE_UNAVAILABLE = 0;
public final int STATE_AVAILABLE = 1;
public final int STATE_TETHERED = 2;
public final int STATE_LOCAL_HOTSPOT = 3;
/**
* Notify that |who| has changed its tethering state. This may be called from any thread.

View File

@@ -24,12 +24,17 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkState;
import android.net.RouteInfo;
import android.net.util.NetworkConstants;
import android.util.Log;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random;
/**
@@ -45,29 +50,65 @@ public class IPv6TetheringCoordinator {
private static final boolean DBG = false;
private static final boolean VDBG = false;
private static class Downstream {
public final TetherInterfaceStateMachine tism;
public final int mode; // IControlsTethering.STATE_*
// Used to append to a ULA /48, constructing a ULA /64 for local use.
public final short subnetId;
Downstream(TetherInterfaceStateMachine tism, int mode, short subnetId) {
this.tism = tism;
this.mode = mode;
this.subnetId = subnetId;
}
}
private final ArrayList<TetherInterfaceStateMachine> mNotifyList;
private final LinkedList<TetherInterfaceStateMachine> mActiveDownstreams;
// NOTE: mActiveDownstreams is a list and not a hash data structure because
// we keep active downstreams in arrival order. This is done so /64s can
// be parceled out on a "first come, first served" basis and a /64 used by
// a downstream that is no longer active can be redistributed to any next
// waiting active downstream (again, in arrival order).
private final LinkedList<Downstream> mActiveDownstreams;
private final byte[] mUniqueLocalPrefix;
private short mNextSubnetId;
private NetworkState mUpstreamNetworkState;
public IPv6TetheringCoordinator(ArrayList<TetherInterfaceStateMachine> notifyList) {
mNotifyList = notifyList;
mActiveDownstreams = new LinkedList<>();
mUniqueLocalPrefix = generateUniqueLocalPrefix();
mNextSubnetId = 0;
}
public void addActiveDownstream(TetherInterfaceStateMachine downstream) {
if (mActiveDownstreams.indexOf(downstream) == -1) {
public void addActiveDownstream(TetherInterfaceStateMachine downstream, int mode) {
if (findDownstream(downstream) == null) {
// Adding a new downstream appends it to the list. Adding a
// downstream a second time without first removing it has no effect.
mActiveDownstreams.offer(downstream);
// We never change the mode of a downstream except by first removing
// it and then re-adding it (with its new mode specified);
if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) {
// Make sure subnet IDs are always positive. They are appended
// to a ULA /48 to make a ULA /64 for local use.
mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1);
}
updateIPv6TetheringInterfaces();
}
}
public void removeActiveDownstream(TetherInterfaceStateMachine downstream) {
stopIPv6TetheringOn(downstream);
if (mActiveDownstreams.remove(downstream)) {
if (mActiveDownstreams.remove(findDownstream(downstream))) {
updateIPv6TetheringInterfaces();
}
// When tethering is stopping we can reset the subnet counter.
if (mNotifyList.isEmpty()) {
if (!mActiveDownstreams.isEmpty()) {
Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty.");
}
mNextSubnetId = 0;
}
}
public void updateUpstreamNetworkState(NetworkState ns) {
@@ -123,20 +164,31 @@ public class IPv6TetheringCoordinator {
}
private LinkProperties getInterfaceIPv6LinkProperties(TetherInterfaceStateMachine sm) {
if (mUpstreamNetworkState == null) return null;
if (sm.interfaceType() == ConnectivityManager.TETHERING_BLUETOOTH) {
// TODO: Figure out IPv6 support on PAN interfaces.
return null;
}
final Downstream ds = findDownstream(sm);
if (ds == null) return null;
if (ds.mode == IControlsTethering.STATE_LOCAL_HOTSPOT) {
// Build a Unique Locally-assigned Prefix configuration.
return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
}
// This downstream is in IControlsTethering.STATE_TETHERED mode.
if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) {
return null;
}
// NOTE: Here, in future, we would have policies to decide how to divvy
// up the available dedicated prefixes among downstream interfaces.
// At this time we have no such mechanism--we only support tethering
// IPv6 toward the oldest (first requested) active downstream.
final TetherInterfaceStateMachine currentActive = mActiveDownstreams.peek();
if (currentActive != null && currentActive == sm) {
final Downstream currentActive = mActiveDownstreams.peek();
if (currentActive != null && currentActive.tism == sm) {
final LinkProperties lp = getIPv6OnlyLinkProperties(
mUpstreamNetworkState.linkProperties);
if (lp.hasIPv6DefaultRoute() && lp.hasGlobalIPv6Address()) {
@@ -147,6 +199,13 @@ public class IPv6TetheringCoordinator {
return null;
}
Downstream findDownstream(TetherInterfaceStateMachine tism) {
for (Downstream ds : mActiveDownstreams) {
if (ds.tism == tism) return ds;
}
return null;
}
private static boolean canTetherIPv6(NetworkState ns) {
// Broadly speaking:
//
@@ -263,6 +322,44 @@ public class IPv6TetheringCoordinator {
!ip.isMulticastAddress();
}
private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) {
final LinkProperties lp = new LinkProperties();
final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48);
lp.addRoute(new RouteInfo(local48, null, null));
final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64);
// Because this is a locally-generated ULA, we don't have an upstream
// address. But because the downstream IP address management code gets
// its prefix from the upstream's IP address, we create a fake one here.
lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64));
lp.setMtu(NetworkConstants.ETHER_MTU);
return lp;
}
private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) {
final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length);
bytes[7] = (byte) (subnetId >> 8);
bytes[8] = (byte) subnetId;
return new IpPrefix(bytes, prefixlen);
}
// Generates a Unique Locally-assigned Prefix:
//
// https://tools.ietf.org/html/rfc4193#section-3.1
//
// The result is a /48 that can be used for local-only communications.
private static byte[] generateUniqueLocalPrefix() {
final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte
(new Random()).nextBytes(ulp);
final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN);
in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1
return in6addr;
}
private static String toDebugString(NetworkState ns) {
if (ns == null) {
return "NetworkState{null}";

View File

@@ -42,6 +42,7 @@ import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Objects;
import java.util.Random;
/**
@@ -66,10 +67,17 @@ public class IPv6TetheringInterfaceServices {
}
public boolean start() {
// TODO: Refactor for testability (perhaps passing an android.system.Os
// instance and calling getifaddrs() directly).
try {
mNetworkInterface = NetworkInterface.getByName(mIfName);
} catch (SocketException e) {
Log.e(TAG, "Failed to find NetworkInterface for " + mIfName, e);
Log.e(TAG, "Error looking up NetworkInterfaces for " + mIfName, e);
stop();
return false;
}
if (mNetworkInterface == null) {
Log.e(TAG, "Failed to find NetworkInterface for " + mIfName);
stop();
return false;
}
@@ -267,10 +275,10 @@ public class IPv6TetheringInterfaceServices {
return localRoutes;
}
// Given a prefix like 2001:db8::/64 return 2001:db8::1.
// Given a prefix like 2001:db8::/64 return an address like 2001:db8::1.
private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) {
final byte[] dnsBytes = localPrefix.getRawAddress();
dnsBytes[dnsBytes.length - 1] = 0x1;
dnsBytes[dnsBytes.length - 1] = getRandomNonZeroByte();
try {
return Inet6Address.getByAddress(null, dnsBytes, 0);
} catch (UnknownHostException e) {
@@ -278,4 +286,11 @@ public class IPv6TetheringInterfaceServices {
return null;
}
}
private static byte getRandomNonZeroByte() {
final byte random = (byte) (new Random()).nextInt();
// Don't pick the subnet-router anycast address, since that might be
// in use on the upstream already.
return (random != 0) ? random : 0x1;
}
}

View File

@@ -78,6 +78,8 @@ public class TetherInterfaceStateMachine extends StateMachine {
public static final int CMD_IPV6_TETHER_UPDATE = BASE_IFACE + 13;
private final State mInitialState;
private final State mServingState;
private final State mLocalHotspotState;
private final State mTetheredState;
private final State mUnavailableState;
@@ -105,10 +107,14 @@ public class TetherInterfaceStateMachine extends StateMachine {
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
mInitialState = new InitialState();
addState(mInitialState);
mServingState = new ServingState();
mLocalHotspotState = new LocalHotspotState();
mTetheredState = new TetheredState();
addState(mTetheredState);
mUnavailableState = new UnavailableState();
addState(mInitialState);
addState(mServingState);
addState(mLocalHotspotState, mServingState);
addState(mTetheredState, mServingState);
addState(mUnavailableState);
setInitialState(mInitialState);
@@ -172,12 +178,15 @@ public class TetherInterfaceStateMachine extends StateMachine {
}
}
private void sendInterfaceState(int newInterfaceState) {
mTetherController.notifyInterfaceStateChange(
mIfaceName, TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
}
class InitialState extends State {
@Override
public void enter() {
mTetherController.notifyInterfaceStateChange(
mIfaceName, TetherInterfaceStateMachine.this,
IControlsTethering.STATE_AVAILABLE, mLastError);
sendInterfaceState(IControlsTethering.STATE_AVAILABLE);
}
@Override
@@ -187,7 +196,16 @@ public class TetherInterfaceStateMachine extends StateMachine {
switch (message.what) {
case CMD_TETHER_REQUESTED:
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
transitionTo(mTetheredState);
switch (message.arg1) {
case IControlsTethering.STATE_LOCAL_HOTSPOT:
transitionTo(mLocalHotspotState);
break;
case IControlsTethering.STATE_TETHERED:
transitionTo(mTetheredState);
break;
default:
Log.e(TAG, "Invalid tethering interface serving state specified.");
}
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
@@ -204,7 +222,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
}
}
class TetheredState extends State {
class ServingState extends State {
@Override
public void enter() {
if (!configureIfaceIp(true)) {
@@ -225,11 +243,6 @@ public class TetherInterfaceStateMachine extends StateMachine {
if (!mIPv6TetherSvc.start()) {
Log.e(TAG, "Failed to start IPv6TetheringInterfaceServices");
}
if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
mTetherController.notifyInterfaceStateChange(
mIfaceName, TetherInterfaceStateMachine.this,
IControlsTethering.STATE_TETHERED, mLastError);
}
@Override
@@ -238,7 +251,6 @@ public class TetherInterfaceStateMachine extends StateMachine {
// of these operations, but it doesn't really change that we have to try them
// all in sequence.
mIPv6TetherSvc.stop();
cleanupUpstream();
try {
mNMService.untetherInterface(mIfaceName);
@@ -250,6 +262,73 @@ public class TetherInterfaceStateMachine extends StateMachine {
configureIfaceIp(false);
}
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
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_IPV6_TETHER_UPDATE:
mIPv6TetherSvc.updateUpstreamIPv6LinkProperties(
(LinkProperties) message.obj);
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:
mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
transitionTo(mInitialState);
break;
default:
return false;
}
return true;
}
}
class LocalHotspotState extends State {
@Override
public void enter() {
if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
sendInterfaceState(IControlsTethering.STATE_LOCAL_HOTSPOT);
}
@Override
public boolean processMessage(Message message) {
maybeLogMessage(this, message.what);
switch (message.what) {
case CMD_TETHER_REQUESTED:
Log.e(TAG, "CMD_TETHER_REQUESTED while in local hotspot mode.");
break;
case CMD_TETHER_CONNECTION_CHANGED:
// Ignored in local hotspot state.
break;
default:
return false;
}
return true;
}
}
class TetheredState extends State {
@Override
public void enter() {
if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
sendInterfaceState(IControlsTethering.STATE_TETHERED);
}
@Override
public void exit() {
cleanupUpstream();
}
private void cleanupUpstream() {
if (mMyUpstreamIfaceName == null) return;
@@ -285,13 +364,8 @@ public class TetherInterfaceStateMachine extends StateMachine {
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);
case CMD_TETHER_REQUESTED:
Log.e(TAG, "CMD_TETHER_REQUESTED while already tethering.");
break;
case CMD_TETHER_CONNECTION_CHANGED:
String newUpstreamIfaceName = (String)(message.obj);
@@ -317,18 +391,6 @@ public class TetherInterfaceStateMachine extends StateMachine {
}
mMyUpstreamIfaceName = newUpstreamIfaceName;
break;
case CMD_IPV6_TETHER_UPDATE:
mIPv6TetherSvc.updateUpstreamIPv6LinkProperties(
(LinkProperties) message.obj);
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:
mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
transitionTo(mInitialState);
break;
default:
retValue = false;
break;
@@ -348,9 +410,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
@Override
public void enter() {
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
mTetherController.notifyInterfaceStateChange(
mIfaceName, TetherInterfaceStateMachine.this,
IControlsTethering.STATE_UNAVAILABLE, mLastError);
sendInterfaceState(IControlsTethering.STATE_UNAVAILABLE);
}
}
}

View File

@@ -308,7 +308,8 @@ public class UpstreamNetworkMonitor {
// Fetch (and cache) a ConnectivityManager only if and when we need one.
private ConnectivityManager cm() {
if (mCM == null) {
mCM = mContext.getSystemService(ConnectivityManager.class);
// MUST call the String variant to be able to write unittests.
mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
}
return mCM;
}

View File

@@ -16,6 +16,8 @@
package android.net.ip;
import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
import static android.system.OsConstants.*;
import android.net.IpPrefix;
@@ -68,7 +70,6 @@ public class RouterAdvertisementDaemon {
private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName();
private static final byte ICMPV6_ND_ROUTER_SOLICIT = asByte(133);
private static final byte ICMPV6_ND_ROUTER_ADVERT = asByte(134);
private static final int IPV6_MIN_MTU = 1280;
private static final int MIN_RA_HEADER_SIZE = 16;
// Summary of various timers and lifetimes.
@@ -542,6 +543,14 @@ public class RouterAdvertisementDaemon {
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
final HashSet<Inet6Address> filteredDnses = new HashSet<>();
for (Inet6Address dns : dnses) {
if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) {
filteredDnses.add(dns);
}
}
if (filteredDnses.isEmpty()) return;
final byte ND_OPTION_RDNSS = 25;
final byte RDNSS_NUM_8OCTETS = asByte(dnses.size() * 2 + 1);
ra.put(ND_OPTION_RDNSS)
@@ -549,7 +558,7 @@ public class RouterAdvertisementDaemon {
.putShort(asShort(0))
.putInt(lifetime);
for (Inet6Address dns : dnses) {
for (Inet6Address dns : filteredDnses) {
// NOTE: If the full of list DNS servers doesn't fit in the packet,
// this code will cause a buffer overflow and the RA won't include
// this instance of the option at all.

View File

@@ -36,6 +36,7 @@ public final class NetworkConstants {
*
* See also:
* - https://tools.ietf.org/html/rfc894
* - https://tools.ietf.org/html/rfc2464
* - https://tools.ietf.org/html/rfc7042
* - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
* - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
@@ -57,6 +58,8 @@ public final class NetworkConstants {
FF, FF, FF, FF, FF, FF
};
public static final int ETHER_MTU = 1500;
/**
* ARP constants.
*
@@ -97,6 +100,7 @@ public final class NetworkConstants {
public static final int IPV6_SRC_ADDR_OFFSET = 8;
public static final int IPV6_DST_ADDR_OFFSET = 24;
public static final int IPV6_ADDR_LEN = 16;
public static final int IPV6_MIN_MTU = 1280;
public static final int RFC7421_PREFIX_LENGTH = 64;
/**

View File

@@ -16,22 +16,43 @@
package com.android.server.connectivity;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.Resources;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
import android.net.InterfaceConfiguration;
import android.net.NetworkRequest;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.INetworkManagementService;
import android.os.PersistableBundle;
import android.os.test.TestLooper;
import android.os.UserHandle;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.telephony.CarrierConfigManager;
import com.android.internal.util.test.BroadcastInterceptingContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,34 +65,60 @@ public class TetheringTest {
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
@Mock private Context mContext;
@Mock private ConnectivityManager mConnectivityManager;
@Mock private INetworkManagementService mNMService;
@Mock private INetworkStatsService mStatsService;
@Mock private INetworkPolicyManager mPolicyManager;
@Mock private MockableSystemProperties mSystemProperties;
@Mock private Resources mResources;
@Mock private UsbManager mUsbManager;
@Mock private WifiManager mWifiManager;
@Mock private CarrierConfigManager mCarrierConfigManager;
// Like so many Android system APIs, these cannot be mocked because it is marked final.
// We have to use the real versions.
private final PersistableBundle mCarrierConfig = new PersistableBundle();
private final TestLooper mLooper = new TestLooper();
private final String mTestIfname = "test_wlan0";
private BroadcastInterceptingContext mServiceContext;
private Tethering mTethering;
private class MockContext extends BroadcastInterceptingContext {
MockContext(Context base) {
super(base);
}
@Override
public Resources getResources() { return mResources; }
@Override
public Object getSystemService(String name) {
if (Context.CONNECTIVITY_SERVICE.equals(name)) return mConnectivityManager;
if (Context.WIFI_SERVICE.equals(name)) return mWifiManager;
return super.getSystemService(name);
}
}
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mContext.getResources()).thenReturn(mResources);
when(mResources.getStringArray(com.android.internal.R.array.config_tether_dhcp_range))
.thenReturn(new String[0]);
when(mResources.getStringArray(com.android.internal.R.array.config_tether_usb_regexs))
.thenReturn(new String[0]);
when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_regexs))
.thenReturn(new String[0]);
.thenReturn(new String[]{ "test_wlan\\d" });
when(mResources.getStringArray(com.android.internal.R.array.config_tether_bluetooth_regexs))
.thenReturn(new String[0]);
when(mResources.getIntArray(com.android.internal.R.array.config_tether_upstream_types))
.thenReturn(new int[0]);
mTethering = new Tethering(mContext, mNMService, mStatsService, mPolicyManager,
when(mNMService.listInterfaces())
.thenReturn(new String[]{ "test_rmnet_data0", mTestIfname });
when(mNMService.getInterfaceConfig(anyString()))
.thenReturn(new InterfaceConfiguration());
mServiceContext = new MockContext(mContext);
mTethering = new Tethering(mServiceContext, mNMService, mStatsService, mPolicyManager,
mLooper.getLooper(), mSystemProperties);
}
@@ -126,4 +173,144 @@ public class TetheringTest {
.thenReturn(new String[] {"malformedApp"});
assertTrue(!mTethering.isTetherProvisioningRequired());
}
private void sendWifiApStateChanged(int state) {
final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
intent.putExtra(WifiManager.EXTRA_WIFI_AP_STATE, state);
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
@Test
public void workingLocalOnlyHotspot() throws Exception {
when(mConnectivityManager.isTetheringSupported()).thenReturn(true);
when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean()))
.thenReturn(true);
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
mTethering.interfaceStatusChanged(mTestIfname, true);
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED);
mLooper.dispatchAll();
verify(mNMService, times(1)).listInterfaces();
verify(mNMService, times(1)).getInterfaceConfig(mTestIfname);
verify(mNMService, times(1))
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
verify(mNMService, times(1)).tetherInterface(mTestIfname);
verify(mNMService, times(1)).setIpForwardingEnabled(true);
verify(mNMService, times(1)).startTethering(any(String[].class));
verifyNoMoreInteractions(mNMService);
// UpstreamNetworkMonitor will be started, and will register two callbacks:
// a "listen all" and a "track default".
verify(mConnectivityManager, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback(
any(NetworkCallback.class), any(Handler.class));
// TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast().
verify(mConnectivityManager, atLeastOnce()).isTetheringSupported();
verifyNoMoreInteractions(mConnectivityManager);
// Emulate externally-visible WifiManager effects, when hotspot mode
// is being torn down.
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
mTethering.interfaceRemoved(mTestIfname);
mLooper.dispatchAll();
verify(mNMService, times(1)).untetherInterface(mTestIfname);
// TODO: Why is {g,s}etInterfaceConfig() called more than once?
verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname);
verify(mNMService, atLeastOnce())
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
verify(mNMService, times(1)).stopTethering();
verify(mNMService, times(1)).setIpForwardingEnabled(false);
verifyNoMoreInteractions(mNMService);
// Asking for the last error after the per-interface state machine
// has been reaped yields an unknown interface error.
assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE,
mTethering.getLastTetherError(mTestIfname));
}
@Test
public void workingWifiTethering() throws Exception {
when(mConnectivityManager.isTetheringSupported()).thenReturn(true);
when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean()))
.thenReturn(true);
// Emulate pressing the WiFi tethering button.
mTethering.startTethering(ConnectivityManager.TETHERING_WIFI, null, false);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).setWifiApEnabled(null, true);
verifyNoMoreInteractions(mWifiManager);
verifyNoMoreInteractions(mConnectivityManager);
verifyNoMoreInteractions(mNMService);
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// tethering mode is to be started.
mTethering.interfaceStatusChanged(mTestIfname, true);
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED);
mLooper.dispatchAll();
verify(mNMService, times(1)).listInterfaces();
verify(mNMService, times(1)).getInterfaceConfig(mTestIfname);
verify(mNMService, times(1))
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
verify(mNMService, times(1)).tetherInterface(mTestIfname);
verify(mNMService, times(1)).setIpForwardingEnabled(true);
verify(mNMService, times(1)).startTethering(any(String[].class));
verifyNoMoreInteractions(mNMService);
// UpstreamNetworkMonitor will be started, and will register two callbacks:
// a "listen all" and a "track default".
verify(mConnectivityManager, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback(
any(NetworkCallback.class), any(Handler.class));
// In tethering mode, in the default configuration, an explicit request
// for a mobile network is also made.
verify(mConnectivityManager, atLeastOnce()).getNetworkInfo(anyInt());
verify(mConnectivityManager, times(1)).requestNetwork(
any(NetworkRequest.class), any(NetworkCallback.class), eq(0), anyInt(),
any(Handler.class));
// TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast().
verify(mConnectivityManager, atLeastOnce()).isTetheringSupported();
verifyNoMoreInteractions(mConnectivityManager);
/////
// We do not currently emulate any upstream being found.
//
// This is why there are no calls to verify mNMService.enableNat() or
// mNMService.startInterfaceForwarding().
/////
// Emulate pressing the WiFi tethering button.
mTethering.stopTethering(ConnectivityManager.TETHERING_WIFI);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).setWifiApEnabled(null, false);
verifyNoMoreInteractions(mWifiManager);
verifyNoMoreInteractions(mConnectivityManager);
verifyNoMoreInteractions(mNMService);
// Emulate externally-visible WifiManager effects, when tethering mode
// is being torn down.
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
mTethering.interfaceRemoved(mTestIfname);
mLooper.dispatchAll();
verify(mNMService, times(1)).untetherInterface(mTestIfname);
// TODO: Why is {g,s}etInterfaceConfig() called more than once?
verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname);
verify(mNMService, atLeastOnce())
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
verify(mNMService, times(1)).stopTethering();
verify(mNMService, times(1)).setIpForwardingEnabled(false);
verifyNoMoreInteractions(mNMService);
// Asking for the last error after the per-interface state machine
// has been reaped yields an unknown interface error.
assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE,
mTethering.getLastTetherError(mTestIfname));
}
// TODO: Test that a request for hotspot mode doesn't interface with an
// already operating tethering mode interface.
}

View File

@@ -32,6 +32,7 @@ import static android.net.ConnectivityManager.TETHERING_BLUETOOTH;
import static android.net.ConnectivityManager.TETHERING_USB;
import static android.net.ConnectivityManager.TETHERING_WIFI;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_AVAILABLE;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_LOCAL_HOTSPOT;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_TETHERED;
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_UNAVAILABLE;
@@ -80,7 +81,7 @@ public class TetherInterfaceStateMachineTest {
private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception {
initStateMachine(interfaceType);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
if (upstreamIface != null) {
dispatchTetherConnectionChanged(upstreamIface);
}
@@ -138,7 +139,7 @@ public class TetherInterfaceStateMachineTest {
public void canBeTethered() throws Exception {
initStateMachine(TETHERING_BLUETOOTH);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder inOrder = inOrder(mTetherHelper, mNMService);
inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
@@ -162,7 +163,7 @@ public class TetherInterfaceStateMachineTest {
public void canBeTetheredAsUsb() throws Exception {
initStateMachine(TETHERING_USB);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder inOrder = inOrder(mTetherHelper, mNMService);
inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
@@ -272,7 +273,7 @@ public class TetherInterfaceStateMachineTest {
initStateMachine(TETHERING_USB);
doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
usbTeardownOrder.verify(mNMService).setInterfaceConfig(
@@ -306,6 +307,17 @@ public class TetherInterfaceStateMachineTest {
}
}
/**
* Send a command to the state machine under test, and run the event loop to idle.
*
* @param command One of the TetherInterfaceStateMachine.CMD_* constants.
* @param obj An additional argument to pass.
*/
private void dispatchCommand(int command, int arg1) {
mTestedSm.sendMessage(command, arg1);
mLooper.dispatchAll();
}
/**
* Send a command to the state machine under test, and run the event loop to idle.
*