Merge "Delete unused function" into main
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..347e497
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,2 @@
+IndentWidth: 4
+AllowShortIfStatementsOnASingleLine: WithoutElse
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index 9d6b6f6..199661d 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -256,10 +256,11 @@
 
 #define ABSOLUTE(x) ((x) < 0 ? -(x) : (x))
 
-#define DEFAULT_BPF_MAP_FLAGS(type, num_entries, mapflags)    \
-    ( (mapflags) |                                            \
-      ((num_entries) < 0 ? BPF_F_NO_PREALLOC : 0) |           \
-      (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0) \
+#define DEFAULT_BPF_MAP_FLAGS(type, num_entries, mapflags)         \
+    ( (mapflags) |                                                 \
+      ((num_entries) < 0 ? BPF_F_NO_PREALLOC : 0) |                \
+      ( (type == BPF_MAP_TYPE_LPM_TRIE ||                          \
+         type == BPF_MAP_TYPE_SK_STORAGE) ? BPF_F_NO_PREALLOC : 0) \
     )
 
 #define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index bdc2e8c..74c37c4 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1832,7 +1832,9 @@
         int v = fscanf(f, "# %d %d %d %d %d #", &y, &q, &a, &b, &c);
         ALOGI("detected %d of 5: %dQ%d api:%d.%d.%d", v, y, q, a, b, c);
         fclose(f);
-        if (v != 5 || y != 2025 || q != 2 || a != 36 || b || c) return 1;
+        if (v != 5 || y != 2025 || a != 36 || b) return 1;
+        if (q < 2 || q > 3) return 1;
+        if (c < 0 || c > 1) return 1;
     }
 
     // Ensure we can determine the Android build type.
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 41b58fa..a477cc8 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -32,7 +32,6 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.security.GeneralSecurityException;
 import java.util.Collection;
 import java.util.Optional;
 
@@ -151,13 +150,19 @@
             return;
         }
 
+        LogListUpdateStatus updateStatus;
         try {
-            mSignatureVerifier.setPublicKeyFrom(publicKeyUri);
-        } catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
+            updateStatus = mSignatureVerifier.setPublicKeyFrom(publicKeyUri);
+        } catch (IOException e) {
             Log.e(TAG, "Error setting the public Key", e);
             return;
         }
 
+        if (!updateStatus.isPublicKeySet()) {
+            mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+            return;
+        }
+
         startMetadataDownload();
     }
 
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
index 2a37d8f..8d13e45 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -36,6 +36,8 @@
         UNKNOWN_STATE,
         HTTP_ERROR,
         LOG_LIST_INVALID,
+        PUBLIC_KEY_INVALID,
+        PUBLIC_KEY_NOT_ALLOWED,
         PUBLIC_KEY_NOT_FOUND,
         SIGNATURE_INVALID,
         SIGNATURE_NOT_FOUND,
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
index f617523..12cdef3 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -22,6 +22,8 @@
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_ALLOWED;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
@@ -133,6 +135,10 @@
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
             case LOG_LIST_INVALID:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
+            case PUBLIC_KEY_INVALID:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_INVALID;
+            case PUBLIC_KEY_NOT_ALLOWED:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_ALLOWED;
             case PUBLIC_KEY_NOT_FOUND:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
             case SIGNATURE_INVALID:
diff --git a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
index 3f9b762..df32e42 100644
--- a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.net.ct;
 
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_ALLOWED;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
@@ -41,6 +43,11 @@
 
     abstract Optional<Integer> downloadStatus();
 
+    boolean isPublicKeySet() {
+        // Check that none of the public key setting failures have been set as the state
+        return state() != PUBLIC_KEY_INVALID && state() != PUBLIC_KEY_NOT_ALLOWED;
+    }
+
     boolean isSignatureVerified() {
         // Check that none of the signature verification failures have been set as the state
         return state() != PUBLIC_KEY_NOT_FOUND
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 87a4973..22953f4 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.net.ct;
 
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_ALLOWED;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.PUBLIC_KEY_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
@@ -84,28 +86,35 @@
         mPublicKey = Optional.empty();
     }
 
