Merge "Rewrite lingering." into nyc-mr1-dev
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 995a910..b12a961 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -94,6 +94,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -125,6 +126,7 @@
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.MessageUtils;
+import com.android.internal.util.WakeupMessage;
 import com.android.internal.util.XmlUtils;
 import com.android.server.am.BatteryStatsService;
 import com.android.server.connectivity.DataConnectionStats;
@@ -171,7 +173,7 @@
  */
 public class ConnectivityService extends IConnectivityManager.Stub
         implements PendingIntent.OnFinished {
-    private static final String TAG = "ConnectivityService";
+    private static final String TAG = ConnectivityService.class.getSimpleName();
 
     private static final boolean DBG = true;
     private static final boolean VDBG = false;
@@ -191,6 +193,12 @@
     // connect anyway?" dialog after the user selects a network that doesn't validate.
     private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
 
+    // Default to 30s linger time-out. Modifiable only for testing.
+    private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
+    private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
+    @VisibleForTesting
+    protected int mLingerDelayMs;  // Can't be final, or test subclass constructors can't change it.
+
     // How long to delay to removal of a pending intent based request.
     // See Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
     private final int mReleasePendingIntentDelayMs;
@@ -239,7 +247,8 @@
     private static final int DISABLED = 0;
 
     private static final SparseArray<String> sMagicDecoderRing = MessageUtils.findMessageNames(
-            new Class[] { AsyncChannel.class, ConnectivityService.class, NetworkAgent.class });
+            new Class[] { AsyncChannel.class, ConnectivityService.class, NetworkAgent.class,
+                    NetworkAgentInfo.class });
 
     private enum ReapUnvalidatedNetworks {
         // Tear down networks that have no chance (e.g. even if validated) of becoming
@@ -681,6 +690,8 @@
         mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(),
                 Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000);
 
+        mLingerDelayMs = SystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
+
         mContext = checkNotNull(context, "missing Context");
         mNetd = checkNotNull(netManager, "missing INetworkManagementService");
         mStatsService = checkNotNull(statsService, "missing INetworkStatsService");
@@ -1905,7 +1916,8 @@
         for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
             pw.println(nai.toString());
             pw.increaseIndent();
-            pw.println("Requests:");
+            pw.println(String.format("Requests: %d request/%d total",
+                    nai.numRequestNetworkRequests(), nai.numNetworkRequests()));
             pw.increaseIndent();
             for (int i = 0; i < nai.numNetworkRequests(); i++) {
                 pw.println(nai.requestAt(i).toString());
@@ -1913,7 +1925,7 @@
             pw.decreaseIndent();
             pw.println("Lingered:");
             pw.increaseIndent();
-            for (NetworkRequest nr : nai.networkLingered) pw.println(nr.toString());
+            nai.dumpLingerTimers(pw);
             pw.decreaseIndent();
             pw.decreaseIndent();
         }
@@ -2158,13 +2170,6 @@
                     }
                     break;
                 }
-                case NetworkMonitor.EVENT_NETWORK_LINGER_COMPLETE: {
-                    NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj;
-                    if (isLiveNetworkAgent(nai, msg.what)) {
-                        handleLingerComplete(nai);
-                    }
-                    break;
-                }
                 case NetworkMonitor.EVENT_PROVISIONING_NOTIFICATION: {
                     final int netId = msg.arg2;
                     final boolean visible = (msg.arg1 != 0);
@@ -2197,33 +2202,50 @@
             return true;
         }
 
+        private boolean maybeHandleNetworkAgentInfoMessage(Message msg) {
+            switch (msg.what) {
+                default:
+                    return false;
+                case NetworkAgentInfo.EVENT_NETWORK_LINGER_COMPLETE: {
+                    NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+                    if (nai != null && isLiveNetworkAgent(nai, msg.what)) {
+                        handleLingerComplete(nai);
+                    }
+                    break;
+                }
+            }
+            return true;
+        }
+
         @Override
         public void handleMessage(Message msg) {
-            if (!maybeHandleAsyncChannelMessage(msg) && !maybeHandleNetworkMonitorMessage(msg)) {
+            if (!maybeHandleAsyncChannelMessage(msg) &&
+                    !maybeHandleNetworkMonitorMessage(msg) &&
+                    !maybeHandleNetworkAgentInfoMessage(msg)) {
                 maybeHandleNetworkAgentMessage(msg);
             }
         }
     }
 
