Merge "Drop circular dependency" into main
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 46c435f..8552eec 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -275,7 +275,7 @@
     }
 
     @VisibleForTesting
-    static class MdnsListener implements MdnsServiceBrowserListener {
+    abstract static class MdnsListener implements MdnsServiceBrowserListener {
         protected final int mClientRequestId;
         protected final int mTransactionId;
         @NonNull
@@ -321,6 +321,10 @@
 
         @Override
         public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) { }
+
+        // Ensure toString gets overridden
+        @NonNull
+        public abstract String toString();
     }
 
     private class DiscoveryListener extends MdnsListener {
@@ -351,13 +355,21 @@
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("DiscoveryListener: serviceType=%s", getListenedServiceType());
+        }
     }
 
     private class ResolutionListener extends MdnsListener {
+        private final String mServiceName;
 
         ResolutionListener(int clientRequestId, int transactionId,
-                @NonNull String listenServiceType) {
+                @NonNull String listenServiceType, @NonNull String serviceName) {
             super(clientRequestId, transactionId, listenServiceType);
+            mServiceName = serviceName;
         }
 
         @Override
@@ -373,13 +385,22 @@
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("ResolutionListener serviceName=%s, serviceType=%s",
+                    mServiceName, getListenedServiceType());
+        }
     }
 
     private class ServiceInfoListener extends MdnsListener {
+        private final String mServiceName;
 
         ServiceInfoListener(int clientRequestId, int transactionId,
-                @NonNull String listenServiceType) {
+                @NonNull String listenServiceType, @NonNull String serviceName) {
             super(clientRequestId, transactionId, listenServiceType);
+            this.mServiceName = serviceName;
         }
 
         @Override
@@ -410,6 +431,13 @@
             mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
                     DISCOVERY_QUERY_SENT_CALLBACK, new MdnsEvent(mClientRequestId));
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return String.format("ServiceInfoListener serviceName=%s, serviceType=%s",
+                    mServiceName, getListenedServiceType());
+        }
     }
 
     private class SocketRequestMonitor implements MdnsSocketProvider.SocketRequestMonitor {
@@ -656,9 +684,12 @@
             }
 
             private void storeAdvertiserRequestMap(int clientRequestId, int transactionId,
-                    ClientInfo clientInfo, @Nullable Network requestedNetwork) {
+                    ClientInfo clientInfo, @NonNull NsdServiceInfo serviceInfo) {
+                final String serviceFullName =
+                        serviceInfo.getServiceName() + "." + serviceInfo.getServiceType();
                 clientInfo.mClientRequests.put(clientRequestId, new AdvertiserClientRequest(
-                        transactionId, requestedNetwork, mClock.elapsedRealtime()));
+                        transactionId, serviceInfo.getNetwork(), serviceFullName,
+                        mClock.elapsedRealtime()));
                 mTransactionIdToClientInfoMap.put(transactionId, clientInfo);
                 updateMulticastLock();
             }
@@ -1010,7 +1041,7 @@
                             mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
                                     mdnsAdvertisingOptions, clientInfo.mUid);
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
-                                    serviceInfo.getNetwork());
+                                    serviceInfo);
                         } else {
                             maybeStartDaemon();
                             transactionId = getUniqueId();
@@ -1104,7 +1135,7 @@
 
                             maybeStartMonitoringSockets();
                             final MdnsListener listener = new ResolutionListener(clientRequestId,
-                                    transactionId, resolveServiceType);
+                                    transactionId, resolveServiceType, info.getServiceName());
                             final int ifaceIdx = info.getNetwork() != null
                                     ? 0 : info.getInterfaceIndex();
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
@@ -1207,7 +1238,7 @@
 
                         maybeStartMonitoringSockets();
                         final MdnsListener listener = new ServiceInfoListener(clientRequestId,
-                                transactionId, resolveServiceType);
+                                transactionId, resolveServiceType, info.getServiceName());
                         final int ifIndex = info.getNetwork() != null
                                 ? 0 : info.getInterfaceIndex();
                         final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
@@ -2551,6 +2582,17 @@
         // Dump state machine logs
         mNsdStateMachine.dump(fd, pw, args);
 
+        // Dump clients
+        pw.println();
+        pw.println("Active clients:");
+        pw.increaseIndent();
+        HandlerUtils.runWithScissorsForDump(mNsdStateMachine.getHandler(), () -> {
+            for (ClientInfo clientInfo : mClients.values()) {
+                pw.println(clientInfo.toString());
+            }
+        }, 10_000);
+        pw.decreaseIndent();
+
         // Dump service and clients logs
         pw.println();
         pw.println("Logs:");
@@ -2623,6 +2665,21 @@
         public int getSentQueryCount() {
             return mSentQueryCount;
         }
+
+        @NonNull
+        @Override
+        public String toString() {
+            return getRequestDescriptor() + " {" + mTransactionId
+                    + ", startTime " + mStartTimeMs
+                    + ", foundServices " + mFoundServiceCount
+                    + ", lostServices " + mLostServiceCount
+                    + ", fromCache " + mIsServiceFromCache
+                    + ", sentQueries " + mSentQueryCount
+                    + "}";
+        }
+
+        @NonNull
+        protected abstract String getRequestDescriptor();
     }
 
     private static class LegacyClientRequest extends ClientRequest {
@@ -2632,6 +2689,12 @@
             super(transactionId, startTimeMs);
             mRequestCode = requestCode;
         }
+
+        @NonNull
+        @Override
+        protected String getRequestDescriptor() {
+            return "Legacy (" + mRequestCode + ")";
+        }
     }
 
     private abstract static class JavaBackendClientRequest extends ClientRequest {
@@ -2651,9 +2714,20 @@
     }
 
     private static class AdvertiserClientRequest extends JavaBackendClientRequest {
+        @NonNull
+        private final String mServiceFullName;
+
         private AdvertiserClientRequest(int transactionId, @Nullable Network requestedNetwork,
-                long startTimeMs) {
+                @NonNull String serviceFullName, long startTimeMs) {
             super(transactionId, requestedNetwork, startTimeMs);
+            mServiceFullName = serviceFullName;
+        }
+
+        @NonNull
+        @Override
+        public String getRequestDescriptor() {
+            return String.format("Advertiser: serviceFullName=%s, net=%s",
+                    mServiceFullName, getRequestedNetwork());
         }
     }
 
@@ -2666,6 +2740,12 @@
             super(transactionId, requestedNetwork, startTimeMs);
             mListener = listener;
         }
+
+        @NonNull
+        @Override
+        public String getRequestDescriptor() {
+            return String.format("Discovery/%s, net=%s", mListener, getRequestedNetwork());
+        }
     }
 
     /* Information tracked per client */
