Merge "ConnectivityServiceTest: eliminate remaining sleep()"
diff --git a/core/java/android/app/timezone/DistroRulesVersion.java b/core/java/android/app/timezone/DistroRulesVersion.java
index 5503ce1..1eb9f45 100644
--- a/core/java/android/app/timezone/DistroRulesVersion.java
+++ b/core/java/android/app/timezone/DistroRulesVersion.java
@@ -125,4 +125,8 @@
                 + ", mRevision='" + mRevision + '\''
                 + '}';
     }
+
+    public String toDumpString() {
+        return mRulesVersion + "," + mRevision;
+    }
 }
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 98f16c2..cac5d92 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -281,6 +281,13 @@
          Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. -->
     <integer translatable="false" name="config_networkAvoidBadWifi">1</integer>
 
+    <!-- If the hardware supports specially marking packets that caused a wakeup of the
+         main CPU, set this value to the mark used. -->
+    <integer name="config_networkWakeupPacketMark">0</integer>
+
+    <!-- Mask to use when checking skb mark defined in config_networkWakeupPacketMark above. -->
+    <integer name="config_networkWakeupPacketMask">0</integer>
+
     <!-- Default value for ConnectivityManager.getMultipathPreference() on metered networks. Actual
          device behaviour is controlled by Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE.
          This is the default value of that setting. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 7363a9b..758ee4a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1788,6 +1788,8 @@
   <java-symbol type="integer" name="config_networkNotifySwitchType" />
   <java-symbol type="array" name="config_networkNotifySwitches" />
   <java-symbol type="integer" name="config_networkAvoidBadWifi" />
+  <java-symbol type="integer" name="config_networkWakeupPacketMark" />
+  <java-symbol type="integer" name="config_networkWakeupPacketMask" />
   <java-symbol type="integer" name="config_networkMeteredMultipathPreference" />
   <java-symbol type="integer" name="config_notificationsBatteryFullARGB" />
   <java-symbol type="integer" name="config_notificationsBatteryLedOff" />
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 6112267..eafa88f 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -215,13 +215,6 @@
     // See Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
     private final int mReleasePendingIntentDelayMs;
 
-    // Driver specific constants used to select packets received via
-    // WiFi that caused the phone to exit sleep state. Currently there
-    // is only one kernel implementation so we can get away with
-    // constants.
-    private static final int mWakeupPacketMark = 0x80000000;
-    private static final int mWakeupPacketMask = 0x80000000;
-
     private MockableSystemProperties mSystemProperties;
 
     private Tethering mTethering;
@@ -2410,6 +2403,10 @@
             notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
             mKeepaliveTracker.handleStopAllKeepalives(nai,
                     ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK);
+            for (String iface : nai.linkProperties.getAllInterfaceNames()) {
+                // Disable wakeup packet monitoring for each interface.
+                wakeupModifyInterface(iface, nai.networkCapabilities, false);
+            }
             nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED);
             mNetworkAgentInfos.remove(msg.replyTo);
             updateClat(null, nai.linkProperties, nai);
@@ -4532,22 +4529,35 @@
         }
     }
 
-    private void wakeupAddInterface(String iface, NetworkCapabilities caps) throws RemoteException {
+    private void wakeupModifyInterface(String iface, NetworkCapabilities caps, boolean add) {
         // Marks are only available on WiFi interaces. Checking for
         // marks on unsupported interfaces is harmless.
         if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
             return;
         }
-        mNetd.getNetdService().wakeupAddInterface(
-            iface, "iface:" + iface, mWakeupPacketMark, mWakeupPacketMask);
-    }
 
-    private void wakeupDelInterface(String iface, NetworkCapabilities caps) throws RemoteException {
-        if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+        int mark = mContext.getResources().getInteger(
+            com.android.internal.R.integer.config_networkWakeupPacketMark);
+        int mask = mContext.getResources().getInteger(
+            com.android.internal.R.integer.config_networkWakeupPacketMask);
+
+        // Mask/mark of zero will not detect anything interesting.
+        // Don't install rules unless both values are nonzero.
+        if (mark == 0 || mask == 0) {
             return;
         }
-        mNetd.getNetdService().wakeupDelInterface(
-            iface, "iface:" + iface, mWakeupPacketMark, mWakeupPacketMask);
+
+        final String prefix = "iface:" + iface;
+        try {
+            if (add) {
+                mNetd.getNetdService().wakeupAddInterface(iface, prefix, mark, mask);
+            } else {
+                mNetd.getNetdService().wakeupDelInterface(iface, prefix, mark, mask);
+            }
+        } catch (Exception e) {
+            loge("Exception modifying wakeup packet monitoring: " + e);
+        }
+
     }
 
     private void updateInterfaces(LinkProperties newLp, LinkProperties oldLp, int netId,
@@ -4562,7 +4572,7 @@
             try {
                 if (DBG) log("Adding iface " + iface + " to network " + netId);
                 mNetd.addInterfaceToNetwork(iface, netId);
-                wakeupAddInterface(iface, caps);
+                wakeupModifyInterface(iface, caps, true);
             } catch (Exception e) {
                 loge("Exception adding interface: " + e);
             }
@@ -4570,8 +4580,8 @@
         for (String iface : interfaceDiff.removed) {
             try {
                 if (DBG) log("Removing iface " + iface + " from network " + netId);
+                wakeupModifyInterface(iface, caps, false);
                 mNetd.removeInterfaceFromNetwork(iface, netId);
-                wakeupDelInterface(iface, caps);
             } catch (Exception e) {
                 loge("Exception removing interface: " + e);
             }
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index 66347e6..56859e6 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -29,6 +29,7 @@
 import android.net.CaptivePortal;
 import android.net.ConnectivityManager;
 import android.net.ICaptivePortal;
+import android.net.Network;
 import android.net.NetworkRequest;
 import android.net.ProxyInfo;
 import android.net.TrafficStats;
@@ -71,6 +72,9 @@
 import java.net.URL;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
@@ -95,7 +99,7 @@
             "http://play.googleapis.com/generate_204";
     private static final String DEFAULT_USER_AGENT    = "Mozilla/5.0 (X11; Linux x86_64) "
                                                       + "AppleWebKit/537.36 (KHTML, like Gecko) "
-                                                      + "Chrome/52.0.2743.82 Safari/537.36";
+                                                      + "Chrome/60.0.3112.32 Safari/537.36";
 
     private static final int SOCKET_TIMEOUT_MS = 10000;
     private static final int PROBE_TIMEOUT_MS  = 3000;
@@ -228,6 +232,7 @@
     private final Context mContext;
     private final Handler mConnectivityServiceHandler;
     private final NetworkAgentInfo mNetworkAgentInfo;
+    private final Network mNetwork;
     private final int mNetId;
     private final TelephonyManager mTelephonyManager;
     private final WifiManager mWifiManager;
@@ -286,7 +291,8 @@
         mMetricsLog = logger;
         mConnectivityServiceHandler = handler;
         mNetworkAgentInfo = networkAgentInfo;
-        mNetId = mNetworkAgentInfo.network.netId;
+        mNetwork = new OneAddressPerFamilyNetwork(networkAgentInfo.network);
+        mNetId = mNetwork.netId;
         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
@@ -415,7 +421,7 @@
             maybeLogEvaluationResult(
                     networkEventType(validationStage(), EvaluationResult.VALIDATED));
             mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
-                    NETWORK_TEST_RESULT_VALID, mNetworkAgentInfo.network.netId, null));
+                    NETWORK_TEST_RESULT_VALID, mNetId, null));
             mValidations++;
         }
 
@@ -440,7 +446,8 @@
                 case CMD_LAUNCH_CAPTIVE_PORTAL_APP:
                     final Intent intent = new Intent(
                             ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
-                    intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network);
+                    // OneAddressPerFamilyNetwork is not parcelable across processes.
+                    intent.putExtra(ConnectivityManager.EXTRA_NETWORK, new Network(mNetwork));
                     intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
                             new CaptivePortal(new ICaptivePortal.Stub() {
                                 @Override
@@ -468,8 +475,7 @@
 
         @Override
         public void exit() {
-            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0,
-                    mNetworkAgentInfo.network.netId, null);
+            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0, mNetId, null);
             mConnectivityServiceHandler.sendMessage(message);
         }
     }
@@ -623,7 +629,7 @@
         CustomIntentReceiver(String action, int token, int what) {
             mToken = token;
             mWhat = what;
-            mAction = action + "_" + mNetworkAgentInfo.network.netId + "_" + token;
+            mAction = action + "_" + mNetId + "_" + token;
             mContext.registerReceiver(this, new IntentFilter(mAction));
         }
         public PendingIntent getPendingIntent() {
@@ -659,8 +665,7 @@
                         CMD_LAUNCH_CAPTIVE_PORTAL_APP);
             }
             // Display the sign in notification.
-            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1,
-                    mNetworkAgentInfo.network.netId,
+            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1, mNetId,
                     mLaunchCaptivePortalAppBroadcastReceiver.getPendingIntent());
             mConnectivityServiceHandler.sendMessage(message);
             // Retest for captive portal occasionally.
@@ -675,6 +680,31 @@
         }
     }
 