-    private void linger(NetworkAgentInfo nai) {
-        nai.lingering = true;
-        logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
-        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_LINGER);
-        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING);
-    }
-
-    // Cancel any lingering so the linger timeout doesn't teardown a network.
-    // This should be called when a network begins satisfying a NetworkRequest.
-    // Note: depending on what state the NetworkMonitor is in (e.g.,
-    // if it's awaiting captive portal login, or if validation failed), this
-    // may trigger a re-evaluation of the network.
-    private void unlinger(NetworkAgentInfo nai) {
-        nai.networkLingered.clear();
-        if (!nai.lingering) return;
-        nai.lingering = false;
-        logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
-        if (VDBG) log("Canceling linger of " + nai.name());
-        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+    private void updateLingerState(NetworkAgentInfo nai, long now) {
+        // 1. Update the linger timer. If it's changed, reschedule or cancel the alarm.
+        // 2. If the network was lingering and there are now requests, unlinger it.
+        // 3. If this network is unneeded (which implies it is not lingering), and there is at least
+        //    one lingered request, start lingering.
+        nai.updateLingerTimer();
+        if (nai.isLingering() && nai.numRequestNetworkRequests() > 0) {
+            if (DBG) log("Unlingering " + nai.name());
+            nai.unlinger();
+            logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
+        } else if (unneeded(nai) && nai.getLingerExpiry() > 0) {  // unneeded() calls isLingering()
+            int lingerTime = (int) (nai.getLingerExpiry() - now);
+            if (DBG) {
+                Log.d(TAG, "Lingering " + nai.name() + " for " + lingerTime + "ms");
+            }
+            nai.linger();
+            logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+        }
     }
 
     private void handleAsyncChannelHalfConnect(Message msg) {
@@ -2313,6 +2335,7 @@
                     sendUpdatedScoreToFactories(request, 0);
                 }
             }
+            nai.clearLingerState();
             if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) {
                 removeDataActivityTracking(nai);
                 notifyLockdownVpn(nai);
@@ -2400,7 +2423,10 @@
     // This is whether it is satisfying any NetworkRequests or were it to become validated,
     // would it have a chance of satisfying any NetworkRequests.
     private boolean unneeded(NetworkAgentInfo nai) {
-        if (!nai.everConnected || nai.isVPN() || nai.lingering) return false;
+        if (!nai.everConnected || nai.isVPN() ||
+               nai.isLingering() || nai.numRequestNetworkRequests() > 0) {
+            return false;
+        }
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
             // If this Network is already the highest scoring Network for a request, or if
             // there is hope for it to become one if it validated, then it is needed.
@@ -2453,6 +2479,9 @@
                         log(" Removing from current network " + nai.name() +
                                 ", leaving " + nai.numNetworkRequests() + " requests.");
                     }
+                    // If there are still lingered requests on this network, don't tear it down,
+                    // but resume lingering instead.
+                    updateLingerState(nai, SystemClock.elapsedRealtime());
                     if (unneeded(nai)) {
                         if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
                         teardownUnneededNetwork(nai);
@@ -2516,7 +2545,7 @@
                     }
                 }
             }
-            callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED);
+            callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED, 0);
         }
     }
 
