Add LocalNetworkInfo and send callbacks when it changes

Test: CSLocalAgentTest
Change-Id: I8caca97b891081f9212a01d428a34ed1a08d5126
diff --git a/framework/aidl-export/android/net/LocalNetworkInfo.aidl b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
new file mode 100644
index 0000000..fa0bc41
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable LocalNetworkInfo;
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
index bd513d2..09abd17 100644
--- a/framework/jarjar-excludes.txt
+++ b/framework/jarjar-excludes.txt
@@ -23,6 +23,7 @@
 # of these classes must be protected by a check for >= S SDK.
 # It's unlikely anybody else declares a hidden class with this name ?
 android\.net\.RoutingCoordinatorManager(\$.+)?
+android\.net\.LocalNetworkInfo(\$.+)?
 
 # KeepaliveUtils is used by ConnectivityManager CTS
 # TODO: move into service-connectivity so framework-connectivity stops using
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index eb8f8c3..66b2840 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -3963,16 +3963,21 @@
          * @param network The {@link Network} of the satisfying network.
          * @param networkCapabilities The {@link NetworkCapabilities} of the satisfying network.
          * @param linkProperties The {@link LinkProperties} of the satisfying network.
+         * @param localInfo The {@link LocalNetworkInfo} of the satisfying network, or null
+         *                  if this network is not a local network.
          * @param blocked Whether access to the {@link Network} is blocked due to system policy.
          * @hide
          */
         public final void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties, @BlockedReason int blocked) {
+                @NonNull LinkProperties linkProperties,
+                @Nullable LocalNetworkInfo localInfo,
+                @BlockedReason int blocked) {
             // Internally only this method is called when a new network is available, and
             // it calls the callback in the same way and order that older versions used
             // to call so as not to change the behavior.
             onAvailable(network, networkCapabilities, linkProperties, blocked != 0);
+            if (null != localInfo) onLocalNetworkInfoChanged(network, localInfo);
             onBlockedStatusChanged(network, blocked);
         }
 
@@ -3989,7 +3994,8 @@
          */
         public void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