+    // Limits the list of IP addresses returned by getAllByName or tried by openConnection to at
+    // most one per address family. This ensures we only wait up to 20 seconds for TCP connections
+    // to complete, regardless of how many IP addresses a host has.
+    private static class OneAddressPerFamilyNetwork extends Network {
+        public OneAddressPerFamilyNetwork(Network network) {
+            super(network);
+        }
+
+        @Override
+        public InetAddress[] getAllByName(String host) throws UnknownHostException {
+            List<InetAddress> addrs = Arrays.asList(super.getAllByName(host));
+
+            // Ensure the address family of the first address is tried first.
+            LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
+            addressByFamily.put(addrs.get(0).getClass(), addrs.get(0));
+            Collections.shuffle(addrs);
+
+            for (InetAddress addr : addrs) {
+                addressByFamily.put(addr.getClass(), addr);
+            }
+
+            return addressByFamily.values().toArray(new InetAddress[addressByFamily.size()]);
+        }
+    }
+
     private static String getCaptivePortalServerHttpsUrl(Context context) {
         return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL);
     }
@@ -805,7 +835,7 @@
         int result;
         String connectInfo;
         try {
-            InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(host);
+            InetAddress[] addresses = mNetwork.getAllByName(host);
             StringBuffer buffer = new StringBuffer();
             for (InetAddress address : addresses) {
                 buffer.append(',').append(address.getHostAddress());
@@ -833,7 +863,7 @@
         String redirectUrl = null;
         final Stopwatch probeTimer = new Stopwatch().start();
         try {
-            urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url);
+            urlConnection = (HttpURLConnection) mNetwork.openConnection(url);
             urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
             urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
             urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 3327bec..c524033 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -47,6 +47,7 @@
 import android.net.ConnectivityManager;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
+import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -104,6 +105,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
@@ -210,7 +212,7 @@
                 mContext.getContentResolver(),
                 mLog);
         mUpstreamNetworkMonitor = new UpstreamNetworkMonitor(
-                mContext, mTetherMasterSM, TetherMasterSM.EVENT_UPSTREAM_CALLBACK, mLog);
+                mContext, mTetherMasterSM, mLog, TetherMasterSM.EVENT_UPSTREAM_CALLBACK );
         mForwardedDownstreams = new HashSet<>();
         mSimChange = new SimChangeListener(
                 mContext, mTetherMasterSM.getHandler(), () -> reevaluateSimCardProvisioning());
@@ -1066,7 +1068,7 @@
     // 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.