@@ -4503,7 +4532,7 @@
     }
 
     private void callCallbackForRequest(NetworkRequestInfo nri,
-            NetworkAgentInfo networkAgent, int notificationType) {
+            NetworkAgentInfo networkAgent, int notificationType, int arg1) {
         if (nri.messenger == null) return;  // Default request has no msgr
         Bundle bundle = new Bundle();
         bundle.putParcelable(NetworkRequest.class.getSimpleName(),
@@ -4515,7 +4544,7 @@
         }
         switch (notificationType) {
             case ConnectivityManager.CALLBACK_LOSING: {
-                msg.arg1 = 30 * 1000; // TODO - read this from NetworkMonitor
+                msg.arg1 = arg1;
                 break;
             }
             case ConnectivityManager.CALLBACK_CAP_CHANGED: {
@@ -4562,7 +4591,14 @@
             return;
         }
         if (DBG) log("handleLingerComplete for " + oldNetwork.name());
-        teardownUnneededNetwork(oldNetwork);
+
+        // If we get here it means that the last linger timeout for this network expired. So there
+        // must be no other active linger timers, and we must stop lingering.
+        oldNetwork.clearLingerState();
+
+        if (unneeded(oldNetwork)) {
+            teardownUnneededNetwork(oldNetwork);
+        }
     }
 
     private void makeDefault(NetworkAgentInfo newNetwork) {
@@ -4607,7 +4643,7 @@
     //               performed to tear down unvalidated networks that have no chance (i.e. even if
     //               validated) of becoming the highest scoring network.
     private void rematchNetworkAndRequests(NetworkAgentInfo newNetwork,
-            ReapUnvalidatedNetworks reapUnvalidatedNetworks) {
+            ReapUnvalidatedNetworks reapUnvalidatedNetworks, long now) {
         if (!newNetwork.everConnected) return;
         boolean keep = newNetwork.isVPN();
         boolean isNewDefault = false;
@@ -4653,12 +4689,12 @@
                     if (currentNetwork != null) {
                         if (VDBG) log("   accepting network in place of " + currentNetwork.name());
                         currentNetwork.removeRequest(nri.request.requestId);
-                        currentNetwork.networkLingered.add(nri.request);
+                        currentNetwork.lingerRequest(nri.request, now, mLingerDelayMs);
                         affectedNetworks.add(currentNetwork);
                     } else {
                         if (VDBG) log("   accepting network in place of null");
                     }
-                    unlinger(newNetwork);
+                    newNetwork.unlingerRequest(nri.request);
                     mNetworkForRequestId.put(nri.request.requestId, newNetwork);
                     if (!newNetwork.addRequest(nri.request)) {
                         Slog.wtf(TAG, "BUG: " + newNetwork.name() + " already has " + nri.request);
@@ -4706,23 +4742,7 @@
                 // a) be requested and b) change is NET_CAPABILITY_TRUSTED,
                 // so this code is only incorrect for a network that loses
                 // the TRUSTED capability, which is a rare case.
-                callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST);
-            }
-        }
-        // Linger any networks that are no longer needed.
-        for (NetworkAgentInfo nai : affectedNetworks) {
-            if (nai.lingering) {
-                // Already lingered.  Nothing to do.  This can only happen if "nai" is in
-                // "affectedNetworks" twice.  The reasoning being that to get added to
-                // "affectedNetworks", "nai" must have been satisfying a NetworkRequest
-                // (i.e. not lingered) so it could have only been lingered by this loop.
-                // unneeded(nai) will be false and we'll call unlinger() below which would
-                // be bad, so handle it here.
-            } else if (unneeded(nai)) {
-                linger(nai);
-            } else {
-                // Clear nai.networkLingered we might have added above.
-                unlinger(nai);
+                callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST, 0);
             }
         }
         if (isNewDefault) {
@@ -4747,6 +4767,15 @@
         // before LegacyTypeTracker sends legacy broadcasts
         for (NetworkRequestInfo nri : addedRequests) notifyNetworkCallback(newNetwork, nri);
 
+        // Linger any networks that are no longer needed. This should be done after sending the
+        // available callback for newNetwork.
+        for (NetworkAgentInfo nai : affectedNetworks) {
+            updateLingerState(nai, now);
+        }
+        // Possibly unlinger newNetwork. Unlingering a network does not send any callbacks so it
+        // does not need to be done in any particular order.
+        updateLingerState(newNetwork, now);
+
         if (isNewDefault) {
             // Maintain the illusion: since the legacy API only
             // understands one network at a time, we must pretend
@@ -4812,8 +4841,19 @@
         if (reapUnvalidatedNetworks == ReapUnvalidatedNetworks.REAP) {
             for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
                 if (unneeded(nai)) {
-                    if (DBG) log("Reaping " + nai.name());
-                    teardownUnneededNetwork(nai);
+                    if (nai.getLingerExpiry() > 0) {
+                        // This network has active linger timers and no requests, but is not
+                        // lingering. Linger it.
+                        //
+                        // One way (the only way?) this can happen if this network is unvalidated
+                        // and became unneeded due to another network improving its score to the
+                        // point where this network will no longer be able to satisfy any requests
+                        // even if it validates.
+                        updateLingerState(nai, now);
+                    } else {
+                        if (DBG) log("Reaping " + nai.name());
+                        teardownUnneededNetwork(nai);
+                    }
                 }
             }
         }
@@ -4840,8 +4880,9 @@
         // Optimization: Only reprocess "changed" if its score improved.  This is safe because it
         // can only add more NetworkRequests satisfied by "changed", and this is exactly what
         // rematchNetworkAndRequests() handles.
+        final long now = SystemClock.elapsedRealtime();
         if (changed != null && oldScore < changed.getCurrentScore()) {
-            rematchNetworkAndRequests(changed, ReapUnvalidatedNetworks.REAP);
+            rematchNetworkAndRequests(changed, ReapUnvalidatedNetworks.REAP, now);
         } else {
             final NetworkAgentInfo[] nais = mNetworkAgentInfos.values().toArray(
                     new NetworkAgentInfo[mNetworkAgentInfos.size()]);
@@ -4855,7 +4896,8 @@
                         // is complete could incorrectly teardown a network that hasn't yet been
                         // rematched.
                         (nai != nais[nais.length-1]) ? ReapUnvalidatedNetworks.DONT_REAP
-                                : ReapUnvalidatedNetworks.REAP);
+                                : ReapUnvalidatedNetworks.REAP,
+                        now);
             }
         }
     }
