Merge "Remove apex_available from libremoteauth_jni_rust_defaults" into main
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 4d173a5..091849b 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -58,6 +58,7 @@
         ":framework-connectivity-shared-srcs",
         ":services-tethering-shared-srcs",
         ":statslog-connectivity-java-gen",
+        ":statslog-framework-connectivity-java-gen",
         ":statslog-tethering-java-gen",
     ],
     static_libs: [
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index a942166..900b505 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -62,6 +62,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
 import com.android.net.module.util.SharedLog;
 
 import java.io.PrintWriter;
@@ -154,14 +155,27 @@
 
             // Only launch entitlement UI for the current user if it is allowed to
             // change tethering. This usually means the system user or the admin users in HSUM.
-            // TODO (b/382624069): Figure out whether it is safe to call createContextAsUser
-            //  from secondary user. And re-enable the check or remove the code accordingly.
-            if (false) {
+            if (SdkLevel.isAtLeastT()) {
                 // Create a user context for the current foreground user as UserManager#isAdmin()
                 // operates on the context user.
                 final int currentUserId = getCurrentUser();
                 final UserHandle currentUser = UserHandle.of(currentUserId);
-                final Context userContext = mContext.createContextAsUser(currentUser, 0);
+                final Context userContext;
+                try {
+                    // There is no safe way to invoke this method since tethering package
+                    // might not be installed for a certain user on the OEM devices,
+                    // refer to b/382628161.
+                    userContext = mContext.createContextAsUser(currentUser, 0);
+                } catch (IllegalStateException e) {
+                    FrameworkConnectivityStatsLog.write(
+                            FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                            FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ENTITLEMENT_CREATE_CONTEXT_AS_USER_THROWS
+                    );
+                    // Fallback to startActivity if createContextAsUser failed.
+                    mLog.e("createContextAsUser failed, fallback to startActivity", e);
+                    mContext.startActivity(intent);
+                    return intent;
+                }
                 final UserManager userManager = userContext.getSystemService(UserManager.class);
 
                 if (userManager.isAdminUser()) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 16ebbbb..58e1894 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,6 +38,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -49,6 +50,7 @@
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -159,10 +161,20 @@
             return super.getSystemServiceName(serviceClass);
         }
 
+        @NonNull
         @Override
         public Context createContextAsUser(UserHandle user, int flags) {
+            if (mCreateContextAsUserException != null) {
+                throw mCreateContextAsUserException;
+            }
             return mMockContext; // Return self for easier test injection.
         }
+
+        private RuntimeException mCreateContextAsUserException = null;
+
+        private void setCreateContextAsUserException(RuntimeException e) {
+            mCreateContextAsUserException = e;
+        }
     }
 
     class TestDependencies extends EntitlementManager.Dependencies {
@@ -591,8 +603,24 @@
                 .onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
     }
 
+    @IgnoreUpTo(SC_V2)
     @Test
-    public void testUiProvisioningMultiUser() {
+    public void testUiProvisioningMultiUser_aboveT_createContextAsUserThrows() {
+        mMockContext.setCreateContextAsUserException(new IllegalStateException());
+        doTestUiProvisioningMultiUser(true, 1);
+        doTestUiProvisioningMultiUser(false, 1);
+    }
+
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testUiProvisioningMultiUser_aboveT() {
+        doTestUiProvisioningMultiUser(true, 1);
+        doTestUiProvisioningMultiUser(false, 0);
+    }
+
+    @IgnoreAfter(SC_V2)
+    @Test
+    public void testUiProvisioningMultiUser_belowT() {
         doTestUiProvisioningMultiUser(true, 1);
         doTestUiProvisioningMultiUser(false, 1);
     }
@@ -630,6 +658,7 @@
         doReturn(isAdminUser).when(mUserManager).isAdminUser();
 
         mDeps.reset();
+        clearInvocations(mTetherProvisioningFailedListener);
         mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
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 1fbb3f3..fb42c03 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -199,7 +199,6 @@
         }
 
         LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
-        // TODO(b/391327942): parse file and log the timestamp of the log list
 
         if (!updateStatus.isSignatureVerified()) {
             Log.w(TAG, "Log list did not pass verification");
@@ -209,42 +208,30 @@
             return;
         }
 
-        boolean success = false;
-
         try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
-            success = compatVersion.install(inputStream);
+            updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
         } catch (IOException e) {
             Log.e(TAG, "Could not install new content", e);
             return;
         }
 
