[NS06] Implement the don't-reap mechanism

This exposes a mechanism for network providers to tell
the network stack that a given network must be kept up
for some specific reason. This is meant to be easier
for them than to have to file a request, in particular
because there is no guaranteed way to make sure the
request will be best matched by any given network.

Test: new test for this
Bug: 167544279
Merged-In: I3c2563d4ae4e3715d0c6270344ba8f7ef067872f
Merged-In: I238a3ee5ee9262477a23b897e4141769dd1505d1
Change-Id: I238a3ee5ee9262477a23b897e4141769dd1505d1
  (cherry-picked from ag/13929760)
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index f8ffb58..ab0f8f2 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -341,14 +341,18 @@
 
   public final class NetworkScore implements android.os.Parcelable {
     method public int describeContents();
+    method public int getKeepConnectedReason();
     method public int getLegacyInt();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR;
+    field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1
+    field public static final int KEEP_CONNECTED_NONE = 0; // 0x0
   }
 
   public static final class NetworkScore.Builder {
     ctor public NetworkScore.Builder();
     method @NonNull public android.net.NetworkScore build();
+    method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int);
     method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int);
   }
 
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 6584993..9786b09 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcel;
@@ -23,6 +24,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Object representing the quality of a network as perceived by the user.
  *
@@ -36,6 +40,17 @@
     // a migration.
     private final int mLegacyInt;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            KEEP_CONNECTED_NONE,
+            KEEP_CONNECTED_FOR_HANDOVER
+    })
+    public @interface KeepConnectedReason { }
+
+    public static final int KEEP_CONNECTED_NONE = 0;
+    public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+
     // Agent-managed policies
     // TODO : add them here, starting from 1
     /** @hide */
@@ -46,15 +61,20 @@
     // Bitmask of all the policies applied to this score.
     private final long mPolicies;
 
+    private final int mKeepConnectedReason;
+
     /** @hide */
-    NetworkScore(final int legacyInt, final long policies) {
+    NetworkScore(final int legacyInt, final long policies,
+            @KeepConnectedReason final int keepConnectedReason) {
         mLegacyInt = legacyInt;
         mPolicies = policies;
+        mKeepConnectedReason = keepConnectedReason;
     }
 
     private NetworkScore(@NonNull final Parcel in) {
         mLegacyInt = in.readInt();
         mPolicies = in.readLong();
+        mKeepConnectedReason = in.readInt();
     }
 
     public int getLegacyInt() {
@@ -62,6 +82,13 @@
     }
 
     /**
+     * Returns the keep-connected reason, or KEEP_CONNECTED_NONE.
+     */
+    public int getKeepConnectedReason() {
+        return mKeepConnectedReason;
+    }
+
+    /**
      * @return whether this score has a particular policy.
      *
      * @hide
@@ -80,6 +107,7 @@
     public void writeToParcel(@NonNull final Parcel dest, final int flags) {
         dest.writeInt(mLegacyInt);
         dest.writeLong(mPolicies);
+        dest.writeInt(mKeepConnectedReason);
     }
 
     @Override
@@ -108,6 +136,7 @@
         private static final long POLICY_NONE = 0L;
         private static final int INVALID_LEGACY_INT = Integer.MIN_VALUE;
         private int mLegacyInt = INVALID_LEGACY_INT;
+        private int mKeepConnectedReason = KEEP_CONNECTED_NONE;
 
         /**
          * Sets the legacy int for this score.
@@ -124,12 +153,23 @@
         }
 
         /**
+         * Set the keep-connected reason.
+         *
+         * This can be reset by calling it again with {@link KEEP_CONNECTED_NONE}.
+         */
+        @NonNull
+        public Builder setKeepConnectedReason(@KeepConnectedReason final int reason) {
+            mKeepConnectedReason = reason;
+            return this;
+        }
+
+        /**
          * Builds this NetworkScore.
          * @return The built NetworkScore object.
          */
         @NonNull
         public NetworkScore build() {
-            return new NetworkScore(mLegacyInt, POLICY_NONE);
+            return new NetworkScore(mLegacyInt, POLICY_NONE, mKeepConnectedReason);
         }
     }
 }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f7d8aa0..9540e06 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -3974,6 +3974,12 @@
     //   then it should be lingered.
     private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
         ensureRunningOnConnectivityServiceThread();
+
+        if (!nai.everConnected || nai.isVPN() || nai.isInactive()
+                || nai.getScore().getKeepConnectedReason() != NetworkScore.KEEP_CONNECTED_NONE) {
+            return false;
+        }
+
         final int numRequests;
         switch (reason) {
             case TEARDOWN:
@@ -3987,9 +3993,8 @@
                 return true;
         }
 