@@ -2710,17 +2790,15 @@
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder();
-            sb.append("mResolvedService ").append(mResolvedService).append("\n");
-            sb.append("mIsLegacy ").append(mIsPreSClient).append("\n");
-            sb.append("mUseJavaBackend ").append(mUseJavaBackend).append("\n");
-            sb.append("mUid ").append(mUid).append("\n");
+            sb.append("mUid ").append(mUid).append(", ");
+            sb.append("mResolvedService ").append(mResolvedService).append(", ");
+            sb.append("mIsLegacy ").append(mIsPreSClient).append(", ");
+            sb.append("mUseJavaBackend ").append(mUseJavaBackend).append(", ");
+            sb.append("mClientRequests:\n");
             for (int i = 0; i < mClientRequests.size(); i++) {
                 int clientRequestId = mClientRequests.keyAt(i);
-                sb.append("clientRequestId ")
-                        .append(clientRequestId)
-                        .append(" transactionId ").append(mClientRequests.valueAt(i).mTransactionId)
-                        .append(" type ").append(
-                                mClientRequests.valueAt(i).getClass().getSimpleName())
+                sb.append("  ").append(clientRequestId)
+                        .append(": ").append(mClientRequests.valueAt(i).toString())
                         .append("\n");
             }
             return sb.toString();
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 9c8fd99..71f289e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -131,6 +131,10 @@
     // returned when a tethered interface is requested; until then, it remains in client mode. Its
     // current mode is reflected in mTetheringInterfaceMode.
     private String mTetheringInterface;
+    // If the tethering interface is in server mode, it is not tracked by factory. The HW address
+    // must be maintained by the EthernetTracker. Its current mode is reflected in
+    // mTetheringInterfaceMode.
+    private String mTetheringInterfaceHwAddr;
     private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
     // Tracks whether clients were notified that the tethered interface is available
     private boolean mTetheredInterfaceWasAvailable = false;
@@ -582,6 +586,7 @@
         removeInterface(iface);
         if (iface.equals(mTetheringInterface)) {
             mTetheringInterface = null;
+            mTetheringInterfaceHwAddr = null;
         }
         broadcastInterfaceStateChange(iface);
     }
@@ -610,13 +615,14 @@
             return;
         }
 
+        final String hwAddress = config.hwAddr;
+
         if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
             maybeUpdateServerModeInterfaceState(iface, true);
+            mTetheringInterfaceHwAddr = hwAddress;
             return;
         }
 
-        final String hwAddress = config.hwAddr;
-
         NetworkCapabilities nc = mNetworkCapabilities.get(iface);
         if (nc == null) {
             // Try to resolve using mac address
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
index 2e9a99b..586f19d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NduseroptMessage.java
@@ -156,8 +156,9 @@
 
     @Override
     public String toString() {
-        return String.format("Nduseroptmsg(%d, %d, %d, %d, %d, %s)",
+        return String.format("Nduseroptmsg(family:%d, opts_len:%d, ifindex:%d, icmp_type:%d, "
+                + "icmp_code:%d, srcaddr: %s, %s)",
                 family, opts_len, ifindex, Byte.toUnsignedInt(icmp_type),
-                Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress());
+                Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress(), option);
     }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
index 4fc5ec2..d1df3a6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NduseroptMessageTest.java
@@ -252,8 +252,10 @@
     public void testToString() {
         NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
         assertNotNull(msg);
-        assertEquals("Nduseroptmsg(10, 16, 1431655765, 134, 0, fe80:2:3:4:5:6:7:8%1431655765)",
-                msg.toString());
+        final String expected = "Nduseroptmsg(family:10, opts_len:16, ifindex:1431655765, "
+                + "icmp_type:134, icmp_code:0, srcaddr: fe80:2:3:4:5:6:7:8%1431655765, "
+                + "NdOptPref64(2001:db8:3:4:5:6::/96, 10064))";
+        assertEquals(expected, msg.toString());
     }
 
     // Convenience method to parse a NduseroptMessage that's not part of a netlink message.
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 18a5897..33761dc 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -21,9 +21,7 @@
     name: "CtsHostsideNetworkTestsAidl",
     sdk_version: "current",
     srcs: [
-        "com/android/cts/net/hostside/IMyService.aidl",
-        "com/android/cts/net/hostside/INetworkCallback.aidl",
-        "com/android/cts/net/hostside/INetworkStateObserver.aidl",
-        "com/android/cts/net/hostside/IRemoteSocketFactory.aidl",
+        "com/android/cts/net/hostside/*.aidl",
+        "com/android/cts/net/hostside/*.java",
     ],
 }
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index e7b2815..906024b 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -19,11 +19,12 @@
 import android.app.job.JobInfo;
 
 import com.android.cts.net.hostside.INetworkCallback;