-        if (success) {
-            // Reset the number of consecutive log list failure updates back to zero.
-            mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
-            mDataStore.store();
-        } else {
-            mLogger.logCTLogListUpdateStateChangedEvent(
-                    updateStatus
-                            .toBuilder()
-                            .setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS)
-                            .build());
-            }
-        }
+        mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+    }
 
     private void handleDownloadFailed(DownloadStatus status) {
         Log.e(TAG, "Download failed with " + status);
 
-        LogListUpdateStatus.Builder updateStatus = LogListUpdateStatus.builder();
+        LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
         if (status.isHttpError()) {
-            updateStatus
+            updateStatusBuilder
                     .setState(CTLogListUpdateState.HTTP_ERROR)
                     .setHttpErrorStatusCode(status.reason());
         } else {
             // TODO(b/384935059): handle blocked domain logging
-            updateStatus.setDownloadStatus(Optional.of(status.reason()));
+            updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
         }
 
-        mLogger.logCTLogListUpdateStateChangedEvent(updateStatus.build());
+        mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
     }
 
     private long download(String url) {
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 967a04b..2a37d8f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -35,10 +35,12 @@
     enum CTLogListUpdateState {
         UNKNOWN_STATE,
         HTTP_ERROR,
+        LOG_LIST_INVALID,
         PUBLIC_KEY_NOT_FOUND,
         SIGNATURE_INVALID,
         SIGNATURE_NOT_FOUND,
         SIGNATURE_VERIFICATION_FAILED,
+        SUCCESS,
         VERSION_ALREADY_EXISTS
     }
 }
\ No newline at end of file
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 9c3210d..f617523 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -20,6 +20,7 @@
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
 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_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
@@ -29,6 +30,7 @@
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
 import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
 
 import android.app.DownloadManager;
 
@@ -43,6 +45,12 @@
 
     @Override
     public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+        if (updateStatus.isSuccessful()) {
+            resetFailureCount();
+        } else {
+            updateFailureCount();
+        }
+
         int updateState =
                 updateStatus
                         .downloadStatus()
@@ -56,20 +64,31 @@
                 updateState,
                 failureCount,
                 updateStatus.httpErrorStatusCode(),
-                updateStatus.signature());
+                updateStatus.signature(),
+                updateStatus.logListTimestamp());
     }
 
     private void logCTLogListUpdateStateChangedEvent(
-            int updateState, int failureCount, int httpErrorStatusCode, String signature) {
-        updateFailureCount();
-
+            int updateState,
+            int failureCount,
+            int httpErrorStatusCode,
+            String signature,
+            long logListTimestamp) {
         CertificateTransparencyStatsLog.write(
                 CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
                 updateState,
                 failureCount,
                 httpErrorStatusCode,
                 signature,
-                /* logListTimestampMs= */ 0);
+                logListTimestamp);
+    }
+
+    /**
+     * Resets the number of consecutive log list update failures in the data store back to zero.
+     */
+    private void resetFailureCount() {
+        mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
+        mDataStore.store();
     }
 
     /**
@@ -112,6 +131,8 @@
         switch (updateState) {
             case HTTP_ERROR:
                 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_NOT_FOUND:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
             case SIGNATURE_INVALID:
@@ -120,6 +141,8 @@
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
             case SIGNATURE_VERIFICATION_FAILED:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+            case SUCCESS:
+                return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
             case VERSION_ALREADY_EXISTS:
                 return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
             case UNKNOWN_STATE:
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 9d60163..e8a6e64 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -23,6 +23,8 @@
 import android.system.Os;
 import android.util.Log;
 
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -69,22 +71,29 @@
      * Installs a log list within this compatibility version directory.
      *
      * @param newContent an input stream providing the log list
+     * @param statusBuilder status obj builder containing details of the log list update process
      * @return true if the log list was installed successfully, false otherwise.
      * @throws IOException if the list cannot be saved in the CT directory.
      */