-    void setPublicKeyFrom(Uri file) throws GeneralSecurityException, IOException {
+    LogListUpdateStatus setPublicKeyFrom(Uri file) throws IOException {
         try (InputStream fileStream = mContext.getContentResolver().openInputStream(file)) {
-            setPublicKey(new String(fileStream.readAllBytes()));
+            return setPublicKey(new String(fileStream.readAllBytes()));
         }
     }
 
-    void setPublicKey(String publicKey) throws GeneralSecurityException {
+    private LogListUpdateStatus setPublicKey(String publicKey) {
         byte[] decodedPublicKey = null;
+        LogListUpdateStatus.Builder statusBuilder = LogListUpdateStatus.builder();
+
         try {
             decodedPublicKey = Base64.getDecoder().decode(publicKey);
-        } catch (IllegalArgumentException e) {
-            throw new GeneralSecurityException("Invalid public key base64 encoding", e);
-        }
-        setPublicKey(
+            setPublicKey(
                 KeyFactory.getInstance("RSA")
                         .generatePublic(new X509EncodedKeySpec(decodedPublicKey)));
+        } catch (IllegalArgumentException e) {
+            statusBuilder.setState(PUBLIC_KEY_INVALID);
+            Log.w(TAG, "Invalid public key base64 encoding", e);
+        } catch (GeneralSecurityException e) {
+            statusBuilder.setState(PUBLIC_KEY_NOT_ALLOWED);
+            Log.e(TAG, "Public key not in allowlist", e);
+        }
+
+        return statusBuilder.build();
     }
 
     @VisibleForTesting
     void setPublicKey(PublicKey publicKey) throws GeneralSecurityException {
         if (!mAllowedKeys.contains(publicKey)) {
-            // TODO(b/400704086): add logging for this failure.
             throw new GeneralSecurityException("Public key not in allowlist");
         }
         mPublicKey = Optional.of(publicKey);
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 956bad5..f5e3b7d 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -184,6 +184,41 @@
 
     @Test
     public void
+            testDownloader_publicKeyDownloadSuccess_publicKeyNotAllowed_logsFailure()
+                    throws Exception {
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
+        PublicKey notAllowed = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic();
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(notAllowed)));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(
+                        LogListUpdateStatus.builder()
+                                .setState(CTLogListUpdateState.PUBLIC_KEY_NOT_ALLOWED)
+                                .build());
+    }
+
+    @Test
+    public void
+            testDownloader_publicKeyDownloadSuccess_publicKeyInvalidEncoding_logsFailure()
+                    throws Exception {
+        mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext,
+                makePublicKeyDownloadCompleteIntent(
+                        writeToFile("i_am_not_a_base64_encoded_public_key".getBytes())));
+
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(
+                        LogListUpdateStatus.builder()
+                                .setState(CTLogListUpdateState.PUBLIC_KEY_INVALID)
+                                .build());
+    }
+
+    @Test
+    public void
             testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
                     throws Exception {
         mCertificateTransparencyDownloader.startPublicKeyDownload();
@@ -216,7 +251,7 @@
     }
 
     @Test
-    public void testDownloader_publicKeyDownloadFail_logsFailure() throws Exception {
+    public void testDownloader_publicKeyDownloadFail_logsDownloadFailure() throws Exception {
         mCertificateTransparencyDownloader.startPublicKeyDownload();
 
         mCertificateTransparencyDownloader.onReceive(
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index bd4bfbd..c3749bd 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -791,8 +791,6 @@
                 new NetworkCapabilities.Builder(DEFAULT_CAPABILITIES);
         if (isTestIface) {
             builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
-            // TODO: do not remove INTERNET capability for test networks.
-            builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
         }
 
         return builder.build();
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 2c44b62..45b431f 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -5787,6 +5787,10 @@
             mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
         }
 
+        // Removes the interfaces associated with the network being destroyed from the tracker.
+        for (String interfaceName : nai.linkProperties.getAllInterfaceNames()) {
+            mInterfaceTracker.removeInterface(interfaceName);
+        }
         nai.setDestroyed();
         nai.onNetworkDestroyed();
     }
diff --git a/service/src/com/android/server/connectivity/InterfaceTracker.java b/service/src/com/android/server/connectivity/InterfaceTracker.java
index 0b4abeb..c6686af 100644
--- a/service/src/com/android/server/connectivity/InterfaceTracker.java
+++ b/service/src/com/android/server/connectivity/InterfaceTracker.java
@@ -22,6 +22,7 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.BpfNetMaps;
 
@@ -39,6 +40,7 @@
     }
     private static final String TAG = "InterfaceTracker";
     private final Dependencies mDeps;
