Merge "Add additional data elements into scan results" into tm-mainline-prod
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 2905e28..109bbda 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,7 +1,10 @@
 # Keep class's integer static field for MessageUtils to parsing their name.
--keep class com.android.networkstack.tethering.Tethering$TetherMainSM {
-    static final int CMD_*;
-    static final int EVENT_*;
+-keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
+    static final % POLICY_*;
+    static final % NOTIFY_TYPE_*;
+    static final % TRANSPORT_*;
+    static final % CMD_*;
+    static final % EVENT_*;
 }
 
 -keep class com.android.networkstack.tethering.util.BpfMap {
@@ -21,12 +24,8 @@
     *;
 }
 
--keepclassmembers class android.net.ip.IpServer {
-    static final int CMD_*;
-}
-
 # The lite proto runtime uses reflection to access fields based on the names in
 # the schema, keep all the fields.
 -keepclassmembers class * extends com.android.networkstack.tethering.protobuf.MessageLite {
     <fields>;
-}
\ No newline at end of file
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index d8e631e..2c6054d 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -75,6 +75,8 @@
                     .setUserType(userTypeToEnum(callerPkg))
                     .setUpstreamType(UpstreamType.UT_UNKNOWN)
                     .setErrorCode(ErrorCode.EC_NO_ERROR)
+                    .setUpstreamEvents(UpstreamEvents.newBuilder())
+                    .setDurationMillis(0)
                     .build();
         mBuilderMap.put(downstreamType, statsBuilder);
     }
@@ -110,7 +112,8 @@
                 reported.getErrorCode().getNumber(),
                 reported.getDownstreamType().getNumber(),
                 reported.getUpstreamType().getNumber(),