@@ -4965,7 +5007,8 @@
             updateSignalStrengthThresholds(networkAgent, "CONNECT", null);
 
             // Consider network even though it is not yet validated.
-            rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP);
+            final long now = SystemClock.elapsedRealtime();
+            rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP, now);
 
             // This has to happen after matching the requests, because callbacks are just requests.
             notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
@@ -5013,14 +5056,8 @@
     // notify only this one new request of the current state
     protected void notifyNetworkCallback(NetworkAgentInfo nai, NetworkRequestInfo nri) {
         int notifyType = ConnectivityManager.CALLBACK_AVAILABLE;
-        // TODO - read state from monitor to decide what to send.
-//        if (nai.networkMonitor.isLingering()) {
-//            notifyType = NetworkCallbacks.LOSING;
-//        } else if (nai.networkMonitor.isEvaluating()) {
-//            notifyType = NetworkCallbacks.callCallbackForRequest(request, nai, notifyType);
-//        }
         if (nri.mPendingIntent == null) {
-            callCallbackForRequest(nri, nai, notifyType);
+            callCallbackForRequest(nri, nai, notifyType, 0);
         } else {
             sendPendingIntentForRequest(nri, nai, notifyType);
         }
@@ -5072,20 +5109,24 @@
         }
     }
 
