Allow network providers to set the linger duration.

Test: atest CtsNetTestCases:NetworkAgentTest#testSetLingerDuration
CTS-Coverage-Bug: 184796264
Bug: 184227264
Merged-In: I3c2563d4ae4e3715d0c6270344ba8f7ef067872f
Merged-In: I7f420faa40863385114705d6971cf00887d03318
Change-Id: I7f420faa40863385114705d6971cf00887d03318
  (cherry-picked from ag/14100410)
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index bec736e..d1d51da 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -239,6 +239,7 @@
     method public final void sendQosSessionLost(int, int, int);
     method public final void sendSocketKeepaliveEvent(int, int);
     method @Deprecated public void setLegacySubtype(int, @NonNull String);
+    method public void setLingerDuration(@NonNull java.time.Duration);
     method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
     method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method public void unregister();
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index 26cb1ed..9a58add 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -42,4 +42,5 @@
     void sendQosSessionLost(int qosCallbackId, in QosSession session);
     void sendQosCallbackError(int qosCallbackId, int exceptionType);
     void sendTeardownDelayMs(int teardownDelayMs);
+    void sendLingerDuration(int durationMs);
 }
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index ffeb661..adcf338 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.os.Build;
@@ -106,6 +107,9 @@
     private final String LOG_TAG;
     private static final boolean DBG = true;
     private static final boolean VDBG = false;
+    /** @hide */
+    @TestApi
+    public static final int MIN_LINGER_TIMER_MS = 2000;
     private final ArrayList<RegistryAction> mPreConnectedQueue = new ArrayList<>();
     private volatile long mLastBwRefreshTime = 0;
     private static final long BW_REFRESH_MIN_WIN_MS = 500;
@@ -391,6 +395,15 @@
      */
     public static final int CMD_NETWORK_DESTROYED = BASE + 23;
 
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to set the linger duration for this network
+     * agent.
+     * arg1 = the linger duration, represents by {@link Duration}.
+     *
+     * @hide
+     */
+    public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24;
+
     private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
         final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
                 config.legacyTypeName, config.legacySubTypeName);
@@ -1287,6 +1300,22 @@
         queueOrSendMessage(ra -> ra.sendQosCallbackError(qosCallbackId, exceptionType));
     }
 
+    /**
+     * Set the linger duration for this network agent.
+     * @param duration the delay between the moment the network becomes unneeded and the
+     *                 moment the network is disconnected or moved into the background.
+     *                 Note that If this duration has greater than millisecond precision, then
+     *                 the internal implementation will drop any excess precision.
+     */
+    public void setLingerDuration(@NonNull final Duration duration) {
+        Objects.requireNonNull(duration);
+        final long durationMs = duration.toMillis();
+        if (durationMs < MIN_LINGER_TIMER_MS || durationMs > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Duration must be within ["
+                    + MIN_LINGER_TIMER_MS + "," + Integer.MAX_VALUE + "]ms");
+        }
+        queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs));
+    }
 
     /** @hide */
     protected void log(final String s) {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b804a51..eb7dccc 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -1400,8 +1400,8 @@
                 new NetworkInfo(TYPE_NONE, 0, "", ""),
                 new LinkProperties(), new NetworkCapabilities(),
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
-                new NetworkAgentConfig(), this, null, null, 0, INVALID_UID, mQosCallbackTracker,
-                mDeps);
+                new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
+                mLingerDelayMs, mQosCallbackTracker, mDeps);
     }
 
     private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
@@ -3234,6 +3234,11 @@
                     } else {
                         logwtf(nai.toShortString() + " set invalid teardown delay " + msg.arg1);
                     }
+                    break;
+                }
+                case NetworkAgent.EVENT_LINGER_DURATION_CHANGED: {
+                    nai.setLingerDuration((int) arg.second);
+                    break;
                 }
             }
         }
@@ -6597,7 +6602,8 @@
         final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                 new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), lp, nc,
                 currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
-                this, mNetd, mDnsResolver, providerId, uid, mQosCallbackTracker, mDeps);
+                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
+                mQosCallbackTracker, mDeps);
 
         // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says.
         processCapabilitiesFromAgent(nai, nc);
@@ -7841,7 +7847,7 @@
                     log("   accepting network in place of " + previousSatisfier.toShortString());
                 }
                 previousSatisfier.removeRequest(previousRequest.requestId);
-                previousSatisfier.lingerRequest(previousRequest.requestId, now, mLingerDelayMs);
+                previousSatisfier.lingerRequest(previousRequest.requestId, now);
             } else {
                 if (VDBG || DDBG) log("   accepting network in place of null");
             }
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 851096d..18becd4 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -59,6 +59,7 @@
 import com.android.server.ConnectivityService;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.NoSuchElementException;
@@ -281,6 +282,9 @@
      */
     public static final int ARG_AGENT_SUCCESS = 1;
 
+    // How long this network should linger for.
+    private int mLingerDurationMs;
+
     // All inactivity timers for this network, sorted by expiry time. A 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. An inactivity timer is also added when a network connects
@@ -349,7 +353,8 @@
             @NonNull NetworkScore score, Context context,
             Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
             IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