-                reported.getUserType().getNumber());
+                reported.getUserType().getNumber(),
+                null, 0);
         if (DBG) {
             Log.d(TAG, "Write errorCode: " + reported.getErrorCode().getNumber()
                     + ", downstreamType: " + reported.getDownstreamType().getNumber()
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
index 46a47af..27f2126 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto
@@ -21,12 +21,38 @@
 
 import "frameworks/proto_logging/stats/enums/stats/connectivity/tethering.proto";
 
+// Logs each upstream for a successful switch over
+message UpstreamEvent {
+  // Transport type of upstream network
+  optional .android.stats.connectivity.UpstreamType upstream_type = 1;
+
+  // A time period that an upstream continued
+  optional int64 duration_millis = 2;
+}
+
+message UpstreamEvents {
+  repeated UpstreamEvent upstream_event = 1;
+}
+
 /**
  * Logs Tethering events
  */
 message NetworkTetheringReported {
-   optional .android.stats.connectivity.ErrorCode error_code = 1;
-   optional .android.stats.connectivity.DownstreamType downstream_type = 2;
-   optional .android.stats.connectivity.UpstreamType upstream_type = 3;
-   optional .android.stats.connectivity.UserType user_type = 4;
+    // Tethering error code
+    optional .android.stats.connectivity.ErrorCode error_code = 1;
+
+    // Tethering downstream type
+    optional .android.stats.connectivity.DownstreamType downstream_type = 2;
+
+    // Transport type of upstream network
+    optional .android.stats.connectivity.UpstreamType upstream_type = 3  [deprecated = true];
+
+    // The user type of switching tethering
+    optional .android.stats.connectivity.UserType user_type = 4;
+
+    // Log each transport type of upstream network event
+    optional UpstreamEvents upstream_events = 5;
+
+    // A time period that a downstreams exists
+    optional int64 duration_millis = 6;
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index 6a85718..7fdde97 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -88,6 +88,8 @@
                 .setUserType(user)
                 .setUpstreamType(UpstreamType.UT_UNKNOWN)
                 .setErrorCode(error)
+                .setUpstreamEvents(UpstreamEvents.newBuilder())
+                .setDurationMillis(0)
                 .build();
         verify(mTetheringMetrics).write(expectedReport);
     }
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 2196bf8..00f6c56 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -469,7 +469,9 @@
             // TODO: Update this logic to only do a restart if required. Although a restart may
             //  be required due to the capabilities or ipConfiguration values, not all
             //  capabilities changes require a restart.
-            restart();
+            if (mIpClient != null) {
+                restart();
+            }
         }
 
         boolean isRestricted() {
@@ -558,6 +560,13 @@
 
         void updateNeighborLostEvent(String logMsg) {
             Log.i(TAG, "updateNeighborLostEvent " + logMsg);
+            if (mIpConfig.getIpAssignment() == IpAssignment.STATIC) {
+                // Ignore NUD failures for static IP configurations, where restarting the IpClient
+                // will not fix connectivity.
+                // In this scenario, NetworkMonitor will not verify the network, so it will
+                // eventually be torn down.
+                return;
+            }
             // Reachability lost will be seen only if the gateway is not reachable.
             // Since ethernet FW doesn't have the mechanism to scan for new networks
             // like WiFi, simply restart.
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 6ec478b..95baf81 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -302,9 +302,14 @@
         final int state = getInterfaceState(iface);
         final int role = getInterfaceRole(iface);
         final IpConfiguration config = getIpConfigurationForCallback(iface, state);
+        final boolean isRestricted = isRestrictedInterface(iface);
         final int n = mListeners.beginBroadcast();
         for (int i = 0; i < n; i++) {
             try {
+                if (isRestricted) {
+                    final ListenerInfo info = (ListenerInfo) mListeners.getBroadcastCookie(i);
+                    if (!info.canUseRestrictedNetworks) continue;
+                }
                 mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config);
             } catch (RemoteException e) {
                 // Do nothing here.
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index 2c4e431..9b39fd3 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -35,7 +35,7 @@
   <string-array name="network_switch_type_name">
     <item msgid="3004933964374161223">"datu-konexioa"</item>
     <item msgid="5624324321165953608">"Wifia"</item>
-    <item msgid="5667906231066981731">"Bluetooth-a"</item>
+    <item msgid="5667906231066981731">"Bluetootha"</item>
     <item msgid="346574747471703768">"Ethernet-a"</item>
     <item msgid="5734728378097476003">"VPNa"</item>
   </string-array>
diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml
index 4439048..fff6530 100644
--- a/service/ServiceConnectivityResources/res/values-nb/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml
@@ -18,7 +18,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"Ressurser for systemtilkobling"</string>
-    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Logg på Wi-Fi-nettverket"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Logg på Wifi-nettverket"</string>
     <string name="network_available_sign_in" msgid="2622520134876355561">"Logg på nettverk"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
diff --git a/service/ServiceConnectivityResources/res/values-ro/strings.xml b/service/ServiceConnectivityResources/res/values-ro/strings.xml
index 4ff5290..bf4479a 100644
--- a/service/ServiceConnectivityResources/res/values-ro/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-ro/strings.xml
@@ -28,7 +28,7 @@
     <string name="other_networks_no_internet" msgid="5693932964749676542">"Rețeaua nu are acces la internet"</string>
     <string name="private_dns_broken_detailed" msgid="2677123850463207823">"Serverul DNS privat nu poate fi accesat"</string>
     <string name="network_partial_connectivity" msgid="5549503845834993258">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> are conectivitate limitată"</string>
-    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Atingeți pentru a vă conecta oricum"</string>
+    <string name="network_partial_connectivity_detailed" msgid="4732435946300249845">"Atinge pentru a te conecta oricum"</string>
     <string name="network_switch_metered" msgid="5016937523571166319">"S-a comutat la <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
     <string name="network_switch_metered_detail" msgid="1257300152739542096">"Dispozitivul folosește <xliff:g id="NEW_NETWORK">%1$s</xliff:g> când <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nu are acces la internet. Se pot aplica taxe."</string>
     <string name="network_switch_metered_toast" msgid="70691146054130335">"S-a comutat de la <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> la <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
diff --git a/service/proguard.flags b/service/proguard.flags
index 478566c..864a28b 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -6,3 +6,12 @@
 -keepclassmembers public class * extends **.com.android.net.module.util.Struct {
     *;
 }
+
+-keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
+    static final % POLICY_*;
+    static final % NOTIFY_TYPE_*;
+    static final % TRANSPORT_*;
+    static final % CMD_*;
+    static final % EVENT_*;
+}
+
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 10b3dc8..f8b47d2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -3650,8 +3650,18 @@
                     break;
                 }
                 case NetworkAgent.EVENT_UNREGISTER_AFTER_REPLACEMENT: {
-                    // If nai is not yet created, or is already destroyed, ignore.
-                    if (!shouldDestroyNativeNetwork(nai)) break;
+                    if (!nai.isCreated()) {
+                        Log.d(TAG, "unregisterAfterReplacement on uncreated " + nai.toShortString()
+                                + ", tearing down instead");
+                        teardownUnneededNetwork(nai);
+                        break;
+                    }
+
+                    if (nai.isDestroyed()) {
+                        Log.d(TAG, "unregisterAfterReplacement on destroyed " + nai.toShortString()
+                                + ", ignoring");
+                        break;
+                    }
 
                     final int timeoutMs = (int) arg.second;
                     if (timeoutMs < 0 || timeoutMs > NetworkAgent.MAX_TEARDOWN_DELAY_MS) {
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 310d7bf..4887a78 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -270,7 +270,10 @@
     private static final int MIN_KEEPALIVE_INTERVAL = 10;
 
     private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
-    private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 5_000;
+    // Timeout for waiting network to be validated. Set the timeout to 30s, which is more than
+    // DNS timeout.
+    // TODO(b/252972908): reset the original timer when aosp/2188755 is ramped up.
+    private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
     private static final int NO_CALLBACK_TIMEOUT_MS = 100;
     private static final int SOCKET_TIMEOUT_MS = 100;
     private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 3635b7b..122eb15 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -381,7 +381,10 @@
         setIncludeTestInterfaces(false)
 
         for (listener in addedListeners) {
+            // Even if a given listener was not registered as both an interface and ethernet state
+            // listener, calling remove is safe.
             em.removeInterfaceStateListener(listener)
+            em.removeEthernetStateListener(listener)
         }
         registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
         releaseTetheredInterface()
@@ -409,6 +412,11 @@
         addedListeners.add(listener)
     }
 
+    private fun addEthernetStateListener(listener: EthernetStateListener) {
+        em.addEthernetStateListener(handler::post, listener)
+        addedListeners.add(listener)
+    }
+
     // WARNING: setting hasCarrier to false requires kernel support. Call
     // assumeChangingCarrierSupported() at the top of your test.
     private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
@@ -503,13 +511,8 @@
         runAsShell(NETWORK_SETTINGS) { em.setEthernetEnabled(enabled) }
 
         val listener = EthernetStateListener()
-        em.addEthernetStateListener(handler::post, listener)
-        try {
-            listener.eventuallyExpect(
-                    if (enabled) ETHERNET_STATE_ENABLED else ETHERNET_STATE_DISABLED)
-        } finally {
-            em.removeEthernetStateListener(listener)
-        }
+        addEthernetStateListener(listener)
+        listener.eventuallyExpect(if (enabled) ETHERNET_STATE_ENABLED else ETHERNET_STATE_DISABLED)
     }
 
     // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to
@@ -979,4 +982,23 @@
         cb.eventuallyExpectCapabilities(TEST_CAPS)
         cb.eventuallyExpectLpForStaticConfig(STATIC_IP_CONFIGURATION.staticIpConfiguration)
     }
+
+    @Test
+    fun testUpdateConfiguration_withLinkDown() {
+        assumeChangingCarrierSupported()
+        // createInterface without carrier is racy, so create it and then remove carrier.
+        val iface = createInterface()
+        val cb = requestNetwork(ETH_REQUEST)
+        cb.expectAvailable()
+
+        iface.setCarrierEnabled(false)
+        cb.eventuallyExpectLost()
+
+        updateConfiguration(iface, STATIC_IP_CONFIGURATION, TEST_CAPS).expectResult(iface.name)
+        cb.assertNoCallback()
+
+        iface.setCarrierEnabled(true)
+        cb.eventuallyExpectCapabilities(TEST_CAPS)
+        cb.eventuallyExpectLpForStaticConfig(STATIC_IP_CONFIGURATION.staticIpConfiguration)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index d2cd7aa..867d672 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -335,6 +335,28 @@
         mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
     }
 
+    fun assertLinkPropertiesEventually(
+        n: Network,
+        description: String,
+        condition: (LinkProperties?) -> Boolean
+    ): LinkProperties? {
+        val deadline = SystemClock.elapsedRealtime() + DEFAULT_TIMEOUT_MS
+        do {
+            val lp = mCM.getLinkProperties(n)
+            if (condition(lp)) return lp
+            SystemClock.sleep(10 /* ms */)
+        } while (SystemClock.elapsedRealtime() < deadline)
+        fail("Network $n LinkProperties did not $description after $DEFAULT_TIMEOUT_MS ms")
+    }
+
+    fun assertLinkPropertiesEventuallyNotNull(n: Network) {
+        assertLinkPropertiesEventually(n, "become non-null") { it != null }
+    }
+
+    fun assertLinkPropertiesEventuallyNull(n: Network) {
+        assertLinkPropertiesEventually(n, "become null") { it == null }
+    }
+
     @Test
     fun testSetSubtypeNameAndExtraInfoByAgentConfig() {
         val subtypeLTE = TelephonyManager.NETWORK_TYPE_LTE
@@ -1269,8 +1291,40 @@
         agent5.expectCallback<OnNetworkDestroyed>()
         agent5.expectCallback<OnNetworkUnwanted>()
 
+        // If unregisterAfterReplacement is called before markConnected, the network disconnects.
+        val specifier6 = UUID.randomUUID().toString()
+        val callback = TestableNetworkCallback()
+        requestNetwork(makeTestNetworkRequest(specifier = specifier6), callback)
+        val agent6 = createNetworkAgent(specifier = specifier6)
+        val network6 = agent6.register()
+        // No callbacks are sent, so check the LinkProperties to see if the network has connected.
+        assertLinkPropertiesEventuallyNotNull(agent6.network!!)
+
+        // unregisterAfterReplacement tears down the network immediately.
+        // Approximately check that this is the case by picking an unregister timeout that's longer
+        // than the timeout of the expectCallback<OnNetworkUnwanted> below.
+        // TODO: consider adding configurable timeouts to TestableNetworkAgent expectations.
+        val timeoutMs = agent6.DEFAULT_TIMEOUT_MS.toInt() + 1_000
+        agent6.unregisterAfterReplacement(timeoutMs)
+        agent6.expectCallback<OnNetworkUnwanted>()
+        if (!SdkLevel.isAtLeastT()) {
+            // Before T, onNetworkDestroyed is called even if the network was never created.
+            agent6.expectCallback<OnNetworkDestroyed>()
+        }
+        // Poll for LinkProperties becoming null, because when onNetworkUnwanted is called, the
+        // network has not yet been removed from the CS data structures.
+        assertLinkPropertiesEventuallyNull(agent6.network!!)
+        assertFalse(mCM.getAllNetworks().contains(agent6.network!!))
+
+        // After unregisterAfterReplacement is called, the network is no longer usable and
+        // markConnected has no effect.
+        agent6.markConnected()
+        agent6.assertNoCallback()
+        assertNull(mCM.getLinkProperties(agent6.network!!))
+        matchAllCallback.assertNoCallback(200 /* timeoutMs */)
+
         // If wifi is replaced within the timeout, the device does not switch to cellular.
-        val (cellAgent, cellNetwork) = connectNetwork(TRANSPORT_CELLULAR)
+        val (_, cellNetwork) = connectNetwork(TRANSPORT_CELLULAR)
         testCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
         matchAllCallback.expectAvailableThenValidatedCallbacks(cellNetwork)
 
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index cb68235..437622b 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -101,7 +101,7 @@
     ],
     static_libs: [
         "androidx.test.rules",
-        "androidx.test.uiautomator",
+        "androidx.test.uiautomator_uiautomator",
         "bouncycastle-repackaged-unbundled",
         "core-tests-support",
         "FrameworksNetCommonTests",
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
index 709b722..126ad55 100644
--- a/tests/unit/java/android/net/NetworkStatsTest.java
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -1067,6 +1067,38 @@
         }
     }
 