-    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType, int arg1) {
         if (VDBG) log("notifyType " + notifyTypeToName(notifyType) + " for " + networkAgent.name());
         for (int i = 0; i < networkAgent.numNetworkRequests(); i++) {
             NetworkRequest nr = networkAgent.requestAt(i);
             NetworkRequestInfo nri = mNetworkRequests.get(nr);
             if (VDBG) log(" sending notification for " + nr);
             if (nri.mPendingIntent == null) {
-                callCallbackForRequest(nri, networkAgent, notifyType);
+                callCallbackForRequest(nri, networkAgent, notifyType, arg1);
             } else {
                 sendPendingIntentForRequest(nri, networkAgent, notifyType);
             }
         }
     }
 
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+        notifyNetworkCallbacks(networkAgent, notifyType, 0);
+    }
+
     private String notifyTypeToName(int notifyType) {
         switch (notifyType) {
             case ConnectivityManager.CALLBACK_PRECHECK:    return "PRECHECK";
@@ -5216,6 +5257,11 @@
         return new NetworkMonitor(context, handler, nai, defaultRequest);
     }
 
+    @VisibleForTesting
+    public WakeupMessage makeWakeupMessage(Context c, Handler h, String s, int cmd, Object obj) {
+        return new WakeupMessage(c, h, s, cmd, 0, 0, obj);
+    }
+
     private void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
         int newNetid = NETID_UNSET;
         int prevNetid = NETID_UNSET;
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 15b872d..7a25df6 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -28,14 +28,21 @@
 import android.net.NetworkState;
 import android.os.Handler;
 import android.os.Messenger;
+import android.os.SystemClock;
+import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.WakeupMessage;
 import com.android.server.ConnectivityService;
 import com.android.server.connectivity.NetworkMonitor;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 /**
  * A bag class used by ConnectivityService for holding a collection of most recent
@@ -143,12 +150,69 @@
     // Whether a captive portal was found during the last network validation attempt.
     public boolean lastCaptivePortalDetected;
 
-    // Indicates whether the network is lingering.  Networks are lingered when they become unneeded
-    // as a result of their NetworkRequests being satisfied by a different network, so as to allow
-    // communication to wrap up before the network is taken down.  This usually only happens to the
-    // default network.  Lingering ends with either the linger timeout expiring and the network
-    // being taken down, or the network satisfying a request again.
-    public boolean lingering;
+    // Networks are lingered when they become unneeded as a result of their NetworkRequests being
+    // satisfied by a higher-scoring network. so as to allow communication to wrap up before the
+    // network is taken down.  This usually only happens to the default network. Lingering ends with
+    // either the linger timeout expiring and the network being taken down, or the network
+    // satisfying a request again.
+    public static class LingerTimer implements Comparable<LingerTimer> {
+        public final NetworkRequest request;
+        public final long expiryMs;
+
+        public LingerTimer(NetworkRequest request, long expiryMs) {
+            this.request = request;
+            this.expiryMs = expiryMs;
+        }
+        public boolean equals(Object o) {
+            if (!(o instanceof LingerTimer)) return false;
+            LingerTimer other = (LingerTimer) o;
+            return (request.requestId == other.request.requestId) && (expiryMs == other.expiryMs);
+        }
+        public int hashCode() {
+            return Objects.hash(request.requestId, expiryMs);
+        }
+        public int compareTo(LingerTimer other) {
+            return (expiryMs != other.expiryMs) ?
+                    Long.compare(expiryMs, other.expiryMs) :
+                    Integer.compare(request.requestId, other.request.requestId);
+        }
+        public String toString() {
+            return String.format("%s, expires %dms", request.toString(),
+                    expiryMs - SystemClock.elapsedRealtime());
+        }
+    }
+
+    /**
+     * Inform ConnectivityService that the network LINGER period has
+     * expired.
+     * obj = this NetworkAgentInfo
+     */
+    public static final int EVENT_NETWORK_LINGER_COMPLETE = 1001;
+
+    // All linger timers for this network, sorted by expiry time. A linger timer is added whenever
+    // a request is moved to a network with a better score, regardless of whether the network is or
+    // was lingering or not.
+    // TODO: determine if we can replace this with a smaller or unsorted data structure. (e.g.,
+    // SparseLongArray) combined with the timestamp of when the last timer is scheduled to fire.
+    private final SortedSet<LingerTimer> mLingerTimers = new TreeSet<>();
+
+    // For fast lookups. Indexes into mLingerTimers by request ID.
+    private final SparseArray<LingerTimer> mLingerTimerForRequest = new SparseArray<>();
+
+    // Linger expiry timer. Armed whenever mLingerTimers is non-empty, regardless of whether the
+    // network is lingering or not. Always set to the expiry of the LingerTimer that expires last.
+    // When the timer fires, all linger state is cleared, and if the network has no requests, it is
+    // torn down.
+    private WakeupMessage mLingerMessage;
+
+    // Linger expiry. Holds the expiry time of the linger timer, or 0 if the timer is not armed.
+    private long mLingerExpiryMs;
+
+    // Whether the network is lingering or not. Must be maintained separately from the above because
+    // it depends on the state of other networks and requests, which only ConnectivityService knows.
+    // (Example: we don't linger a network if it would become the best for a NetworkRequest if it
+    // validated).
+    private boolean mLingering;
 
     // This represents the last score received from the NetworkAgent.
     private int currentScore;
@@ -165,8 +229,6 @@
     private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
     // The list of NetworkRequests that this Network previously satisfied with the highest
     // score.  A non-empty list indicates that if this Network was validated it is lingered.
-    // NOTE: This list is only used for debugging.
-    public final ArrayList<NetworkRequest> networkLingered = new ArrayList<NetworkRequest>();
     // How many of the satisfied requests are actual requests and not listens.
     private int mNumRequestNetworkRequests = 0;
 
@@ -176,6 +238,12 @@
     // Used by ConnectivityService to keep track of 464xlat.
     public Nat464Xlat clatd;
 
+    private static final String TAG = ConnectivityService.class.getSimpleName();
+    private static final boolean VDBG = false;
+    private final ConnectivityService mConnService;
+    private final Context mContext;
+    private final Handler mHandler;
+
     public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
             LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
             NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) {
@@ -186,7 +254,10 @@
         linkProperties = lp;
         networkCapabilities = nc;
         currentScore = score;
-        networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest);
+        mConnService = connService;
+        mContext = context;
+        mHandler = handler;
+        networkMonitor = mConnService.createNetworkMonitor(context, handler, this, defaultRequest);
         networkMisc = misc;
     }
 