-        if (!nai.everConnected || nai.isVPN() || nai.isInactive() || numRequests > 0) {
-            return false;
-        }
+        if (numRequests > 0) return false;
+
         for (NetworkRequestInfo nri : mNetworkRequests.values()) {
             if (reason == UnneededFor.LINGER
                     && !nri.isMultilayerRequest()
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index 9326d69..375d005 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -19,12 +19,14 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkScore.KEEP_CONNECTED_NONE;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkScore;
+import android.net.NetworkScore.KeepConnectedReason;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -95,9 +97,13 @@
     // Bitmask of all the policies applied to this score.
     private final long mPolicies;
 
-    FullScore(final int legacyInt, final long policies) {
+    private final int mKeepConnectedReason;
+
+    FullScore(final int legacyInt, final long policies,
+            @KeepConnectedReason final int keepConnectedReason) {
         mLegacyInt = legacyInt;
         mPolicies = policies;
+        mKeepConnectedReason = keepConnectedReason;
     }
 
     /**
@@ -110,14 +116,15 @@
      */
     public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
             @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config) {
-        return withPolicies(score.getLegacyInt(), caps.hasCapability(NET_CAPABILITY_VALIDATED),
+        return withPolicies(score.getLegacyInt(), score.getKeepConnectedReason(),
+                caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
                 config.explicitlySelected,
                 config.acceptUnvalidated);
     }
 
     /**
-     * Given a score supplied by the NetworkAgent, produce a prospective score for an offer.
+     * Given a score supplied by a NetworkProvider, produce a prospective score for an offer.
      *
      * NetworkOffers have score filters that are compared to the scores of actual networks
      * to see if they could possibly beat the current satisfier. Some things the agent can't
@@ -139,8 +146,8 @@
         final boolean everUserSelected = false;
         // Don't assume the user will accept unvalidated connectivity.
         final boolean acceptUnvalidated = false;
-        return withPolicies(score.getLegacyInt(), mayValidate, vpn, everUserSelected,
-                acceptUnvalidated);
+        return withPolicies(score.getLegacyInt(), KEEP_CONNECTED_NONE,
+                mayValidate, vpn, everUserSelected, acceptUnvalidated);
     }
 
     /**
@@ -152,13 +159,15 @@
      */
     public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
             @NonNull final NetworkAgentConfig config) {
-        return withPolicies(mLegacyInt, caps.hasCapability(NET_CAPABILITY_VALIDATED),
+        return withPolicies(mLegacyInt, mKeepConnectedReason,
+                caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
                 config.explicitlySelected,
                 config.acceptUnvalidated);
     }
 
     private static FullScore withPolicies(@NonNull final int legacyInt,
+            @KeepConnectedReason final int keepConnectedReason,
             final boolean isValidated,
             final boolean isVpn,
             final boolean everUserSelected,
@@ -167,7 +176,8 @@
                 (isValidated         ? 1L << POLICY_IS_VALIDATED : 0)
                 | (isVpn             ? 1L << POLICY_IS_VPN : 0)
                 | (everUserSelected  ? 1L << POLICY_EVER_USER_SELECTED : 0)
-                | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0));
+                | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0),
+                keepConnectedReason);
     }
 
     /**
@@ -219,13 +229,21 @@
         return 0 != (mPolicies & (1L << policy));
     }
 
+    /**
+     * Returns the keep-connected reason, or KEEP_CONNECTED_NONE.
+     */
+    public int getKeepConnectedReason() {
+        return mKeepConnectedReason;
+    }
+
     // Example output :
     // Score(50 ; Policies : EVER_USER_SELECTED&IS_VALIDATED)
     @Override
     public String toString() {
         final StringJoiner sj = new StringJoiner(
                 "&", // delimiter
-                "Score(" + mLegacyInt + " ; Policies : ", // prefix
+                "Score(" + mLegacyInt + " ; KeepConnected : " + mKeepConnectedReason
+                        + " ; Policies : ", // prefix
                 ")"); // suffix
         for (int i = NetworkScore.MIN_AGENT_MANAGED_POLICY;
                 i <= NetworkScore.MAX_AGENT_MANAGED_POLICY; ++i) {
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index e809550..b22fe3c 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -41,6 +41,7 @@
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkProvider;
+import android.net.NetworkScore;
 import android.net.NetworkSpecifier;
 import android.net.QosFilter;
 import android.net.SocketKeepalive;
@@ -199,6 +200,11 @@
         }
     }
 
+    public void setScore(@NonNull final NetworkScore score) {
+        mScore = score.getLegacyInt();
+        mNetworkAgent.sendNetworkScore(score);
+    }
+
     public void adjustScore(int change) {
         mScore += change;
         mNetworkAgent.sendNetworkScore(mScore);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 029740b..bf28255 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -104,6 +104,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkScore.KEEP_CONNECTED_FOR_HANDOVER;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
@@ -10226,6 +10227,83 @@
         }
     }
 
