diff options
author | 2017-04-10 14:57:45 +0000 | |
---|---|---|
committer | 2017-04-10 14:57:49 +0000 | |
commit | e9ec87d0d40bdd1fa659f217732fc39a7a6e9f9b (patch) | |
tree | 196dc64a30a9b27516dd8a11cd946acac0fce458 | |
parent | 9c80a16c458e719fcbdd13ce35caa00ba5874796 (diff) | |
parent | 5f2b7992cfe1149de115c6f7056d0da128b8afba (diff) |
Merge "Tethering: support Local-only Hotspot mode for downstreams" into oc-dev
10 files changed, 605 insertions, 107 deletions
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java index 0e593bd02629..dad969cadf47 100644 --- a/services/core/java/com/android/server/connectivity/Tethering.java +++ b/services/core/java/com/android/server/connectivity/Tethering.java @@ -93,7 +93,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -125,12 +127,23 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering public final TetherInterfaceStateMachine stateMachine; public int lastState; public int lastError; + public TetherState(TetherInterfaceStateMachine sm) { stateMachine = sm; // Assume all state machines start out available and with no errors. lastState = IControlsTethering.STATE_AVAILABLE; lastError = ConnectivityManager.TETHER_ERROR_NO_ERROR; } + + public boolean isCurrentlyServing() { + switch (lastState) { + case IControlsTethering.STATE_TETHERED: + case IControlsTethering.STATE_LOCAL_HOTSPOT: + return true; + default: + return false; + } + } } // used to synchronize public access to members @@ -146,11 +159,13 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering private final StateMachine mTetherMasterSM; private final OffloadController mOffloadController; private final UpstreamNetworkMonitor mUpstreamNetworkMonitor; + private final HashSet<TetherInterfaceStateMachine> mForwardedDownstreams; private volatile TetheringConfiguration mConfig; private String mCurrentUpstreamIface; private Notification.Builder mTetheredNotificationBuilder; private int mLastNotificationId; + private boolean mRndisEnabled; // track the RNDIS function enabled state private boolean mUsbTetherRequested; // true if USB tethering should be started // when RNDIS is enabled @@ -177,6 +192,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering mOffloadController = new OffloadController(mTetherMasterSM.getHandler()); mUpstreamNetworkMonitor = new UpstreamNetworkMonitor( mContext, mTetherMasterSM, TetherMasterSM.EVENT_UPSTREAM_CALLBACK); + mForwardedDownstreams = new HashSet<>(); mStateReceiver = new StateReceiver(); IntentFilter filter = new IntentFilter(); @@ -514,6 +530,10 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering } public int tether(String iface) { + return tether(iface, IControlsTethering.STATE_TETHERED); + } + + private int tether(String iface, int requestedState) { if (DBG) Log.d(TAG, "Tethering " + iface); synchronized (mPublicSync) { TetherState tetherState = mTetherStates.get(iface); @@ -527,7 +547,13 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring"); return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE; } - tetherState.stateMachine.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); + // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's + // queue but not yet processed, this will be a no-op and it will not + // return an error. + // + // TODO: reexamine the threading and messaging model. + tetherState.stateMachine.sendMessage( + TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, requestedState); return ConnectivityManager.TETHER_ERROR_NO_ERROR; } } @@ -540,8 +566,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring"); return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE; } - if (tetherState.lastState != IControlsTethering.STATE_TETHERED) { - Log.e(TAG, "Tried to untether an untethered iface :" + iface + ", ignoring"); + if (!tetherState.isCurrentlyServing()) { + Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring"); return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE; } tetherState.stateMachine.sendMessage( @@ -568,6 +594,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering } } + // TODO: Figure out how to update for local hotspot mode interfaces. private void sendTetherStateChangedBroadcast() { if (!getConnectivityManager().isTetheringSupported()) return; @@ -745,7 +772,9 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering mRndisEnabled = rndisEnabled; // start tethering if we have a request pending if (usbConnected && mRndisEnabled && mUsbTetherRequested) { - tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB); + tetherMatchingInterfaces( + IControlsTethering.STATE_TETHERED, + ConnectivityManager.TETHERING_USB); } mUsbTetherRequested = false; } @@ -760,9 +789,11 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering break; case WifiManager.WIFI_AP_STATE_ENABLED: // When the AP comes up and we've been requested to tether it, do so. - if (mWifiTetherRequested) { - tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_WIFI); - } + // Otherwise, assume it's a local-only hotspot request. + final int state = mWifiTetherRequested + ? IControlsTethering.STATE_TETHERED + : IControlsTethering.STATE_LOCAL_HOTSPOT; + tetherMatchingInterfaces(state, ConnectivityManager.TETHERING_WIFI); break; case WifiManager.WIFI_AP_STATE_DISABLED: case WifiManager.WIFI_AP_STATE_DISABLING: @@ -792,8 +823,16 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering } } - private void tetherMatchingInterfaces(boolean enable, int interfaceType) { - if (VDBG) Log.d(TAG, "tetherMatchingInterfaces(" + enable + ", " + interfaceType + ")"); + // TODO: Consider renaming to something more accurate in its description. + // This method: + // - allows requesting either tethering or local hotspot serving states + // - handles both enabling and disabling serving states + // - only tethers the first matching interface in listInterfaces() + // order of a given type + private void tetherMatchingInterfaces(int requestedState, int interfaceType) { + if (VDBG) { + Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")"); + } String[] ifaces = null; try { @@ -816,7 +855,20 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering return; } - int result = (enable ? tether(chosenIface) : untether(chosenIface)); + final int result; + switch (requestedState) { + case IControlsTethering.STATE_UNAVAILABLE: + case IControlsTethering.STATE_AVAILABLE: + result = untether(chosenIface); + break; + case IControlsTethering.STATE_TETHERED: + case IControlsTethering.STATE_LOCAL_HOTSPOT: + result = tether(chosenIface, requestedState); + break; + default: + Log.wtf(TAG, "Unknown interface state: " + requestedState); + return; + } if (result != ConnectivityManager.TETHER_ERROR_NO_ERROR) { Log.e(TAG, "unable start or stop tethering on iface " + chosenIface); return; @@ -861,7 +913,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering if (mRndisEnabled) { final long ident = Binder.clearCallingIdentity(); try { - tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB); + tetherMatchingInterfaces(IControlsTethering.STATE_TETHERED, + ConnectivityManager.TETHERING_USB); } finally { Binder.restoreCallingIdentity(ident); } @@ -872,7 +925,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering } else { final long ident = Binder.clearCallingIdentity(); try { - tetherMatchingInterfaces(false, ConnectivityManager.TETHERING_USB); + tetherMatchingInterfaces(IControlsTethering.STATE_AVAILABLE, + ConnectivityManager.TETHERING_USB); } finally { Binder.restoreCallingIdentity(ident); } @@ -936,6 +990,14 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering } } + private boolean upstreamWanted() { + if (!mForwardedDownstreams.isEmpty()) return true; + + synchronized (mPublicSync) { + return mUsbTetherRequested || mWifiTetherRequested; + } + } + // Needed because the canonical source of upstream truth is just the // upstream interface name, |mCurrentUpstreamIface|. This is ripe for // future simplification, once the upstream Network is canonical. @@ -952,10 +1014,10 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering class TetherMasterSM extends StateMachine { private static final int BASE_MASTER = Protocol.BASE_TETHERING; - // an interface SM has requested Tethering - static final int CMD_TETHER_MODE_REQUESTED = BASE_MASTER + 1; - // an interface SM has unrequested Tethering - static final int CMD_TETHER_MODE_UNREQUESTED = BASE_MASTER + 2; + // an interface SM has requested Tethering/Local Hotspot + static final int EVENT_IFACE_SERVING_STATE_ACTIVE = BASE_MASTER + 1; + // an interface SM has unrequested Tethering/Local Hotspot + static final int EVENT_IFACE_SERVING_STATE_INACTIVE = BASE_MASTER + 2; // upstream connection change - do the right thing static final int CMD_UPSTREAM_CHANGED = BASE_MASTER + 3; // we don't have a valid upstream conn, check again after a delay @@ -1040,7 +1102,9 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering transitionTo(mSetIpForwardingEnabledErrorState); return false; } + // TODO: Randomize DHCPv4 ranges, especially in hotspot mode. try { + // TODO: Find a more accurate method name (startDHCPv4()?). mNMService.startTethering(cfg.dhcpRanges); } catch (Exception e) { try { @@ -1342,26 +1406,41 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering } } + private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) { + if (mNotifyList.indexOf(who) < 0) { + mNotifyList.add(who); + mIPv6TetheringCoordinator.addActiveDownstream(who, mode); + } + + if (mode == IControlsTethering.STATE_TETHERED) { + mForwardedDownstreams.add(who); + } else { + mForwardedDownstreams.remove(who); + } + } + + private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) { + mNotifyList.remove(who); + mIPv6TetheringCoordinator.removeActiveDownstream(who); + mForwardedDownstreams.remove(who); + } + class InitialState extends TetherMasterUtilState { @Override public boolean processMessage(Message message) { maybeLogMessage(this, message.what); boolean retValue = true; switch (message.what) { - case CMD_TETHER_MODE_REQUESTED: + case EVENT_IFACE_SERVING_STATE_ACTIVE: TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj; if (VDBG) Log.d(TAG, "Tether Mode requested by " + who); - if (mNotifyList.indexOf(who) < 0) { - mNotifyList.add(who); - mIPv6TetheringCoordinator.addActiveDownstream(who); - } + handleInterfaceServingStateActive(message.arg1, who); transitionTo(mTetherModeAliveState); break; - case CMD_TETHER_MODE_UNREQUESTED: + case EVENT_IFACE_SERVING_STATE_INACTIVE: who = (TetherInterfaceStateMachine)message.obj; if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who); - mNotifyList.remove(who); - mIPv6TetheringCoordinator.removeActiveDownstream(who); + handleInterfaceServingStateInactive(who); break; default: retValue = false; @@ -1373,6 +1452,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering class TetherModeAliveState extends TetherMasterUtilState { final SimChangeListener simChange = new SimChangeListener(mContext); + boolean mUpstreamWanted = false; boolean mTryCell = true; @Override @@ -1383,9 +1463,11 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering mUpstreamNetworkMonitor.start(); mOffloadController.start(); - // Better try something first pass or crazy tests cases will fail. - chooseUpstreamType(true); - mTryCell = false; + if (upstreamWanted()) { + mUpstreamWanted = true; + chooseUpstreamType(true); + mTryCell = false; + } } @Override @@ -1398,54 +1480,74 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering handleNewUpstreamNetworkState(null); } + private boolean updateUpstreamWanted() { + final boolean previousUpstreamWanted = mUpstreamWanted; + mUpstreamWanted = upstreamWanted(); + return previousUpstreamWanted; + } + @Override public boolean processMessage(Message message) { maybeLogMessage(this, message.what); boolean retValue = true; switch (message.what) { - case CMD_TETHER_MODE_REQUESTED: { + case EVENT_IFACE_SERVING_STATE_ACTIVE: { TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj; if (VDBG) Log.d(TAG, "Tether Mode requested by " + who); - if (mNotifyList.indexOf(who) < 0) { - mNotifyList.add(who); - mIPv6TetheringCoordinator.addActiveDownstream(who); - } + handleInterfaceServingStateActive(message.arg1, who); who.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED, mCurrentUpstreamIface); + // If there has been a change and an upstream is now + // desired, kick off the selection process. + final boolean previousUpstreamWanted = updateUpstreamWanted(); + if (!previousUpstreamWanted && mUpstreamWanted) { + chooseUpstreamType(true); + } break; } - case CMD_TETHER_MODE_UNREQUESTED: { + case EVENT_IFACE_SERVING_STATE_INACTIVE: { TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj; if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who); - if (mNotifyList.remove(who)) { - if (DBG) Log.d(TAG, "TetherModeAlive removing notifyee " + who); - if (mNotifyList.isEmpty()) { - turnOffMasterTetherSettings(); // transitions appropriately - } else { - if (DBG) { - Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() + - " live requests:"); - for (TetherInterfaceStateMachine o : mNotifyList) { - Log.d(TAG, " " + o); - } + handleInterfaceServingStateInactive(who); + + if (mNotifyList.isEmpty()) { + turnOffMasterTetherSettings(); // transitions appropriately + } else { + if (DBG) { + Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() + + " live requests:"); + for (TetherInterfaceStateMachine o : mNotifyList) { + Log.d(TAG, " " + o); } } - } else { - Log.e(TAG, "TetherModeAliveState UNREQUESTED has unknown who: " + who); } - mIPv6TetheringCoordinator.removeActiveDownstream(who); + // If there has been a change and an upstream is no + // longer desired, release any mobile requests. + final boolean previousUpstreamWanted = updateUpstreamWanted(); + if (previousUpstreamWanted && !mUpstreamWanted) { + mUpstreamNetworkMonitor.releaseMobileNetworkRequest(); + } break; } case CMD_UPSTREAM_CHANGED: + updateUpstreamWanted(); + if (!mUpstreamWanted) break; + // Need to try DUN immediately if Wi-Fi goes down. chooseUpstreamType(true); mTryCell = false; break; case CMD_RETRY_UPSTREAM: + updateUpstreamWanted(); + if (!mUpstreamWanted) break; + chooseUpstreamType(mTryCell); mTryCell = !mTryCell; break; case EVENT_UPSTREAM_CALLBACK: { + updateUpstreamWanted(); + if (!mUpstreamWanted) break; + final NetworkState ns = (NetworkState) message.obj; if (ns == null || !pertainsToCurrentUpstream(ns)) { @@ -1507,7 +1609,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering public boolean processMessage(Message message) { boolean retValue = true; switch (message.what) { - case CMD_TETHER_MODE_REQUESTED: + case EVENT_IFACE_SERVING_STATE_ACTIVE: TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj; who.sendMessage(mErrorNotification); break; @@ -1615,12 +1717,16 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering case IControlsTethering.STATE_TETHERED: pw.print("TetheredState"); break; + case IControlsTethering.STATE_LOCAL_HOTSPOT: + pw.print("LocalHotspotState"); + break; default: pw.print("UnknownState"); break; } pw.println(" - lastError = " + tetherState.lastError); } + pw.println("Upstream wanted: " + upstreamWanted()); pw.decreaseIndent(); } pw.decreaseIndent(); @@ -1659,15 +1765,21 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering if (error == ConnectivityManager.TETHER_ERROR_MASTER_ERROR) { mTetherMasterSM.sendMessage(TetherMasterSM.CMD_CLEAR_ERROR, who); } + int which; switch (state) { case IControlsTethering.STATE_UNAVAILABLE: case IControlsTethering.STATE_AVAILABLE: - mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who); + which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_INACTIVE; break; case IControlsTethering.STATE_TETHERED: - mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, who); + case IControlsTethering.STATE_LOCAL_HOTSPOT: + which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_ACTIVE; break; + default: + Log.wtf(TAG, "Unknown interface state: " + state); + return; } + mTetherMasterSM.sendMessage(which, state, 0, who); sendTetherStateChangedBroadcast(); } 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 449b8a8354e7..f3914b7cb299 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 interface IControlsTethering { public final int STATE_UNAVAILABLE = 0; public final int STATE_AVAILABLE = 1; public final int STATE_TETHERED = 2; + public final int STATE_LOCAL_HOTSPOT = 3; /** * Notify that |who| has changed its tethering state. This may be called from any thread. 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 9173febd0ef3..5f496ca59e6a 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.Network; import android.net.NetworkCapabilities; import android.net.NetworkState; import android.net.RouteInfo; +import android.net.util.NetworkConstants; import android.util.Log; import java.net.Inet6Address; import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; +import java.util.Random; /** @@ -45,29 +50,65 @@ public class IPv6TetheringCoordinator { private static final boolean DBG = false; private static final boolean VDBG = false; + private static class Downstream { + public final TetherInterfaceStateMachine tism; + public final int mode; // IControlsTethering.STATE_* + // Used to append to a ULA /48, constructing a ULA /64 for local use. + public final short subnetId; + + Downstream(TetherInterfaceStateMachine tism, int mode, short subnetId) { + this.tism = tism; + this.mode = mode; + this.subnetId = subnetId; + } + } + private final ArrayList<TetherInterfaceStateMachine> mNotifyList; - private final LinkedList<TetherInterfaceStateMachine> mActiveDownstreams; + // NOTE: mActiveDownstreams is a list and not a hash data structure because + // we keep active downstreams in arrival order. This is done so /64s can + // be parceled out on a "first come, first served" basis and a /64 used by + // a downstream that is no longer active can be redistributed to any next + // waiting active downstream (again, in arrival order). + private final LinkedList<Downstream> mActiveDownstreams; + private final byte[] mUniqueLocalPrefix; + private short mNextSubnetId; private NetworkState mUpstreamNetworkState; public IPv6TetheringCoordinator(ArrayList<TetherInterfaceStateMachine> notifyList) { mNotifyList = notifyList; mActiveDownstreams = new LinkedList<>(); + mUniqueLocalPrefix = generateUniqueLocalPrefix(); + mNextSubnetId = 0; } - public void addActiveDownstream(TetherInterfaceStateMachine downstream) { - if (mActiveDownstreams.indexOf(downstream) == -1) { + public void addActiveDownstream(TetherInterfaceStateMachine downstream, int mode) { + if (findDownstream(downstream) == null) { // Adding a new downstream appends it to the list. Adding a // downstream a second time without first removing it has no effect. - mActiveDownstreams.offer(downstream); + // We never change the mode of a downstream except by first removing + // it and then re-adding it (with its new mode specified); + if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) { + // Make sure subnet IDs are always positive. They are appended + // to a ULA /48 to make a ULA /64 for local use. + mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1); + } updateIPv6TetheringInterfaces(); } } public void removeActiveDownstream(TetherInterfaceStateMachine downstream) { stopIPv6TetheringOn(downstream); - if (mActiveDownstreams.remove(downstream)) { + if (mActiveDownstreams.remove(findDownstream(downstream))) { updateIPv6TetheringInterfaces(); } + + // When tethering is stopping we can reset the subnet counter. + if (mNotifyList.isEmpty()) { + if (!mActiveDownstreams.isEmpty()) { + Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty."); + } + mNextSubnetId = 0; + } } public void updateUpstreamNetworkState(NetworkState ns) { @@ -123,20 +164,31 @@ public class IPv6TetheringCoordinator { } private LinkProperties getInterfaceIPv6LinkProperties(TetherInterfaceStateMachine sm) { - if (mUpstreamNetworkState == null) return null; - if (sm.interfaceType() == ConnectivityManager.TETHERING_BLUETOOTH) { // TODO: Figure out IPv6 support on PAN interfaces. return null; } + final Downstream ds = findDownstream(sm); + if (ds == null) return null; + + if (ds.mode == IControlsTethering.STATE_LOCAL_HOTSPOT) { + // Build a Unique Locally-assigned Prefix configuration. + return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId); + } + + // This downstream is in IControlsTethering.STATE_TETHERED mode. + if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) { + return null; + } + // NOTE: Here, in future, we would have policies to decide how to divvy // up the available dedicated prefixes among downstream interfaces. // At this time we have no such mechanism--we only support tethering // IPv6 toward the oldest (first requested) active downstream. - final TetherInterfaceStateMachine currentActive = mActiveDownstreams.peek(); - if (currentActive != null && currentActive == sm) { + final Downstream currentActive = mActiveDownstreams.peek(); + if (currentActive != null && currentActive.tism == sm) { final LinkProperties lp = getIPv6OnlyLinkProperties( mUpstreamNetworkState.linkProperties); if (lp.hasIPv6DefaultRoute() && lp.hasGlobalIPv6Address()) { @@ -147,6 +199,13 @@ public class IPv6TetheringCoordinator { return null; } + Downstream findDownstream(TetherInterfaceStateMachine tism) { + for (Downstream ds : mActiveDownstreams) { + if (ds.tism == tism) return ds; + } + return null; + } + private static boolean canTetherIPv6(NetworkState ns) { // Broadly speaking: // @@ -263,6 +322,44 @@ public class IPv6TetheringCoordinator { !ip.isMulticastAddress(); } + private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) { + final LinkProperties lp = new LinkProperties(); + + final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48); + lp.addRoute(new RouteInfo(local48, null, null)); + + final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64); + // Because this is a locally-generated ULA, we don't have an upstream + // address. But because the downstream IP address management code gets + // its prefix from the upstream's IP address, we create a fake one here. + lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64)); + + lp.setMtu(NetworkConstants.ETHER_MTU); + return lp; + } + + private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) { + final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length); + bytes[7] = (byte) (subnetId >> 8); + bytes[8] = (byte) subnetId; + return new IpPrefix(bytes, prefixlen); + } + + // Generates a Unique Locally-assigned Prefix: + // + // https://tools.ietf.org/html/rfc4193#section-3.1 + // + // The result is a /48 that can be used for local-only communications. + private static byte[] generateUniqueLocalPrefix() { + final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte + (new Random()).nextBytes(ulp); + + final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN); + in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1 + + return in6addr; + } + private static String toDebugString(NetworkState ns) { if (ns == null) { return "NetworkState{null}"; 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 8c6430c8a2ae..c6a7925f2b5e 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.net.UnknownHostException; import java.util.ArrayList; import java.util.HashSet; import java.util.Objects; +import java.util.Random; /** @@ -66,10 +67,17 @@ public class IPv6TetheringInterfaceServices { } public boolean start() { + // TODO: Refactor for testability (perhaps passing an android.system.Os + // instance and calling getifaddrs() directly). try { mNetworkInterface = NetworkInterface.getByName(mIfName); } catch (SocketException e) { - Log.e(TAG, "Failed to find NetworkInterface for " + mIfName, e); + Log.e(TAG, "Error looking up NetworkInterfaces for " + mIfName, e); + stop(); + return false; + } + if (mNetworkInterface == null) { + Log.e(TAG, "Failed to find NetworkInterface for " + mIfName); stop(); return false; } @@ -267,10 +275,10 @@ public class IPv6TetheringInterfaceServices { return localRoutes; } - // Given a prefix like 2001:db8::/64 return 2001:db8::1. + // Given a prefix like 2001:db8::/64 return an address like 2001:db8::1. private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) { final byte[] dnsBytes = localPrefix.getRawAddress(); - dnsBytes[dnsBytes.length - 1] = 0x1; + dnsBytes[dnsBytes.length - 1] = getRandomNonZeroByte(); try { return Inet6Address.getByAddress(null, dnsBytes, 0); } catch (UnknownHostException e) { @@ -278,4 +286,11 @@ public class IPv6TetheringInterfaceServices { return null; } } + + private static byte getRandomNonZeroByte() { + final byte random = (byte) (new Random()).nextInt(); + // Don't pick the subnet-router anycast address, since that might be + // in use on the upstream already. + return (random != 0) ? random : 0x1; + } } 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 710ab33874cd..1ffa86440ae2 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 class TetherInterfaceStateMachine extends StateMachine { public static final int CMD_IPV6_TETHER_UPDATE = BASE_IFACE + 13; private final State mInitialState; + private final State mServingState; + private final State mLocalHotspotState; private final State mTetheredState; private final State mUnavailableState; @@ -105,10 +107,14 @@ public class TetherInterfaceStateMachine extends StateMachine { mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR; mInitialState = new InitialState(); - addState(mInitialState); + mServingState = new ServingState(); + mLocalHotspotState = new LocalHotspotState(); mTetheredState = new TetheredState(); - addState(mTetheredState); mUnavailableState = new UnavailableState(); + addState(mInitialState); + addState(mServingState); + addState(mLocalHotspotState, mServingState); + addState(mTetheredState, mServingState); addState(mUnavailableState); setInitialState(mInitialState); @@ -172,12 +178,15 @@ public class TetherInterfaceStateMachine extends StateMachine { } } + private void sendInterfaceState(int newInterfaceState) { + mTetherController.notifyInterfaceStateChange( + mIfaceName, TetherInterfaceStateMachine.this, newInterfaceState, mLastError); + } + class InitialState extends State { @Override public void enter() { - mTetherController.notifyInterfaceStateChange( - mIfaceName, TetherInterfaceStateMachine.this, - IControlsTethering.STATE_AVAILABLE, mLastError); + sendInterfaceState(IControlsTethering.STATE_AVAILABLE); } @Override @@ -187,7 +196,16 @@ public class TetherInterfaceStateMachine extends StateMachine { switch (message.what) { case CMD_TETHER_REQUESTED: mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR; - transitionTo(mTetheredState); + switch (message.arg1) { + case IControlsTethering.STATE_LOCAL_HOTSPOT: + transitionTo(mLocalHotspotState); + break; + case IControlsTethering.STATE_TETHERED: + transitionTo(mTetheredState); + break; + default: + Log.e(TAG, "Invalid tethering interface serving state specified."); + } break; case CMD_INTERFACE_DOWN: transitionTo(mUnavailableState); @@ -204,7 +222,7 @@ public class TetherInterfaceStateMachine extends StateMachine { } } - class TetheredState extends State { + class ServingState extends State { @Override public void enter() { if (!configureIfaceIp(true)) { @@ -225,11 +243,6 @@ public class TetherInterfaceStateMachine extends StateMachine { if (!mIPv6TetherSvc.start()) { Log.e(TAG, "Failed to start IPv6TetheringInterfaceServices"); } - - if (DBG) Log.d(TAG, "Tethered " + mIfaceName); - mTetherController.notifyInterfaceStateChange( - mIfaceName, TetherInterfaceStateMachine.this, - IControlsTethering.STATE_TETHERED, mLastError); } @Override @@ -238,7 +251,6 @@ public class TetherInterfaceStateMachine extends StateMachine { // of these operations, but it doesn't really change that we have to try them // all in sequence. mIPv6TetherSvc.stop(); - cleanupUpstream(); try { mNMService.untetherInterface(mIfaceName); @@ -250,6 +262,73 @@ public class TetherInterfaceStateMachine extends StateMachine { configureIfaceIp(false); } + @Override + public boolean processMessage(Message message) { + maybeLogMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_UNREQUESTED: + transitionTo(mInitialState); + if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName); + break; + case CMD_INTERFACE_DOWN: + transitionTo(mUnavailableState); + if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName); + break; + case CMD_IPV6_TETHER_UPDATE: + mIPv6TetherSvc.updateUpstreamIPv6LinkProperties( + (LinkProperties) message.obj); + break; + case CMD_IP_FORWARDING_ENABLE_ERROR: + case CMD_IP_FORWARDING_DISABLE_ERROR: + case CMD_START_TETHERING_ERROR: + case CMD_STOP_TETHERING_ERROR: + case CMD_SET_DNS_FORWARDERS_ERROR: + mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR; + transitionTo(mInitialState); + break; + default: + return false; + } + return true; + } + } + + class LocalHotspotState extends State { + @Override + public void enter() { + if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName); + sendInterfaceState(IControlsTethering.STATE_LOCAL_HOTSPOT); + } + + @Override + public boolean processMessage(Message message) { + maybeLogMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_REQUESTED: + Log.e(TAG, "CMD_TETHER_REQUESTED while in local hotspot mode."); + break; + case CMD_TETHER_CONNECTION_CHANGED: + // Ignored in local hotspot state. + break; + default: + return false; + } + return true; + } + } + + class TetheredState extends State { + @Override + public void enter() { + if (DBG) Log.d(TAG, "Tethered " + mIfaceName); + sendInterfaceState(IControlsTethering.STATE_TETHERED); + } + + @Override + public void exit() { + cleanupUpstream(); + } + private void cleanupUpstream() { if (mMyUpstreamIfaceName == null) return; @@ -285,13 +364,8 @@ public class TetherInterfaceStateMachine extends StateMachine { maybeLogMessage(this, message.what); boolean retValue = true; switch (message.what) { - case CMD_TETHER_UNREQUESTED: - transitionTo(mInitialState); - if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName); - break; - case CMD_INTERFACE_DOWN: - transitionTo(mUnavailableState); - if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName); + case CMD_TETHER_REQUESTED: + Log.e(TAG, "CMD_TETHER_REQUESTED while already tethering."); break; case CMD_TETHER_CONNECTION_CHANGED: String newUpstreamIfaceName = (String)(message.obj); @@ -317,18 +391,6 @@ public class TetherInterfaceStateMachine extends StateMachine { } mMyUpstreamIfaceName = newUpstreamIfaceName; break; - case CMD_IPV6_TETHER_UPDATE: - mIPv6TetherSvc.updateUpstreamIPv6LinkProperties( - (LinkProperties) message.obj); - break; - case CMD_IP_FORWARDING_ENABLE_ERROR: - case CMD_IP_FORWARDING_DISABLE_ERROR: - case CMD_START_TETHERING_ERROR: - case CMD_STOP_TETHERING_ERROR: - case CMD_SET_DNS_FORWARDERS_ERROR: - mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR; - transitionTo(mInitialState); - break; default: retValue = false; break; @@ -348,9 +410,7 @@ public class TetherInterfaceStateMachine extends StateMachine { @Override public void enter() { mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR; - mTetherController.notifyInterfaceStateChange( - mIfaceName, TetherInterfaceStateMachine.this, - IControlsTethering.STATE_UNAVAILABLE, mLastError); + sendInterfaceState(IControlsTethering.STATE_UNAVAILABLE); } } } 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 62099293b31c..97a2d5ed6f5c 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 @@ public class UpstreamNetworkMonitor { // Fetch (and cache) a ConnectivityManager only if and when we need one. private ConnectivityManager cm() { if (mCM == null) { - mCM = mContext.getSystemService(ConnectivityManager.class); + // MUST call the String variant to be able to write unittests. + mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); } return mCM; } diff --git a/services/net/java/android/net/ip/RouterAdvertisementDaemon.java b/services/net/java/android/net/ip/RouterAdvertisementDaemon.java index ba1621d7d6bc..cb3123ce466a 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; @@ -69,7 +71,6 @@ public class RouterAdvertisementDaemon { private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName(); private static final byte ICMPV6_ND_ROUTER_SOLICIT = asByte(133); private static final byte ICMPV6_ND_ROUTER_ADVERT = asByte(134); - private static final int IPV6_MIN_MTU = 1280; private static final int MIN_RA_HEADER_SIZE = 16; // Summary of various timers and lifetimes. @@ -543,6 +544,14 @@ public class RouterAdvertisementDaemon { +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ + final HashSet<Inet6Address> filteredDnses = new HashSet<>(); + for (Inet6Address dns : dnses) { + if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) { + filteredDnses.add(dns); + } + } + if (filteredDnses.isEmpty()) return; + final byte ND_OPTION_RDNSS = 25; final byte RDNSS_NUM_8OCTETS = asByte(dnses.size() * 2 + 1); ra.put(ND_OPTION_RDNSS) @@ -550,7 +559,7 @@ public class RouterAdvertisementDaemon { .putShort(asShort(0)) .putInt(lifetime); - for (Inet6Address dns : dnses) { + for (Inet6Address dns : filteredDnses) { // NOTE: If the full of list DNS servers doesn't fit in the packet, // this code will cause a buffer overflow and the RA won't include // this instance of the option at all. diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java index 26f3050468dd..a012e0cb0547 100644 --- a/services/net/java/android/net/util/NetworkConstants.java +++ b/services/net/java/android/net/util/NetworkConstants.java @@ -36,6 +36,7 @@ public final class NetworkConstants { * * See also: * - https://tools.ietf.org/html/rfc894 + * - https://tools.ietf.org/html/rfc2464 * - https://tools.ietf.org/html/rfc7042 * - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml * - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml @@ -57,6 +58,8 @@ public final class NetworkConstants { FF, FF, FF, FF, FF, FF }; + public static final int ETHER_MTU = 1500; + /** * ARP constants. * @@ -97,6 +100,7 @@ public final class NetworkConstants { public static final int IPV6_SRC_ADDR_OFFSET = 8; public static final int IPV6_DST_ADDR_OFFSET = 24; public static final int IPV6_ADDR_LEN = 16; + public static final int IPV6_MIN_MTU = 1280; public static final int RFC7421_PREFIX_LENGTH = 64; /** diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java index a9f68c88c8c5..e527d57f7367 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 @@ public class TetheringTest { private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; @Mock private Context mContext; + @Mock private ConnectivityManager mConnectivityManager; @Mock private INetworkManagementService mNMService; @Mock private INetworkStatsService mStatsService; @Mock private INetworkPolicyManager mPolicyManager; @Mock private MockableSystemProperties mSystemProperties; @Mock private Resources mResources; + @Mock private UsbManager mUsbManager; + @Mock private WifiManager mWifiManager; @Mock private CarrierConfigManager mCarrierConfigManager; // Like so many Android system APIs, these cannot be mocked because it is marked final. // We have to use the real versions. private final PersistableBundle mCarrierConfig = new PersistableBundle(); private final TestLooper mLooper = new TestLooper(); + private final String mTestIfname = "test_wlan0"; + private BroadcastInterceptingContext mServiceContext; private Tethering mTethering; + private class MockContext extends BroadcastInterceptingContext { + MockContext(Context base) { + super(base); + } + + @Override + public Resources getResources() { return mResources; } + + @Override + public Object getSystemService(String name) { + if (Context.CONNECTIVITY_SERVICE.equals(name)) return mConnectivityManager; + if (Context.WIFI_SERVICE.equals(name)) return mWifiManager; + return super.getSystemService(name); + } + } + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - when(mContext.getResources()).thenReturn(mResources); when(mResources.getStringArray(com.android.internal.R.array.config_tether_dhcp_range)) .thenReturn(new String[0]); when(mResources.getStringArray(com.android.internal.R.array.config_tether_usb_regexs)) .thenReturn(new String[0]); when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_regexs)) - .thenReturn(new String[0]); + .thenReturn(new String[]{ "test_wlan\\d" }); when(mResources.getStringArray(com.android.internal.R.array.config_tether_bluetooth_regexs)) .thenReturn(new String[0]); when(mResources.getIntArray(com.android.internal.R.array.config_tether_upstream_types)) .thenReturn(new int[0]); - mTethering = new Tethering(mContext, mNMService, mStatsService, mPolicyManager, + when(mNMService.listInterfaces()) + .thenReturn(new String[]{ "test_rmnet_data0", mTestIfname }); + when(mNMService.getInterfaceConfig(anyString())) + .thenReturn(new InterfaceConfiguration()); + + mServiceContext = new MockContext(mContext); + mTethering = new Tethering(mServiceContext, mNMService, mStatsService, mPolicyManager, mLooper.getLooper(), mSystemProperties); } @@ -126,4 +173,144 @@ public class TetheringTest { .thenReturn(new String[] {"malformedApp"}); assertTrue(!mTethering.isTetherProvisioningRequired()); } + + private void sendWifiApStateChanged(int state) { + final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); + intent.putExtra(WifiManager.EXTRA_WIFI_AP_STATE, state); + mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } + + @Test + public void workingLocalOnlyHotspot() throws Exception { + when(mConnectivityManager.isTetheringSupported()).thenReturn(true); + when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean())) + .thenReturn(true); + + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // hotspot mode is to be started. + mTethering.interfaceStatusChanged(mTestIfname, true); + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED); + mLooper.dispatchAll(); + + verify(mNMService, times(1)).listInterfaces(); + verify(mNMService, times(1)).getInterfaceConfig(mTestIfname); + verify(mNMService, times(1)) + .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class)); + verify(mNMService, times(1)).tetherInterface(mTestIfname); + verify(mNMService, times(1)).setIpForwardingEnabled(true); + verify(mNMService, times(1)).startTethering(any(String[].class)); + verifyNoMoreInteractions(mNMService); + // UpstreamNetworkMonitor will be started, and will register two callbacks: + // a "listen all" and a "track default". + verify(mConnectivityManager, times(1)).registerNetworkCallback( + any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class)); + verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback( + any(NetworkCallback.class), any(Handler.class)); + // TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast(). + verify(mConnectivityManager, atLeastOnce()).isTetheringSupported(); + verifyNoMoreInteractions(mConnectivityManager); + + // Emulate externally-visible WifiManager effects, when hotspot mode + // is being torn down. + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED); + mTethering.interfaceRemoved(mTestIfname); + mLooper.dispatchAll(); + + verify(mNMService, times(1)).untetherInterface(mTestIfname); + // TODO: Why is {g,s}etInterfaceConfig() called more than once? + verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname); + verify(mNMService, atLeastOnce()) + .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class)); + verify(mNMService, times(1)).stopTethering(); + verify(mNMService, times(1)).setIpForwardingEnabled(false); + verifyNoMoreInteractions(mNMService); + // Asking for the last error after the per-interface state machine + // has been reaped yields an unknown interface error. + assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE, + mTethering.getLastTetherError(mTestIfname)); + } + + @Test + public void workingWifiTethering() throws Exception { + when(mConnectivityManager.isTetheringSupported()).thenReturn(true); + when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean())) + .thenReturn(true); + + // Emulate pressing the WiFi tethering button. + mTethering.startTethering(ConnectivityManager.TETHERING_WIFI, null, false); + mLooper.dispatchAll(); + verify(mWifiManager, times(1)).setWifiApEnabled(null, true); + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mConnectivityManager); + verifyNoMoreInteractions(mNMService); + + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // tethering mode is to be started. + mTethering.interfaceStatusChanged(mTestIfname, true); + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED); + mLooper.dispatchAll(); + + verify(mNMService, times(1)).listInterfaces(); + verify(mNMService, times(1)).getInterfaceConfig(mTestIfname); + verify(mNMService, times(1)) + .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class)); + verify(mNMService, times(1)).tetherInterface(mTestIfname); + verify(mNMService, times(1)).setIpForwardingEnabled(true); + verify(mNMService, times(1)).startTethering(any(String[].class)); + verifyNoMoreInteractions(mNMService); + // UpstreamNetworkMonitor will be started, and will register two callbacks: + // a "listen all" and a "track default". + verify(mConnectivityManager, times(1)).registerNetworkCallback( + any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class)); + verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback( + any(NetworkCallback.class), any(Handler.class)); + // In tethering mode, in the default configuration, an explicit request + // for a mobile network is also made. + verify(mConnectivityManager, atLeastOnce()).getNetworkInfo(anyInt()); + verify(mConnectivityManager, times(1)).requestNetwork( + any(NetworkRequest.class), any(NetworkCallback.class), eq(0), anyInt(), + any(Handler.class)); + // TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast(). + verify(mConnectivityManager, atLeastOnce()).isTetheringSupported(); + verifyNoMoreInteractions(mConnectivityManager); + + ///// + // We do not currently emulate any upstream being found. + // + // This is why there are no calls to verify mNMService.enableNat() or + // mNMService.startInterfaceForwarding(). + ///// + + // Emulate pressing the WiFi tethering button. + mTethering.stopTethering(ConnectivityManager.TETHERING_WIFI); + mLooper.dispatchAll(); + verify(mWifiManager, times(1)).setWifiApEnabled(null, false); + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mConnectivityManager); + verifyNoMoreInteractions(mNMService); + + // Emulate externally-visible WifiManager effects, when tethering mode + // is being torn down. + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED); + mTethering.interfaceRemoved(mTestIfname); + mLooper.dispatchAll(); + + verify(mNMService, times(1)).untetherInterface(mTestIfname); + // TODO: Why is {g,s}etInterfaceConfig() called more than once? + verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname); + verify(mNMService, atLeastOnce()) + .setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class)); + verify(mNMService, times(1)).stopTethering(); + verify(mNMService, times(1)).setIpForwardingEnabled(false); + verifyNoMoreInteractions(mNMService); + // Asking for the last error after the per-interface state machine + // has been reaped yields an unknown interface error. + assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE, + mTethering.getLastTetherError(mTestIfname)); + } + + // TODO: Test that a request for hotspot mode doesn't interface with an + // already operating tethering mode interface. } 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 32e1b96cf798..caf1a5583a5b 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_BLUETOOTH; import static android.net.ConnectivityManager.TETHERING_USB; import static android.net.ConnectivityManager.TETHERING_WIFI; import static com.android.server.connectivity.tethering.IControlsTethering.STATE_AVAILABLE; +import static com.android.server.connectivity.tethering.IControlsTethering.STATE_LOCAL_HOTSPOT; import static com.android.server.connectivity.tethering.IControlsTethering.STATE_TETHERED; import static com.android.server.connectivity.tethering.IControlsTethering.STATE_UNAVAILABLE; @@ -80,7 +81,7 @@ public class TetherInterfaceStateMachineTest { private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception { initStateMachine(interfaceType); - dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); + dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED); if (upstreamIface != null) { dispatchTetherConnectionChanged(upstreamIface); } @@ -138,7 +139,7 @@ public class TetherInterfaceStateMachineTest { public void canBeTethered() throws Exception { initStateMachine(TETHERING_BLUETOOTH); - dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); + dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED); InOrder inOrder = inOrder(mTetherHelper, mNMService); inOrder.verify(mNMService).tetherInterface(IFACE_NAME); inOrder.verify(mTetherHelper).notifyInterfaceStateChange( @@ -162,7 +163,7 @@ public class TetherInterfaceStateMachineTest { public void canBeTetheredAsUsb() throws Exception { initStateMachine(TETHERING_USB); - dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); + dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED); InOrder inOrder = inOrder(mTetherHelper, mNMService); inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME); inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration); @@ -272,7 +273,7 @@ public class TetherInterfaceStateMachineTest { initStateMachine(TETHERING_USB); doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME); - dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); + dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED); InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper); usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown(); usbTeardownOrder.verify(mNMService).setInterfaceConfig( @@ -310,6 +311,17 @@ public class TetherInterfaceStateMachineTest { * Send a command to the state machine under test, and run the event loop to idle. * * @param command One of the TetherInterfaceStateMachine.CMD_* constants. + * @param obj An additional argument to pass. + */ + private void dispatchCommand(int command, int arg1) { + mTestedSm.sendMessage(command, arg1); + mLooper.dispatchAll(); + } + + /** + * Send a command to the state machine under test, and run the event loop to idle. + * + * @param command One of the TetherInterfaceStateMachine.CMD_* constants. */ private void dispatchCommand(int command) { mTestedSm.sendMessage(command); |