@@ -213,8 +284,12 @@
      */
     public void removeRequest(int requestId) {
         NetworkRequest existing = mNetworkRequests.get(requestId);
-        if (existing != null && existing.isRequest()) mNumRequestNetworkRequests--;
+        if (existing == null) return;
         mNetworkRequests.remove(requestId);
+        if (existing.isRequest()) {
+            mNumRequestNetworkRequests--;
+            unlingerRequest(existing);
+        }
     }
 
     /**
@@ -316,13 +391,100 @@
         }
     }
 
+    /**
+     * Sets the specified request to linger on this network for the specified time. Called by
+     * ConnectivityService when the request is moved to another network with a higher score.
+     */
+    public void lingerRequest(NetworkRequest request, long now, long duration) {
+        if (mLingerTimerForRequest.get(request.requestId) != null) {
+            // Cannot happen. Once a request is lingering on a particular network, we cannot
+            // re-linger it unless that network becomes the best for that request again, in which
+            // case we should have unlingered it.
+            Log.wtf(TAG, this.name() + ": request " + request.requestId + " already lingered");
+        }
+        final long expiryMs = now + duration;
+        LingerTimer timer = new LingerTimer(request, expiryMs);
+        if (VDBG) Log.d(TAG, "Adding LingerTimer " + timer + " to " + this.name());
+        mLingerTimers.add(timer);
+        mLingerTimerForRequest.put(request.requestId, timer);
+    }
+
+    /**
+     * Cancel lingering. Called by ConnectivityService when a request is added to this network.
+     */
+    public void unlingerRequest(NetworkRequest request) {
+        LingerTimer timer = mLingerTimerForRequest.get(request.requestId);
+        if (timer != null) {
+            if (VDBG) Log.d(TAG, "Removing LingerTimer " + timer + " from " + this.name());
+            mLingerTimers.remove(timer);
+            mLingerTimerForRequest.remove(request.requestId);
+        }
+    }
+
+    public long getLingerExpiry() {
+        return mLingerExpiryMs;
+    }
+
+    public void updateLingerTimer() {
+        long newExpiry = mLingerTimers.isEmpty() ? 0 : mLingerTimers.last().expiryMs;
+        if (newExpiry == mLingerExpiryMs) return;
+
+        // Even if we're going to reschedule the timer, cancel it first. This is because the
+        // semantics of WakeupMessage guarantee that if cancel is called then the alarm will
+        // never call its callback (handleLingerComplete), even if it has already fired.
+        // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage
+        // has already been dispatched, rescheduling to some time in the future it won't stop it
+        // from calling its callback immediately.
+        if (mLingerMessage != null) {
+            mLingerMessage.cancel();
+            mLingerMessage = null;
+        }
+
+        if (newExpiry > 0) {
+            mLingerMessage = mConnService.makeWakeupMessage(
+                    mContext, mHandler,
+                    "NETWORK_LINGER_COMPLETE." + network.netId,
+                    EVENT_NETWORK_LINGER_COMPLETE, this);
+            mLingerMessage.schedule(newExpiry);
+        }
+
+        mLingerExpiryMs = newExpiry;
+    }
+
+    public void linger() {
+        mLingering = true;
+    }
+
+    public void unlinger() {
+        mLingering = false;
+    }
+
+    public boolean isLingering() {
+        return mLingering;
+    }
+
+    public void clearLingerState() {
+        if (mLingerMessage != null) {
+            mLingerMessage.cancel();
+            mLingerMessage = null;
+        }
+        mLingerTimers.clear();
+        mLingerTimerForRequest.clear();
+        updateLingerTimer();  // Sets mLingerExpiryMs, cancels and nulls out mLingerMessage.
+        mLingering = false;
+    }
+
+    public void dumpLingerTimers(PrintWriter pw) {
+        for (LingerTimer timer : mLingerTimers) { pw.println(timer); }
+    }
+
     public String toString() {
         return "NetworkAgentInfo{ ni{" + networkInfo + "}  " +
                 "network{" + network + "}  nethandle{" + network.getNetworkHandle() + "}  " +
                 "lp{" + linkProperties + "}  " +
                 "nc{" + networkCapabilities + "}  Score{" + getCurrentScore() + "}  " +
                 "everValidated{" + everValidated + "}  lastValidated{" + lastValidated + "}  " +
-                "created{" + created + "} lingering{" + lingering + "} " +
+                "created{" + created + "} lingering{" + isLingering() + "} " +
                 "explicitlySelected{" + networkMisc.explicitlySelected + "} " +
                 "acceptUnvalidated{" + networkMisc.acceptUnvalidated + "} " +
                 "everCaptivePortalDetected{" + everCaptivePortalDetected + "} " +
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index d424717..ba77b03 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -96,6 +96,7 @@
     private static final String TAG = "ConnectivityServiceTest";
 
     private static final int TIMEOUT_MS = 500;
+    private static final int TEST_LINGER_DELAY_MS = 120;
 
     private BroadcastInterceptingContext mServiceContext;
     private WrappedConnectivityService mService;
@@ -330,7 +331,8 @@
          * @param validated Indicate if network should pretend to be validated.
          */
         public void connect(boolean validated) {
-            assertEquals(mNetworkInfo.getDetailedState(), DetailedState.IDLE);
+            assertEquals("MockNetworkAgents can only be connected once",
+                    mNetworkInfo.getDetailedState(), DetailedState.IDLE);
             assertFalse(mNetworkCapabilities.hasCapability(NET_CAPABILITY_INTERNET));
 
             NetworkCallback callback = null;
@@ -548,6 +550,11 @@
             super(context, handler, cmdName, cmd);
         }
 
+        public FakeWakeupMessage(Context context, Handler handler, String cmdName, int cmd,
+                int arg1, int arg2, Object obj) {
+            super(context, handler, cmdName, cmd, arg1, arg2, obj);
+        }
+
         @Override
         public void schedule(long when) {
             long delayMs = when - SystemClock.elapsedRealtime();
@@ -556,12 +563,13 @@
                 fail("Attempting to send msg more than " + UNREASONABLY_LONG_WAIT +
                         "ms into the future: " + delayMs);
             }
-            mHandler.sendEmptyMessageDelayed(mCmd, delayMs);
+            Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+            mHandler.sendMessageDelayed(msg, delayMs);
         }
 
         @Override
         public void cancel() {
-            mHandler.removeMessages(mCmd);
+            mHandler.removeMessages(mCmd, mObj);
         }
 
         @Override