-                @NonNull LinkProperties linkProperties, boolean blocked) {
+                @NonNull LinkProperties linkProperties,
+                boolean blocked) {
             onAvailable(network);
             if (!networkCapabilities.hasCapability(
                     NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -4116,6 +4122,19 @@
                 @NonNull LinkProperties linkProperties) {}
 
         /**
+         * Called when there is a change in the {@link LocalNetworkInfo} for this network.
+         *
+         * This is only called for local networks, that is those with the
+         * NET_CAPABILITY_LOCAL_NETWORK network capability.
+         *
+         * @param network the {@link Network} whose local network info has changed.
+         * @param localNetworkInfo the new {@link LocalNetworkInfo} for this network.
+         * @hide
+         */
+        public void onLocalNetworkInfoChanged(@NonNull Network network,
+                @NonNull LocalNetworkInfo localNetworkInfo) {}
+
+        /**
          * Called when the network the framework connected to for this request suspends data
          * transmission temporarily.
          *
@@ -4209,27 +4228,29 @@
     }
 
     /** @hide */
-    public static final int CALLBACK_PRECHECK            = 1;
+    public static final int CALLBACK_PRECHECK                   = 1;
     /** @hide */
-    public static final int CALLBACK_AVAILABLE           = 2;
+    public static final int CALLBACK_AVAILABLE                  = 2;
     /** @hide arg1 = TTL */
-    public static final int CALLBACK_LOSING              = 3;
+    public static final int CALLBACK_LOSING                     = 3;
     /** @hide */
-    public static final int CALLBACK_LOST                = 4;
+    public static final int CALLBACK_LOST                       = 4;
     /** @hide */
-    public static final int CALLBACK_UNAVAIL             = 5;
+    public static final int CALLBACK_UNAVAIL                    = 5;
     /** @hide */
-    public static final int CALLBACK_CAP_CHANGED         = 6;
+    public static final int CALLBACK_CAP_CHANGED                = 6;
     /** @hide */
-    public static final int CALLBACK_IP_CHANGED          = 7;
+    public static final int CALLBACK_IP_CHANGED                 = 7;
     /** @hide obj = NetworkCapabilities, arg1 = seq number */
-    private static final int EXPIRE_LEGACY_REQUEST       = 8;
+    private static final int EXPIRE_LEGACY_REQUEST              = 8;
     /** @hide */
-    public static final int CALLBACK_SUSPENDED           = 9;
+    public static final int CALLBACK_SUSPENDED                  = 9;
     /** @hide */
-    public static final int CALLBACK_RESUMED             = 10;
+    public static final int CALLBACK_RESUMED                    = 10;
     /** @hide */
-    public static final int CALLBACK_BLK_CHANGED         = 11;
+    public static final int CALLBACK_BLK_CHANGED                = 11;
+    /** @hide */
+    public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
 
     /** @hide */
     public static String getCallbackName(int whichCallback) {
@@ -4245,6 +4266,7 @@
             case CALLBACK_SUSPENDED:    return "CALLBACK_SUSPENDED";
             case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
             case CALLBACK_BLK_CHANGED:  return "CALLBACK_BLK_CHANGED";
+            case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
             default:
                 return Integer.toString(whichCallback);
         }
@@ -4299,7 +4321,8 @@
                 case CALLBACK_AVAILABLE: {
                     NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
                     LinkProperties lp = getObject(message, LinkProperties.class);
-                    callback.onAvailable(network, cap, lp, message.arg1);
+                    LocalNetworkInfo lni = getObject(message, LocalNetworkInfo.class);
+                    callback.onAvailable(network, cap, lp, lni, message.arg1);
                     break;
                 }
                 case CALLBACK_LOSING: {
@@ -4324,6 +4347,11 @@
                     callback.onLinkPropertiesChanged(network, lp);
                     break;
                 }
+                case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+                    final LocalNetworkInfo info = getObject(message, LocalNetworkInfo.class);
+                    callback.onLocalNetworkInfoChanged(network, info);
+                    break;
+                }
                 case CALLBACK_SUSPENDED: {
                     callback.onNetworkSuspended(network);
                     break;
diff --git a/framework/src/android/net/LocalNetworkInfo.java b/framework/src/android/net/LocalNetworkInfo.java
new file mode 100644
index 0000000..f945133
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkInfo.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * Information about a local network.
+ *
+ * This is sent to ConnectivityManager.NetworkCallback.
+ * @hide
+ */
+// TODO : make public
+public final class LocalNetworkInfo implements Parcelable {
+    @Nullable private final Network mUpstreamNetwork;
+
+    public LocalNetworkInfo(@Nullable final Network upstreamNetwork) {
+        this.mUpstreamNetwork = upstreamNetwork;
+    }
+
+    /**
+     * Return the upstream network, or null if none.
+     */
+    @Nullable
+    public Network getUpstreamNetwork() {
+        return mUpstreamNetwork;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeParcelable(mUpstreamNetwork, flags);
+    }
+
+    @Override
+    public String toString() {
+        return "LocalNetworkInfo { upstream=" + mUpstreamNetwork + " }";
+    }
+
+    public static final @NonNull Creator<LocalNetworkInfo> CREATOR = new Creator<>() {
+        public LocalNetworkInfo createFromParcel(Parcel in) {
+            final Network upstreamNetwork = in.readParcelable(null);
+            return new LocalNetworkInfo(upstreamNetwork);
+        }
+
+        @Override
+        public LocalNetworkInfo[] newArray(final int size) {
+            return new LocalNetworkInfo[size];
+        }
+    };
+
+    /**
+     * Builder for LocalNetworkInfo
+     */
+    public static final class Builder {
+        @Nullable private Network mUpstreamNetwork;
+
+        /**
+         * Set the upstream network, or null if none.
+         * @return the builder
+         */
+        @NonNull public Builder setUpstreamNetwork(@Nullable final Network network) {
+            mUpstreamNetwork = network;
+            return this;
+        }
+
+        /**
+         * Build the LocalNetworkInfo
+         */
+        @NonNull public LocalNetworkInfo build() {
+            return new LocalNetworkInfo(mUpstreamNetwork);
+        }
+    }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 50b4134..d8417c1 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -168,6 +168,7 @@
 import android.net.IpPrefix;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
+import android.net.LocalNetworkInfo;
 import android.net.MatchAllNetworkSpecifier;
 import android.net.NativeNetworkConfig;
 import android.net.NativeNetworkType;
@@ -4183,7 +4184,7 @@
                 }
                 case NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED: {
                     final LocalNetworkConfig config = (LocalNetworkConfig) arg.second;
-                    updateLocalNetworkConfig(nai, nai.localNetworkConfig, config);
+                    handleUpdateLocalNetworkConfig(nai, nai.localNetworkConfig, config);
                     break;
                 }
                 case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
@@ -4946,7 +4947,7 @@
         notifyIfacesChangedForNetworkStats();
         // If this was a local network forwarded to some upstream, or if some local network was
         // forwarded to this nai, then disable forwarding rules now.
-        maybeDisableForwardRulesForDisconnectingNai(nai);
+        maybeDisableForwardRulesForDisconnectingNai(nai, true /* sendCallbacks */);
         // If this is a local network with an upstream selector, remove the associated network
         // request.
         if (nai.isLocalNetwork()) {
@@ -5069,7 +5070,7 @@
     }
 
     private void maybeDisableForwardRulesForDisconnectingNai(
-            @NonNull final NetworkAgentInfo disconnecting) {
+            @NonNull final NetworkAgentInfo disconnecting, final boolean sendCallbacks) {
         // Step 1 : maybe this network was the upstream for one or more local networks.
         for (final NetworkAgentInfo local : mNetworkAgentInfos) {
             if (!local.isLocalNetwork()) continue;
@@ -5082,6 +5083,13 @@
             final NetworkAgentInfo satisfier = nri.getSatisfier();
             if (disconnecting != satisfier) continue;
             removeLocalNetworkUpstream(local, disconnecting);
+            // Set the satisfier to null immediately so that the LOCAL_NETWORK_CHANGED callback
+            // correctly contains null as an upstream.
+            if (sendCallbacks) {
+                nri.setSatisfier(null, null);
+                notifyNetworkCallbacks(local,
+                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+            }
         }
 
         // Step 2 : maybe this is a local network that had an upstream.
@@ -5148,8 +5156,10 @@
             mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
         }
         // Remove any forwarding rules to and from the interface for this network, since
-        // the interface is going to go away.
-        maybeDisableForwardRulesForDisconnectingNai(nai);
+        // the interface is going to go away. Don't send the callbacks however ; if the network
+        // was is being disconnected the callbacks have already been sent, and if it is being
+        // destroyed pending replacement they will be sent when it is disconnected.
+        maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
         try {
             mNetd.networkDestroy(nai.network.getNetId());
         } catch (RemoteException | ServiceSpecificException e) {
@@ -8320,7 +8330,7 @@
         }
 
         if (nai.isLocalNetwork()) {
-            updateLocalNetworkConfig(nai, null /* oldConfig */, nai.localNetworkConfig);
+            handleUpdateLocalNetworkConfig(nai, null /* oldConfig */, nai.localNetworkConfig);
         }
         nai.notifyRegistered();
         NetworkInfo networkInfo = nai.networkInfo;
@@ -8988,7 +8998,7 @@
     }
 
     // oldConfig is null iff this is the original registration of the local network config
-    private void updateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
+    private void handleUpdateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
             @Nullable final LocalNetworkConfig oldConfig,
             @NonNull final LocalNetworkConfig newConfig) {
         if (!nai.isLocalNetwork()) {
@@ -9021,6 +9031,12 @@
                 // If there is an old satisfier, but no new request, then remove the old upstream.
                 removeLocalNetworkUpstream(nai, oldSatisfier);
                 nai.localNetworkConfig = configBuilder.build();
+                // When there is a new request, the rematch sees the new request and sends the
+                // LOCAL_NETWORK_INFO_CHANGED callbacks accordingly.
+                // But here there is no new request, so the rematch won't see anything. Send
+                // callbacks to apps now to tell them about the loss of upstream.
+                notifyNetworkCallbacks(nai,
+                        ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
                 return;
             }
         }
@@ -9042,12 +9058,14 @@
                 nri.setSatisfier(oldSatisfier, nr);
             }
             nai.localNetworkConfig = configBuilder.build();
+            // handleRegisterNetworkRequest causes a rematch. The rematch must happen after
+            // nai.localNetworkConfig is set, since it will base its callbacks on the old
+            // satisfier and the new request.
             handleRegisterNetworkRequest(nri);
         } else {
             configBuilder.setUpstreamSelector(oldRequest);
             nai.localNetworkConfig = configBuilder.build();
         }