+import com.android.cts.net.hostside.NetworkCheckResult;
 
 interface IMyService {
     void registerBroadcastReceiver();
     int getCounters(String receiverName, String action);
-    String checkNetworkStatus();
+    NetworkCheckResult checkNetworkStatus(String customUrl);
     String getRestrictBackgroundStatus();
     void sendNotification(int notificationId, String notificationType);
     void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
index 19198c5..8ef4659 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl
@@ -16,8 +16,12 @@
 
 package com.android.cts.net.hostside;
 
+import android.net.NetworkInfo;
+
+import com.android.cts.net.hostside.NetworkCheckResult;
+
 interface INetworkStateObserver {
-    void onNetworkStateChecked(int resultCode, String resultData);
+    void onNetworkStateChecked(int resultCode, in NetworkCheckResult networkCheckResult);
 
     const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0;
     const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1;
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/NetworkCheckResult.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/NetworkCheckResult.aidl
new file mode 100644
index 0000000..cdd6b70
--- /dev/null
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/NetworkCheckResult.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.cts.net.hostside;
+
+import android.net.NetworkInfo;
+
+@JavaDerive(toString=true)
+parcelable NetworkCheckResult {
+   boolean connected;
+   String details;
+   NetworkInfo networkInfo;
+}
\ No newline at end of file
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 2ca8832..4437986 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -23,6 +23,7 @@
 import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
 import static android.os.BatteryManager.BATTERY_PLUGGED_ANY;
 
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
 import static com.android.cts.net.arguments.InstrumentationArguments.ARG_WAIVE_BIND_PRIORITY;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCommand;
 import static com.android.cts.net.hostside.NetworkPolicyTestUtils.forceRunJob;
@@ -51,6 +52,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
@@ -78,10 +80,10 @@
 import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
-import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Predicate;
 
 /**
@@ -136,6 +138,7 @@
 
     private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
     private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
+    private static final String KEY_CUSTOM_URL = TEST_PKG + ".custom_url";
 
     private static final String EMPTY_STRING = "";
 
@@ -165,6 +168,7 @@
     protected ConnectivityManager mCm;
     protected int mUid;
     private int mMyUid;
+    private @Nullable String mCustomUrl;
     private MyServiceClient mServiceClient;
     private DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper;
     private PowerManager mPowerManager;
@@ -185,6 +189,11 @@
         mServiceClient = new MyServiceClient(mContext);
 
         final Bundle args = InstrumentationRegistry.getArguments();
+        mCustomUrl = args.getString(ARG_CONNECTION_CHECK_CUSTOM_URL);
+        if (mCustomUrl != null) {
+            Log.d(TAG, "Using custom URL " + mCustomUrl + " for network checks");
+        }
+
         final int bindPriorityFlags;
         if (Boolean.valueOf(args.getString(ARG_WAIVE_BIND_PRIORITY, "false"))) {
             bindPriorityFlags = Context.BIND_WAIVE_PRIORITY;
@@ -502,25 +511,23 @@
      */
     private String checkNetworkAccess(boolean expectAvailable,
             @Nullable final String expectedUnavailableError) throws Exception {
-        final String resultData = mServiceClient.checkNetworkStatus();
-        return checkForAvailabilityInResultData(resultData, expectAvailable,
+        final NetworkCheckResult checkResult = mServiceClient.checkNetworkStatus(mCustomUrl);
+        return checkForAvailabilityInNetworkCheckResult(checkResult, expectAvailable,
                 expectedUnavailableError);
     }
 
-    private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
-            @Nullable final String expectedUnavailableError) {
-        if (resultData == null) {
-            assertNotNull("Network status from app2 is null", resultData);
-        }
-        // Network status format is described on MyBroadcastReceiver.checkNetworkStatus()
-        final String[] parts = resultData.split(NETWORK_STATUS_SEPARATOR);
-        assertEquals("Wrong network status: " + resultData, 5, parts.length);
-        final State state = parts[0].equals("null") ? null : State.valueOf(parts[0]);
-        final DetailedState detailedState = parts[1].equals("null")
-                ? null : DetailedState.valueOf(parts[1]);
-        final boolean connected = Boolean.valueOf(parts[2]);
-        final String connectionCheckDetails = parts[3];
-        final String networkInfo = parts[4];
+    private String checkForAvailabilityInNetworkCheckResult(NetworkCheckResult networkCheckResult,
+            boolean expectAvailable, @Nullable final String expectedUnavailableError) {
+        assertNotNull("NetworkCheckResult from app2 is null", networkCheckResult);
+
+        final NetworkInfo networkInfo = networkCheckResult.networkInfo;
+        assertNotNull("NetworkInfo from app2 is null", networkInfo);
+
+        final State state = networkInfo.getState();
+        final DetailedState detailedState = networkInfo.getDetailedState();
+
+        final boolean connected = networkCheckResult.connected;
+        final String connectionCheckDetails = networkCheckResult.details;
 
         final StringBuilder errors = new StringBuilder();
         final State expectedState;
@@ -926,33 +933,36 @@
         if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
             startForegroundService();
             assertForegroundServiceNetworkAccess();
-            return;
         } else if (type == TYPE_COMPONENT_ACTIVTIY) {
             turnScreenOn();
             final CountDownLatch latch = new CountDownLatch(1);
             final Intent launchIntent = getIntentForComponent(type);
             final Bundle extras = new Bundle();
-            final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+            final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
+                    new AtomicReference<>();
             extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
             extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+            extras.putString(KEY_CUSTOM_URL, mCustomUrl);
             launchIntent.putExtras(extras);
             mContext.startActivity(launchIntent);
             if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                final int resultCode = result.get(0).first;
-                final String resultData = result.get(0).second;
+                final int resultCode = result.get().first;
+                final NetworkCheckResult networkCheckResult = result.get().second;
                 if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
-                    final String error = checkForAvailabilityInResultData(
-                            resultData, expectAvailable, null /* expectedUnavailableError */);
+                    final String error = checkForAvailabilityInNetworkCheckResult(
+                            networkCheckResult, expectAvailable,
+                            null /* expectedUnavailableError */);
                     if (error != null) {
                         fail("Network is not available for activity in app2 (" + mUid + "): "
                                 + error);
                     }
                 } else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) {
-                    Log.d(TAG, resultData);
+                    Log.d(TAG, networkCheckResult.details);
                     // App didn't come to foreground when the activity is started, so try again.
                     assertTopNetworkAccess(true);
                 } else {
-                    fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+                    fail("Unexpected resultCode=" + resultCode
+                            + "; networkCheckResult=[" + networkCheckResult + "]");
                 }
             } else {
                 fail("Timed out waiting for network availability status from app2's activity ("
@@ -960,10 +970,12 @@
             }
         } else if (type == TYPE_EXPEDITED_JOB) {
             final Bundle extras = new Bundle();
-            final ArrayList<Pair<Integer, String>> result = new ArrayList<>(1);
+            final AtomicReference<Pair<Integer, NetworkCheckResult>> result =
+                    new AtomicReference<>();
             final CountDownLatch latch = new CountDownLatch(1);
             extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result));
             extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable);
+            extras.putString(KEY_CUSTOM_URL, mCustomUrl);
             final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT)
                     .setExpedited(true)
                     .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
@@ -973,11 +985,12 @@
                     RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
             forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
             if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-                final int resultCode = result.get(0).first;
-                final String resultData = result.get(0).second;
+                final int resultCode = result.get().first;
+                final NetworkCheckResult networkCheckResult = result.get().second;
                 if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