@@ -585,12 +593,6 @@
         protected CaptivePortalProbeResult isCaptivePortal() {
             return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl);
         }
-
-        @Override
-        protected WakeupMessage makeWakeupMessage(
-                Context context, Handler handler, String cmdName, int cmd) {
-            return new FakeWakeupMessage(context, handler, cmdName, cmd);
-        }
     }
 
     private class WrappedConnectivityService extends ConnectivityService {
@@ -599,6 +601,7 @@
         public WrappedConnectivityService(Context context, INetworkManagementService netManager,
                 INetworkStatsService statsService, INetworkPolicyManager policyManager) {
             super(context, netManager, statsService, policyManager);
+            mLingerDelayMs = TEST_LINGER_DELAY_MS;
         }
 
         @Override
@@ -642,6 +645,12 @@
             return monitor;
         }
 
+        @Override
+        public WakeupMessage makeWakeupMessage(
+                Context context, Handler handler, String cmdName, int cmd, Object obj) {
+            return new FakeWakeupMessage(context, handler, cmdName, cmd, 0, 0, obj);
+        }
+
         public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
             return mLastCreatedNetworkMonitor;
         }
@@ -686,8 +695,6 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        NetworkMonitor.SetDefaultLingerTime(120);
-
         // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
         // http://b/25897652 .
         if (Looper.myLooper() == null) {
@@ -1051,42 +1058,58 @@
         private class CallbackInfo {
             public final CallbackState state;
             public final Network network;
-            public CallbackInfo(CallbackState s, Network n) { state = s; network = n; }
+            public Object arg;
+            public CallbackInfo(CallbackState s, Network n, Object o) {
+                state = s; network = n; arg = o;
+            }
             public String toString() { return String.format("%s (%s)", state, network); }
             public boolean equals(Object o) {
                 if (!(o instanceof CallbackInfo)) return false;
+                // Ignore timeMs, since it's unpredictable.
                 CallbackInfo other = (CallbackInfo) o;
                 return state == other.state && Objects.equals(network, other.network);
             }
         }
         private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
 
-        private void setLastCallback(CallbackState state, Network network) {
-            mCallbacks.offer(new CallbackInfo(state, network));
+        private void setLastCallback(CallbackState state, Network network, Object o) {
+            mCallbacks.offer(new CallbackInfo(state, network, o));
         }
 
         public void onAvailable(Network network) {
-            setLastCallback(CallbackState.AVAILABLE, network);
+            setLastCallback(CallbackState.AVAILABLE, network, null);
         }
 
         public void onLosing(Network network, int maxMsToLive) {
-            setLastCallback(CallbackState.LOSING, network);
+            setLastCallback(CallbackState.LOSING, network, maxMsToLive /* autoboxed int */);
         }
 
         public void onLost(Network network) {
-            setLastCallback(CallbackState.LOST, network);
+            setLastCallback(CallbackState.LOST, network, null);
+        }
+
+        void expectCallback(CallbackState state, MockNetworkAgent mockAgent, int timeoutMs) {
+            CallbackInfo expected = new CallbackInfo(
+                    state, (mockAgent != null) ? mockAgent.getNetwork() : null, 0);
+            CallbackInfo actual;
+            try {
+                actual = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
+                assertEquals("Unexpected callback:", expected, actual);
+            } catch (InterruptedException e) {
+                fail("Did not receive expected " + expected + " after " + TIMEOUT_MS + "ms");
+                actual = null;  // Or the compiler can't tell it's never used uninitialized.
+            }
+            if (state == CallbackState.LOSING) {
+                String msg = String.format(
+                        "Invalid linger time value %d, must be between %d and %d",
+                        actual.arg, 0, TEST_LINGER_DELAY_MS);
+                int maxMsToLive = (Integer) actual.arg;
+                assertTrue(msg, 0 <= maxMsToLive && maxMsToLive <= TEST_LINGER_DELAY_MS);
+            }
         }
 
         void expectCallback(CallbackState state, MockNetworkAgent mockAgent) {
-            CallbackInfo expected = new CallbackInfo(
-                    state,
-                    (mockAgent != null) ? mockAgent.getNetwork() : null);
-            try {
-                assertEquals("Unexpected callback:",
-                        expected, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            } catch (InterruptedException e) {
-                fail("Did not receive expected " + expected + " after " + TIMEOUT_MS + "ms");
-            }
+            expectCallback(state, mockAgent, TIMEOUT_MS);
         }
 
         void assertNoCallback() {
@@ -1249,6 +1272,8 @@
 
             }
             callback.expectCallback(CallbackState.LOSING, oldNetwork);
+            // TODO: should we send an AVAILABLE callback to newNetwork, to indicate that it is no
+            // longer lingering?
             defaultCallback.expectCallback(CallbackState.AVAILABLE, newNetwork);
             assertEquals(newNetwork.getNetwork(), mCm.getActiveNetwork());
         }
@@ -1306,8 +1331,8 @@
         mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         mWiFiNetworkAgent.adjustScore(50);
         mWiFiNetworkAgent.connect(false);   // Score: 70
-        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
         callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
         defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
@@ -1318,24 +1343,24 @@
         defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
         assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
-        // Bring up wifi, then validate it. In this case we do not linger cell. What happens is that
-        // when wifi connects, we don't linger because cell could potentially become the default
-        // network if it validated. Then, when wifi validates, we re-evaluate cell, see it has no
-        // requests, and tear it down because it's unneeded.
-        // TODO: can we linger in this case?
+        // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but
+        // it's arguably correct to linger it, since it was the default network before it validated.
         mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
         mWiFiNetworkAgent.connect(true);
         callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
-        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
         defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
         mWiFiNetworkAgent.disconnect();
         callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
         defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
 
-        // The current code has a bug: if a network is lingering, and we add and then remove a
-        // request from it, we forget that the network was lingering and tear it down immediately.
+        // If a network is lingering, and we add and remove a request from it, resume lingering.
         mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
         mCellNetworkAgent.connect(true);
         callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
@@ -1350,11 +1375,43 @@
                 .addTransportType(TRANSPORT_CELLULAR).build();
         NetworkCallback noopCallback = new NetworkCallback();
         mCm.requestNetwork(cellRequest, noopCallback);
+        // TODO: should this cause an AVAILABLE callback, to indicate that the network is no longer
+        // lingering?
         mCm.unregisterNetworkCallback(noopCallback);
-        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
 
+        // Similar to the above: lingering can start even after the lingered request is removed.
+        // Disconnect wifi and switch to cell.
         mWiFiNetworkAgent.disconnect();
         callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+
+        // Cell is now the default network. Pin it with a cell-specific request.
+        noopCallback = new NetworkCallback();  // Can't reuse NetworkCallbacks. http://b/20701525
+        mCm.requestNetwork(cellRequest, noopCallback);
+
+        // Now connect wifi, and expect it to become the default network.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        // The default request is lingering on cell, but nothing happens to cell, and we send no
+        // callbacks for it, because it's kept up by cellRequest.
+        callback.assertNoCallback();
+        // Now unregister cellRequest and expect cell to start lingering.
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+        // Let linger run its course.
+        callback.assertNoCallback();
+        callback.expectCallback(CallbackState.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS /* timeoutMs */);
+
+        // Clean up.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
 
         mCm.unregisterNetworkCallback(callback);
         mCm.unregisterNetworkCallback(defaultCallback);