+    @Test
+    public void testClearInterfaces() {
+        final NetworkStats stats = new NetworkStats(TEST_START, 1);
+        final NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
+
+        final NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, 0xF0DD, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 51200, 25L, 101010L, 50L, 0L);
+
+        stats.insertEntry(entry1);
+        stats.insertEntry(entry2);
+
+        // Verify that the interfaces have indeed been recorded.
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
+        assertValues(stats, 1, "test2", 10101, SET_DEFAULT, 0xF0DD, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 51200, 25L, 101010L, 50L, 0L);
+
+        // Clear interfaces.
+        stats.clearInterfaces();
+
+        // Verify that the interfaces are cleared.
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, null /* iface */, 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 1024L, 50L, 100L, 20L, 0L);
+        assertValues(stats, 1, null /* iface */, 10101, SET_DEFAULT, 0xF0DD, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 51200, 25L, 101010L, 50L, 0L);
+    }
+
     private static void assertContains(NetworkStats stats,  String iface, int uid, int set,
             int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets,
             long txBytes, long txPackets, long operations) {
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 1c54651..39fd780 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -20,6 +20,8 @@
 import static android.Manifest.permission.CONTROL_VPN;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
 import static android.net.ConnectivityManager.NetworkCallback;
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
@@ -76,6 +78,7 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
+import android.net.ConnectivityDiagnosticsManager;
 import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.Ikev2VpnProfile;
@@ -115,6 +118,7 @@
 import android.os.ConditionVariable;
 import android.os.INetworkManagementService;
 import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
 import android.os.PowerWhitelistManager;
 import android.os.Process;
 import android.os.UserHandle;
@@ -245,6 +249,7 @@
     @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
     @Mock private Vpn.VpnNetworkAgentWrapper mMockNetworkAgent;
     @Mock private ConnectivityManager mConnectivityManager;
+    @Mock private ConnectivityDiagnosticsManager mCdm;
     @Mock private IpSecService mIpSecService;
     @Mock private VpnProfileStore mVpnProfileStore;
     @Mock private ScheduledThreadPoolExecutor mExecutor;
@@ -283,6 +288,8 @@
         mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager);
         mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager);
         mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager);