-                    final String error = checkForAvailabilityInResultData(
-                            resultData, expectAvailable, null /* expectedUnavailableError */);
+                    final String error = checkForAvailabilityInNetworkCheckResult(
+                            networkCheckResult, expectAvailable,
+                            null /* expectedUnavailableError */);
                     if (error != null) {
                         Log.d(TAG, "Network state is unexpected, checking again. " + error);
                         // Right now we could end up in an unexpected state if expedited job
@@ -985,7 +998,8 @@
                         assertNetworkAccess(expectAvailable, false /* needScreenOn */);
                     }
                 } else {
-                    fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]");
+                    fail("Unexpected resultCode=" + resultCode
+                            + "; networkCheckResult=[" + networkCheckResult + "]");
                 }
             } else {
                 fail("Timed out waiting for network availability status from app2's expedited job ("
@@ -1028,11 +1042,12 @@
     }
 
     private Binder getNewNetworkStateObserver(final CountDownLatch latch,
-            final ArrayList<Pair<Integer, String>> result) {
+            final AtomicReference<Pair<Integer, NetworkCheckResult>> result) {
         return new INetworkStateObserver.Stub() {
             @Override
-            public void onNetworkStateChecked(int resultCode, String resultData) {
-                result.add(Pair.create(resultCode, resultData));
+            public void onNetworkStateChecked(int resultCode,
+                    NetworkCheckResult networkCheckResult) {
+                result.set(Pair.create(resultCode, networkCheckResult));
                 latch.countDown();
             }
         };
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
index c1d576d..3e22a23 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -69,6 +69,7 @@
     @RequiredProperties({BATTERY_SAVER_MODE})
     public void testStartActivity_batterySaver() throws Exception {
         setBatterySaverMode(true);
+        assertNetworkAccess(false, null);
         assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver", null);
     }
 
@@ -76,6 +77,7 @@
     @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
     public void testStartActivity_dataSaver() throws Exception {
         setRestrictBackground(true);
+        assertNetworkAccess(false, null);
         assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver", null);
     }
 
@@ -83,6 +85,7 @@
     @RequiredProperties({DOZE_MODE})
     public void testStartActivity_doze() throws Exception {
         setDozeMode(true);
+        assertNetworkAccess(false, null);
         // TODO (235284115): We need to turn on Doze every time before starting
         // the activity.
         assertLaunchedActivityHasNetworkAccess("testStartActivity_doze", null);
@@ -93,6 +96,7 @@
     public void testStartActivity_appStandby() throws Exception {
         turnBatteryOn();
         setAppIdle(true);
+        assertNetworkAccess(false, null);
         // TODO (235284115): We need to put the app into app standby mode every
         // time before starting the activity.
         assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby", null);
@@ -104,6 +108,7 @@
         assertLaunchedActivityHasNetworkAccess("testStartActivity_default", () -> {
             assertProcessStateBelow(PROCESS_STATE_TOP_SLEEPING);
             SystemClock.sleep(PROCESS_STATE_TRANSITION_DELAY_MS);
+            assertNetworkAccess(false, null);
         });
     }
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 980ecd5..494192f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -98,9 +98,10 @@
         return mService.getCounters(receiverName, action);
     }
 
-    public String checkNetworkStatus() throws RemoteException {
+    /** Retrieves the network state as observed from the bound test app */
+    public NetworkCheckResult checkNetworkStatus(String address) throws RemoteException {
         ensureServiceConnection();
-        return mService.checkNetworkStatus();
+        return mService.checkNetworkStatus(address);
     }
 
     public String getRestrictBackgroundStatus() throws RemoteException {
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
index 37dc7a0..1c45579 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java
@@ -15,10 +15,16 @@
  */
 package com.android.cts.net.hostside.app2;
 
+import static com.android.cts.net.hostside.INetworkStateObserver.RESULT_ERROR_OTHER;
+import static com.android.cts.net.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES;
+import static com.android.cts.net.hostside.INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE;
+
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Process;
@@ -26,6 +32,12 @@
 import android.util.Log;
 
 import com.android.cts.net.hostside.INetworkStateObserver;
+import com.android.cts.net.hostside.NetworkCheckResult;
+
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
 
 public final class Common {
 
@@ -48,6 +60,9 @@
     static final String ACTION_SNOOZE_WARNING =
             "com.android.server.net.action.SNOOZE_WARNING";
 
+    private static final String DEFAULT_TEST_URL =
+            "https://connectivitycheck.android.com/generate_204";
+
     static final String NOTIFICATION_TYPE_CONTENT = "CONTENT";
     static final String NOTIFICATION_TYPE_DELETE = "DELETE";
     static final String NOTIFICATION_TYPE_FULL_SCREEN = "FULL_SCREEN";
@@ -59,10 +74,12 @@
     static final String TEST_PKG = "com.android.cts.net.hostside";
     static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
     static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks";
+    static final String KEY_CUSTOM_URL =  TEST_PKG + ".custom_url";
 
     static final int TYPE_COMPONENT_ACTIVTY = 0;
     static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
     static final int TYPE_COMPONENT_EXPEDITED_JOB = 2;
+    private static final int NETWORK_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(10);
 
     static int getUid(Context context) {
         final String packageName = context.getPackageName();
@@ -73,6 +90,15 @@
         }
     }
 
+    private static NetworkCheckResult createNetworkCheckResult(boolean connected, String details,
+            NetworkInfo networkInfo) {
+        final NetworkCheckResult checkResult = new NetworkCheckResult();
+        checkResult.connected = connected;
+        checkResult.details = details;
+        checkResult.networkInfo = networkInfo;
+        return checkResult;
+    }
+
     private static boolean validateComponentState(Context context, int componentType,
             INetworkStateObserver observer) throws RemoteException {
         final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
@@ -80,9 +106,9 @@
             case TYPE_COMPONENT_ACTIVTY: {
                 final int procState = activityManager.getUidProcessState(Process.myUid());
                 if (procState != ActivityManager.PROCESS_STATE_TOP) {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
-                            "Unexpected procstate: " + procState);
+                    observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
+                            createNetworkCheckResult(false, "Unexpected procstate: " + procState,
+                                    null));
                     return false;
                 }
                 return true;
@@ -90,9 +116,9 @@
             case TYPE_COMPONENT_FOREGROUND_SERVICE: {
                 final int procState = activityManager.getUidProcessState(Process.myUid());
                 if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE,
-                            "Unexpected procstate: " + procState);
+                    observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_PROC_STATE,
+                            createNetworkCheckResult(false, "Unexpected procstate: " + procState,
+                                    null));
                     return false;
                 }
                 return true;
@@ -101,16 +127,17 @@
                 final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid());
                 if ((capabilities
                         & ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) == 0) {
-                    observer.onNetworkStateChecked(
-                            INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES,
-                            "Unexpected capabilities: " + capabilities);
+                    observer.onNetworkStateChecked(RESULT_ERROR_UNEXPECTED_CAPABILITIES,
+                            createNetworkCheckResult(false,
+                                    "Unexpected capabilities: " + capabilities, null));
                     return false;
                 }
                 return true;
             }
             default: {
-                observer.onNetworkStateChecked(INetworkStateObserver.RESULT_ERROR_OTHER,
-                        "Unknown component type: " + componentType);
+                observer.onNetworkStateChecked(RESULT_ERROR_OTHER,
+                        createNetworkCheckResult(false, "Unknown component type: " + componentType,
+                                null));
                 return false;
             }
         }
@@ -131,6 +158,7 @@
         final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface(
                 extras.getBinder(KEY_NETWORK_STATE_OBSERVER));
         if (observer != null) {
+            final String customUrl = extras.getString(KEY_CUSTOM_URL);
             try {
                 final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS);
                 if (!skipValidation && !validateComponentState(context, componentType, observer)) {
@@ -143,11 +171,64 @@
                 try {
                     observer.onNetworkStateChecked(
                             INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED,
-                            MyBroadcastReceiver.checkNetworkStatus(context));
+                            checkNetworkStatus(context, customUrl));
                 } catch (RemoteException e) {
                     Log.e(TAG, "Error occurred while notifying the observer: " + e);
                 }
             });
         }
     }
+
+    /**
+     * Checks whether the network is available by attempting a connection to the given address
+     * and returns a {@link NetworkCheckResult} object containing all the relevant details for
+     * debugging. Uses a default address if the given address is {@code null}.
+     *
+     * <p>
+     * The returned object has the following fields:
+     *
+     * <ul>
+     * <li>{@code connected}: whether or not the connection was successful.
+     * <li>{@code networkInfo}: the {@link NetworkInfo} describing the current active network as
+     * visible to this app.
+     * <li>{@code details}: A human readable string giving useful information about the success or
+     * failure.
+     * </ul>
+     */
+    static NetworkCheckResult checkNetworkStatus(Context context, String customUrl) {
+        final String address = (customUrl == null) ? DEFAULT_TEST_URL : customUrl;
+
+        // The current Android DNS resolver returns an UnknownHostException whenever network access
+        // is blocked. This can get cached in the current process-local InetAddress cache. Clearing
+        // the cache before attempting a connection ensures we never report a failure due to a
+        // negative cache entry.
+        InetAddress.clearDnsCache();
+
+        final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+
+        final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+        Log.d(TAG, "Running checkNetworkStatus() on thread "
+                + Thread.currentThread().getName() + " for UID " + getUid(context)
+                + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
+        boolean checkStatus = false;
+        String checkDetails = "N/A";
+        try {
+            final URL url = new URL(address);
+            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setReadTimeout(NETWORK_TIMEOUT_MS);
+            conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
+            conn.setRequestMethod("GET");
+            conn.connect();
+            final int response = conn.getResponseCode();
+            checkStatus = true;
+            checkDetails = "HTTP response for " + address + ": " + response;
+        } catch (Exception e) {
+            checkStatus = false;
+            checkDetails = "Exception getting " + address + ": " + e;
+        }
+        final NetworkCheckResult result = createNetworkCheckResult(checkStatus, checkDetails,
+                networkInfo);
+        Log.d(TAG, "Offering: " + result);
+        return result;
+    }
 }
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
index 825f2c9..1fd3745 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyBroadcastReceiver.java
@@ -30,7 +30,6 @@
 import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_DELETE;
 import static com.android.cts.net.hostside.app2.Common.NOTIFICATION_TYPE_FULL_SCREEN;
 import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.cts.net.hostside.app2.Common.getUid;
 
 import android.app.Notification;
 import android.app.Notification.Action;