+    @GuardedBy("mInterfaceMap")
     private final Map<String, Integer> mInterfaceMap;
 
     public InterfaceTracker(final Context context) {
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index fd41ee6..c5b2762 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -38,6 +38,7 @@
 import android.os.UserHandle;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.text.BidiFormatter;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
@@ -87,6 +88,7 @@
 
     // The context is for the current user (system server)
     private final Context mContext;
+    private final Dependencies mDependencies;
     private final ConnectivityResources mResources;
     private final TelephonyManager mTelephonyManager;
     // The notification manager is created from a context for User.ALL, so notifications
@@ -96,7 +98,15 @@
     private final SparseIntArray mNotificationTypeMap;
 
     public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) {
+        this(c, t, new Dependencies());
+    }
+
+    @VisibleForTesting
+    protected NetworkNotificationManager(@NonNull final Context c,
+            @NonNull final TelephonyManager t,
+            @NonNull Dependencies dependencies) {
         mContext = c;
+        mDependencies = dependencies;
         mTelephonyManager = t;
         mNotificationManager =
                 (NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */)
@@ -106,6 +116,13 @@
     }
 
     @VisibleForTesting
+    protected static class Dependencies {
+        public BidiFormatter getBidiFormatter() {
+            return BidiFormatter.getInstance();
+        }
+    }
+
+    @VisibleForTesting
     protected static int approximateTransportType(NetworkAgentInfo nai) {
         return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai);
     }
@@ -174,7 +191,7 @@
                 name = extraInfo;
             } else {
                 final String ssid = WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid());
-                name = ssid == null ? "" : ssid;
+                name = ssid == null ? "" : mDependencies.getBidiFormatter().unicodeWrap(ssid);
             }
             // Only notify for Internet-capable networks.
             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index 60285a8..1db5765 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -36,6 +36,7 @@
 import android.net.wifi.WifiManager
 import android.os.Build
 import android.os.ParcelFileDescriptor
+import android.os.ParcelFileDescriptor.AutoCloseInputStream
 import android.telephony.TelephonyManager
 import android.telephony.TelephonyManager.SIM_STATE_UNKNOWN
 import android.util.Log
@@ -46,6 +47,7 @@
 import java.io.CharArrayWriter
 import java.io.File
 import java.io.FileReader
+import java.io.InputStream
 import java.io.OutputStream
 import java.io.OutputStreamWriter
 import java.io.PrintWriter
@@ -438,13 +440,44 @@
      * <p>The output will be collected immediately, and exported to a test artifact file when the
      * test ends.
      * @param cmd The command to run. Stdout of the command will be collected.