-    boolean install(InputStream newContent) throws IOException {
+    LogListUpdateStatus install(
+            InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
         String content = new String(newContent.readAllBytes(), UTF_8);
         try {
+            JSONObject contentJson = new JSONObject(content);
             return install(
                     new ByteArrayInputStream(content.getBytes()),
-                    new JSONObject(content).getString("version"));
+                    contentJson.getString("version"),
+                    statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
         } catch (JSONException e) {
             Log.e(TAG, "invalid log list format", e);
-            return false;
+
+            return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
         }
     }
 
-    private boolean install(InputStream newContent, String version) throws IOException {
+    LogListUpdateStatus install(
+            InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
+            throws IOException {
         // To support atomically replacing the old configuration directory with the new
         // there's a bunch of steps. We create a new directory with the logs and then do
         // an atomic update of the current symlink to point to the new directory.
@@ -100,7 +109,7 @@
             if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
                 Log.i(TAG, newLogsDir + " already exists, skipping install.");
                 deleteOldLogDirectories();
-                return false;
+                return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
             }
             // If the symlink has not been updated then the previous installation failed and
             // this is a re-attempt. Clean-up leftover files and try again.
@@ -134,7 +143,7 @@
         // 7. Cleanup
         Log.i(TAG, "New logs installed at " + newLogsDir);
         deleteOldLogDirectories();
-        return true;
+        return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
     }
 
     String getCompatVersion() {
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 3d05857..3f9b762 100644
--- a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -19,6 +19,7 @@
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
 import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
 
 import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
 
@@ -52,6 +53,14 @@
         return signature() != null && signature().length() > 0;
     }
 
+    boolean isSuccessful() {
+        return state() == SUCCESS;
+    }
+
+    static LogListUpdateStatus getDefaultInstance() {
+        return builder().build();
+    }
+
     @AutoValue.Builder
     abstract static class Builder {
         abstract Builder setState(CTLogListUpdateState updateState);
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 3ba56db..6040ef6 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -32,7 +32,6 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.KeyFactory;
@@ -105,9 +104,9 @@
             verifier.update(fileStream.readAllBytes());
 
             byte[] signatureBytes = signatureStream.readAllBytes();
+            statusBuilder.setSignature(new String(signatureBytes));
             try {
                 byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
-                statusBuilder.setSignature(new String(decodedSigBytes, StandardCharsets.UTF_8));
 
                 if (!verifier.verify(decodedSigBytes)) {
                     // Leave the UpdateState as UNKNOWN_STATE if successful as there are other
@@ -116,7 +115,6 @@
                 }
             } catch (IllegalArgumentException e) {
                 Log.w(TAG, "Invalid signature base64 encoding", e);
-                statusBuilder.setSignature(new String(signatureBytes, StandardCharsets.UTF_8));
                 statusBuilder.setState(SIGNATURE_INVALID);
                 return statusBuilder.build();
             }
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 08704d1..2af0122 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
@@ -24,8 +24,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import android.app.DownloadManager;
 import android.app.DownloadManager.Query;
 import android.app.DownloadManager.Request;
@@ -56,7 +54,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -84,6 +81,7 @@
     private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
 
     private long mNextDownloadId = 666;
+    private static final long LOG_LIST_TIMESTAMP = 123456789L;
 
     @Before
     public void setUp() throws IOException, GeneralSecurityException {
@@ -398,14 +396,12 @@
                 mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
         // Assert
-        byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
         verify(mLogger, times(1))
                 .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
         LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
         assertThat(statusValue.state())
                 .isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
-        assertThat(statusValue.signature())
-                .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
+        assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
     }
 
     @Test
@@ -422,13 +418,11 @@
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
 
-        byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
         verify(mLogger, times(1))
                 .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
         LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
-        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.VERSION_ALREADY_EXISTS);
-        assertThat(statusValue.signature())
-                .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
+        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
+        assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
     }
 
     @Test
@@ -466,7 +460,8 @@
     }
 
     @Test