@@ -42,15 +41,10 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
 import android.os.Bundle;
 import android.util.Log;
 import android.widget.Toast;
 
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.URL;
-
 /**
  * Receiver used to:
  * <ol>
@@ -60,8 +54,6 @@
  */
 public class MyBroadcastReceiver extends BroadcastReceiver {
 
-    private static final int NETWORK_TIMEOUT_MS = 5 * 1000;
-
     private final String mName;
 
     public MyBroadcastReceiver() {
@@ -126,82 +118,6 @@
         return String.valueOf(apiStatus);
     }
 
-    private static final String NETWORK_STATUS_TEMPLATE = "%s|%s|%s|%s|%s";
-    /**
-     * Checks whether the network is available and return a string which can then be send as a
-     * result data for the ordered broadcast.
-     *
-     * <p>
-     * The string has the following format:
-     *
-     * <p><pre><code>
-     * NetinfoState|NetinfoDetailedState|RealConnectionCheck|RealConnectionCheckDetails|Netinfo
-     * </code></pre>
-     *
-     * <p>Where:
-     *
-     * <ul>
-     * <li>{@code NetinfoState}: enum value of {@link NetworkInfo.State}.
-     * <li>{@code NetinfoDetailedState}: enum value of {@link NetworkInfo.DetailedState}.
-     * <li>{@code RealConnectionCheck}: boolean value of a real connection check (i.e., an attempt
-     *     to access an external website.
-     * <li>{@code RealConnectionCheckDetails}: if HTTP output core or exception string of the real
-     *     connection attempt
-     * <li>{@code Netinfo}: string representation of the {@link NetworkInfo}.
-     * </ul>
-     *
-     * For example, if the connection was established fine, the result would be something like:
-     * <p><pre><code>
-     * CONNECTED|CONNECTED|true|200|[type: WIFI[], state: CONNECTED/CONNECTED, reason: ...]
-     * </code></pre>
-     *
-     */
-    // TODO: now that it uses Binder, it counl return a Bundle with the data parts instead...
-    static String checkNetworkStatus(Context context) {
-        final ConnectivityManager cm =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        // TODO: connect to a hostside server instead
-        final String address = "http://example.com";
-        final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
-        Log.d(TAG, "Running checkNetworkStatus() on thread "
-                + Thread.currentThread().getName() + " for UID " + getUid(context)
-                + "\n\tactiveNetworkInfo: " + networkInfo + "\n\tURL: " + address);
-        boolean checkStatus = false;
-        String checkDetails = "N/A";
-        try {
-            final URL url = new URL(address);
-            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-            conn.setReadTimeout(NETWORK_TIMEOUT_MS);
-            conn.setConnectTimeout(NETWORK_TIMEOUT_MS / 2);
-            conn.setRequestMethod("GET");
-            conn.setDoInput(true);
-            conn.connect();
-            final int response = conn.getResponseCode();
-            checkStatus = true;
-            checkDetails = "HTTP response for " + address + ": " + response;
-        } catch (Exception e) {
-            checkStatus = false;
-            checkDetails = "Exception getting " + address + ": " + e;
-        }
-        // If the app tries to make a network connection in the foreground immediately after
-        // trying to do the same when it's network access was blocked, it could receive a
-        // UnknownHostException due to the cached DNS entry. So, clear the dns cache after
-        // every network access for now until we have a fix on the platform side.
-        InetAddress.clearDnsCache();
-        Log.d(TAG, checkDetails);
-        final String state, detailedState;
-        if (networkInfo != null) {
-            state = networkInfo.getState().name();
-            detailedState = networkInfo.getDetailedState().name();
-        } else {
-            state = detailedState = "null";
-        }
-        final String status = String.format(NETWORK_STATUS_TEMPLATE, state, detailedState,
-                Boolean.valueOf(checkStatus), checkDetails, networkInfo);
-        Log.d(TAG, "Offering " + status);
-        return status;
-    }
-
     /**
      * Sends a system notification containing actions with pending intents to launch the app's
      * main activitiy or service.
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index 3ed5391..5010234 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -21,7 +21,6 @@
 import static com.android.cts.net.hostside.app2.Common.ACTION_SNOOZE_WARNING;
 import static com.android.cts.net.hostside.app2.Common.DYNAMIC_RECEIVER;
 import static com.android.cts.net.hostside.app2.Common.TAG;
-import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
 
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -41,6 +40,7 @@
 
 import com.android.cts.net.hostside.IMyService;
 import com.android.cts.net.hostside.INetworkCallback;
+import com.android.cts.net.hostside.NetworkCheckResult;
 import com.android.modules.utils.build.SdkLevel;
 
 /**
@@ -56,9 +56,7 @@
 
     // TODO: move MyBroadcast static functions here - they were kept there to make git diff easier.
 
-    private IMyService.Stub mBinder =
-        new IMyService.Stub() {
-
+    private IMyService.Stub mBinder = new IMyService.Stub() {
         @Override
         public void registerBroadcastReceiver() {
             if (mReceiver != null) {
@@ -83,8 +81,8 @@
         }
 
         @Override
-        public String checkNetworkStatus() {
-            return MyBroadcastReceiver.checkNetworkStatus(getApplicationContext());
+        public NetworkCheckResult checkNetworkStatus(String customUrl) {
+            return Common.checkNetworkStatus(getApplicationContext(), customUrl);
         }
 
         @Override
@@ -94,7 +92,7 @@
 
         @Override
         public void sendNotification(int notificationId, String notificationType) {
-            MyBroadcastReceiver .sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
+            MyBroadcastReceiver.sendNotification(getApplicationContext(), NOTIFICATION_CHANNEL_ID,
                     notificationId, notificationType);
         }
 
@@ -170,7 +168,7 @@
                     .getSystemService(JobScheduler.class);
             return jobScheduler.schedule(jobInfo);
         }
-      };
+    };
 
     @Override
     public IBinder onBind(Intent intent) {
diff --git a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
index 472e347..911b129 100644
--- a/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
+++ b/tests/cts/hostside/instrumentation_arguments/src/com/android/cts/net/arguments/InstrumentationArguments.java
@@ -18,4 +18,5 @@
 
 public interface InstrumentationArguments {
     String ARG_WAIVE_BIND_PRIORITY = "waive_bind_priority";
+    String ARG_CONNECTION_CHECK_CUSTOM_URL = "connection_check_custom_url";
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
index 880e826..fff716d 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -47,29 +47,29 @@
 
     @Test
     public void testStartActivity_batterySaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
     }
 
     @Test
     public void testStartActivity_dataSaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
     }
 
     @FlakyTest(bugId = 231440256)
     @Test
     public void testStartActivity_doze() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
     }
 
     @Test
     public void testStartActivity_appStandby() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
     }
 
     // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
     @Test
     public void testStartActivity_default() throws Exception {
-        runDeviceTestsWithArgs(TEST_PKG, TEST_CLASS, "testStartActivity_default",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_CLASS, "testStartActivity_default",
                 Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
     }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
index 0d01fc1..faabbef 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideDefaultNetworkRestrictionsTests.java
@@ -46,12 +46,12 @@
     }
 
     private void runMeteredTest(String methodName) throws DeviceNotAvailableException {
-        runDeviceTestsWithArgs(TEST_PKG, METERED_TEST_CLASS, methodName,
+        runDeviceTestsWithCustomOptions(TEST_PKG, METERED_TEST_CLASS, methodName,
                 Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
     }
 
     private void runNonMeteredTest(String methodName) throws DeviceNotAvailableException {
-        runDeviceTestsWithArgs(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
+        runDeviceTestsWithCustomOptions(TEST_PKG, NON_METERED_TEST_CLASS, methodName,
                 Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
     }
 
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
index 361f7c7..c4bcdfd 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkCallbackTests.java
@@ -41,20 +41,20 @@
 
     @Test
     public void testOnBlockedStatusChanged_dataSaver() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_dataSaver");
     }
 
     @Test
     public void testOnBlockedStatusChanged_powerSaver() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkCallbackTest", "testOnBlockedStatusChanged_powerSaver");
     }
 
     // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
     @Test
     public void testOnBlockedStatusChanged_default() throws Exception {
-        runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkCallbackTest",
                 "testOnBlockedStatusChanged_default", Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
     }
 }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
index e97db58..4730b14 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
@@ -38,48 +38,48 @@
 
     @Test
     public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest",
                 "testIsUidNetworkingBlocked_withUidNotBlocked");
     }
 
     @Test
     public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
     }
 
     @Test
     public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest",
                 "testIsUidNetworkingBlocked_withDataSaverMode");
     }
 
     @Test
     public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest",
                 "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
     }
 
     @Test
     public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest",
                 "testIsUidNetworkingBlocked_withPowerSaverMode");
     }
 
     @Test
     public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
-        runDeviceTests(TEST_PKG,
+        runDeviceTestsWithCustomOptions(TEST_PKG,
                 TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
     }
 
     // TODO(b/321848487): Annotate with @RequiresFlagsEnabled to mirror the device-side test.
     @Test
     public void testIsUidNetworkingBlocked_whenInBackground() throws Exception {
-        runDeviceTestsWithArgs(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".NetworkPolicyManagerTest",
                 "testIsUidNetworkingBlocked_whenInBackground",
                 Map.of(ARG_WAIVE_BIND_PRIORITY, "true"));
     }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index ca95ed6..d7dfa80 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -16,12 +16,15 @@
 
 package com.android.cts.net;
 
+import static com.android.cts.net.arguments.InstrumentationArguments.ARG_CONNECTION_CHECK_CUSTOM_URL;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
 import com.android.ddmlib.Log;
 import com.android.modules.utils.build.testing.DeviceSdkLevel;
+import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.targetprep.BuildError;
@@ -48,6 +51,10 @@
     protected static final String TEST_APP2_PKG = "com.android.cts.net.hostside.app2";
     protected static final String TEST_APP2_APK = "CtsHostsideNetworkTestsApp2.apk";
 
+    @Option(name = "custom-url", importance = Option.Importance.IF_UNSET,
+            description = "A custom url to use for testing network connections")
+    protected String mCustomUrl;
+
     @BeforeClassWithInfo
     public static void setUpOnceBase(TestInformation testInfo) throws Exception {
         DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(testInfo.getDevice());
@@ -149,13 +156,31 @@
                 + packageName + ", u=" + currentUser);
     }
 
-    protected boolean runDeviceTestsWithArgs(String packageName, String className,
-            String methodName, Map<String, String> args) throws DeviceNotAvailableException {
+    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className)
+            throws DeviceNotAvailableException {
+        return runDeviceTestsWithCustomOptions(packageName, className, null);
+    }
+
+    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
+            String methodName) throws DeviceNotAvailableException {
+        return runDeviceTestsWithCustomOptions(packageName, className, methodName, null);
+    }
+
+    protected boolean runDeviceTestsWithCustomOptions(String packageName, String className,
+            String methodName, Map<String, String> testArgs) throws DeviceNotAvailableException {
         final DeviceTestRunOptions deviceTestRunOptions = new DeviceTestRunOptions(packageName)
                 .setTestClassName(className)
                 .setTestMethodName(methodName);
-        for (Map.Entry<String, String> arg : args.entrySet()) {
-            deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
+
+        // Currently there is only one custom option that the test exposes.
+        if (mCustomUrl != null) {
+            deviceTestRunOptions.addInstrumentationArg(ARG_CONNECTION_CHECK_CUSTOM_URL, mCustomUrl);
+        }
+        // Pass over any test specific arguments.
+        if (testArgs != null) {
+            for (Map.Entry<String, String> arg : testArgs.entrySet()) {
+                deviceTestRunOptions.addInstrumentationArg(arg.getKey(), arg.getValue());
+            }
         }
         return runDeviceTests(deviceTestRunOptions);
     }
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index 9c3751d..7b9d3b5 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -46,7 +46,7 @@
     @SecurityTest
     @Test
     public void testDataWarningReceiver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataWarningReceiverTest",
                 "testSnoozeWarningNotReceived");
     }
 
@@ -56,25 +56,25 @@
 
     @Test
     public void testDataSaverMode_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
                 "testGetRestrictBackgroundStatus_disabled");
     }
 
     @Test
     public void testDataSaverMode_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
                 "testGetRestrictBackgroundStatus_whitelisted");
     }
 
     @Test
     public void testDataSaverMode_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
                 "testGetRestrictBackgroundStatus_enabled");
     }
 
     @Test
     public void testDataSaverMode_blacklisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
                 "testGetRestrictBackgroundStatus_blacklisted");
     }
 
@@ -97,13 +97,13 @@
 
     @Test
     public void testDataSaverMode_requiredWhitelistedPackages() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
                 "testGetRestrictBackgroundStatus_requiredWhitelistedPackages");
     }
 
     @Test
     public void testDataSaverMode_broadcastNotSentOnUnsupportedDevices() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DataSaverModeTest",
                 "testBroadcastNotSentOnUnsupportedDevices");
     }
 
@@ -113,19 +113,19 @@
 
     @Test
     public void testBatterySaverModeMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
                 "testBackgroundNetworkAccess_disabled");
     }
 
     @Test
     public void testBatterySaverModeMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
                 "testBackgroundNetworkAccess_whitelisted");
     }
 
     @Test
     public void testBatterySaverModeMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeMeteredTest",
                 "testBackgroundNetworkAccess_enabled");
     }
 
@@ -149,19 +149,19 @@
 
     @Test
     public void testBatterySaverModeNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
                 "testBackgroundNetworkAccess_disabled");
     }
 
     @Test
     public void testBatterySaverModeNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
                 "testBackgroundNetworkAccess_whitelisted");
     }
 
     @Test
     public void testBatterySaverModeNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".BatterySaverModeNonMeteredTest",
                 "testBackgroundNetworkAccess_enabled");
     }
 
@@ -171,31 +171,31 @@
 
     @Test
     public void testAppIdleMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
                 "testBackgroundNetworkAccess_disabled");
     }
 
     @Test
     public void testAppIdleMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
                 "testBackgroundNetworkAccess_whitelisted");
     }
 
     @Test
     public void testAppIdleMetered_tempWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
                 "testBackgroundNetworkAccess_tempWhitelisted");
     }
 
     @Test
     public void testAppIdleMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
                 "testBackgroundNetworkAccess_enabled");
     }
 
     @Test
     public void testAppIdleMetered_idleWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
                 "testAppIdleNetworkAccess_idleWhitelisted");
     }
 
@@ -206,51 +206,51 @@
 
     @Test
     public void testAppIdleNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testBackgroundNetworkAccess_disabled");
     }
 
 
     @Test
     public void testAppIdleNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testBackgroundNetworkAccess_whitelisted");
     }
 
     @Test
     public void testAppIdleNonMetered_tempWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testBackgroundNetworkAccess_tempWhitelisted");
     }
 
     @Test
     public void testAppIdleNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testBackgroundNetworkAccess_enabled");
     }
 
     @Test
     public void testAppIdleNonMetered_idleWhitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testAppIdleNetworkAccess_idleWhitelisted");
     }
 
     @Test
     public void testAppIdleNonMetered_whenCharging() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testAppIdleNetworkAccess_whenCharging");
     }
 
     @Test
     public void testAppIdleMetered_whenCharging() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleMeteredTest",
                 "testAppIdleNetworkAccess_whenCharging");
     }
 
     @Test
     public void testAppIdle_toast() throws Exception {
         // Check that showing a toast doesn't bring an app out of standby
-        runDeviceTests(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".AppIdleNonMeteredTest",
                 "testAppIdle_toast");
     }
 
@@ -260,25 +260,25 @@
 
     @Test
     public void testDozeModeMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
                 "testBackgroundNetworkAccess_disabled");
     }
 
     @Test
     public void testDozeModeMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
                 "testBackgroundNetworkAccess_whitelisted");
     }
 
     @Test
     public void testDozeModeMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
                 "testBackgroundNetworkAccess_enabled");
     }
 
     @Test
     public void testDozeModeMetered_enabledButWhitelistedOnNotificationAction() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeMeteredTest",
                 "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
     }
 
@@ -289,26 +289,26 @@
 
     @Test
     public void testDozeModeNonMetered_disabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
                 "testBackgroundNetworkAccess_disabled");
     }
 
     @Test
     public void testDozeModeNonMetered_whitelisted() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
                 "testBackgroundNetworkAccess_whitelisted");
     }
 
     @Test
     public void testDozeModeNonMetered_enabled() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
                 "testBackgroundNetworkAccess_enabled");
     }
 
     @Test
     public void testDozeModeNonMetered_enabledButWhitelistedOnNotificationAction()
             throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".DozeModeNonMeteredTest",
                 "testBackgroundNetworkAccess_enabledButWhitelistedOnNotificationAction");
     }
 
@@ -318,55 +318,55 @@
 
     @Test
     public void testDataAndBatterySaverModes_meteredNetwork() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testDataAndBatterySaverModes_meteredNetwork");
     }
 
     @Test
     public void testDataAndBatterySaverModes_nonMeteredNetwork() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testDataAndBatterySaverModes_nonMeteredNetwork");
     }
 
     @Test
     public void testDozeAndBatterySaverMode_powerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testDozeAndBatterySaverMode_powerSaveWhitelists");
     }
 
     @Test
     public void testDozeAndAppIdle_powerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testDozeAndAppIdle_powerSaveWhitelists");
     }
 
     @Test
     public void testAppIdleAndDoze_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testAppIdleAndDoze_tempPowerSaveWhitelists");
     }
 
     @Test
     public void testAppIdleAndBatterySaver_tempPowerSaveWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testAppIdleAndBatterySaver_tempPowerSaveWhitelists");
     }
 
     @Test
     public void testDozeAndAppIdle_appIdleWhitelist() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testDozeAndAppIdle_appIdleWhitelist");
     }
 
     @Test
     public void testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testAppIdleAndDoze_tempPowerSaveAndAppIdleWhitelists");
     }
 
     @Test
     public void testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".MixedModesTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".MixedModesTest",
                 "testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
     }
 
@@ -376,13 +376,13 @@
 
     @Test
     public void testNetworkAccess_restrictedMode() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
                 "testNetworkAccess");
     }
 
     @Test
     public void testNetworkAccess_restrictedMode_withBatterySaver() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
                 "testNetworkAccess_withBatterySaver");
     }
 
@@ -392,12 +392,12 @@
 
     @Test
     public void testMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest");
     }
 
     @Test
     public void testNonMeteredNetworkAccess_expeditedJob() throws Exception {
-        runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
+        runDeviceTestsWithCustomOptions(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest");
     }
 
     /*******************
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index a2e4ab6..159af00 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -13,6 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// ktlint does not allow annotating function argument literals inline. Disable the specific rule
+// since this negatively affects readability.
+@file:Suppress("ktlint:standard:comment-wrapping")
+
 package android.net.cts
 
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
@@ -22,21 +26,28 @@
 import android.net.NetworkRequest
 import android.net.apf.ApfCapabilities
 import android.os.Build
+import android.os.PowerManager
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
 import android.system.OsConstants
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.compatibility.common.util.PropertyUtil.isVendorApiLevelNewerThan
+import com.android.compatibility.common.util.PropertyUtil.getVsrApiLevel
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.internal.util.HexDump
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.NetworkStackModuleTest
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.SkipPresubmit
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.runAsShell
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import com.google.common.truth.TruthJUnit.assume
+import java.lang.Thread
+import kotlin.random.Random
 import kotlin.test.assertNotNull
 import org.junit.After
 import org.junit.Before
@@ -44,17 +55,19 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private const val TAG = "ApfIntegrationTest"
 private const val TIMEOUT_MS = 2000L
 private const val APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"
+private const val POLLING_INTERVAL_MS: Int = 100
 
 @AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
 @RunWith(DevSdkIgnoreRunner::class)
 @NetworkStackModuleTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class ApfIntegrationTest {
     companion object {
         @BeforeClass
         @JvmStatic
+        @Suppress("ktlint:standard:no-multi-spaces")
         fun setupOnce() {
             // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
             // created.
@@ -74,20 +87,52 @@
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
     private val pm by lazy { context.packageManager }
+    private val powerManager by lazy { context.getSystemService(PowerManager::class.java)!! }
+    private val wakeLock by lazy { powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG) }
     private lateinit var ifname: String
     private lateinit var networkCallback: TestableNetworkCallback
     private lateinit var caps: ApfCapabilities
 
     fun getApfCapabilities(): ApfCapabilities {
-        val caps = runShellCommandOrThrow("cmd network_stack apf $ifname capabilities").trim()
+        val caps = runShellCommand("cmd network_stack apf $ifname capabilities").trim()
+        if (caps.isEmpty()) {
+            return ApfCapabilities(0, 0, 0)
+        }
         val (version, maxLen, packetFormat) = caps.split(",").map { it.toInt() }
         return ApfCapabilities(version, maxLen, packetFormat)
     }
 
+    fun pollingCheck(condition: () -> Boolean, timeout_ms: Int): Boolean {
+        var polling_time = 0
+        do {
+            Thread.sleep(POLLING_INTERVAL_MS.toLong())
+            polling_time += POLLING_INTERVAL_MS
+            if (condition()) return true
+        } while (polling_time < timeout_ms)
+        return false
+    }
+
+    fun turnScreenOff() {
+        if (!wakeLock.isHeld()) wakeLock.acquire()
+        runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
+        val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
+        assertThat(result).isTrue()
+    }
+
+    fun turnScreenOn() {
+        if (wakeLock.isHeld()) wakeLock.release()
+        runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
+        val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+        assertThat(result).isTrue()
+    }
+
     @Before
     fun setUp() {
         assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
-        assume().that(isVendorApiLevelNewerThan(Build.VERSION_CODES.TIRAMISU)).isTrue()
+        // APF must run when the screen is off and the device is not interactive.
+        // TODO: consider running some of the tests with screen on (capabilities, read / write).
+        turnScreenOff()
+
         networkCallback = TestableNetworkCallback()
         cm.requestNetwork(
                 NetworkRequest.Builder()
@@ -100,27 +145,90 @@
             ifname = assertNotNull(it.lp.interfaceName)
             true
         }
-        runShellCommandOrThrow("cmd network_stack apf $ifname pause")
+        // It's possible the device does not support APF, in which case this command will not be
+        // successful. Ignore the error as testApfCapabilities() already asserts APF support on the
+        // respective VSR releases and all other tests are based on the capabilities indicated.
+        runShellCommand("cmd network_stack apf $ifname pause")
         caps = getApfCapabilities()
     }
 
     @After
     fun tearDown() {
+        if (::ifname.isInitialized) {
+            runShellCommand("cmd network_stack apf $ifname resume")
+        }
         if (::networkCallback.isInitialized) {
             cm.unregisterNetworkCallback(networkCallback)
         }
-        if (::ifname.isInitialized) {
-            runShellCommandOrThrow("cmd network_stack apf $ifname resume")
-        }
+        turnScreenOn()
     }
 
     @Test
-    fun testGetApfCapabilities() {
+    fun testApfCapabilities() {
+        // APF became mandatory in Android 14 VSR.
+        assume().that(getVsrApiLevel()).isAtLeast(34)
+
+        // ApfFilter does not support anything but ARPHRD_ETHER.
+        assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+
+        // DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
+        // - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
+        //   the getApfPacketFilterCapabilities HAL method.
+        // - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
+        //   the getApfPacketFilterCapabilities HAL method.
+        // TODO: check whether above text should be changed "34 or higher"
+        // This should assert apfVersionSupported >= 4 as per the VSR requirements, but there are
+        // currently no tests for APFv6 and there cannot be a valid implementation as the
+        // interpreter has yet to be finalized.
         assertThat(caps.apfVersionSupported).isEqualTo(4)
         assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
-        if (isVendorApiLevelNewerThan(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)) {
+
+        // DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
+        // ro.board.first_api_level or ro.board.api_level to 202404 or higher:
+        // - [GMS-VSR-5.3.12-009] MUST indicate at least 2000 bytes of usable memory from calls to
+        //   the getApfPacketFilterCapabilities HAL method.
+        if (getVsrApiLevel() >= 202404) {
             assertThat(caps.maximumApfProgramSize).isAtLeast(2000)
         }
-        assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+    }
+
+    // APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
+    fun assumeApfVersionSupportAtLeast(version: Int) {
+        assume().that(caps.apfVersionSupported).isAtLeast(version)
+    }
+
+    fun installProgram(bytes: ByteArray) {
+        val prog = HexDump.toHexString(bytes, 0 /* offset */, bytes.size, false /* upperCase */)
+        val result = runShellCommandOrThrow("cmd network_stack apf $ifname install $prog").trim()
+        // runShellCommandOrThrow only throws on S+.
+        assertThat(result).isEqualTo("success")
+    }
+
+    fun readProgram(): ByteArray {
+        val progHexString = runShellCommandOrThrow("cmd network_stack apf $ifname read").trim()
+        // runShellCommandOrThrow only throws on S+.
+        assertThat(progHexString).isNotEmpty()
+        return HexDump.hexStringToByteArray(progHexString)
+    }
+
+    @SkipPresubmit(reason = "This test takes longer than 1 minute, do not run it on presubmit.")
+    // APF integration is mostly broken before V, only run the full read / write test on V+.
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testReadWriteProgram() {
+        assumeApfVersionSupportAtLeast(4)
+
+        // Only test down to 2 bytes. The first byte always stays PASS.
+        val program = ByteArray(caps.maximumApfProgramSize)
+        for (i in caps.maximumApfProgramSize downTo 2) {
+            // Randomize bytes in range [1, i). And install first [0, i) bytes of program.
+            // Note that only the very first instruction (PASS) is valid APF bytecode.
+            Random.nextBytes(program, 1 /* fromIndex */, i /* toIndex */)
+            installProgram(program.sliceArray(0..<i))
+
+            // Compare entire memory region.
+            val readResult = readProgram()
+            assertWithMessage("read/write $i byte prog failed").that(readResult).isEqualTo(program)
+        }
     }
 }
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index d29e657..8f8aa5f 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -21,6 +21,7 @@
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
 import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@@ -65,6 +66,9 @@
     // The byte[] buffer size for UDP tests
     private static final int UDP_BUFFER_SIZE = 1024;
 
