Tethering: support Local-only Hotspot mode for downstreams
Test: as follows
- built (bullhead)
- flashed
- booted
- "runtest frameworks-net" passes
Bug: 31466854
Change-Id: Ia50e28c8ce0af8cdd7ac63217d921aff213668e7
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 3bf55965..76c895c 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -90,7 +90,9 @@
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 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 @@
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 @@
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 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 @@
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 @@
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 @@
}
}
+ // TODO: Figure out how to update for local hotspot mode interfaces.
private void sendTetherStateChangedBroadcast() {
if (!getConnectivityManager().isTetheringSupported()) return;
@@ -728,7 +755,9 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
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 @@
} 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 @@
}
}
+ 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 @@
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 @@
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 @@
}
}
+ 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 @@
class TetherModeAliveState extends TetherMasterUtilState {
final SimChangeListener simChange = new SimChangeListener(mContext);
+ boolean mUpstreamWanted = false;
boolean mTryCell = true;
@Override
@@ -1366,9 +1446,11 @@
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 @@
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 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 @@
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 @@
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();
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
index 449b8a8..f3914b7 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java
@@ -25,6 +25,7 @@
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.
diff --git a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
index 9173feb..5f496ca 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
@@ -24,12 +24,17 @@
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 @@
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 @@
}
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 @@
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 @@
!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}";
diff --git a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java
index 8c6430c..c6a7925 100644
--- a/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java
+++ b/services/core/java/com/android/server/connectivity/tethering/IPv6TetheringInterfaceServices.java
@@ -42,6 +42,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Objects;
+import java.util.Random;
/**
@@ -66,10 +67,17 @@
}
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 @@
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 @@
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;
+ }
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
index 710ab33..1ffa864 100644
--- a/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
+++ b/services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -78,6 +78,8 @@
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 @@
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 @@
}
}
+ 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 @@
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 @@
}
}
- class TetheredState extends State {
+ class ServingState extends State {
@Override
public void enter() {
if (!configureIfaceIp(true)) {
@@ -225,11 +243,6 @@
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 @@
// 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 @@
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 @@
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 @@
}
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 @@
@Override
public void enter() {
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
- mTetherController.notifyInterfaceStateChange(
- mIfaceName, TetherInterfaceStateMachine.this,
- IControlsTethering.STATE_UNAVAILABLE, mLastError);
+ sendInterfaceState(IControlsTethering.STATE_UNAVAILABLE);
}
}
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
index 6209929..97a2d5e 100644
--- a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -308,7 +308,8 @@
// 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;
}
diff --git a/services/net/java/android/net/ip/RouterAdvertisementDaemon.java b/services/net/java/android/net/ip/RouterAdvertisementDaemon.java
index 6802cff..25d3329 100644
--- a/services/net/java/android/net/ip/RouterAdvertisementDaemon.java
+++ b/services/net/java/android/net/ip/RouterAdvertisementDaemon.java
@@ -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 @@
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 @@
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
+ 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 @@
.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.
diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java
index 26f3050..a012e0c 100644
--- a/services/net/java/android/net/util/NetworkConstants.java
+++ b/services/net/java/android/net/util/NetworkConstants.java
@@ -36,6 +36,7 @@
*
* 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 @@
FF, FF, FF, FF, FF, FF
};
+ public static final int ETHER_MTU = 1500;
+
/**
* ARP constants.
*
@@ -97,6 +100,7 @@
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;
/**
diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java
index a9f68c8..e527d57 100644
--- a/tests/net/java/com/android/server/connectivity/TetheringTest.java
+++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java
@@ -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 @@
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 @@
.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.
}
diff --git a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
index 32e1b96..caf1a55 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachineTest.java
@@ -32,6 +32,7 @@
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 @@
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 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 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 @@
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(
@@ -310,6 +311,17 @@
* 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.
+ *
+ * @param command One of the TetherInterfaceStateMachine.CMD_* constants.
*/
private void dispatchCommand(int command) {
mTestedSm.sendMessage(command);