-            QosCallbackTracker qosCallbackTracker, ConnectivityService.Dependencies deps) {
+            int lingerDurationMs, QosCallbackTracker qosCallbackTracker,
+            ConnectivityService.Dependencies deps) {
         Objects.requireNonNull(net);
         Objects.requireNonNull(info);
         Objects.requireNonNull(lp);
@@ -370,6 +375,7 @@
         mHandler = handler;
         this.factorySerialNumber = factorySerialNumber;
         this.creatorUid = creatorUid;
+        mLingerDurationMs = lingerDurationMs;
         mQosCallbackTracker = qosCallbackTracker;
     }
 
@@ -685,6 +691,12 @@
             mHandler.obtainMessage(NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED,
                     teardownDelayMs, 0, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
         }
+
+        @Override
+        public void sendLingerDuration(final int durationMs) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget();
+        }
     }
 
     /**
@@ -974,13 +986,14 @@
 
     /**
      * Sets the specified requestId to linger on this network for the specified time. Called by
-     * ConnectivityService when the request is moved to another network with a higher score, or
+     * ConnectivityService when any request is moved to another network with a higher score, or
      * when a network is newly created.
      *
      * @param requestId The requestId of the request that no longer need to be served by this
      *                  network. Or {@link NetworkRequest.REQUEST_ID_NONE} if this is the
-     *                  {@code LingerTimer} for a newly created network.
+     *                  {@code InactivityTimer} for a newly created network.
      */
+    // TODO: Consider creating a dedicated function for nascent network, e.g. start/stopNascent.
     public void lingerRequest(int requestId, long now, long duration) {
         if (mInactivityTimerForRequest.get(requestId) != null) {
             // Cannot happen. Once a request is lingering on a particular network, we cannot
@@ -996,6 +1009,19 @@
     }
 
     /**
+     * Sets the specified requestId to linger on this network for the timeout set when
+     * initializing or modified by {@link #setLingerDuration(int)}. Called by
+     * ConnectivityService when any request is moved to another network with a higher score.
+     *
+     * @param requestId The requestId of the request that no longer need to be served by this
+     *                  network.
+     * @param now current system timestamp obtained by {@code SystemClock.elapsedRealtime}.
+     */
+    public void lingerRequest(int requestId, long now) {
+        lingerRequest(requestId, now, mLingerDurationMs);
+    }
+
+    /**
      * Cancel lingering. Called by ConnectivityService when a request is added to this network.
      * Returns true if the given requestId was lingering on this network, false otherwise.
      */
@@ -1032,6 +1058,7 @@
         }
 
         if (newExpiry > 0) {
+            // If the newExpiry timestamp is in the past, the wakeup message will fire immediately.
             mInactivityMessage = new WakeupMessage(
                     mContext, mHandler,
                     "NETWORK_LINGER_COMPLETE." + network.getNetId() /* cmdName */,
@@ -1061,8 +1088,33 @@
     }
 
     /**
-     * Return whether the network is just connected and about to be torn down because of not
-     * satisfying any request.
+     * Set the linger duration for this NAI.
+     * @param durationMs The new linger duration, in milliseconds.
+     */
+    public void setLingerDuration(final int durationMs) {
+        final long diff = durationMs - mLingerDurationMs;
+        final ArrayList<InactivityTimer> newTimers = new ArrayList<>();
+        for (final InactivityTimer timer : mInactivityTimers) {
+            if (timer.requestId == NetworkRequest.REQUEST_ID_NONE) {
+                // Don't touch nascent timer, re-add as is.
+                newTimers.add(timer);
+            } else {
+                newTimers.add(new InactivityTimer(timer.requestId, timer.expiryMs + diff));
+            }
+        }
+        mInactivityTimers.clear();
+        mInactivityTimers.addAll(newTimers);
+        updateInactivityTimer();
+        mLingerDurationMs = durationMs;
+    }
+
+    /**
+     * Return whether the network satisfies no request, but is still being kept up
+     * because it has just connected less than
+     * {@code ConnectivityService#DEFAULT_NASCENT_DELAY_MS}ms ago and is thus still considered
+     * nascent. Note that nascent mechanism uses inactivity timer which isn't
+     * associated with a request. Thus, use {@link NetworkRequest#REQUEST_ID_NONE} to identify it.
+     *
      */
     public boolean isNascent() {
         return mInactive && mInactivityTimers.size() == 1
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 52f7a1a..19f94a5 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -9832,7 +9832,8 @@
         return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
                 nc, new NetworkScore.Builder().setLegacyInt(0).build(),
                 mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
-                INVALID_UID, mQosCallbackTracker, new ConnectivityService.Dependencies());
+                INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
+                new ConnectivityService.Dependencies());
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index 116d755..36e229d 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -71,6 +71,8 @@
     static final int LOW_DAILY_LIMIT = 2;
     static final int HIGH_DAILY_LIMIT = 1000;
 
+    private static final int TEST_LINGER_DELAY_MS = 400;
+
     LingerMonitor mMonitor;
 
     @Mock ConnectivityService mConnService;
@@ -366,7 +368,7 @@
         NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info,
                 new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(),
                 mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
-                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(),
+                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
                 mQosCallbackTracker, new ConnectivityService.Dependencies());
         nai.everValidated = true;
         return nai;