-    boolean pertainsToCurrentUpstream(NetworkState ns) {
+    private boolean pertainsToCurrentUpstream(NetworkState ns) {
         if (ns != null && ns.linkProperties != null && mCurrentUpstreamIface != null) {
             for (String ifname : ns.linkProperties.getAllInterfaceNames()) {
                 if (mCurrentUpstreamIface.equals(ifname)) {
@@ -1100,6 +1102,12 @@
         }
     }
 
+    private void startOffloadController() {
+        mOffloadController.start();
+        mOffloadController.updateExemptPrefixes(
+                mUpstreamNetworkMonitor.getOffloadExemptPrefixes());
+    }
+
     class TetherMasterSM extends StateMachine {
         private static final int BASE_MASTER                    = Protocol.BASE_TETHERING;
         // an interface SM has requested Tethering/Local Hotspot
@@ -1193,146 +1201,138 @@
             }
         }
 
-        class TetherMasterUtilState extends State {
-            @Override
-            public boolean processMessage(Message m) {
+        protected boolean turnOnMasterTetherSettings() {
+            final TetheringConfiguration cfg = mConfig;
+            try {
+                mNMService.setIpForwardingEnabled(true);
+            } catch (Exception e) {
+                mLog.e(e);
+                transitionTo(mSetIpForwardingEnabledErrorState);
                 return false;
             }
-
-            protected boolean turnOnMasterTetherSettings() {
-                final TetheringConfiguration cfg = mConfig;
-                try {
-                    mNMService.setIpForwardingEnabled(true);
-                } catch (Exception e) {
-                    mLog.e(e);
-                    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 {
-                        mNMService.stopTethering();
-                        mNMService.startTethering(cfg.dhcpRanges);
-                    } catch (Exception ee) {
-                        mLog.e(ee);
-                        transitionTo(mStartTetheringErrorState);
-                        return false;
-                    }
-                }
-                mLog.log("SET master tether settings: ON");
-                return true;
-            }
-
-            protected boolean turnOffMasterTetherSettings() {
+            // 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 {
                     mNMService.stopTethering();
-                } catch (Exception e) {
-                    mLog.e(e);
-                    transitionTo(mStopTetheringErrorState);
+                    mNMService.startTethering(cfg.dhcpRanges);
+                } catch (Exception ee) {
+                    mLog.e(ee);
+                    transitionTo(mStartTetheringErrorState);
                     return false;
                 }
-                try {
-                    mNMService.setIpForwardingEnabled(false);
-                } catch (Exception e) {
-                    mLog.e(e);
-                    transitionTo(mSetIpForwardingDisabledErrorState);
-                    return false;
-                }
-                transitionTo(mInitialState);
-                mLog.log("SET master tether settings: OFF");
-                return true;
             }
+            mLog.log("SET master tether settings: ON");
+            return true;
+        }
 
-            protected void chooseUpstreamType(boolean tryCell) {
-                updateConfiguration(); // TODO - remove?
-
-                final NetworkState ns = mUpstreamNetworkMonitor.selectPreferredUpstreamType(
-                        mConfig.preferredUpstreamIfaceTypes);
-                if (ns == null) {
-                    if (tryCell) {
-                        mUpstreamNetworkMonitor.registerMobileNetworkRequest();
-                        // We think mobile should be coming up; don't set a retry.
-                    } else {
-                        sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
-                    }
-                }
-                setUpstreamNetwork(ns);
+        protected boolean turnOffMasterTetherSettings() {
+            try {
+                mNMService.stopTethering();
+            } catch (Exception e) {
+                mLog.e(e);
+                transitionTo(mStopTetheringErrorState);
+                return false;
             }
+            try {
+                mNMService.setIpForwardingEnabled(false);
+            } catch (Exception e) {
+                mLog.e(e);
+                transitionTo(mSetIpForwardingDisabledErrorState);
+                return false;
+            }
+            transitionTo(mInitialState);
+            mLog.log("SET master tether settings: OFF");
+            return true;
+        }
 
-            protected void setUpstreamNetwork(NetworkState ns) {
-                String iface = null;
-                if (ns != null && ns.linkProperties != null) {
-                    // Find the interface with the default IPv4 route. It may be the
-                    // interface described by linkProperties, or one of the interfaces
-                    // stacked on top of it.
-                    Log.i(TAG, "Finding IPv4 upstream interface on: " + ns.linkProperties);
-                    RouteInfo ipv4Default = RouteInfo.selectBestRoute(
-                        ns.linkProperties.getAllRoutes(), Inet4Address.ANY);
-                    if (ipv4Default != null) {
-                        iface = ipv4Default.getInterface();
-                        Log.i(TAG, "Found interface " + ipv4Default.getInterface());
-                    } else {
-                        Log.i(TAG, "No IPv4 upstream interface, giving up.");
-                    }
-                }
+        protected void chooseUpstreamType(boolean tryCell) {
+            updateConfiguration(); // TODO - remove?
 
-                if (iface != null) {
-                    setDnsForwarders(ns.network, ns.linkProperties);
+            final NetworkState ns = mUpstreamNetworkMonitor.selectPreferredUpstreamType(
+                    mConfig.preferredUpstreamIfaceTypes);
+            if (ns == null) {
+                if (tryCell) {
+                    mUpstreamNetworkMonitor.registerMobileNetworkRequest();
+                    // We think mobile should be coming up; don't set a retry.
+                } else {
+                    sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
                 }
-                notifyTetheredOfNewUpstreamIface(iface);
-                if (ns != null && pertainsToCurrentUpstream(ns)) {
-                    // If we already have NetworkState for this network examine
-                    // it immediately, because there likely will be no second
-                    // EVENT_ON_AVAILABLE (it was already received).
-                    handleNewUpstreamNetworkState(ns);
-                } else if (mCurrentUpstreamIface == null) {
-                    // There are no available upstream networks, or none that
-                    // have an IPv4 default route (current metric for success).
-                    handleNewUpstreamNetworkState(null);
+            }
+            setUpstreamNetwork(ns);
+        }
+
+        protected void setUpstreamNetwork(NetworkState ns) {
+            String iface = null;
+            if (ns != null && ns.linkProperties != null) {
+                // Find the interface with the default IPv4 route. It may be the
+                // interface described by linkProperties, or one of the interfaces
+                // stacked on top of it.
+                Log.i(TAG, "Finding IPv4 upstream interface on: " + ns.linkProperties);
+                RouteInfo ipv4Default = RouteInfo.selectBestRoute(
+                    ns.linkProperties.getAllRoutes(), Inet4Address.ANY);
+                if (ipv4Default != null) {
+                    iface = ipv4Default.getInterface();
+                    Log.i(TAG, "Found interface " + ipv4Default.getInterface());
+                } else {
+                    Log.i(TAG, "No IPv4 upstream interface, giving up.");
                 }
             }
 
-            protected void setDnsForwarders(final Network network, final LinkProperties lp) {
-                // TODO: Set v4 and/or v6 DNS per available connectivity.
-                String[] dnsServers = mConfig.defaultIPv4DNS;
-                final Collection<InetAddress> dnses = lp.getDnsServers();
-                // TODO: Properly support the absence of DNS servers.
-                if (dnses != null && !dnses.isEmpty()) {
-                    // TODO: remove this invocation of NetworkUtils.makeStrings().
-                    dnsServers = NetworkUtils.makeStrings(dnses);
-                }
-                try {
-                    mNMService.setDnsForwarders(network, dnsServers);
-                    mLog.log(String.format(
-                            "SET DNS forwarders: network=%s dnsServers=%s",
-                            network, Arrays.toString(dnsServers)));
-                } catch (Exception e) {
-                    // TODO: Investigate how this can fail and what exactly
-                    // happens if/when such failures occur.
-                    mLog.e("setting DNS forwarders failed, " + e);
-                    transitionTo(mSetDnsForwardersErrorState);
-                }
+            if (iface != null) {
+                setDnsForwarders(ns.network, ns.linkProperties);
             }
+            notifyTetheredOfNewUpstreamIface(iface);
+            if (ns != null && pertainsToCurrentUpstream(ns)) {
+                // If we already have NetworkState for this network examine
+                // it immediately, because there likely will be no second
+                // EVENT_ON_AVAILABLE (it was already received).
+                handleNewUpstreamNetworkState(ns);
+            } else if (mCurrentUpstreamIface == null) {
+                // There are no available upstream networks, or none that
+                // have an IPv4 default route (current metric for success).
+                handleNewUpstreamNetworkState(null);
+            }
+        }
 
-            protected void notifyTetheredOfNewUpstreamIface(String ifaceName) {
-                if (DBG) Log.d(TAG, "Notifying tethered with upstream=" + ifaceName);
-                mCurrentUpstreamIface = ifaceName;
-                for (TetherInterfaceStateMachine sm : mNotifyList) {
-                    sm.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
-                            ifaceName);
-                }
+        protected void setDnsForwarders(final Network network, final LinkProperties lp) {
+            // TODO: Set v4 and/or v6 DNS per available connectivity.
+            String[] dnsServers = mConfig.defaultIPv4DNS;
+            final Collection<InetAddress> dnses = lp.getDnsServers();
+            // TODO: Properly support the absence of DNS servers.
+            if (dnses != null && !dnses.isEmpty()) {
+                // TODO: remove this invocation of NetworkUtils.makeStrings().
+                dnsServers = NetworkUtils.makeStrings(dnses);
             }
+            try {
+                mNMService.setDnsForwarders(network, dnsServers);
+                mLog.log(String.format(
+                        "SET DNS forwarders: network=%s dnsServers=%s",
+                        network, Arrays.toString(dnsServers)));
+            } catch (Exception e) {
+                // TODO: Investigate how this can fail and what exactly
+                // happens if/when such failures occur.
+                mLog.e("setting DNS forwarders failed, " + e);
+                transitionTo(mSetDnsForwardersErrorState);
+            }
+        }
 
-            protected void handleNewUpstreamNetworkState(NetworkState ns) {
-                mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
-                mOffloadController.setUpstreamLinkProperties(
-                        (ns != null) ? ns.linkProperties : null);
+        protected void notifyTetheredOfNewUpstreamIface(String ifaceName) {
+            if (DBG) Log.d(TAG, "Notifying tethered with upstream=" + ifaceName);
+            mCurrentUpstreamIface = ifaceName;
+            for (TetherInterfaceStateMachine sm : mNotifyList) {
+                sm.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
+                        ifaceName);
             }
         }
 
+        protected void handleNewUpstreamNetworkState(NetworkState ns) {
+            mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
+            mOffloadController.setUpstreamLinkProperties((ns != null) ? ns.linkProperties : null);
+        }
+
         private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
             if (mNotifyList.indexOf(who) < 0) {
                 mNotifyList.add(who);
@@ -1379,7 +1379,61 @@
             }
         }
 
-        class TetherModeAliveState extends TetherMasterUtilState {
+        private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
+            if (arg1 == UpstreamNetworkMonitor.NOTIFY_EXEMPT_PREFIXES) {
+                mOffloadController.updateExemptPrefixes((Set<IpPrefix>) o);
+                return;
+            }
+
+            final NetworkState ns = (NetworkState) o;
+
+            if (ns == null || !pertainsToCurrentUpstream(ns)) {
+                // TODO: In future, this is where upstream evaluation and selection
+                // could be handled for notifications which include sufficient data.
+                // For example, after CONNECTIVITY_ACTION listening is removed, here
+                // is where we could observe a Wi-Fi network becoming available and
+                // passing validation.
+                if (mCurrentUpstreamIface == null) {
+                    // If we have no upstream interface, try to run through upstream
+                    // selection again.  If, for example, IPv4 connectivity has shown up
+                    // after IPv6 (e.g., 464xlat became available) we want the chance to
+                    // notice and act accordingly.
+                    chooseUpstreamType(false);
+                }
+                return;
+            }
+
+            switch (arg1) {
+                case UpstreamNetworkMonitor.EVENT_ON_AVAILABLE:
+                    // The default network changed, or DUN connected
+                    // before this callback was processed. Updates
+                    // for the current NetworkCapabilities and
+                    // LinkProperties have been requested (default
+                    // request) or are being sent shortly (DUN). Do
+                    // nothing until they arrive; if no updates
+                    // arrive there's nothing to do.
+                    break;
+                case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
+                    handleNewUpstreamNetworkState(ns);
+                    break;
+                case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
+                    setDnsForwarders(ns.network, ns.linkProperties);
+                    handleNewUpstreamNetworkState(ns);
+                    break;
+                case UpstreamNetworkMonitor.EVENT_ON_LOST:
+                    // TODO: Re-evaluate possible upstreams. Currently upstream
+                    // reevaluation is triggered via received CONNECTIVITY_ACTION
+                    // broadcasts that result in being passed a
+                    // TetherMasterSM.CMD_UPSTREAM_CHANGED.
+                    handleNewUpstreamNetworkState(null);
+                    break;
+                default:
+                    mLog.e("Unknown arg1 value: " + arg1);
+                    break;
+            }
+        }
+
+        class TetherModeAliveState extends State {
             boolean mUpstreamWanted = false;
             boolean mTryCell = true;
 
@@ -1397,7 +1451,7 @@
                 // TODO: De-duplicate with updateUpstreamWanted() below.
                 if (upstreamWanted()) {
                     mUpstreamWanted = true;
-                    mOffloadController.start();
+                    startOffloadController();
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
@@ -1417,7 +1471,7 @@
                 mUpstreamWanted = upstreamWanted();
                 if (mUpstreamWanted != previousUpstreamWanted) {
                     if (mUpstreamWanted) {
-                        mOffloadController.start();
+                        startOffloadController();
                     } else {
                         mOffloadController.stop();
                     }
@@ -1497,52 +1551,8 @@
                         break;
                     case EVENT_UPSTREAM_CALLBACK: {
                         updateUpstreamWanted();
-                        if (!mUpstreamWanted) break;
-
-                        final NetworkState ns = (NetworkState) message.obj;
-
-                        if (ns == null || !pertainsToCurrentUpstream(ns)) {
-                            // TODO: In future, this is where upstream evaluation and selection
-                            // could be handled for notifications which include sufficient data.
-                            // For example, after CONNECTIVITY_ACTION listening is removed, here
-                            // is where we could observe a Wi-Fi network becoming available and
-                            // passing validation.
-                            if (mCurrentUpstreamIface == null) {
-                                // If we have no upstream interface, try to run through upstream
-                                // selection again.  If, for example, IPv4 connectivity has shown up
-                                // after IPv6 (e.g., 464xlat became available) we want the chance to
-                                // notice and act accordingly.
-                                chooseUpstreamType(false);
-                            }
-                            break;
-                        }
-
-                        switch (message.arg1) {
-                            case UpstreamNetworkMonitor.EVENT_ON_AVAILABLE:
-                                // The default network changed, or DUN connected
-                                // before this callback was processed. Updates
-                                // for the current NetworkCapabilities and
-                                // LinkProperties have been requested (default
-                                // request) or are being sent shortly (DUN). Do
-                                // nothing until they arrive; if no updates
-                                // arrive there's nothing to do.
-                                break;
-                            case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
-                                handleNewUpstreamNetworkState(ns);
-                                break;
-                            case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
-                                setDnsForwarders(ns.network, ns.linkProperties);
-                                handleNewUpstreamNetworkState(ns);
-                                break;
-                            case UpstreamNetworkMonitor.EVENT_ON_LOST:
-                                // TODO: Re-evaluate possible upstreams. Currently upstream
-                                // reevaluation is triggered via received CONNECTIVITY_ACTION
-                                // broadcasts that result in being passed a
-                                // TetherMasterSM.CMD_UPSTREAM_CHANGED.
-                                handleNewUpstreamNetworkState(null);
-                                break;
-                            default:
-                                break;
+                        if (mUpstreamWanted) {
+                            handleUpstreamNetworkMonitorCallback(message.arg1, message.obj);
                         }
                         break;
                     }
diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
index 08deef8..78487b7 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
 import android.content.ContentResolver;
+import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.RouteInfo;
 import android.net.util.SharedLog;
@@ -28,6 +29,7 @@
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.util.ArrayList;
+import java.util.Set;
 
 /**
  * A class to encapsulate the business logic of programming the tethering
@@ -45,6 +47,7 @@
     private boolean mConfigInitialized;
     private boolean mControlInitialized;
     private LinkProperties mUpstreamLinkProperties;
+    private Set<IpPrefix> mExemptPrefixes;
 
     public OffloadController(Handler h, OffloadHardwareInterface hwi,
             ContentResolver contentResolver, SharedLog log) {
@@ -108,6 +111,17 @@
         pushUpstreamParameters();
     }
 
+    public void updateExemptPrefixes(Set<IpPrefix> exemptPrefixes) {
+        if (!started()) return;
+
+        mExemptPrefixes = exemptPrefixes;
+        // TODO:
+        //     - add IP addresses from all downstream link properties
+        //     - add routes from all non-tethering downstream link properties
+        //     - remove any 64share prefixes
+        //     - push this to the HAL
+    }
+
     public void notifyDownstreamLinkProperties(LinkProperties lp) {
         if (!started()) return;
 
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 9ebfaf7..eb66767 100644
--- a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -26,18 +26,24 @@
 import android.os.Looper;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.NetworkState;
+import android.net.util.NetworkConstants;
 import android.net.util.SharedLog;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.StateMachine;
 
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
 
 
 /**
@@ -66,10 +72,16 @@
     private static final boolean DBG = false;
     private static final boolean VDBG = false;
 
+    private static final IpPrefix[] MINIMUM_LOCAL_PREFIXES_SET = {
+            prefix("127.0.0.0/8"), prefix("169.254.0.0/16"),
+            prefix("::/3"), prefix("fe80::/64"), prefix("fc00::/7"), prefix("ff00::/8"),
+    };
+
     public static final int EVENT_ON_AVAILABLE      = 1;
     public static final int EVENT_ON_CAPABILITIES   = 2;
     public static final int EVENT_ON_LINKPROPERTIES = 3;
     public static final int EVENT_ON_LOST           = 4;
+    public static final int NOTIFY_EXEMPT_PREFIXES  = 10;
 
     private static final int CALLBACK_LISTEN_ALL = 1;
     private static final int CALLBACK_TRACK_DEFAULT = 2;
@@ -81,6 +93,7 @@
     private final Handler mHandler;
     private final int mWhat;
     private final HashMap<Network, NetworkState> mNetworkMap = new HashMap<>();
+    private HashSet<IpPrefix> mOffloadExemptPrefixes;
     private ConnectivityManager mCM;
     private NetworkCallback mListenAllCallback;
     private NetworkCallback mDefaultNetworkCallback;
@@ -88,18 +101,19 @@
     private boolean mDunRequired;
     private Network mCurrentDefault;
 
-    public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, int what, SharedLog log) {
+    public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
         mContext = ctx;
         mTarget = tgt;
         mHandler = mTarget.getHandler();
-        mWhat = what;
         mLog = log.forSubComponent(TAG);
+        mWhat = what;
+        mOffloadExemptPrefixes = allOffloadExemptPrefixes(mNetworkMap.values());
     }
 
     @VisibleForTesting
     public UpstreamNetworkMonitor(
-            StateMachine tgt, int what, ConnectivityManager cm, SharedLog log) {
-        this(null, tgt, what, log);
+            ConnectivityManager cm, StateMachine tgt, SharedLog log, int what) {
+        this((Context) null, tgt, log, what);
         mCM = cm;
     }
 
@@ -209,6 +223,10 @@
         return typeStatePair.ns;
     }
 
+    public Set<IpPrefix> getOffloadExemptPrefixes() {
+        return (Set<IpPrefix>) mOffloadExemptPrefixes.clone();
+    }
+
     private void handleAvailable(int callbackType, Network network) {
         if (VDBG) Log.d(TAG, "EVENT_ON_AVAILABLE for " + network);
 
@@ -342,6 +360,14 @@
         notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
     }
 
+    private void recomputeOffloadExemptPrefixes() {
+        final HashSet<IpPrefix> exemptPrefixes = allOffloadExemptPrefixes(mNetworkMap.values());
+        if (!mOffloadExemptPrefixes.equals(exemptPrefixes)) {
+            mOffloadExemptPrefixes = exemptPrefixes;
+            notifyTarget(NOTIFY_EXEMPT_PREFIXES, exemptPrefixes.clone());
+        }
+    }
+
     // Fetch (and cache) a ConnectivityManager only if and when we need one.
     private ConnectivityManager cm() {
         if (mCM == null) {
@@ -376,6 +402,7 @@
         @Override
         public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
             handleLinkProp(network, newLp);
+            recomputeOffloadExemptPrefixes();
         }
 
         // TODO: Handle onNetworkSuspended();
@@ -384,6 +411,7 @@
         @Override
         public void onLost(Network network) {
             handleLost(mCallbackType, network);
+            recomputeOffloadExemptPrefixes();
         }
     }
 
@@ -395,16 +423,16 @@
         notifyTarget(which, mNetworkMap.get(network));
     }
 
-    private void notifyTarget(int which, NetworkState netstate) {
-        mTarget.sendMessage(mWhat, which, 0, netstate);
+    private void notifyTarget(int which, Object obj) {
+        mTarget.sendMessage(mWhat, which, 0, obj);
     }
 
-    static private class TypeStatePair {
+    private static class TypeStatePair {
         public int type = TYPE_NONE;
         public NetworkState ns = null;
     }
 
-    static private TypeStatePair findFirstAvailableUpstreamByType(
+    private static TypeStatePair findFirstAvailableUpstreamByType(
             Iterable<NetworkState> netStates, Iterable<Integer> preferredTypes) {
         final TypeStatePair result = new TypeStatePair();
 
@@ -431,4 +459,36 @@
 
         return result;
     }
+
+    private static HashSet<IpPrefix> allOffloadExemptPrefixes(Iterable<NetworkState> netStates) {
+        final HashSet<IpPrefix> prefixSet = new HashSet<>();
+
+        addDefaultLocalPrefixes(prefixSet);
+
+        for (NetworkState ns : netStates) {
+            addOffloadExemptPrefixes(prefixSet, ns.linkProperties);
+        }
+
+        return prefixSet;
+    }
+
+    private static void addDefaultLocalPrefixes(Set<IpPrefix> prefixSet) {
+        Collections.addAll(prefixSet, MINIMUM_LOCAL_PREFIXES_SET);
+    }
+
+    private static void addOffloadExemptPrefixes(Set<IpPrefix> prefixSet, LinkProperties lp) {
+        if (lp == null) return;
+
+        for (LinkAddress linkAddr : lp.getAllLinkAddresses()) {
+            prefixSet.add(new IpPrefix(linkAddr.getAddress(), linkAddr.getPrefixLength()));
+        }
+
+        // TODO: Consider adding other non-default routes associated with this
+        // network. Traffic to these destinations should perhaps not go through
+        // the Internet (upstream).
+    }
+
+    private static IpPrefix prefix(String prefixStr) {
+        return new IpPrefix(prefixStr);
+    }
 }
diff --git a/services/core/java/com/android/server/timezone/PackageStatusStorage.java b/services/core/java/com/android/server/timezone/PackageStatusStorage.java
index 05e97c7..fe82dc4 100644
--- a/services/core/java/com/android/server/timezone/PackageStatusStorage.java
+++ b/services/core/java/com/android/server/timezone/PackageStatusStorage.java
@@ -32,6 +32,7 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
+import java.io.PrintWriter;
 
 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE;
 import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS;
@@ -375,4 +376,8 @@
         }
         return value;
     }
+
+    public void dump(PrintWriter printWriter) {
+        printWriter.println("Package status: " + getPackageStatus());
+    }
 }
diff --git a/services/core/java/com/android/server/timezone/PackageTracker.java b/services/core/java/com/android/server/timezone/PackageTracker.java
index f9af2ea..e8dfd77 100644
--- a/services/core/java/com/android/server/timezone/PackageTracker.java
+++ b/services/core/java/com/android/server/timezone/PackageTracker.java
@@ -26,6 +26,7 @@
 import android.util.Slog;
 
 import java.io.File;
+import java.io.PrintWriter;
 
 /**
  * Monitors the installed applications associated with time zone updates. If the app packages are
@@ -510,4 +511,23 @@
         Slog.wtf(TAG, message, cause);
         throw new RuntimeException(message, cause);
     }
+
+    public void dump(PrintWriter fout) {
+        fout.println("PackageTrackerState: " + toString());
+        mPackageStatusStorage.dump(fout);
+    }
+
+    @Override
+    public String toString() {
+        return "PackageTracker{" +
+                "mTrackingEnabled=" + mTrackingEnabled +
+                ", mUpdateAppPackageName='" + mUpdateAppPackageName + '\'' +
+                ", mDataAppPackageName='" + mDataAppPackageName + '\'' +
+                ", mCheckTimeAllowedMillis=" + mCheckTimeAllowedMillis +
+                ", mFailedCheckRetryCount=" + mFailedCheckRetryCount +
+                ", mLastTriggerTimestamp=" + mLastTriggerTimestamp +
+                ", mCheckTriggered=" + mCheckTriggered +
+                ", mCheckFailureCount=" + mCheckFailureCount +
+                '}';
+    }
 }
diff --git a/services/core/java/com/android/server/timezone/PermissionHelper.java b/services/core/java/com/android/server/timezone/PermissionHelper.java
index ba91c7f..2ec31e2 100644
--- a/services/core/java/com/android/server/timezone/PermissionHelper.java
+++ b/services/core/java/com/android/server/timezone/PermissionHelper.java
@@ -16,10 +16,14 @@
 
 package com.android.server.timezone;
 
+import java.io.PrintWriter;
+
 /**
  * An easy-to-mock interface around permission checks for use by {@link RulesManagerService}.
  */
 public interface PermissionHelper {
 
     void enforceCallerHasPermission(String requiredPermission) throws SecurityException;
+
+    boolean checkDumpPermission(String tag, PrintWriter printWriter);
 }
diff --git a/services/core/java/com/android/server/timezone/RulesManagerService.java b/services/core/java/com/android/server/timezone/RulesManagerService.java
index 5724398..3d60dcf6 100644
--- a/services/core/java/com/android/server/timezone/RulesManagerService.java
+++ b/services/core/java/com/android/server/timezone/RulesManagerService.java
@@ -37,12 +37,24 @@
 import android.util.Slog;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
+import libcore.icu.ICU;
+import libcore.util.ZoneInfoDB;
+
+import static android.app.timezone.RulesState.DISTRO_STATUS_INSTALLED;
+import static android.app.timezone.RulesState.DISTRO_STATUS_NONE;
+import static android.app.timezone.RulesState.DISTRO_STATUS_UNKNOWN;
+import static android.app.timezone.RulesState.STAGED_OPERATION_INSTALL;
+import static android.app.timezone.RulesState.STAGED_OPERATION_NONE;
+import static android.app.timezone.RulesState.STAGED_OPERATION_UNINSTALL;
+import static android.app.timezone.RulesState.STAGED_OPERATION_UNKNOWN;
 
 // TODO(nfuller) Add EventLog calls where useful in the system server.
 // TODO(nfuller) Check logging best practices in the system server.
@@ -113,6 +125,11 @@
     public RulesState getRulesState() {
         mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION);
 
+        return getRulesStateInternal();
+    }
+
+    /** Like {@link #getRulesState()} without the permission check. */
+    private RulesState getRulesStateInternal() {
         synchronized(this) {
             String systemRulesVersion;
             try {
@@ -126,18 +143,18 @@
 
             // Determine the staged operation status, if possible.
             DistroRulesVersion stagedDistroRulesVersion = null;
-            int stagedOperationStatus = RulesState.STAGED_OPERATION_UNKNOWN;
+            int stagedOperationStatus = STAGED_OPERATION_UNKNOWN;
             if (!operationInProgress) {
                 StagedDistroOperation stagedDistroOperation;
                 try {
                     stagedDistroOperation = mInstaller.getStagedDistroOperation();
                     if (stagedDistroOperation == null) {
-                        stagedOperationStatus = RulesState.STAGED_OPERATION_NONE;
+                        stagedOperationStatus = STAGED_OPERATION_NONE;
                     } else if (stagedDistroOperation.isUninstall) {
-                        stagedOperationStatus = RulesState.STAGED_OPERATION_UNINSTALL;
+                        stagedOperationStatus = STAGED_OPERATION_UNINSTALL;
                     } else {
                         // Must be an install.
-                        stagedOperationStatus = RulesState.STAGED_OPERATION_INSTALL;
+                        stagedOperationStatus = STAGED_OPERATION_INSTALL;
                         DistroVersion stagedDistroVersion = stagedDistroOperation.distroVersion;
                         stagedDistroRulesVersion = new DistroRulesVersion(
                                 stagedDistroVersion.rulesVersion,
@@ -150,16 +167,16 @@
 
             // Determine the installed distro state, if possible.
             DistroVersion installedDistroVersion;
-            int distroStatus = RulesState.DISTRO_STATUS_UNKNOWN;
+            int distroStatus = DISTRO_STATUS_UNKNOWN;
             DistroRulesVersion installedDistroRulesVersion = null;
             if (!operationInProgress) {
                 try {
                     installedDistroVersion = mInstaller.getInstalledDistroVersion();
                     if (installedDistroVersion == null) {
-                        distroStatus = RulesState.DISTRO_STATUS_NONE;
+                        distroStatus = DISTRO_STATUS_NONE;
                         installedDistroRulesVersion = null;
                     } else {
-                        distroStatus = RulesState.DISTRO_STATUS_INSTALLED;
+                        distroStatus = DISTRO_STATUS_INSTALLED;
                         installedDistroRulesVersion = new DistroRulesVersion(
                                 installedDistroVersion.rulesVersion,
                                 installedDistroVersion.revision);
@@ -358,6 +375,87 @@
         mPackageTracker.recordCheckResult(checkToken, success);
     }
 
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!mPermissionHelper.checkDumpPermission(TAG, pw)) {
+            return;
+        }
+
+        RulesState rulesState = getRulesStateInternal();
+        if (args != null && args.length == 2) {
+            // Formatting options used for automated tests. The format is less free-form than
+            // the -format options, which are intended to be easier to parse.
+            if ("-format_state".equals(args[0]) && args[1] != null) {
+                for (char c : args[1].toCharArray()) {
+                    switch (c) {
+                        case 'p': // Report operation in progress
+                            pw.println("Operation in progress: "
+                                    + rulesState.isOperationInProgress());
+                            break;
+                        case 's': // Report system image rules version
+                            pw.println("System rules version: "
+                                    + rulesState.getSystemRulesVersion());
+                            break;
+                        case 'c': // Report current installation state
+                            pw.println("Current install state: "
+                                    + distroStatusToString(rulesState.getDistroStatus()));
+                            break;
+                        case 'i': // Report currently installed version
+                            DistroRulesVersion installedRulesVersion =
+                                    rulesState.getInstalledDistroRulesVersion();
+                            pw.print("Installed rules version: ");
+                            if (installedRulesVersion == null) {
+                                pw.println("<None>");
+                            } else {
+                                pw.println(installedRulesVersion.toDumpString());
+                            }
+                            break;
+                        case 'o': // Report staged operation type
+                            int stagedOperationType = rulesState.getStagedOperationType();
+                            pw.println("Staged operation: "
+                                    + stagedOperationToString(stagedOperationType));
+                            break;
+                        case 't':
+                            // Report staged version (i.e. the one that will be installed next boot
+                            // if the staged operation is an install).
+                            pw.print("Staged rules version: ");
+                            DistroRulesVersion stagedDistroRulesVersion =
+                                    rulesState.getStagedDistroRulesVersion();
+                            if (stagedDistroRulesVersion == null) {
+                                pw.println("<None>");
+                            } else {
+                                pw.println("Staged install version: "
+                                        + stagedDistroRulesVersion.toDumpString());
+                            }
+                            break;
+                        case 'a':
+                            // Report the active rules version (i.e. the rules in use by the current
+                            // process).
+                            pw.println("Active rules version (ICU, libcore): "
+                                    + ICU.getTZDataVersion() + ","
+                                    + ZoneInfoDB.getInstance().getVersion());
+                            break;
+                        default:
+                            pw.println("Unknown option: " + c);
+                    }
+                }
+                return;
+            }
+        }
+
+        pw.println("RulesManagerService state: " + toString());
+        pw.println("Active rules version (ICU, libcore): " + ICU.getTZDataVersion() + ","
+                + ZoneInfoDB.getInstance().getVersion());
+        mPackageTracker.dump(pw);
+    }
+
+    @Override
+    public String toString() {
+        return "RulesManagerService{" +
+                "mOperationInProgress=" + mOperationInProgress +
+                '}';
+    }
+
     private static CheckToken createCheckTokenOrThrow(byte[] checkTokenBytes) {
         CheckToken checkToken;
         try {
@@ -368,4 +466,30 @@
         }
         return checkToken;
     }
+
+    private static String distroStatusToString(int distroStatus) {
+        switch(distroStatus) {
+            case DISTRO_STATUS_NONE:
+                return "None";
+            case DISTRO_STATUS_INSTALLED:
+                return "Installed";
+            case DISTRO_STATUS_UNKNOWN:
+            default:
+                return "Unknown";
+        }
+    }
+
+    private static String stagedOperationToString(int stagedOperationType) {
+        switch(stagedOperationType) {
+            case STAGED_OPERATION_NONE:
+                return "None";
+            case STAGED_OPERATION_UNINSTALL:
+                return "Uninstall";
+            case STAGED_OPERATION_INSTALL:
+                return "Install";
+            case STAGED_OPERATION_UNKNOWN:
+            default:
+                return "Unknown";
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
index 482d8e2..767f0e0 100644
--- a/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
+++ b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java
@@ -17,10 +17,13 @@
 package com.android.server.timezone;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.ParcelFileDescriptor;
 
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.concurrent.Executor;
 import libcore.io.Streams;
 
@@ -40,6 +43,19 @@
         mContext.enforceCallingPermission(requiredPermission, null /* message */);
     }
 
+    @Override
+    public boolean checkDumpPermission(String tag, PrintWriter pw) {
+        // TODO(nfuller): Switch to DumpUtils.checkDumpPermission() when it is available in AOSP.
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump LocationManagerService from from pid="
+                    + Binder.getCallingPid()
+                    + ", uid=" + Binder.getCallingUid());
+            return false;
+        }
+        return true;
+    }
+
     // TODO Wake lock required?
     @Override
     public void execute(Runnable runnable) {
diff --git a/services/net/java/android/net/ip/IpManager.java b/services/net/java/android/net/ip/IpManager.java
index 9e2d696..9de4ba4 100644
--- a/services/net/java/android/net/ip/IpManager.java
+++ b/services/net/java/android/net/ip/IpManager.java
@@ -35,6 +35,7 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.IpManagerEvent;
 import android.net.util.MultinetworkPolicyTracker;
+import android.net.util.SharedLog;
 import android.os.INetworkManagementService;
 import android.os.Message;
 import android.os.RemoteException;
@@ -187,7 +188,7 @@
         }
 
         private void log(String msg) {
-            mLocalLog.log(PREFIX + msg);
+            mLog.log(PREFIX + msg);
         }
 
         @Override
@@ -414,7 +415,7 @@
     private final WakeupMessage mProvisioningTimeoutAlarm;
     private final WakeupMessage mDhcpActionTimeoutAlarm;
     private final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
-    private final LocalLog mLocalLog;
+    private final SharedLog mLog;
     private final LocalLog mConnectivityPacketLog;
     private final MessageHandlingLogger mMsgStateLogger;
     private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
@@ -455,7 +456,7 @@
         mCallback = new LoggingCallbackWrapper(callback);
         mNwService = nwService;
 
-        mLocalLog = new LocalLog(MAX_LOG_RECORDS);
+        mLog = new SharedLog(MAX_LOG_RECORDS, mTag);
         mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS);
         mMsgStateLogger = new MessageHandlingLogger();
 
@@ -500,7 +501,7 @@
 
             private void logMsg(String msg) {
                 Log.d(mTag, msg);
-                getHandler().post(() -> { mLocalLog.log("OBSERVED " + msg); });
+                getHandler().post(() -> { mLog.log("OBSERVED " + msg); });
             }
         };
 
@@ -508,7 +509,7 @@
         mLinkProperties.setInterfaceName(mInterfaceName);
 
         mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(),
-                () -> { mLocalLog.log("OBSERVED AvoidBadWifi changed"); });
+                () -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
 
         mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                 mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
@@ -658,7 +659,7 @@
         pw.println();
         pw.println(mTag + " StateMachine dump:");
         pw.increaseIndent();
-        mLocalLog.readOnlyLocalLog().dump(fd, pw, args);
+        mLog.dump(fd, pw, args);
         pw.decreaseIndent();
 
         pw.println();
@@ -693,7 +694,7 @@
                 msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);
 
         final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
-        mLocalLog.log(richerLogLine);
+        mLog.log(richerLogLine);
         if (VDBG) {
             Log.d(mTag, richerLogLine);
         }
@@ -717,7 +718,7 @@
     private void logError(String fmt, Object... args) {
         final String msg = "ERROR " + String.format(fmt, args);
         Log.e(mTag, msg);
-        mLocalLog.log(msg);
+        mLog.log(msg);
     }
 
     private void getNetworkInterface() {
@@ -1088,6 +1089,7 @@
             mIpReachabilityMonitor = new IpReachabilityMonitor(
                     mContext,
                     mInterfaceName,
+                    mLog,
                     new IpReachabilityMonitor.Callback() {
                         @Override
                         public void notifyLost(InetAddress ip, String logMsg) {
diff --git a/services/net/java/android/net/ip/IpReachabilityMonitor.java b/services/net/java/android/net/ip/IpReachabilityMonitor.java
index a004dbb..846af17 100644
--- a/services/net/java/android/net/ip/IpReachabilityMonitor.java
+++ b/services/net/java/android/net/ip/IpReachabilityMonitor.java
@@ -35,6 +35,7 @@
 import android.net.netlink.StructNdMsg;
 import android.net.netlink.StructNlMsgHdr;
 import android.net.util.MultinetworkPolicyTracker;
+import android.net.util.SharedLog;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.system.ErrnoException;
@@ -150,6 +151,7 @@
     private final PowerManager.WakeLock mWakeLock;
     private final String mInterfaceName;
     private final int mInterfaceIndex;
+    private final SharedLog mLog;
     private final Callback mCallback;
     private final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
     private final NetlinkSocketObserver mNetlinkSocketObserver;
@@ -222,11 +224,11 @@
         return errno;
     }
 
-    public IpReachabilityMonitor(Context context, String ifName, Callback callback) {
-        this(context, ifName, callback, null);
+    public IpReachabilityMonitor(Context context, String ifName, SharedLog log, Callback callback) {
+        this(context, ifName, log, callback, null);
     }
 
-    public IpReachabilityMonitor(Context context, String ifName, Callback callback,
+    public IpReachabilityMonitor(Context context, String ifName, SharedLog log, Callback callback,
             MultinetworkPolicyTracker tracker) throws IllegalArgumentException {
         mInterfaceName = ifName;
         int ifIndex = -1;
@@ -238,6 +240,7 @@
         }
         mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).newWakeLock(
                 PowerManager.PARTIAL_WAKE_LOCK, TAG + "." + mInterfaceName);
+        mLog = log.forSubComponent(TAG);
         mCallback = callback;
         mMultinetworkPolicyTracker = tracker;
         mNetlinkSocketObserver = new NetlinkSocketObserver();
@@ -410,6 +413,8 @@
                 break;
             }
             final int returnValue = probeNeighbor(mInterfaceIndex, target);
+            mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)",
+                     target.getHostAddress(), returnValue));
             logEvent(IpReachabilityEvent.PROBE, returnValue);
         }
         mLastProbeTimeMs = SystemClock.elapsedRealtime();
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java
index dd56072..b57cac0 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java
@@ -25,6 +25,8 @@
 import android.support.test.filters.SmallTest;
 
 import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 
 import static junit.framework.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
@@ -228,4 +230,27 @@
         assertFalse(writeOk2);
         assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus());
     }
+
+    @Test
+    public void dump() {
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+
+        // Dump initial state.
+        mPackageStatusStorage.dump(printWriter);
+
+        // No crash and it does something.
+        assertFalse(stringWriter.toString().isEmpty());
+
+        // Reset
+        stringWriter.getBuffer().setLength(0);
+        assertTrue(stringWriter.toString().isEmpty());
+
+        // Store something.
+        mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS);
+
+        mPackageStatusStorage.dump(printWriter);
+
+        assertFalse(stringWriter.toString().isEmpty());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
index 4c7680b..a972e4f 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java
@@ -30,6 +30,9 @@
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -1174,6 +1177,16 @@
         assertFalse(token1.equals(token2));
     }
 
+    @Test
+    public void dump() {
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+
+        mPackageTracker.dump(printWriter);
+
+        assertFalse(stringWriter.toString().isEmpty());
+    }
+
     private void simulatePackageInstallation(PackageVersions packageVersions) throws Exception {
         configureApplicationsValidManifests(packageVersions);
 
diff --git a/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
index 1407e26..2887e3b 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java
@@ -32,11 +32,15 @@
 import android.os.ParcelFileDescriptor;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.concurrent.Executor;
 import javax.annotation.Nullable;
 
+import libcore.io.IoUtils;
+
 import static com.android.server.timezone.RulesManagerService.REQUIRED_UPDATER_PERMISSION;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -52,6 +56,7 @@
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 /**
@@ -724,6 +729,97 @@
         verifyPackageTrackerCalled(null /* token */, true /* success */);
     }
 
+    @Test
+    public void dump_noPermission() throws Exception {
+        when(mMockPermissionHelper.checkDumpPermission(any(String.class), any(PrintWriter.class)))
+                .thenReturn(false);
+
+        doDumpCallAndCapture(mRulesManagerService, null);
+        verifyZeroInteractions(mMockPackageTracker, mMockTimeZoneDistroInstaller);
+    }
+
+    @Test
+    public void dump_emptyArgs() throws Exception {
+        doSuccessfulDumpCall(mRulesManagerService, new String[0]);
+
+        // Verify the package tracker was consulted.
+        verify(mMockPackageTracker).dump(any(PrintWriter.class));
+    }
+
+    @Test
+    public void dump_nullArgs() throws Exception {
+        doSuccessfulDumpCall(mRulesManagerService, null);
+        // Verify the package tracker was consulted.
+        verify(mMockPackageTracker).dump(any(PrintWriter.class));
+    }
+
+    @Test
+    public void dump_unknownArgs() throws Exception {
+        String dumpedTextUnknownArgs = doSuccessfulDumpCall(
+                mRulesManagerService, new String[] { "foo", "bar"});
+
+        // Verify the package tracker was consulted.
+        verify(mMockPackageTracker).dump(any(PrintWriter.class));
+
+        String dumpedTextZeroArgs = doSuccessfulDumpCall(mRulesManagerService, null);
+        assertEquals(dumpedTextZeroArgs, dumpedTextUnknownArgs);
+    }
+
+    @Test
+    public void dump_formatState() throws Exception {
+        // Just expect these to not throw exceptions, not return nothing, and not interact with the
+        // package tracker.
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("p"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("s"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("c"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("i"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("o"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("t"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("a"));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("z" /* Unknown */));
+        doSuccessfulDumpCall(mRulesManagerService, dumpFormatArgs("piscotz"));
+
+        verifyZeroInteractions(mMockPackageTracker);
+    }
+
+    private static String[] dumpFormatArgs(String argsString) {
+        return new String[] { "-format_state", argsString};
+    }
+
+    private String doSuccessfulDumpCall(RulesManagerService rulesManagerService, String[] args)
+            throws Exception {
+        when(mMockPermissionHelper.checkDumpPermission(any(String.class), any(PrintWriter.class)))
+                .thenReturn(true);
+
+        // Set up the mocks to return (arbitrary) information about the current device state.
+        when(mMockTimeZoneDistroInstaller.getSystemRulesVersion()).thenReturn("2017a");
+        when(mMockTimeZoneDistroInstaller.getInstalledDistroVersion()).thenReturn(
+                new DistroVersion(2, 3, "2017b", 4));
+        when(mMockTimeZoneDistroInstaller.getStagedDistroOperation()).thenReturn(
+                StagedDistroOperation.install(new DistroVersion(5, 6, "2017c", 7)));
+
+        // Do the dump call.
+        String dumpedOutput = doDumpCallAndCapture(rulesManagerService, args);
+
+        assertFalse(dumpedOutput.isEmpty());
+
+        return dumpedOutput;
+    }
+
+    private static String doDumpCallAndCapture(
+            RulesManagerService rulesManagerService, String[] args) throws IOException {
+        File file = File.createTempFile("dump", null);
+        try {
+            try (FileOutputStream fos = new FileOutputStream(file)) {
+                FileDescriptor fd = fos.getFD();
+                rulesManagerService.dump(fd, args);
+            }
+            return IoUtils.readFileAsString(file.getAbsolutePath());
+        } finally {
+            file.delete();
+        }
+    }
+
     private void verifyNoPackageTrackerCallsMade() {
         verifyNoMoreInteractions(mMockPackageTracker);
         reset(mMockPackageTracker);
diff --git a/tests/net/java/android/net/nsd/NsdManagerTest.java b/tests/net/java/android/net/nsd/NsdManagerTest.java
index adf6998..f77608f 100644
--- a/tests/net/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/net/java/android/net/nsd/NsdManagerTest.java
@@ -349,7 +349,6 @@
                 chan.connect(mContext, this, msg.replyTo);
                 chan.sendMessage(AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED);
             }
-
         }
 
         public static MockServiceHandler create(Context context) {
diff --git a/tests/net/java/android/net/util/SharedLogTest.java b/tests/net/java/android/net/util/SharedLogTest.java
index 3957cb0..d46facf 100644
--- a/tests/net/java/android/net/util/SharedLogTest.java
+++ b/tests/net/java/android/net/util/SharedLogTest.java
@@ -33,8 +33,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class SharedLogTest {
-    private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}\\.\\d{3}";
-    private static final String TIMESTAMP = "HH:MM:SS.xxx";
+    private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+    private static final String TIMESTAMP = "HH:MM:SS";
 
     @Test
     public void testBasicOperation() {
@@ -85,7 +85,7 @@
             String got = lines[i];
             String want = expected[i];
             assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
-            assertTrue(String.format("'%s' did not contain a HH:MM:SS.xxx timestamp", got),
+            assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
                     got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
         }
     }
diff --git a/tests/net/java/com/android/internal/util/TestUtils.java b/tests/net/java/com/android/internal/util/TestUtils.java
new file mode 100644
index 0000000..c9fa340
--- /dev/null
+++ b/tests/net/java/com/android/internal/util/TestUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import static org.junit.Assert.fail;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+public final class TestUtils {
+    private TestUtils() { }
+
+    /**
+     * Block until the given Handler thread becomes idle, or until timeoutMs has passed.
+     */
+    public static void waitForIdleHandler(HandlerThread handlerThread, long timeoutMs) {
+        // TODO: convert to getThreadHandler once it is available on aosp
+        waitForIdleLooper(handlerThread.getLooper(), timeoutMs);
+    }
+
+    /**
+     * Block until the given Looper becomes idle, or until timeoutMs has passed.
+     */
+    public static void waitForIdleLooper(Looper looper, long timeoutMs) {
+        waitForIdleHandler(new Handler(looper), timeoutMs);
+    }
+
+    /**
+     * Block until the given Handler becomes idle, or until timeoutMs has passed.
+     */
+    public static void waitForIdleHandler(Handler handler, long timeoutMs) {
+        final ConditionVariable cv = new ConditionVariable();
+        handler.post(() -> cv.open());
+        if (!cv.block(timeoutMs)) {
+            fail(handler.toString() + " did not become idle after " + timeoutMs + " ms");
+        }
+    }
+}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 91fdb23..158a7ce 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -24,6 +24,8 @@
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.NetworkCapabilities.*;
 
+import static com.android.internal.util.TestUtils.waitForIdleHandler;
+
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
@@ -213,20 +215,8 @@
         }
     }
 
-    /**
-     * Block until the given handler becomes idle, or until timeoutMs has passed.
-     */
-    private static void waitForIdleHandler(HandlerThread handlerThread, int timeoutMs) {
-        final ConditionVariable cv = new ConditionVariable();
-        final Handler handler = new Handler(handlerThread.getLooper());
-        handler.post(() -> cv.open());
-        if (!cv.block(timeoutMs)) {
-            fail("HandlerThread " + handlerThread.getName() +
-                    " did not become idle after " + timeoutMs + " ms");
-        }
-    }
-
-    public void waitForIdle(int timeoutMs) {
+    public void waitForIdle(int timeoutMsAsInt) {
+        long timeoutMs = timeoutMsAsInt;
         waitForIdleHandler(mService.mHandlerThread, timeoutMs);
         waitForIdle(mCellNetworkAgent, timeoutMs);
         waitForIdle(mWiFiNetworkAgent, timeoutMs);
@@ -234,7 +224,7 @@
         waitForIdleHandler(mService.mHandlerThread, timeoutMs);
     }
 
-    public void waitForIdle(MockNetworkAgent agent, int timeoutMs) {
+    public void waitForIdle(MockNetworkAgent agent, long timeoutMs) {
         if (agent == null) {
             return;
         }
diff --git a/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java b/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
index fb5c577..69c93b1 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
@@ -44,6 +44,9 @@
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.IConnectivityManager;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
@@ -66,6 +69,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -77,6 +81,9 @@
 public class UpstreamNetworkMonitorTest {
     private static final int EVENT_UNM_UPDATE = 1;
 
+    private static final boolean INCLUDES = true;
+    private static final boolean EXCLUDES = false;
+
     @Mock private Context mContext;
     @Mock private IConnectivityManager mCS;
     @Mock private SharedLog mLog;
@@ -94,7 +101,8 @@
 
         mCM = spy(new TestConnectivityManager(mContext, mCS));
         mSM = new TestStateMachine();
-        mUNM = new UpstreamNetworkMonitor(mSM, EVENT_UNM_UPDATE, (ConnectivityManager) mCM, mLog);
+        mUNM = new UpstreamNetworkMonitor(
+                (ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE);
     }
 
     @After public void tearDown() throws Exception {
@@ -315,6 +323,101 @@
         assertTrue(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN));
     }
 
+    @Test
+    public void testOffloadExemptPrefixes() throws Exception {
+        mUNM.start();
+
+        // [0] Test minimum set of exempt prefixes.
+        Set<IpPrefix> exempt = mUNM.getOffloadExemptPrefixes();
+        final String[] MINSET = {
+                "127.0.0.0/8", "169.254.0.0/16",
+                "::/3", "fe80::/64", "fc00::/7", "ff00::/8",
+        };
+        assertPrefixSet(exempt, INCLUDES, MINSET);
+        final Set<String> alreadySeen = new HashSet<>();
+        Collections.addAll(alreadySeen, MINSET);
+        assertEquals(alreadySeen.size(), exempt.size());
+
+        // [1] Pretend Wi-Fi connects.
+        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName("wlan0");
+        final String[] WIFI_ADDRS = {
+                "fe80::827a:bfff:fe6f:374d", "100.112.103.18",
+                "2001:db8:4:fd00:827a:bfff:fe6f:374d",
+                "2001:db8:4:fd00:6dea:325a:fdae:4ef4",
+                "fd6a:a640:60bf:e985::123",  // ULA address for good measure.
+        };
+        for (String addrStr : WIFI_ADDRS) {
+            final String cidr = addrStr.contains(":") ? "/64" : "/20";
+            wifiLp.addLinkAddress(new LinkAddress(addrStr + cidr));
+        }
+        wifiAgent.fakeConnect();
+        wifiAgent.sendLinkProperties(wifiLp);
+
+        exempt = mUNM.getOffloadExemptPrefixes();
+        assertPrefixSet(exempt, INCLUDES, alreadySeen);
+        final String[] wifiLinkPrefixes = {
+                // Excludes link-local as that's already tested within MINSET.
+                "100.112.96.0/20", "2001:db8:4:fd00::/64", "fd6a:a640:60bf:e985::/64",
+        };
+        assertPrefixSet(exempt, INCLUDES, wifiLinkPrefixes);
+        Collections.addAll(alreadySeen, wifiLinkPrefixes);
+        assertEquals(alreadySeen.size(), exempt.size());
+
+        // [2] Pretend mobile connects.
+        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName("rmnet_data0");
+        final String[] CELL_ADDRS = {
+                "10.102.211.48", "2001:db8:0:1:b50e:70d9:10c9:433d",
+        };
+        for (String addrStr : CELL_ADDRS) {
+            final String cidr = addrStr.contains(":") ? "/64" : "/27";
+            cellLp.addLinkAddress(new LinkAddress(addrStr + cidr));
+        }
+        cellAgent.fakeConnect();
+        cellAgent.sendLinkProperties(cellLp);
+
+        exempt = mUNM.getOffloadExemptPrefixes();
+        assertPrefixSet(exempt, INCLUDES, alreadySeen);
+        final String[] cellLinkPrefixes = { "10.102.211.32/27", "2001:db8:0:1::/64" };
+        assertPrefixSet(exempt, INCLUDES, cellLinkPrefixes);
+        Collections.addAll(alreadySeen, cellLinkPrefixes);
+        assertEquals(alreadySeen.size(), exempt.size());
+
+        // [3] Pretend DUN connects.
+        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+        dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
+        final LinkProperties dunLp = new LinkProperties();
+        dunLp.setInterfaceName("rmnet_data1");
+        final String[] DUN_ADDRS = {
+                "192.0.2.48", "2001:db8:1:2:b50e:70d9:10c9:433d",
+        };
+        for (String addrStr : DUN_ADDRS) {
+            final String cidr = addrStr.contains(":") ? "/64" : "/27";
+            cellLp.addLinkAddress(new LinkAddress(addrStr + cidr));
+        }
+        dunAgent.fakeConnect();
+        dunAgent.sendLinkProperties(dunLp);
+
+        exempt = mUNM.getOffloadExemptPrefixes();
+        assertPrefixSet(exempt, INCLUDES, alreadySeen);
+        final String[] dunLinkPrefixes = { "192.0.2.32/27", "2001:db8:1:2::/64" };
+        assertPrefixSet(exempt, INCLUDES, dunLinkPrefixes);
+        Collections.addAll(alreadySeen, dunLinkPrefixes);
+        assertEquals(alreadySeen.size(), exempt.size());
+
+        // [4] Pretend Wi-Fi disconnected.  It's addresses/prefixes should no
+        // longer be included (should be properly removed).
+        wifiAgent.fakeDisconnect();
+        exempt = mUNM.getOffloadExemptPrefixes();
+        assertPrefixSet(exempt, INCLUDES, MINSET);
+        assertPrefixSet(exempt, EXCLUDES, wifiLinkPrefixes);
+        assertPrefixSet(exempt, INCLUDES, cellLinkPrefixes);
+        assertPrefixSet(exempt, INCLUDES, dunLinkPrefixes);
+    }
+
     private void assertSatisfiesLegacyType(int legacyType, NetworkState ns) {
         if (legacyType == TYPE_NONE) {
             assertTrue(ns == null);
@@ -476,6 +579,12 @@
                 cb.onLost(networkId);
             }
         }
+
+        public void sendLinkProperties(LinkProperties lp) {
+            for (NetworkCallback cb : cm.listening.keySet()) {
+                cb.onLinkPropertiesChanged(networkId, lp);
+            }
+        }
     }
 
     public static class TestStateMachine extends StateMachine {
@@ -504,4 +613,19 @@
     static NetworkCapabilities copy(NetworkCapabilities nc) {
         return new NetworkCapabilities(nc);
     }
+
+    static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
+        final Set<String> expectedSet = new HashSet<>();
+        Collections.addAll(expectedSet, expected);
+        assertPrefixSet(prefixes, expectation, expectedSet);
+    }
+
+    static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, Set<String> expected) {
+        for (String expectedPrefix : expected) {
+            final String errStr = expectation ? "did not find" : "found";
+            assertEquals(
+                    String.format("Failed expectation: %s prefix: %s", errStr, expectedPrefix),
+                    expectation, prefixes.contains(new IpPrefix(expectedPrefix)));
+        }
+    }
 }
diff --git a/tests/net/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/net/java/com/android/server/net/NetworkStatsObserversTest.java
index 92dcdac..2be5dae 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -27,6 +27,8 @@
 import static android.net.TrafficStats.MB_IN_BYTES;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
+import static com.android.internal.util.TestUtils.waitForIdleHandler;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -56,7 +58,6 @@
 
 import com.android.internal.net.VpnInfo;
 import com.android.server.net.NetworkStatsService;
-import com.android.server.net.NetworkStatsServiceTest.IdleableHandlerThread;
 import com.android.server.net.NetworkStatsServiceTest.LatchedHandler;
 
 import java.util.ArrayList;
@@ -102,7 +103,7 @@
 
     private long mElapsedRealtime;
 
-    private IdleableHandlerThread mObserverHandlerThread;
+    private HandlerThread mObserverHandlerThread;
     private Handler mObserverNoopHandler;
 
     private LatchedHandler mHandler;
@@ -118,7 +119,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mObserverHandlerThread = new IdleableHandlerThread("HandlerThread");
+        mObserverHandlerThread = new HandlerThread("HandlerThread");
         mObserverHandlerThread.start();
         final Looper observerLooper = mObserverHandlerThread.getLooper();
         mStatsObservers = new NetworkStatsObservers() {
@@ -319,7 +320,7 @@
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
                 VPN_INFO, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.mLastMessageType);
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
     }
 
     @Test
@@ -356,7 +357,7 @@
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
                 VPN_INFO, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.mLastMessageType);
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
     }
 
     @Test
@@ -429,7 +430,7 @@
                 xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces,
                 VPN_INFO, TEST_START);
         waitForObserverToIdle();
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.mLastMessageType);
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
     }
 
     @Test
@@ -470,19 +471,7 @@
     }
 
     private void waitForObserverToIdle() {
-        waitForIdleLooper(mObserverHandlerThread.getLooper(), WAIT_TIMEOUT_MS);
-        waitForIdleLooper(mHandler.getLooper(), WAIT_TIMEOUT_MS);
+        waitForIdleHandler(mObserverHandlerThread, WAIT_TIMEOUT_MS);
+        waitForIdleHandler(mHandler, WAIT_TIMEOUT_MS);
     }
-
-    // TODO: unify with ConnectivityService.waitForIdleHandler and
-    // NetworkServiceStatsTest.IdleableHandlerThread
-    private static void waitForIdleLooper(Looper looper, long timeoutMs) {
-        final ConditionVariable cv = new ConditionVariable();
-        final Handler handler = new Handler(looper);
-        handler.post(() -> cv.open());
-        if (!cv.block(timeoutMs)) {
-            fail("Looper did not become idle after " + timeoutMs + " ms");
-        }
-    }
-
 }
diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
index 029693f..feb46d3 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -45,6 +45,7 @@
 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
 
 import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.internal.util.TestUtils.waitForIdleHandler;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -154,7 +155,7 @@
     private @Mock IConnectivityManager mConnManager;
     private @Mock IBinder mBinder;
     private @Mock AlarmManager mAlarmManager;
-    private IdleableHandlerThread mHandlerThread;
+    private HandlerThread mHandlerThread;
     private Handler mHandler;
 
     private NetworkStatsService mService;
@@ -181,7 +182,7 @@
                 mServiceContext, mNetManager, mAlarmManager, wakeLock, mTime,
                 TelephonyManager.getDefault(), mSettings, new NetworkStatsObservers(),
                 mStatsDir, getBaseDir(mStatsDir));