-     * @param shell The shell to run the command in.
+     * @param shell The shell to run the command in, for example "sh".
+     * @param exceptionContext An exception to write a stacktrace to the dump for context.
+     */
+    @RequiresApi(Build.VERSION_CODES.S)
+    fun collectCommandOutput(
+        cmd: String,
+        shell: String,
+        exceptionContext: Throwable? = null
+    ) = collectCommandOutput(cmd, exceptionContext) { c, outputProcessor ->
+        runCommandInShell(c, shell, outputProcessor)
+    }
+
+    /**
+     * Add the output of a command to the test data dump.
+     *
+     * <p>The output will be collected immediately, and exported to a test artifact file when the
+     * test ends.
+     *
+     * <p>Note this does not support shell pipes, redirections, or quoted arguments. See the S+
+     * overload if that is needed.
+     * @param cmd The command to run. Stdout of the command will be collected.
      * @param exceptionContext An exception to write a stacktrace to the dump for context.
      */
     fun collectCommandOutput(
         cmd: String,
-        shell: String = "sh",
         exceptionContext: Throwable? = null
+    ) = collectCommandOutput(cmd, exceptionContext) { c, outputProcessor ->
+        AutoCloseInputStream(
+            InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand(c)
+        ).use {
+            outputProcessor(it)
+        }
+    }
+
+    private fun collectCommandOutput(
+        cmd: String,
+        exceptionContext: Throwable? = null,
+        commandRunner: (String, (InputStream) -> Unit) -> Unit
     ) {
         Log.i(TAG, "Collecting '$cmd' for test artifacts")
         PrintWriter(buffer).let {
@@ -453,7 +486,7 @@
             it.flush()
         }
 
-        runCommandInShell(cmd, shell) { stdout, _ ->
+        commandRunner(cmd) { stdout ->
             stdout.copyTo(buffer)
         }
     }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
index fadc2ab..2b74036 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ShellUtil.kt
@@ -19,8 +19,10 @@
 package com.android.testutils
 
 import android.app.UiAutomation
+import android.os.Build
 import android.os.ParcelFileDescriptor.AutoCloseInputStream
 import android.os.ParcelFileDescriptor.AutoCloseOutputStream
+import androidx.annotation.RequiresApi
 import androidx.test.platform.app.InstrumentationRegistry
 import java.io.InputStream
 
@@ -37,18 +39,17 @@
  *                        when this function returns.
  * @return Result of [outputProcessor].
  */
+@RequiresApi(Build.VERSION_CODES.S) // executeShellCommandRw is 31+
 fun <T> runCommandInShell(
     cmd: String,
     shell: String = "sh",
-    outputProcessor: (InputStream, InputStream) -> T,
+    outputProcessor: (InputStream) -> T,
 ): T {
-    val (stdout, stdin, stderr) = InstrumentationRegistry.getInstrumentation().uiAutomation
-        .executeShellCommandRwe(shell)
+    val (stdout, stdin) = InstrumentationRegistry.getInstrumentation().uiAutomation
+        .executeShellCommandRw(shell)
     AutoCloseOutputStream(stdin).bufferedWriter().use { it.write(cmd) }
     AutoCloseInputStream(stdout).use { outStream ->
-        AutoCloseInputStream(stderr).use { errStream ->
-            return outputProcessor(outStream, errStream)
-        }
+        return outputProcessor(outStream)
     }
 }
 
@@ -57,10 +58,11 @@
  *
  * Overload of [runCommandInShell] that reads and returns stdout as String.
  */
+@RequiresApi(Build.VERSION_CODES.S)
 fun runCommandInShell(
     cmd: String,
     shell: String = "sh",
-) = runCommandInShell(cmd, shell) { stdout, _ ->
+) = runCommandInShell(cmd, shell) { stdout ->
     stdout.reader().use { it.readText() }
 }
 
@@ -70,6 +72,7 @@
  * This is generally only usable on devices on which [DeviceInfoUtils.isDebuggable] is true.
  * @see runCommandInShell
  */
+@RequiresApi(Build.VERSION_CODES.S)
 fun runCommandInRootShell(
     cmd: String
 ) = runCommandInShell(cmd, shell = "su root sh")
diff --git a/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
index 1b288df..1ed54a8 100644
--- a/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
@@ -20,6 +20,7 @@
 import android.net.nsd.NsdServiceInfo
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
 import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
 import com.android.testutils.NsdRegistrationRecord
 import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
@@ -58,6 +59,10 @@
 
     @Rpc(description = "Unregister a mDns service")
     fun unregisterMDnsService() {
+        if (!(registrationRecord.poll(timeoutMs = 0, pos = 0) is ServiceRegistered)) {
+            // Ignore unregistration if the service has not registered
+            return
+        }
         nsdManager.unregisterService(registrationRecord)
         registrationRecord.expectCallback<ServiceUnregistered>()
     }
@@ -86,6 +91,10 @@
 
     @Rpc(description = "Stop discovery")
     fun stopMDnsServiceDiscovery() {
+        if (!(discoveryRecord.poll(timeoutMs = 0, pos = 0) is DiscoveryStarted)) {
+            // Ignore discovery stop if discovery has not started
+            return
+        }
         nsdManager.stopServiceDiscovery(discoveryRecord)
         discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
     }
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 7d93c3a..c8e630f 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -57,4 +57,5 @@
     ],
     per_testcase_directory: true,
     sdk_version: "test_current",