+    // The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
+    private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
     // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
     private static final byte[] DEFAULT_DATASET_TLVS =
             base16().decode(
@@ -149,16 +153,22 @@
         assertThat(ifconfig).doesNotContain("inet6 addr");
     }
 
+    // TODO (b/323300829): add test for removing an OT address
     @Test
     public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
         mController.joinAndWait(DEFAULT_DATASET);
 
-        String ifconfig = runShellCommand("ifconfig thread-wpan");
         List<Inet6Address> otAddresses = mOtCtl.getAddresses();
         assertThat(otAddresses).isNotEmpty();
-        for (Inet6Address otAddress : otAddresses) {
-            assertThat(ifconfig).contains(otAddress.getHostAddress());
-        }
+        // TODO: it's cleaner to have a retry() method to retry failed asserts in given delay so
+        // that we can write assertThat() in the Predicate
+        waitFor(
+                () -> {
+                    String ifconfig = runShellCommand("ifconfig thread-wpan");
+                    return otAddresses.stream()
+                            .allMatch(addr -> ifconfig.contains(addr.getHostAddress()));
+                },
+                TUN_ADDR_UPDATE_TIMEOUT);
     }
 
     @Test
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index 2237e65..3efdf7d 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -85,7 +85,7 @@
      */
     public static void waitFor(Supplier<Boolean> condition, Duration timeout)
             throws TimeoutException {
-        final long intervalMills = 1000;
+        final long intervalMills = 500;
         final long timeoutMills = timeout.toMillis();
 
         for (long i = 0; i < timeoutMills; i += intervalMills) {