-        mHandlerThread = new IdleableHandlerThread("HandlerThread");
+        mHandlerThread = new HandlerThread("HandlerThread");
         mHandlerThread.start();
         Handler.Callback callback = new NetworkStatsService.HandlerCallback(mService);
         mHandler = new Handler(mHandlerThread.getLooper(), callback);
@@ -886,7 +887,7 @@
 
         // Send dummy message to make sure that any previous message has been handled
         mHandler.sendMessage(mHandler.obtainMessage(-1));
-        mHandlerThread.waitForIdle(WAIT_TIMEOUT);
+        waitForIdleHandler(mHandler, WAIT_TIMEOUT);
 
 
 
@@ -908,7 +909,7 @@
         assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
 
         // make sure callback has not being called
-        assertEquals(INVALID_TYPE, latchedHandler.mLastMessageType);
+        assertEquals(INVALID_TYPE, latchedHandler.lastMessageType);
 
         // and bump forward again, with counters going higher. this is
         // important, since it will trigger the data usage callback
@@ -926,7 +927,7 @@
 
         // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED
         assertTrue(cv.block(WAIT_TIMEOUT));
-        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.mLastMessageType);
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType);
         cv.close();
 
         // Allow binder to disconnect
@@ -937,7 +938,7 @@
 
         // Wait for the caller to ack receipt of CALLBACK_RELEASED
         assertTrue(cv.block(WAIT_TIMEOUT));