+    @Test
+    public void testKeepConnected() throws Exception {
+        setAlwaysOnNetworks(false);
+        registerDefaultNetworkCallbacks();
+        final TestNetworkCallback allNetworksCb = new TestNetworkCallback();
+        final NetworkRequest allNetworksRequest = new NetworkRequest.Builder().clearCapabilities()
+                .build();
+        mCm.registerNetworkCallback(allNetworksRequest, allNetworksCb);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true /* validated */);
+
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        // While the default callback doesn't see the network before it's validated, the listen
+        // sees the network come up and validate later
+        allNetworksCb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        allNetworksCb.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS * 2);
+
+        // The cell network has disconnected (see LOST above) because it was outscored and
+        // had no requests (see setAlwaysOnNetworks(false) above)
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final NetworkScore score = new NetworkScore.Builder().setLegacyInt(30).build();
+        mCellNetworkAgent.setScore(score);
+        mCellNetworkAgent.connect(false /* validated */);
+
+        // The cell network gets torn down right away.
+        allNetworksCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_NASCENT_DELAY_MS * 2);
+        allNetworksCb.assertNoCallback();
+
+        // Now create a cell network with KEEP_CONNECTED_FOR_HANDOVER and make sure it's
+        // not disconnected immediately when outscored.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final NetworkScore scoreKeepup = new NetworkScore.Builder().setLegacyInt(30)
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_HANDOVER).build();
+        mCellNetworkAgent.setScore(scoreKeepup);
+        mCellNetworkAgent.connect(true /* validated */);
+
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+
+        mWiFiNetworkAgent.disconnect();
+
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Reconnect a WiFi network and make sure the cell network is still not torn down.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Now remove the reason to keep connected and make sure the network lingers and is
+        // torn down.
+        mCellNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).build());
+        allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent,
+                TEST_NASCENT_DELAY_MS * 2);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS * 2);
+        mDefaultNetworkCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(allNetworksCb);
+        // mDefaultNetworkCallback will be unregistered by tearDown()
+    }
+
     private class QosCallbackMockHelper {
         @NonNull public final QosFilter mFilter;
         @NonNull public final IQosCallback mCallback;
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index eb3b4df..2864bc7 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -18,6 +18,7 @@
 
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
 import android.text.TextUtils
 import android.util.ArraySet
 import androidx.test.filters.SmallTest
@@ -60,11 +61,11 @@
 
     @Test
     fun testGetLegacyInt() {
-        val ns = FullScore(50, 0L /* policy */)
+        val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
         assertEquals(10, ns.legacyInt) // -40 penalty for not being validated
         assertEquals(50, ns.legacyIntAsValidated)
 
-        val vpnNs = FullScore(101, 0L /* policy */).withPolicies(vpn = true)
+        val vpnNs = FullScore(101, 0L /* policy */, KEEP_CONNECTED_NONE).withPolicies(vpn = true)
         assertEquals(101, vpnNs.legacyInt) // VPNs are not subject to unvalidation penalty
         assertEquals(101, vpnNs.legacyIntAsValidated)
         assertEquals(101, vpnNs.withPolicies(validated = true).legacyInt)
@@ -83,7 +84,7 @@
 
     @Test
     fun testToString() {
-        val string = FullScore(10, 0L /* policy */)
+        val string = FullScore(10, 0L /* policy */, KEEP_CONNECTED_NONE)
                 .withPolicies(vpn = true, acceptUnvalidated = true).toString()
         assertTrue(string.contains("Score(10"), string)
         assertTrue(string.contains("ACCEPT_UNVALIDATED"), string)
@@ -107,7 +108,7 @@
 
     @Test
     fun testHasPolicy() {
-        val ns = FullScore(50, 0L /* policy */)
+        val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
         assertFalse(ns.hasPolicy(POLICY_IS_VALIDATED))
         assertFalse(ns.hasPolicy(POLICY_IS_VPN))
         assertFalse(ns.hasPolicy(POLICY_EVER_USER_SELECTED))
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
index 1199341..409f8c3 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
@@ -19,6 +19,7 @@
 import android.net.INetworkOfferCallback
 import android.net.NetworkCapabilities
 import android.net.NetworkRequest
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import org.junit.Test
@@ -38,7 +39,7 @@
 
     @Test
     fun testOfferNeededUnneeded() {
-        val score = FullScore(50, POLICY_NONE)
+        val score = FullScore(50, POLICY_NONE, KEEP_CONNECTED_NONE)
         val offer = NetworkOffer(score, NetworkCapabilities.Builder().build(), mockCallback,
                 1 /* providerId */)
         val request1 = mock(NetworkRequest::class.java)