diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 995a910112..b12a9619e5 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.PowerManager; 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.net.VpnProfile; 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 @@ import java.util.TreeSet; */ 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub private static final int DISABLED = 0; private static final SparseArray 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub } 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub sendUpdatedScoreToFactories(request, 0); } } + nai.clearLingerState(); if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) { removeDataActivityTracking(nai); notifyLockdownVpn(nai); @@ -2400,7 +2423,10 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub } } } - callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED); + callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED, 0); } } @@ -4503,7 +4532,7 @@ public class ConnectivityService extends IConnectivityManager.Stub } 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 @@ public class ConnectivityService extends IConnectivityManager.Stub } 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub } } - 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 15b872d1b3..7a25df6859 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.NetworkRequest; 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 @@ public class NetworkAgentInfo implements Comparable { // 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 { + 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 mLingerTimers = new TreeSet<>(); + + // For fast lookups. Indexes into mLingerTimers by request ID. + private final SparseArray 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 @@ public class NetworkAgentInfo implements Comparable { private final SparseArray 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 networkLingered = new ArrayList(); // How many of the satisfied requests are actual requests and not listens. private int mNumRequestNetworkRequests = 0; @@ -176,6 +238,12 @@ public class NetworkAgentInfo implements Comparable { // 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 @@ public class NetworkAgentInfo implements Comparable { 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 class NetworkAgentInfo implements Comparable { */ 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 @@ public class NetworkAgentInfo implements Comparable { } } + /** + * 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 d424717fec..ba77b03c5e 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { * @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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { } 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { .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);