-
     }
 
     /**
@@ -9378,6 +9396,21 @@
         releasePendingNetworkRequestWithDelay(pendingIntent);
     }
 
+    @Nullable
+    private LocalNetworkInfo localNetworkInfoForNai(@NonNull final NetworkAgentInfo nai) {
+        if (!nai.isLocalNetwork()) return null;
+        final Network upstream;
+        final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+        if (null == selector) {
+            upstream = null;
+        } else {
+            final NetworkRequestInfo upstreamNri = mNetworkRequests.get(selector);
+            final NetworkAgentInfo satisfier = upstreamNri.getSatisfier();
+            upstream = (null == satisfier) ? null : satisfier.network;
+        }
+        return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
+    }
+
     // networkAgent is only allowed to be null if notificationType is
     // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
     // available, while all other cases are about some particular network.
@@ -9413,6 +9446,10 @@
                 putParcelable(bundle, nc);
                 putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
                         networkAgent.linkProperties, nri.mPid, nri.mUid));
+                // The local network info is often null, so can't use the static putParcelable
+                // method here.
+                bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
+                        localNetworkInfoForNai(networkAgent));
                 // For this notification, arg1 contains the blocked status.
                 msg.arg1 = arg1;
                 break;
@@ -9444,6 +9481,14 @@
                 msg.arg1 = arg1;
                 break;
             }
+            case ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+                if (!networkAgent.isLocalNetwork()) {
+                    Log.wtf(TAG, "Callback for local info for a non-local network");
+                    return;
+                }
+                putParcelable(bundle, localNetworkInfoForNai(networkAgent));
+                break;
+            }
         }
         msg.what = notificationType;
         msg.setData(bundle);
@@ -10077,6 +10122,7 @@
                     loge("Can't update forwarding rules", e);
                 }
             }
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
         }
 
         updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index df9c61a..05c0444 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -18,6 +18,7 @@
 
 import android.net.ConnectivityManager.NetworkCallback
 import android.net.LinkProperties
+import android.net.LocalNetworkInfo
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -28,6 +29,7 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
@@ -68,6 +70,10 @@
             override val network: Network,
             val lp: LinkProperties
         ) : CallbackEntry()
+        data class LocalInfoChanged(
+            override val network: Network,
+            val info: LocalNetworkInfo
+        ) : CallbackEntry()
         data class Suspended(override val network: Network) : CallbackEntry()
         data class Resumed(override val network: Network) : CallbackEntry()
         data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
@@ -94,6 +100,8 @@
             @JvmField
             val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
             @JvmField
+            val LOCAL_INFO_CHANGED = LocalInfoChanged::class
+            @JvmField
             val SUSPENDED = Suspended::class
             @JvmField
             val RESUMED = Resumed::class
@@ -131,6 +139,11 @@
         history.add(LinkPropertiesChanged(network, lp))
     }
 
+    override fun onLocalNetworkInfoChanged(network: Network, info: LocalNetworkInfo) {
+        Log.d(TAG, "onLocalNetworkInfoChanged $network $info")
+        history.add(LocalInfoChanged(network, info))
+    }
+
     override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
         Log.d(TAG, "onBlockedStatusChanged $network $blocked")
         history.add(BlockedStatus(network, blocked))
@@ -430,37 +443,63 @@
         suspended: Boolean = false,
         validated: Boolean? = true,
         blocked: Boolean = false,
+        upstream: Network? = null,
         tmt: Long = defaultTimeoutMs
     ) {
-        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
         expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
     }
 
+    // For backward compatibility, add a method that allows callers to specify a timeout but
+    // no upstream.
+    fun expectAvailableCallbacks(
+        net: Network,
+        suspended: Boolean = false,
+        validated: Boolean? = true,
+        blocked: Boolean = false,
+        tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended, validated, blocked, upstream = null, tmt = tmt)
+
     fun expectAvailableCallbacks(
         net: Network,
         suspended: Boolean,
         validated: Boolean,
         blockedReason: Int,
+        upstream: Network? = null,
         tmt: Long
     ) {
-        expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+        expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
         expect<BlockedStatusInt>(net) { it.reason == blockedReason }
     }
 
+    // For backward compatibility, add a method that allows callers to specify a timeout but
+    // no upstream.
+    fun expectAvailableCallbacks(
+            net: Network,
+            suspended: Boolean = false,
+            validated: Boolean = true,
+            blockedReason: Int,
+            tmt: Long = defaultTimeoutMs
+    ) = expectAvailableCallbacks(net, suspended, validated, blockedReason, upstream = null, tmt)
+
     private fun expectAvailableCallbacksCommon(
         net: Network,
         suspended: Boolean,
         validated: Boolean?,
+        upstream: Network?,
         tmt: Long
     ) {
         expect<Available>(net, tmt)
         if (suspended) {
             expect<Suspended>(net, tmt)
         }
-        expect<CapabilitiesChanged>(net, tmt) {
+        val caps = expect<CapabilitiesChanged>(net, tmt) {
             validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
-        }
+        }.caps
         expect<LinkPropertiesChanged>(net, tmt)
+        if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)) {
+            expect<LocalInfoChanged>(net, tmt) { it.info.upstreamNetwork == upstream }
+        }
     }
 
     // Backward compatibility for existing Java code. Use named arguments instead and remove all
@@ -507,13 +546,15 @@
         val network: Network
     }
 
+    @JvmOverloads
     fun expectAvailableCallbacks(
         n: HasNetwork,
         suspended: Boolean,
         validated: Boolean,
         blocked: Boolean,
+        upstream: Network? = null,
         timeoutMs: Long
-    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, timeoutMs)
+    ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, upstream, timeoutMs)
 
     fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
         expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index 3a76ad0..235f7de 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -38,10 +38,7 @@
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.RecorderCallback.CallbackEntry.Available
-import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
-import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
-import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
 import org.junit.Test
@@ -114,10 +111,7 @@
                 .build(),
                 lnc = LocalNetworkConfig.Builder().build())
         agent.connect()
-        cb.expect<Available>(agent.network)
-        cb.expect<CapabilitiesChanged>(agent.network)
-        cb.expect<LinkPropertiesChanged>(agent.network)
-        cb.expect<BlockedStatus>(agent.network)
+        cb.expectAvailableCallbacks(agent.network, validated = false)
         agent.sendNetworkCapabilities(NetworkCapabilities.Builder().build())
         cb.expect<Lost>(agent.network)
 
@@ -125,10 +119,7 @@
                 .build(),
                 lnc = null)
         agent2.connect()
-        cb.expect<Available>(agent2.network)
-        cb.expect<CapabilitiesChanged>(agent2.network)
-        cb.expect<LinkPropertiesChanged>(agent2.network)
-        cb.expect<BlockedStatus>(agent2.network)
+        cb.expectAvailableCallbacks(agent2.network, validated = false)
         agent2.sendNetworkCapabilities(NetworkCapabilities.Builder()
                 .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
                 .build())
@@ -153,10 +144,11 @@
         )
         localAgent.connect()
 
-        cb.expect<Available>(localAgent.network)
-        cb.expect<CapabilitiesChanged>(localAgent.network)
-        cb.expect<LinkPropertiesChanged>(localAgent.network)
-        cb.expect<BlockedStatus>(localAgent.network)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+        val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+        wifiAgent.connect()
 
         val newLnc = LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
@@ -165,6 +157,21 @@
                 .build()
         localAgent.sendLocalNetworkConfig(newLnc)
 
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        localAgent.sendLocalNetworkConfig(LocalNetworkConfig.Builder().build())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        localAgent.sendLocalNetworkConfig(newLnc)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
+
+        wifiAgent.disconnect()
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
         localAgent.disconnect()
     }
 
@@ -204,6 +211,9 @@
         wifiAgent.connect()
 
         cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
 
         clearInvocations(netd)
         val inOrder = inOrder(netd)
@@ -218,6 +228,7 @@
         wifiAgent2.connect()
 
         cb.expectAvailableCallbacks(wifiAgent2.network, validated = false)
+        cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == wifiAgent2.network }
         cb.expect<Lost> { it.network == wifiAgent.network }
 
         inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
@@ -252,7 +263,10 @@
                 nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
         wifiAgent.connect()
 
-        cb.expectAvailableCallbacksUnvalidated(wifiAgent)
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgent.network
+        }
 
         clearInvocations(netd)
         wifiAgent.unregisterAfterReplacement(TIMEOUT_MS.toInt())
@@ -260,6 +274,7 @@
         verify(netd).networkDestroy(wifiAgent.network.netId)
         verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
 
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
         cb.expect<Lost> { it.network == wifiAgent.network }
     }
 
@@ -294,7 +309,12 @@
         val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
         wifiAgent.connect()
 
-        cb.expectAvailableCallbacksUnvalidated(wifiAgent)
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        listOf(cb, localCb).forEach {
+            it.expect<LocalInfoChanged>(localAgent.network) {
+                it.info.upstreamNetwork == wifiAgent.network
+            }
+        }
 
         verify(netd).ipfwdAddInterfaceForward("local0", "wifi0")
 
@@ -303,8 +323,10 @@
         val localAgent2 = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
         localAgent2.connect()
 
-        localCb.expectAvailableCallbacks(localAgent2.network, validated = false)
-        cb.expectAvailableCallbacks(localAgent2.network, validated = false)
+        localCb.expectAvailableCallbacks(localAgent2.network,
+                validated = false, upstream = wifiAgent.network)
+        cb.expectAvailableCallbacks(localAgent2.network,
+                validated = false, upstream = wifiAgent.network)
         cb.expect<Lost> { it.network == localAgent.network }
     }
 
@@ -316,9 +338,11 @@
 
         val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
         wifiAgent.connect()
-        cb.expectAvailableCallbacksUnvalidated(wifiAgent)
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
 
-        // Set up a local agent that should forward its traffic to the best wifi upstream.
+        // Unregister wifi pending replacement, then set up a local agent that would have
+        // this network as its upstream.
+        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
         val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp("local0"),
                 lnc = LocalNetworkConfig.Builder()
@@ -331,14 +355,18 @@
                         .build())
         )
 
-        // ...but destroy the wifi agent before connecting it
-        wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
-
+        // Connect the local agent. The zombie wifi is its upstream, but the stack doesn't
+        // tell netd to add the forward since the wifi0 interface has gone.
         localAgent.connect()
-        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+        cb.expectAvailableCallbacks(localAgent.network,
+                validated = false, upstream = wifiAgent.network)
 
-        verify(netd).ipfwdAddInterfaceForward("local0", "wifi0")
-        verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+        verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+
+        // Disconnect wifi without a replacement. Expect an update with upstream null.
+        wifiAgent.disconnect()
+        verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+        cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == null }
     }
 
     @Test
@@ -366,19 +394,34 @@
         val wifiAgentDun = Agent(score = keepScore(), lp = lp("wifi1"),
                 nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
 
+        val cb = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                cb)
+        cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
         val inOrder = inOrder(netd)
         inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+        cb.assertNoCallback()
 
         wifiAgent.connect()
         inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+        cb.assertNoCallback()
 
         cellAgentDun.connect()
         inOrder.verify(netd).ipfwdEnableForwarding(any())
         inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "cell0")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgentDun.network
+        }
 
         wifiAgentDun.connect()
         inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
         inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi1")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun.network
+        }
 
         // Make sure sending the same config again doesn't do anything
         repeat(5) {
@@ -387,6 +430,10 @@
         inOrder.verifyNoMoreInteractions()
 
         wifiAgentDun.disconnect()
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == cellAgentDun.network
+        }
         inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi1")
         // This can take a little bit of time because it needs to wait for the rematch
         inOrder.verify(netd, timeout(MEDIUM_TIMEOUT_MS)).ipfwdAddInterfaceForward("local0", "cell0")
@@ -394,15 +441,35 @@
         cellAgentDun.disconnect()
         inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
         inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
 
         val wifiAgentDun2 = Agent(score = keepScore(), lp = lp("wifi2"),
                 nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
         wifiAgentDun2.connect()
         inOrder.verify(netd).ipfwdEnableForwarding(any())
         inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi2")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun2.network
+        }
 
-        localAgent.disconnect()
+        wifiAgentDun2.disconnect()
         inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi2")
         inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+        val wifiAgentDun3 = Agent(score = keepScore(), lp = lp("wifi3"),
+                nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+        wifiAgentDun3.connect()
+        inOrder.verify(netd).ipfwdEnableForwarding(any())
+        inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi3")
+        cb.expect<LocalInfoChanged>(localAgent.network) {
+            it.info.upstreamNetwork == wifiAgentDun3.network
+        }
+
+        localAgent.disconnect()
+        inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi3")
+        inOrder.verify(netd).ipfwdDisableForwarding(any())
+        cb.expect<Lost>(localAgent.network)
+        cb.assertNoCallback()
     }
 }