+    host_required: ["net-tests-utils-host-common"],
 }
diff --git a/tests/cts/net/api23Test/AndroidTest.xml b/tests/cts/net/api23Test/AndroidTest.xml
index fcc73f3..cc5568b 100644
--- a/tests/cts/net/api23Test/AndroidTest.xml
+++ b/tests/cts/net/api23Test/AndroidTest.xml
@@ -25,6 +25,10 @@
         <option name="test-file-name" value="CtsNetApi23TestCases.apk" />
         <option name="test-file-name" value="CtsNetTestAppForApi23.apk" />
     </target_preparer>
+    <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
+        <!-- The tests require a working Wi-Fi network only -->
+        <option name="ignore-mobile-data-check" value="true" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.net.cts.api23test" />
         <option name="hidden-api-checks" value="false" />
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 727db58..bcdc4c5 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -27,6 +27,7 @@
 import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
@@ -61,6 +62,7 @@
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
 import android.testing.PollingCheck;
+import android.text.BidiFormatter;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.widget.TextView;
@@ -95,13 +97,14 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkNotificationManagerTest {
 
-    private static final String TEST_SSID = "Test SSID";
+    private static final String TEST_SSID = "?Test SSID+";
     private static final String TEST_EXTRA_INFO = "extra";
     private static final int TEST_NOTIF_ID = 101;
     private static final String TEST_NOTIF_TAG = NetworkNotificationManager.tagFor(TEST_NOTIF_ID);
@@ -166,6 +169,7 @@
     @Mock NetworkAgentInfo mBluetoothNai;
     @Mock NetworkInfo mNetworkInfo;
     @Mock NetworkInfo mEmptyNetworkInfo;
+    @Mock NetworkNotificationManager.Dependencies mDependencies;
     ArgumentCaptor<Notification> mCaptor;
 
     NetworkNotificationManager mManager;
@@ -192,6 +196,7 @@
         doReturn(asUserCtx).when(mCtx).createContextAsUser(eq(UserHandle.ALL), anyInt());
         doReturn(mNotificationManager).when(mCtx)
                 .getSystemService(eq(Context.NOTIFICATION_SERVICE));
+        doReturn(BidiFormatter.getInstance(Locale.US)).when(mDependencies).getBidiFormatter();
         doReturn(TEST_EXTRA_INFO).when(mNetworkInfo).getExtraInfo();
         ConnectivityResources.setResourcesContextForTest(mCtx);
         doReturn(0xFF607D8B).when(mResources).getColor(anyInt(), any());
@@ -209,7 +214,7 @@
             .thenReturn(transportNames);
         when(mResources.getBoolean(R.bool.config_autoCancelNetworkNotifications)).thenReturn(true);
 
-        mManager = new NetworkNotificationManager(mCtx, mTelephonyManager);
+        mManager = new NetworkNotificationManager(mCtx, mTelephonyManager, mDependencies);
     }
 
     @After
@@ -535,6 +540,26 @@
     }
 
     @Test