-    public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+    public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
+        // Arrange
         String newVersion = "456";
         File logListFile = makeLogListFile(newVersion);
         File metadataFile = sign(logListFile);
@@ -474,6 +469,7 @@
 
         assertNoVersionIsInstalled();
 
+        // Act
         // 1. Start download of public key.
         mCertificateTransparencyDownloader.startPublicKeyDownload();
 
@@ -491,7 +487,15 @@
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
 
+        // Assert
         assertInstallSuccessful(newVersion);
+        verify(mLogger, times(1))
+                .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+
+        LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+        assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
+        assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+        assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
     }
 
     private void assertNoVersionIsInstalled() {
@@ -600,7 +604,11 @@
         File logListFile = File.createTempFile("log_list", "json");
 
         try (OutputStream outputStream = new FileOutputStream(logListFile)) {
-            outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+            JSONObject contentJson =
+                    new JSONObject()
+                            .put("version", version)
+                            .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+            outputStream.write(contentJson.toString().getBytes());
         }
 
         return logListFile;
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
index 38fff48..2b8b3cd 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -15,6 +15,11 @@
  */
 package com.android.server.net.ct;
 
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -37,6 +42,8 @@
 public class CompatibilityVersionTest {
 
     private static final String TEST_VERSION = "v123";
+    private static final long LOG_LIST_TIMESTAMP = 123456789L;
+    private static final String SIGNATURE = "fake_signature";
 
     private final File mTestDir =
             InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
@@ -52,6 +59,7 @@
     @Test
     public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
         File versionDir = mCompatVersion.getVersionDir();
+
         assertThat(versionDir.exists()).isFalse();
         assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
         assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
@@ -60,6 +68,7 @@
     @Test
     public void testCompatibilityVersion_symlink_setupSuccessful() {
         File dirSymlink = mCompatVersion.getLogsDirSymlink();
+
         assertThat(dirSymlink.exists()).isFalse();
         assertThat(dirSymlink.getAbsolutePath())
                 .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
@@ -68,18 +77,44 @@
     @Test
     public void testCompatibilityVersion_logsFile_setupSuccessful() {
         File logsFile = mCompatVersion.getLogsFile();
+
         assertThat(logsFile.exists()).isFalse();
         assertThat(logsFile.getAbsolutePath())
                 .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
     }
 
     @Test
+    public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
+        String version = "i_am_version";
+        JSONObject logList = makeLogList(version, "i_am_content");
+
+        try (InputStream inputStream = asStream(logList)) {
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream,
+                                    LogListUpdateStatus.builder()
+                                            .setSignature(SIGNATURE)
+                                            .setState(UNKNOWN_STATE)))
+                    .isEqualTo(
+                            LogListUpdateStatus.builder()
+                                    .setSignature(SIGNATURE)
+                                    .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                                    // Ensure the state is correctly overridden to SUCCESS
+                                    .setState(SUCCESS)
+                                    .build());
+        }
+    }
+
+    @Test
     public void testCompatibilityVersion_installSuccessful() throws Exception {
         String version = "i_am_version";
         JSONObject logList = makeLogList(version, "i_am_content");
 
         try (InputStream inputStream = asStream(logList)) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         File logListFile = mCompatVersion.getLogsFile();
@@ -107,7 +142,10 @@
     @Test
     public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
         try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         mCompatVersion.delete();
@@ -118,7 +156,10 @@
     @Test
     public void testCompatibilityVersion_invalidLogList() throws Exception {
         try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
-            assertThat(mCompatVersion.install(inputStream)).isFalse();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
         }
 
         assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
@@ -138,7 +179,10 @@
 
         JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
         try (InputStream inputStream = asStream(newLogList)) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
@@ -149,11 +193,21 @@
         String existingVersion = "666";
         JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
         try (InputStream inputStream = asStream(existingLogList)) {
-            assertThat(mCompatVersion.install(inputStream)).isTrue();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
-            assertThat(mCompatVersion.install(inputStream)).isFalse();
+            assertThat(
+                            mCompatVersion.install(
+                                    inputStream, LogListUpdateStatus.builder()))
+                    .isEqualTo(
+                            LogListUpdateStatus.builder()
+                                    .setState(VERSION_ALREADY_EXISTS)
+                                    .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                                    .build());
         }
 
         assertThat(readAsString(mCompatVersion.getLogsFile()))
@@ -165,13 +219,22 @@
     }
 
     private static JSONObject makeLogList(String version) throws JSONException {
-        return new JSONObject().put("version", version);
+        return new JSONObject()
+                .put("version", version)
+                .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
     }
 
     private static JSONObject makeLogList(String version, String content) throws JSONException {
         return makeLogList(version).put("content", content);
     }
 