+        mockService(ConnectivityDiagnosticsManager.class, Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
+                mCdm);
         when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
                 .thenReturn(Resources.getSystem().getString(
                         R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
@@ -1925,6 +1932,56 @@
         verifyHandlingNetworkLoss(vpnSnapShot);
     }
 
+    private ConnectivityDiagnosticsCallback getConnectivityDiagCallback() {
+        final ArgumentCaptor<ConnectivityDiagnosticsCallback> cdcCaptor =
+                ArgumentCaptor.forClass(ConnectivityDiagnosticsCallback.class);
+        verify(mCdm).registerConnectivityDiagnosticsCallback(
+                any(), any(), cdcCaptor.capture());
+        return cdcCaptor.getValue();
+    }
+
+    private DataStallReport createDataStallReport() {
+        return new DataStallReport(TEST_NETWORK, 1234 /* reportTimestamp */,
+                1 /* detectionMethod */, new LinkProperties(), new NetworkCapabilities(),
+                new PersistableBundle());
+    }
+
+    private void verifyMobikeTriggered(List<Network> expected) {
+        final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
+        verify(mIkeSessionWrapper).setNetwork(networkCaptor.capture());
+        assertEquals(expected, Collections.singletonList(networkCaptor.getValue()));
+    }
+
+    @Test
+    public void testDataStallInIkev2VpnMobikeDisabled() throws Exception {
+        verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
+        final ConnectivityDiagnosticsCallback connectivityDiagCallback =
+                getConnectivityDiagCallback();
+        final DataStallReport report = createDataStallReport();
+        connectivityDiagCallback.onDataStallSuspected(report);
+
+        // Should not trigger MOBIKE if MOBIKE is not enabled
+        verify(mIkeSessionWrapper, never()).setNetwork(any());
+    }
+
+    @Test
+    public void testDataStallInIkev2VpnMobikeEnabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+
+        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
+        final ConnectivityDiagnosticsCallback connectivityDiagCallback =
+                getConnectivityDiagCallback();
+        final DataStallReport report = createDataStallReport();
+        connectivityDiagCallback.onDataStallSuspected(report);
+
+        // Verify MOBIKE is triggered
+        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+    }
+
     @Test
     public void testStartRacoonNumericAddress() throws Exception {
         startRacoon("1.2.3.4", "1.2.3.4");