+    public void testNotificationText_NoInternet_WithSsid() {
+        doReturn(null).when(mNetworkInfo).getExtraInfo();
+        doNotificationTextTest(NO_INTERNET,
+                R.string.wifi_no_internet, TEST_SSID,
+                R.string.wifi_no_internet_detailed);
+    }
+
+    @Test
+    public void testNotificationText_NoInternet_WithRtlSsid() {
+        final BidiFormatter formatter = BidiFormatter.getInstance(Locale.forLanguageTag("ar"));
+        final String wrappedString = formatter.unicodeWrap(TEST_SSID);
+        doReturn(formatter).when(mDependencies).getBidiFormatter();
+        doReturn(null).when(mNetworkInfo).getExtraInfo();
+        assertNotEquals(TEST_SSID, wrappedString);
+        doNotificationTextTest(NO_INTERNET,
+                R.string.wifi_no_internet, wrappedString,
+                R.string.wifi_no_internet_detailed);
+    }
+
+    @Test
     public void testNotificationText_Partial() {
         doNotificationTextTest(PARTIAL_CONNECTIVITY,
                 R.string.network_partial_connectivity, TEST_EXTRA_INFO,
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index efae244..2253913 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -35,14 +35,14 @@
 import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import java.net.DatagramPacket
+import kotlin.test.assertContentEquals
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.net.DatagramPacket
-import kotlin.test.assertContentEquals
 
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -178,8 +178,13 @@
         val interfaceIndex = 99
         val response = MdnsResponse(0 /* now */, serviceName, interfaceIndex, null /* network */)
         // Set PTR record
-        response.addPointerRecord(MdnsPointerRecord(serviceType.split(".").toTypedArray(),
-                testElapsedRealtime, false /* cacheFlush */, ttlTime, serviceName))
+        response.addPointerRecord(MdnsPointerRecord(
+                serviceType.split(".").toTypedArray(),
+                testElapsedRealtime,
+                false /* cacheFlush */,
+                ttlTime,
+                serviceName
+        ))
         // Set SRV record.
         response.serviceRecord = MdnsServiceRecord(serviceName, testElapsedRealtime,
                 false /* cacheFlush */, ttlTime, 0 /* servicePriority */, 0 /* serviceWeight */,
@@ -189,16 +194,27 @@
                 testElapsedRealtime, true /* cacheFlush */, 0L /* ttlMillis */,
                 listOf(MdnsServiceInfo.TextEntry.fromString("somedifferent=entry")))
         // Set InetAddress record.
-        response.addInet4AddressRecord(MdnsInetAddressRecord(hostName.split(".").toTypedArray(),
-                testElapsedRealtime, true /* cacheFlush */,
-                0L /* ttlMillis */, InetAddresses.parseNumericAddress(v4Address)))
-        response.addInet6AddressRecord(MdnsInetAddressRecord(hostName.split(".").toTypedArray(),
-                testElapsedRealtime, true /* cacheFlush */,
-                0L /* ttlMillis */, InetAddresses.parseNumericAddress(v6Address)))
+        response.addInet4AddressRecord(MdnsInetAddressRecord(
+                hostName.split(".").toTypedArray(),
+                testElapsedRealtime,
+                true /* cacheFlush */,
+                0L /* ttlMillis */,
+                InetAddresses.parseNumericAddress(v4Address)
+        ))
+        response.addInet6AddressRecord(MdnsInetAddressRecord(
+                hostName.split(".").toTypedArray(),
+                testElapsedRealtime,
+                true /* cacheFlush */,
+                0L /* ttlMillis */,
+                InetAddresses.parseNumericAddress(v6Address)
+        ))
 
         // Convert a MdnsResponse to a MdnsServiceInfo
         val serviceInfo = MdnsUtils.buildMdnsServiceInfoFromResponse(
-                response, serviceType.split(".").toTypedArray(), testElapsedRealtime)
+                response,
+                serviceType.split(".").toTypedArray(),
+                testElapsedRealtime
+        )
 
         assertEquals(serviceInstanceName, serviceInfo.serviceInstanceName)
         assertArrayEquals(serviceType.split(".").toTypedArray(), serviceInfo.serviceType)
@@ -210,7 +226,6 @@
         assertEquals(v6Address, serviceInfo.ipv6Addresses[0])
         assertEquals(interfaceIndex, serviceInfo.interfaceIndex)
         assertEquals(null, serviceInfo.network)
-        assertEquals(mapOf("somedifferent" to "entry"),
-                serviceInfo.attributes)
+        assertEquals(mapOf("somedifferent" to "entry"), serviceInfo.attributes)
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSInterfaceTrackerTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSInterfaceTrackerTest.kt
new file mode 100644
index 0000000..b06e113
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSInterfaceTrackerTest.kt
@@ -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 com.android.server.connectivityservice
+
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.server.CSTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+
+private const val WIFI_IFNAME = "wlan0"
+
+private val wifiNc = NetworkCapabilities.Builder()
+    .addTransportType(TRANSPORT_WIFI)
+    .addCapability(NET_CAPABILITY_INTERNET)
+    .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+    .build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+    interfaceName = iface
+    for (linkAddress in linkAddresses) {
+        addLinkAddress(linkAddress)
+    }
+}
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+    .clearCapabilities()
+    .addTransportType(transport).apply {
+        if (transport != TRANSPORT_VPN) {
+            addCapability(NET_CAPABILITY_NOT_VPN)
+        }
+    }.build()
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+class CSInterfaceTrackerTest : CSTest() {
+    private val LOCAL_IPV6_IP_ADDRESS_PREFIX = IpPrefix("fe80::1cf1:35ff:fe8c:db87/64")
+    private val LOCAL_IPV6_LINK_ADDRESS = LinkAddress(
+        LOCAL_IPV6_IP_ADDRESS_PREFIX.getAddress(),
+        LOCAL_IPV6_IP_ADDRESS_PREFIX.getPrefixLength()
+    )
+
+    @Test
+    fun testDisconnectingNetwork_InterfaceRemoved() {
+        val nr = nr(NetworkCapabilities.TRANSPORT_WIFI)
+        val cb = TestableNetworkCallback()
+        val interfaceTrackerInorder = inOrder(interfaceTracker)
+        cm.requestNetwork(nr, cb)
+
+        // Connecting to network with IPv6 local address in LinkProperties
+        val wifiLp = lp(WIFI_IFNAME, LOCAL_IPV6_LINK_ADDRESS)
+        val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+        wifiAgent.connect()
+        cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+        interfaceTrackerInorder.verify(interfaceTracker).addInterface(WIFI_IFNAME)
+
+        wifiAgent.disconnect()
+        cb.expect<Lost>(timeoutMs = 500) { it.network == wifiAgent.network }
+        // onLost is fired before the network is destroyed.
+        waitForIdle()
+
+        interfaceTrackerInorder.verify(interfaceTracker).removeInterface(WIFI_IFNAME)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 0fe61ec..5c55483 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -288,6 +288,8 @@
             netd: INetd,
             interfaceTracker: InterfaceTracker
         ) = this@CSTest.bpfNetMaps
+
+        override fun getInterfaceTracker(context: Context?) = this@CSTest.interfaceTracker
         override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
         override fun getNetworkStack() = this@CSTest.networkStack
 
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
index 5be8f49..6165afa 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
@@ -43,6 +43,8 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ThreadNetworkManagerTest {
+    private static final String THREAD_NETWORK_FEATURE = "android.hardware.thread_network";
+
     @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -64,7 +66,7 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void getManager_hasThreadFeatureOnVOrHigher_returnsNonNull() {
-        assumeTrue(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+        assumeTrue(mPackageManager.hasSystemFeature(THREAD_NETWORK_FEATURE));
 
         assertThat(mManager).isNotNull();
     }
@@ -81,8 +83,9 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public void getManager_onUAndTv_returnsNonNull() {
+    public void getManager_onUAndTvWithThreadFeature_returnsNonNull() {
         assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+        assumeTrue(mPackageManager.hasSystemFeature(THREAD_NETWORK_FEATURE));
 
         assertThat(mManager).isNotNull();
     }