+    private static LogListUpdateStatus getSuccessfulUpdateStatus() {
+        return LogListUpdateStatus.builder()
+                .setState(SUCCESS)
+                .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+                .build();
+    }
+
     private static String readAsString(File file) throws IOException {
         try (InputStream in = new FileInputStream(file)) {
             return new String(in.readAllBytes());
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index c5ec9ee..7440c0a 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -55,6 +55,20 @@
 
 public class L2capNetworkProvider {
     private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+    private static final NetworkCapabilities COMMON_CAPABILITIES =
+            // TODO: add NET_CAPABILITY_NOT_RESTRICTED and check that getRequestorUid() has
+            // BLUETOOTH_CONNECT permission.
+            NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                    .addTransportType(TRANSPORT_BLUETOOTH)
+                    // TODO: remove NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
+                    .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                    .addCapability(NET_CAPABILITY_NOT_METERED)
+                    .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                    .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                    .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                    .addCapability(NET_CAPABILITY_NOT_VPN)
+                    .build();
     private final Dependencies mDeps;
     private final Context mContext;
     private final HandlerThread mHandlerThread;
@@ -78,21 +92,15 @@
         // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
         public static final NetworkCapabilities CAPABILITIES;
         static {
+            // Below capabilities will match any reservation request with an L2capNetworkSpecifier
+            // that specifies ROLE_SERVER or without a NetworkSpecifier.
             final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
                     .setRole(ROLE_SERVER)
                     .build();
-            NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
-                    .addTransportType(TRANSPORT_BLUETOOTH)
-                    // TODO: consider removing NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
-                    .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
-                    .addCapability(NET_CAPABILITY_NOT_CONGESTED)
-                    .addCapability(NET_CAPABILITY_NOT_METERED)
-                    .addCapability(NET_CAPABILITY_NOT_ROAMING)
-                    .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
-                    .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
-                    .addCapability(NET_CAPABILITY_NOT_VPN)
+            NetworkCapabilities caps = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
                     .setNetworkSpecifier(l2capNetworkSpecifier)
                     .build();
+            // TODO: add #setReservationId() to NetworkCapabilities.Builder
             caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
             CAPABILITIES = caps;
         }
@@ -234,11 +242,13 @@
      * Called on CS Handler thread.
      */
     public void start() {
-        final PackageManager pm = mContext.getPackageManager();
-        if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
-            mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
-            mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
-                    BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
-        }
+        mHandler.post(() -> {
+            final PackageManager pm = mContext.getPackageManager();
+            if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+                mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
+                mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
+                        BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+            }
+        });
     }
 }
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
new file mode 100644
index 0000000..b9d5f13
--- /dev/null
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2025 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.net;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+public class L2capNetwork {
+    private static final NetworkScore NETWORK_SCORE = new NetworkScore.Builder().build();
+    private final String mLogTag;
+    private final Handler mHandler;
+    private final String mIfname;
+    private final L2capPacketForwarder mForwarder;
+    private final NetworkCapabilities mNetworkCapabilities;
+    private final L2capIpClient mIpClient;
+    private final NetworkAgent mNetworkAgent;
+
+    /** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
+     *
+     * Note that the IpClient does not need to be stopped.
+     */
+    private static class L2capIpClient extends IpClientCallbacks {
+        private final String mLogTag;
+        private final ConditionVariable mOnIpClientCreatedCv = new ConditionVariable(false);
+        private final ConditionVariable mOnProvisioningSuccessCv = new ConditionVariable(false);
+        @Nullable
+        private IpClientManager mIpClient;
+        @Nullable
+        private LinkProperties mLinkProperties;
+
+        L2capIpClient(String logTag, Context context, String ifname) {
+            mLogTag = logTag;
+            IpClientUtil.makeIpClient(context, ifname, this);
+        }
+
+        @Override
+        public void onIpClientCreated(IIpClient ipClient) {
+            mIpClient = new IpClientManager(ipClient, mLogTag);
+            mOnIpClientCreatedCv.open();
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties lp) {
+            Log.d(mLogTag, "Successfully provisionined l2cap tun: " + lp);
+            mLinkProperties = lp;
+            mOnProvisioningSuccessCv.open();
+        }
+
+        public LinkProperties start() {
+            mOnIpClientCreatedCv.block();
+            // mIpClient guaranteed non-null.
+            final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                    .withoutIPv4()
+                    .withIpv6LinkLocalOnly()
+                    .withRandomMacAddress() // addr_gen_mode EUI64 -> random on tun.
+                    .build();
+            mIpClient.startProvisioning(config);
+            // "Provisioning" is guaranteed to succeed as link-local only mode does not actually
+            // require any provisioning.
+            mOnProvisioningSuccessCv.block();
+            return mLinkProperties;
+        }
+    }
+
+    public interface ICallback {
+        /** Called when an error is encountered */
+        void onError(L2capNetwork network);
+        /** Called when CS triggers NetworkAgent#onNetworkUnwanted */
+        void onNetworkUnwanted(L2capNetwork network);
+    }
+
+    public L2capNetwork(Handler handler, Context context, NetworkProvider provider, String ifname,
+            BluetoothSocket socket, ParcelFileDescriptor tunFd,
+            NetworkCapabilities networkCapabilities, ICallback cb) {
+        // TODO: add a check that this constructor is invoked on the handler thread.
+        mLogTag = String.format("L2capNetwork[%s]", ifname);
+        mHandler = handler;
+        mIfname = ifname;
+        mForwarder = new L2capPacketForwarder(handler, tunFd, socket, () -> {
+            // TODO: add a check that this callback is invoked on the handler thread.
+            cb.onError(L2capNetwork.this);
+        });
+        mNetworkCapabilities = networkCapabilities;
+        mIpClient = new L2capIpClient(mLogTag, context, ifname);
+        final LinkProperties linkProperties = mIpClient.start();
+
+        final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
+        mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
+                networkCapabilities, linkProperties, NETWORK_SCORE, config, provider) {
+            @Override
+            public void onNetworkUnwanted() {
+                Log.i(mLogTag, mIfname + ": Network is unwanted");
+                // TODO: add a check that this callback is invoked on the handler thread.
+                cb.onNetworkUnwanted(L2capNetwork.this);
+            }
+        };
+        mNetworkAgent.register();
+        mNetworkAgent.markConnected();
+    }
+
+    /** Get the NetworkCapabilities used for this Network */
+    public NetworkCapabilities getNetworkCapabilities() {
+        return mNetworkCapabilities;
+    }
+
+    /** Tear down the network and associated resources */
+    public void tearDown() {
+        mNetworkAgent.unregister();
+        mForwarder.tearDown();
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 4878334..026e985 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -153,7 +153,8 @@
             (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
     public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
             (Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
-
+    public static final Inet6Address IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST =
+             (Inet6Address) InetAddresses.parseNumericAddress("ff01::1");
     public static final int IPPROTO_FRAGMENT = 44;
 
     /**
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ac60b0f..a1cf968 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cts.net.hostside;
 
+import static android.net.TetheringManager.TETHERING_WIFI;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static org.junit.Assert.assertEquals;
@@ -71,6 +73,9 @@
                 mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
         assertNotNull(tetheringInterface);
         assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
+        assertEquals(new TetheringInterface(
+                TETHERING_WIFI, tetheringInterface.getInterface(), softApConfig),
+                tetheringInterface);
         TetheringInterface tetheringInterfaceForApp2 =
                 mTetheringHelperClient.getTetheredWifiInterface();
         assertNotNull(tetheringInterfaceForApp2);
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 00c87a3..aa7d618 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -4087,4 +4087,11 @@
         // shims, and @IgnoreUpTo does not check that.
         assumeTrue(TestUtils.shouldTestSApis());
     }
+
+    @Test
+    public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+        assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
+        assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
+    }
 }
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b415382..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -44,7 +44,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -66,7 +65,6 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.net.ConnectivityManager.NetworkCallback;
-import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.Handler;
@@ -671,12 +669,4 @@
                 // No callbacks overridden -> do not use the optimization
                 eq(~0));
     }
-
-    @Test
-    public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
-        assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
-        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
-        assertThrows(UnsupportedOperationException.class, () -> manager.tether("iface"));
-        assertThrows(UnsupportedOperationException.class, () -> manager.untether("iface"));
-    }
 }
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
index 5a7515e..2fd3d4f 100644
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
@@ -35,6 +35,7 @@
 import android.os.HandlerThread
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.waitForIdle
 import kotlin.test.assertTrue
 import org.junit.After
 import org.junit.Before
@@ -52,13 +53,14 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-const val TAG = "L2capNetworkProviderTest"
+private const val TAG = "L2capNetworkProviderTest"
+private const val TIMEOUT_MS = 1000
 
-val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+private val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
     .addTransportType(TRANSPORT_BLUETOOTH)
     .build()
 
-val RESERVATION = NetworkRequest(
+private val RESERVATION = NetworkRequest(
         NetworkCapabilities(RESERVATION_CAPS),
         TYPE_NONE,
         42 /* rId */,
@@ -96,6 +98,7 @@
     @Test
     fun testNetworkProvider_registeredWhenSupported() {
         L2capNetworkProvider(deps, context).start()
+        handlerThread.waitForIdle(TIMEOUT_MS)
         verify(cm).registerNetworkProvider(eq(provider))
         verify(provider).registerNetworkOffer(any(), any(), any(), any())
     }
@@ -104,12 +107,14 @@
     fun testNetworkProvider_notRegisteredWhenNotSupported() {
         doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
         L2capNetworkProvider(deps, context).start()
+        handlerThread.waitForIdle(TIMEOUT_MS)
         verify(cm, never()).registerNetworkProvider(eq(provider))
     }
 
     fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
         clearInvocations(provider)
         L2capNetworkProvider(deps, context).start()
+        handlerThread.waitForIdle(TIMEOUT_MS)
 
         val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
         verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
@@ -124,6 +129,7 @@
     ) {
         clearInvocations(provider)
         L2capNetworkProvider(deps, context).start()
+        handlerThread.waitForIdle(TIMEOUT_MS)
 
         val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
         verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())