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

Change-Id: I6e72a9b90e59ea5abd1dfa11c6c3d16a5c98751c
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);