-        assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.mLastMessageType);
+        assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType);
 
         // Make sure that the caller binder gets disconnected
         verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
@@ -1203,12 +1204,12 @@
         mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
         // Send dummy message to make sure that any previous message has been handled
         mHandler.sendMessage(mHandler.obtainMessage(-1));
-        mHandlerThread.waitForIdle(WAIT_TIMEOUT);
+        waitForIdleHandler(mHandler, WAIT_TIMEOUT);
     }
 
     static class LatchedHandler extends Handler {
         private final ConditionVariable mCv;
-        int mLastMessageType = INVALID_TYPE;
+        int lastMessageType = INVALID_TYPE;
 
         LatchedHandler(Looper looper, ConditionVariable cv) {
             super(looper);
@@ -1217,49 +1218,9 @@
 
         @Override
         public void handleMessage(Message msg) {
-            mLastMessageType = msg.what;
+            lastMessageType = msg.what;
             mCv.open();
             super.handleMessage(msg);
         }
     }
-
-    /**
-     * A subclass of HandlerThread that allows callers to wait for it to become idle. waitForIdle
-     * will return immediately if the handler is already idle.
-     */
-    static class IdleableHandlerThread extends HandlerThread {
-        private IdleHandler mIdleHandler;
-
-        public IdleableHandlerThread(String name) {
-            super(name);
-        }
-
-        public void waitForIdle(long timeoutMs) {
-            final ConditionVariable cv = new ConditionVariable();
-            final MessageQueue queue = getLooper().getQueue();
-
-            synchronized (queue) {
-                if (queue.isIdle()) {
-                    return;
-                }
-
-                assertNull("BUG: only one idle handler allowed", mIdleHandler);
-                mIdleHandler = new IdleHandler() {
-                    public boolean queueIdle() {
-                        cv.open();
-                        mIdleHandler = null;
-                        return false;  // Remove the handler.
-                    }
-                };
-                queue.addIdleHandler(mIdleHandler);
-            }
-
-            if (!cv.block(timeoutMs)) {
-                fail("HandlerThread " + getName() + " did not become idle after " + timeoutMs
-                        + " ms");
-                queue.removeIdleHandler(mIdleHandler);
-            }
-        }
-    }
-
 }