Snap for 12429534 from c90403f22b9cf110d6d2861c66718ba4d32d876a to mainline-mediaprovider-release

Change-Id: I36df895090eff589db5f8cd6909155abefcf986b
diff --git a/Android.bp b/Android.bp
index 8f4997c..734797a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -275,11 +275,13 @@
     ],
     libs: [
         "error_prone_annotations",
+        "framework-annotations-lib",
         "unsupportedappusage",
     ],
     static_libs: [
         "androidx.annotation_annotation",
         "modules-utils-build_system",
+        "modules-utils-expresslog",
         "modules-utils-preconditions",
         "modules-utils-shell-command-handler",
         "modules-utils-statemachine",
diff --git a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
deleted file mode 100644
index 1b2cc78..0000000
--- a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2019 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.networkstack.apishim.api29;
-
-import android.net.Uri;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.networkstack.apishim.common.CaptivePortalDataShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-/**
- * Compatibility implementation of {@link CaptivePortalData}.
- *
- * <p>Use {@link com.android.networkstack.apishim.CaptivePortalDataShimImpl} instead of this
- * fallback implementation.
- */
-@RequiresApi(Build.VERSION_CODES.Q)
-public abstract class CaptivePortalDataShimImpl implements CaptivePortalDataShim {
-    protected CaptivePortalDataShimImpl() {}
-
-    /**
-     * Parse a {@link android.net.CaptivePortalDataShim} from JSON.
-     *
-     * <p>Use
-     * {@link com.android.networkstack.apishim.CaptivePortalDataShimImpl#fromJson(JSONObject)}
-     * instead of this API 29 compatibility version.
-     */
-    @NonNull
-    public static CaptivePortalDataShim fromJson(JSONObject object) throws JSONException,
-            UnsupportedApiLevelException {
-        // Data class not supported in API 29
-        throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
-    }
-
-    @Override
-    public CharSequence getVenueFriendlyName() {
-        // Not supported in API level 29
-        return null;
-    }
-
-    @Override
-    public int getUserPortalUrlSource() {
-        // Not supported in API level 29
-        return ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_OTHER;
-    }
-
-    @VisibleForTesting
-    public static boolean isSupported() {
-        return false;
-    }
-
-    /**
-     * Generate a {@link CaptivePortalDataShim} object with a friendly name set
-     *
-     * @param friendlyName The friendly name to set
-     * @return a {@link CaptivePortalData} object with a friendly name set
-     */
-    @Override
-    public CaptivePortalDataShim withVenueFriendlyName(String friendlyName)
-            throws UnsupportedApiLevelException {
-        // Not supported in API level 29
-        throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
-    }
-
-    /**
-     * Generate a {@link CaptivePortalDataShim} object with a friendly name and Passpoint external
-     * URLs set
-     *
-     * @param friendlyName The friendly name to set
-     * @param venueInfoUrl Venue information URL
-     * @param termsAndConditionsUrl Terms and conditions URL
-     *
-     * @return a {@link CaptivePortalDataShim} object with friendly name, venue info URL and terms
-     * and conditions URL set
-     */
-    @Override
-    public CaptivePortalDataShim withPasspointInfo(@NonNull String friendlyName,
-            @NonNull Uri venueInfoUrl, @NonNull Uri termsAndConditionsUrl)
-            throws UnsupportedApiLevelException {
-        // Not supported in API level 29
-        throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29");
-    }
-}
diff --git a/apishim/29/com/android/networkstack/apishim/api29/BroadcastOptionsShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/BroadcastOptionsShimImpl.java
similarity index 96%
rename from apishim/29/com/android/networkstack/apishim/api29/BroadcastOptionsShimImpl.java
rename to apishim/30/com/android/networkstack/apishim/api30/BroadcastOptionsShimImpl.java
index ab58dc2..2db73c0 100644
--- a/apishim/29/com/android/networkstack/apishim/api29/BroadcastOptionsShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/BroadcastOptionsShimImpl.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.networkstack.apishim.api29;
+package com.android.networkstack.apishim.api30;
 
 import android.app.BroadcastOptions;
 import android.os.Build;
diff --git a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
index 8dce170..d84bb52 100644
--- a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
@@ -22,12 +22,10 @@
 import android.os.Build;
 import android.os.RemoteException;
 
-import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
 import com.android.networkstack.apishim.common.CaptivePortalDataShim;
-import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 
 import org.json.JSONException;
@@ -37,8 +35,7 @@
  * Compatibility implementation of {@link CaptivePortalDataShim}.
  */
 @RequiresApi(Build.VERSION_CODES.R)
-public class CaptivePortalDataShimImpl
-        extends com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl {
+public class CaptivePortalDataShimImpl implements CaptivePortalDataShim {
     @NonNull
     protected final CaptivePortalData mData;
 
@@ -53,16 +50,9 @@
     /**
      * Parse a {@link CaptivePortalDataShim} from a JSON object.
      * @throws JSONException The JSON is not a representation of correct captive portal data.
-     * @throws UnsupportedApiLevelException CaptivePortalData is not available on this API level.
      */
-    @RequiresApi(Build.VERSION_CODES.Q)
     @NonNull
-    public static CaptivePortalDataShim fromJson(JSONObject obj) throws JSONException,
-            UnsupportedApiLevelException {
-        if (!isSupported()) {
-            return com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl.fromJson(obj);
-        }
-
+    public static CaptivePortalDataShim fromJson(JSONObject obj) throws JSONException {
         final long refreshTimeMs = System.currentTimeMillis();
         final long secondsRemaining = getLongOrDefault(obj, "seconds-remaining", -1L);
         final long millisRemaining = secondsRemaining <= Long.MAX_VALUE / 1000
@@ -81,10 +71,8 @@
                 .build());
     }
 
-    @RequiresApi(Build.VERSION_CODES.Q)
-    @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
     public static boolean isSupported() {
-        return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
+        return true;
     }
 
     private static long getLongOrDefault(JSONObject o, String key, long def) throws JSONException {
@@ -123,6 +111,18 @@
     }
 
     @Override
+    public CharSequence getVenueFriendlyName() {
+        // Not supported in API level 30
+        return null;
+    }
+
+    @Override
+    public int getUserPortalUrlSource() {
+        // Not supported in API level 30
+        return ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_OTHER;
+    }
+
+    @Override
     public void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException {
         cb.notifyCaptivePortalDataChanged(mData);
     }
diff --git a/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java
index 5e38766..87c8e94 100644
--- a/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java
+++ b/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java
@@ -31,7 +31,7 @@
  */
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class BroadcastOptionsShimImpl
-        extends com.android.networkstack.apishim.api29.BroadcastOptionsShimImpl {
+        extends com.android.networkstack.apishim.api30.BroadcastOptionsShimImpl {
     protected BroadcastOptionsShimImpl(@NonNull BroadcastOptions options) {
         super(options);
     }
@@ -42,7 +42,7 @@
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public static BroadcastOptionsShim newInstance(@NonNull BroadcastOptions options) {
         if (!isAtLeastT()) {
-            return com.android.networkstack.apishim.api29.BroadcastOptionsShimImpl.newInstance(
+            return com.android.networkstack.apishim.api30.BroadcastOptionsShimImpl.newInstance(
                     options);
         }
         return new BroadcastOptionsShimImpl(options);
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 90bd832..a40d8b6 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -93,6 +93,7 @@
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS;
+import static android.net.apf.ApfCounterTracker.getCounterValue;
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 import static android.net.apf.BaseApfGenerator.Register.R1;
@@ -2461,6 +2462,10 @@
                 rasToFilter.add(ra);
             }
 
+            // Increase the counter before we generate the program.
+            // This keeps the APF_PROGRAM_ID counter in sync with the program.
+            mNumProgramUpdates++;
+
             // Step 2: Actually generate the program
             gen = emitPrologueLocked();
             for (Ra ra : rasToFilter) {
@@ -2483,7 +2488,6 @@
         }
         mLastInstalledProgramMinLifetime = programMinLft;
         mLastInstalledProgram = program;
-        mNumProgramUpdates++;
         mMaxProgramSize = Math.max(mMaxProgramSize, program.length);
 
         if (VDBG) {
@@ -2861,14 +2865,19 @@
             return;
         }
         pw.println("Program updates: " + mNumProgramUpdates);
+        int filterAgeSeconds = secondsSinceBoot() - mLastTimeInstalledProgram;
         pw.println(String.format(
                 "Last program length %d, installed %ds ago, lifetime %ds",
-                mLastInstalledProgram.length, secondsSinceBoot() - mLastTimeInstalledProgram,
+                mLastInstalledProgram.length, filterAgeSeconds,
                 mLastInstalledProgramMinLifetime));
-
-        pw.print("Denylisted Ethertypes:");
-        for (int p : mEthTypeBlackList) {
-            pw.print(String.format(" %04x", p));
+        if (SdkLevel.isAtLeastV()) {
+            pw.print("Hardcoded Allowlisted Ethertypes:");
+            pw.println(" 0800(IPv4) 0806(ARP) 86DD(IPv6) 888E(EAPOL) 88B4(WAPI)");
+        } else {
+            pw.print("Denylisted Ethertypes:");
+            for (int p : mEthTypeBlackList) {
+                pw.print(String.format(" %04x", p));
+            }
         }
         pw.println();
         pw.println("RA filters:");
@@ -2930,16 +2939,61 @@
         } else {
             try {
                 Counter[] counters = Counter.class.getEnumConstants();
+                long counterFilterAgeSeconds =
+                        getCounterValue(mDataSnapshot, Counter.FILTER_AGE_SECONDS);
+                long counterApfProgramId =
+                        getCounterValue(mDataSnapshot, Counter.APF_PROGRAM_ID);
                 for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
-                    long value = ApfCounterTracker.getCounterValue(mDataSnapshot, c);
-                    // Only print non-zero counters
-                    if (value != 0) {
-                        pw.println(c.toString() + ": " + value);
+                    long value = getCounterValue(mDataSnapshot, c);
+
+                    String note = "";
+                    boolean checkValueIncreases = true;
+                    switch (c) {
+                        case FILTER_AGE_SECONDS:
+                            checkValueIncreases = false;
+                            if (value != counterFilterAgeSeconds) {
+                                note = " [ERROR: impossible]";
+                            } else if (counterApfProgramId < mNumProgramUpdates) {
+                                note = " [IGNORE: obsolete program]";
+                            } else if (value > filterAgeSeconds) {
+                                long offset = value - filterAgeSeconds;
+                                note = " [ERROR: in the future by " + offset + "s]";
+                            }
+                            break;
+                        case FILTER_AGE_16384THS:
+                            if (mApfVersionSupported > BaseApfGenerator.APF_VERSION_4) {
+                                checkValueIncreases = false;
+                                if (value % 16384 == 0) {
+                                    // valid, but unlikely
+                                    note = " [INFO: zero fractional portion]";
+                                }
+                                if (value / 16384 != counterFilterAgeSeconds) {
+                                    // should not be able to happen
+                                    note = " [ERROR: mismatch with FILTER_AGE_SECONDS]";
+                                }
+                            } else if (value != 0) {
+                                note = " [UNEXPECTED: APF<=4, yet non-zero]";
+                            }
+                            break;
+                        case APF_PROGRAM_ID:
+                            if (value != counterApfProgramId) {
+                                note = " [ERROR: impossible]";
+                            } else if (value < mNumProgramUpdates) {
+                                note = " [WARNING: OBSOLETE PROGRAM]";
+                            } else if (value > mNumProgramUpdates) {
+                                note = " [ERROR: INVALID FUTURE ID]";
+                            }
+                            break;
+                        default:
+                            break;
                     }
 
-                    final Set<Counter> skipCheckCounters = Set.of(FILTER_AGE_SECONDS,
-                            FILTER_AGE_16384THS);
-                    if (!skipCheckCounters.contains(c)) {
+                    // Only print non-zero counters (or those with a note)
+                    if (value != 0 || !note.equals("")) {
+                        pw.println(c.toString() + ": " + value + note);
+                    }
+
+                    if (checkValueIncreases) {
                         // If the counter's value decreases, it may have been cleaned up or there
                         // may be a bug.
                         long oldValue = mApfCounterTracker.getCounters().getOrDefault(c, 0L);
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 4b94968..76de04d 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -104,7 +104,6 @@
 import com.android.net.module.util.PacketReader;
 import com.android.net.module.util.arp.ArpPacket;
 import com.android.networkstack.R;
-import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
 import com.android.networkstack.apishim.SocketUtilsShimImpl;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.networkstack.util.NetworkStackUtils;
@@ -308,9 +307,7 @@
         final ByteArrayOutputStream params =
                 new ByteArrayOutputStream(DEFAULT_REQUESTED_PARAMS.length + numOptionalParams);
         params.write(DEFAULT_REQUESTED_PARAMS, 0, DEFAULT_REQUESTED_PARAMS.length);
-        if (isCapportApiEnabled()) {
-            params.write(DHCP_CAPTIVE_PORTAL);
-        }
+        params.write(DHCP_CAPTIVE_PORTAL);
         params.write(DHCP_IPV6_ONLY_PREFERRED);
         // Customized DHCP options to be put in PRL.
         for (DhcpOption option : mConfiguration.options) {
@@ -323,10 +320,6 @@
         return params.toByteArray();
     }
 
-    private static boolean isCapportApiEnabled() {
-        return CaptivePortalDataShimImpl.isSupported();
-    }
-
     // DHCP flag that means "yes, we support unicast."
     private static final boolean DO_UNICAST   = false;
 
@@ -657,7 +650,6 @@
 
     private byte[] getOptionsToSkip() {
         final ByteArrayOutputStream optionsToSkip = new ByteArrayOutputStream(2);
-        if (!isCapportApiEnabled()) optionsToSkip.write(DHCP_CAPTIVE_PORTAL);
         if (!mConfiguration.isWifiManagedProfile) {
             optionsToSkip.write(DHCP_DOMAIN_SEARCHLIST);
         }
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 0c90fe6..868c63b 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -166,6 +166,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.WakeupMessage;
+import com.android.modules.expresslog.Counter;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.ConnectivityUtils;
@@ -2585,7 +2586,10 @@
                         @Override
                         public void notifyLost(String logMsg, NudEventType type) {
                             maybeStoreNudFailureToDatabase(type);
-                            if (mIgnoreNudFailure) return;
+                            if (mIgnoreNudFailure) {
+                                Counter.logIncrement("core_networking.value_nud_failure_ignored");
+                                return;
+                            }
                             final int version = mCallback.getInterfaceVersion();
                             if (version >= VERSION_ADDED_REACHABILITY_FAILURE) {
                                 final int reason = nudEventTypeToInt(type);
@@ -3296,6 +3300,7 @@
             sinceTimes[2] = now - SIX_HOURS_IN_MS;
             mIpMemoryStore.retrieveNetworkEventCount(mCluster, sinceTimes,
                     NETWORK_EVENT_NUD_FAILURE_TYPES, mListener);
+            Counter.logIncrement("core_networking.value_nud_failure_queried");
         }
 
         @Override
diff --git a/src/android/net/util/RawPacketTracker.java b/src/android/net/util/RawPacketTracker.java
new file mode 100644
index 0000000..e73834b
--- /dev/null
+++ b/src/android/net/util/RawPacketTracker.java
@@ -0,0 +1,242 @@
+/*
+ * 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 android.net.util;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.net.ip.ConnectivityPacketTracker;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.LocalLog;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.InterfaceParams;
+
+import java.util.Objects;
+
+/**
+ * Tracks and manages raw packet captures on a network interface.
+ *
+ * <p>This class is not a thread-safe and should be only run on the handler thread.
+ * It utilizes a dedicated {@link HandlerThread} to perform capture operations, allowing
+ * the caller to interact with it asynchronously through methods like
+ * {@link #startCapture(String, long)}, {@link #stopCapture(String)},
+ * and {@link #getMatchedPacketCount(String, String)}.</p>
+ *
+ */
+public class RawPacketTracker {
+    /**
+     * Dependencies class for testing.
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    static class Dependencies {
+        public @NonNull ConnectivityPacketTracker createPacketTracker(
+                Handler handler, InterfaceParams ifParams, int maxPktRecords) {
+            return new ConnectivityPacketTracker(
+                    handler, ifParams, new LocalLog(maxPktRecords));
+        }
+
+        public @NonNull HandlerThread createHandlerThread() {
+            final HandlerThread handlerThread = new HandlerThread(TAG + "-handler");
+            handlerThread.start();
+            return handlerThread;
+        }
+
+        public @NonNull Looper getLooper(HandlerThread handlerThread) {
+            return handlerThread.getLooper();
+        }
+    }
+
+    // Maximum number of packet records to store.
+    private static final int MAX_PACKET_RECORDS = 100;
+    // Maximum duration for a packet capture session in milliseconds.
+    public static final long MAX_CAPTURE_TIME_MS = 300_000;
+    @VisibleForTesting(visibility = PRIVATE)
+    public static final int CMD_STOP_CAPTURE = 1;
+    private static final String TAG = RawPacketTracker.class.getSimpleName();
+
+    private final @NonNull HandlerThread mHandlerThread;
+    private final @NonNull Dependencies mDeps;
+    private final @NonNull Handler mHandler;
+
+    /**
+     * A map that stores ConnectivityPacketTracker objects, keyed by their associated
+     * network interface name, e.g: wlan0. This allows for tracking connectivity
+     * packets on a per-interface basis. This is only accessed by handler thread.
+     */
+    private final ArrayMap<String, ConnectivityPacketTracker> mTrackerMap = new ArrayMap<>();
+
+    public RawPacketTracker() {
+        this(new Dependencies());
+    }
+
+    @VisibleForTesting(visibility = PRIVATE)
+    public RawPacketTracker(
+            @NonNull Dependencies deps
+    ) {
+        mDeps = deps;
+        mHandlerThread = deps.createHandlerThread();
+        mHandler = new RawPacketTrackerHandler(deps.getLooper(mHandlerThread), this);
+    }
+
+    private static class RawPacketTrackerHandler extends Handler {
+        private final RawPacketTracker mRawPacketTracker;
+        private RawPacketTrackerHandler(
+                @NonNull Looper looper,
+                @NonNull RawPacketTracker rawPacketTracker) {
+            super(looper);
+            mRawPacketTracker = rawPacketTracker;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            final String ifaceName;
+            switch (msg.what) {
+                case CMD_STOP_CAPTURE:
+                    ifaceName = (String) msg.obj;
+                    mRawPacketTracker.processStopCapture(ifaceName);
+                    break;
+                default:
+                    Log.e(TAG, "unrecognized message: " + msg.what);
+            }
+        }
+    }
+
+    /**
+     * Starts capturing packets on the specified network interface.
+     *
+     * <p>Initiates a packet capture session if one is not already running for the given interface.
+     * A capture timeout is set to automatically stop the capture after {@code maxCaptureTimeMs}
+     * milliseconds. If a previous stop capture event was scheduled, it is canceled.</p>
+     *
+     * @param ifaceName      The name of the network interface to capture packets on.
+     * @param maxCaptureTimeMs The maximum capture duration in milliseconds.
+     * @throws IllegalArgumentException If {@code maxCaptureTimeMs} is less than or equal to 0.
+     * @throws RuntimeException If a capture is already running on the specified interface.
+     * @throws IllegalStateException If this method is not running on handler thread
+     */
+    public void startCapture(
+            String ifaceName, long maxCaptureTimeMs
+    ) throws IllegalArgumentException, RuntimeException, IllegalStateException {
+        ensureRunOnHandlerThread();
+        if (maxCaptureTimeMs <= 0) {
+            throw new IllegalArgumentException("maxCaptureTimeMs " + maxCaptureTimeMs + " <= 0");
+        }
+
+        if (mTrackerMap.containsKey(ifaceName)) {
+            throw new RuntimeException(ifaceName + " is already capturing");
+        }
+
+        final InterfaceParams ifParams = InterfaceParams.getByName(ifaceName);
+        Objects.requireNonNull(ifParams, "invalid interface " + ifaceName);
+
+        final ConnectivityPacketTracker tracker =
+                mDeps.createPacketTracker(mHandler, ifParams, MAX_PACKET_RECORDS);
+        tracker.start(TAG + "." + ifaceName);
+        mTrackerMap.putIfAbsent(ifaceName, tracker);
+        tracker.setCapture(true);
+
+        // remove scheduled stop events if it already in the queue
+        mHandler.removeMessages(CMD_STOP_CAPTURE, ifaceName);
+
+        // capture up to configured capture time and stop capturing
+        final Message stopMsg = mHandler.obtainMessage(CMD_STOP_CAPTURE, ifaceName);
+        mHandler.sendMessageDelayed(stopMsg, maxCaptureTimeMs);
+    }
+
+    /**
+     * Stops capturing packets on the specified network interface.
+     *
+     * <p>Terminates the packet capture session if one is active for the given interface.
+     * Any pending stop capture events for the interface are canceled.</p>
+     *
+     * @param ifaceName The name of the network interface to stop capturing on.
+     * @throws RuntimeException If no capture is running on the specified interface.
+     * @throws IllegalStateException If this method is not running on handler thread
+     */
+    public void stopCapture(String ifaceName) throws RuntimeException, IllegalStateException {
+        ensureRunOnHandlerThread();
+        if (!mTrackerMap.containsKey(ifaceName)) {
+            throw new RuntimeException(ifaceName + " is already stopped");
+        }
+
+        final Message msg = mHandler.obtainMessage(CMD_STOP_CAPTURE, ifaceName);
+        // remove scheduled stop events if it already in the queue
+        mHandler.removeMessages(CMD_STOP_CAPTURE, ifaceName);
+        mHandler.sendMessage(msg);
+    }
+
+    /**
+     * Returns the {@link Handler} associated with this RawTracker.
+     *
+     * <p>This handler is used for posting tasks to the RawTracker's internal thread.
+     * You can use it to execute code that needs to interact with the RawTracker
+     * in a thread-safe manner.
+     *
+     * @return The non-null {@link Handler} instance.
+     */
+    public @NonNull Handler getHandler() {
+        return mHandler;
+    }
+
+    /**
+     * Retrieves the number of captured packets matching a specific pattern.
+     *
+     * <p>Queries the packet capture data for the specified interface and counts the occurrences
+     * of packets that match the provided {@code packet} string. The count is performed
+     * asynchronously on the capture thread.</p>
+     *
+     * @param ifaceName The name of the network interface.
+     * @param packetPattern The packet pattern to match.
+     * @return The number of matched packets, or 0 if an error occurs or no matching packets are
+     *         found.
+     * @throws RuntimeException If no capture is running on the specified interface.
+     * @throws IllegalStateException If this method is not running on handler thread
+     */
+    public int getMatchedPacketCount(
+            String ifaceName, String packetPattern
+    ) throws RuntimeException, IllegalStateException {
+        ensureRunOnHandlerThread();
+        final ConnectivityPacketTracker tracker;
+        tracker = mTrackerMap.getOrDefault(ifaceName, null);
+        if (tracker == null) {
+            throw new RuntimeException(ifaceName + " is not capturing");
+        }
+
+        return tracker.getMatchedPacketCount(packetPattern);
+    }
+
+    private void processStopCapture(String ifaceName) {
+        final ConnectivityPacketTracker tracker = mTrackerMap.get(ifaceName);
+        mTrackerMap.remove(ifaceName);
+        tracker.setCapture(false);
+    }
+
+    private void ensureRunOnHandlerThread() {
+        if (mHandler.getLooper() != Looper.myLooper()) {
+            throw new IllegalStateException(
+                "Not running on Handler thread: " + Thread.currentThread().getName()
+            );
+        }
+    }
+}
diff --git a/src/android/net/util/RawSocketUtils.java b/src/android/net/util/RawSocketUtils.java
index a6c8a40..5823dc4 100644
--- a/src/android/net/util/RawSocketUtils.java
+++ b/src/android/net/util/RawSocketUtils.java
@@ -92,9 +92,9 @@
     }
 
     @RequiresPermission(NETWORK_SETTINGS)
-    private static void enforceTetheredInterface(@NonNull Context context,
+    public static void enforceTetheredInterface(@NonNull Context context,
                                                @NonNull String interfaceName)
-            throws ExecutionException, InterruptedException, TimeoutException {
+            throws ExecutionException, InterruptedException, TimeoutException, SecurityException {
         final TetheringManager tm = context.getSystemService(TetheringManager.class);
         final CompletableFuture<List<String>> tetheredInterfaces = new CompletableFuture<>();
         final TetheringManager.TetheringEventCallback callback =
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index 4d10c3b..76b5e38 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -19,6 +19,7 @@
 import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
+import static android.net.util.RawPacketTracker.MAX_CAPTURE_TIME_MS;
 import static android.net.util.RawSocketUtils.sendRawPacketDownStream;
 
 import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig;
@@ -50,6 +51,8 @@
 import android.net.ip.IpClient;
 import android.net.networkstack.aidl.NetworkMonitorParameters;
 import android.net.shared.PrivateDnsConfig;
+import android.net.util.RawPacketTracker;
+import android.net.util.RawSocketUtils;
 import android.os.Build;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -67,6 +70,7 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.modules.utils.BasicShellCommandHandler;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.NetworkStackNotifier;
 import com.android.networkstack.R;
@@ -90,6 +94,8 @@
 import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Android service used to start the network stack when bound to via an intent.
@@ -99,6 +105,7 @@
 public class NetworkStackService extends Service {
     private static final String TAG = NetworkStackService.class.getSimpleName();
     private static NetworkStackConnector sConnector;
+    private static final RawPacketTracker sRawPacketTracker = new RawPacketTracker();
 
     /**
      * Create a binder connector for the system server to communicate with the network stack.
@@ -523,6 +530,8 @@
         }
 
         private class ShellCmd extends BasicShellCommandHandler {
+            private static final long MAX_CAPTURE_CMD_WAITING_TIMEOUT_MS = 30_000L;
+
             @Override
             public int onCommand(String cmd) {
                 if (cmd == null) {
@@ -567,6 +576,14 @@
                         }
                         return 0;
                     }
+                    case "capture":
+                        // Usage: cmd network_stack capture <cmd>
+                        HandlerUtils.runWithScissorsForDump(
+                                sRawPacketTracker.getHandler(),
+                                () -> captureShellCommand(mContext, peekRemainingArgs()),
+                                MAX_CAPTURE_CMD_WAITING_TIMEOUT_MS
+                        );
+                        return 0;
                     case "apf":
                         // Usage: cmd network_stack apf <iface> <cmd>
                         final String iface = getNextArg();
@@ -609,6 +626,18 @@
                 pw.println("      to tethering downstream for security considerations.");
                 pw.println("    <packet_in_hex>: A valid hexadecimal representation of ");
                 pw.println("      a packet starting from L2 header.");
+                pw.println("  capture <cmd>");
+                pw.println("    APF utility commands for multi-devices tests.");
+                pw.println("    start <interface>");
+                pw.println("      start capture packets in the received buffer.");
+                pw.println("      The capture is up to 300 sec, then it will stop.");
+                pw.println("      <interface>: Target interface name, note that this is limited");
+                pw.println("        to tethering downstream for security considerations.");
+                pw.println("    stop <interface>");
+                pw.println("      stop capture packets and clear the received buffer.");
+                pw.println("    matched-packet-counts <interface> <pkt-hex-string>");
+                pw.println("      the <pkt-hex-string> starts from ether header.");
+                pw.println("      Expect to do full packet match.");
                 pw.println("  apf <iface> <cmd>");
                 pw.println("    APF utility commands for integration tests.");
                 pw.println("    <iface>: the network interface the provided command operates on.");
@@ -628,6 +657,73 @@
                 pw.println("      read");
                 pw.println("        reads and returns the current state of APF memory.");
             }
+
+            private void captureShellCommand(
+                    @NonNull Context context,
+                    @NonNull String[] args
+            ) {
+                if (args.length < 2) {
+                    throw new IllegalArgumentException("Incorrect number of arguments");
+                }
+
+                final String cmd = args[0];
+                final String ifaceName = args[1];
+                try {
+                    RawSocketUtils.enforceTetheredInterface(context, ifaceName);
+                } catch (ExecutionException
+                         | InterruptedException
+                         | TimeoutException
+                         | SecurityException e) {
+                    throw new RuntimeException(e.getMessage());
+                }
+
+                final PrintWriter pw = getOutPrintWriter();
+                switch(cmd) {
+                    case "start":
+                        // Usage : cmd network_stack capture start <interface>
+                        if (args.length != 2) {
+                            throw new IllegalArgumentException("Incorrect number of arguments");
+                        }
+
+                        sRawPacketTracker.startCapture(ifaceName, MAX_CAPTURE_TIME_MS);
+                        pw.println("success");
+                        break;
+                    case "matched-packet-counts":
+                        // Usage : cmd network_stack capture matched-packet-counts
+                        //         <interface> <packet-in-hex>
+                        // for example, there is an usage to get matched arp reply packet count
+                        // in hex string format on the wlan0 interface
+                        // cmd network_stack capture matched-packet-counts wlan0 \
+                        // "00010203040501020304050608060001080006040002010203040506c0a80101" +
+                        // "000102030405c0a80102"
+                        if (args.length != 3) {
+                            throw new IllegalArgumentException("Incorrect number of arguments");
+                        }
+
+                        final String packetInHex = args[2];
+
+                        // limit the input hex string up to 3000 (1500 bytes)
+                        if (packetInHex.length() > 3000) {
+                            throw new IllegalArgumentException("Packet Hex String over the limit");
+                        }
+
+                        final int pktCnt =
+                                sRawPacketTracker.getMatchedPacketCount(ifaceName, packetInHex);
+                        pw.println(pktCnt);
+                        break;
+                    case "stop":
+                        // Usage : cmd network_stack capture stop <interface>
+                        if (args.length != 2) {
+                            throw new IllegalArgumentException("Incorrect number of arguments");
+                        }
+
+                        sRawPacketTracker.stopCapture(ifaceName);
+                        pw.println("success");
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Invalid apf command: " + cmd);
+                }
+            }
         }
 
         /**
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 60bb927..1a36f25 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -177,7 +177,6 @@
 import com.android.networkstack.apishim.common.NetworkAgentConfigShim;
 import com.android.networkstack.apishim.common.NetworkInformationShim;
 import com.android.networkstack.apishim.common.ShimUtils;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.networkstack.metrics.DataStallDetectionStats;
 import com.android.networkstack.metrics.DataStallStatsUtils;
 import com.android.networkstack.metrics.NetworkValidationMetrics;
@@ -611,13 +610,6 @@
         } catch (RemoteException e) {
             version = 0;
         }
-        // The AIDL was freezed from Q beta 5 but it's unfreezing from R before releasing. In order
-        // to distinguish the behavior between R and Q beta 5 and before Q beta 5, add SDK and
-        // CODENAME check here. Basically, it's only expected to return 0 for Q beta 4 and below
-        // because the test result has changed.
-        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q
-                && Build.VERSION.CODENAME.equals("REL")
-                && version == Build.VERSION_CODES.CUR_DEVELOPMENT) version = 0;
         return version;
     }
 
@@ -3359,11 +3351,6 @@
             } catch (JSONException e) {
                 validationLog("Could not parse capport API JSON: " + e.getMessage());
                 return null;
-            } catch (UnsupportedApiLevelException e) {
-                // This should never happen because LinkProperties would not have a capport URL
-                // before R.
-                validationLog("Platform API too low to support capport API");
-                return null;
             }
         }
 
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index f7e1c4d..469a944 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -86,6 +86,7 @@
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION;
@@ -219,6 +220,7 @@
 import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
 import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
 import com.android.net.module.util.netlink.StructNdOptPref64;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.IaPrefixOption;
@@ -313,6 +315,7 @@
 
     // TODO: move to NetlinkConstants, NetworkStackConstants, or OsConstants.
     private static final int IFA_F_STABLE_PRIVACY = 0x800;
+    private static final int IFNAMSIZ = 16;
     // To fix below AndroidLint warning:
     // [InlinedApi] Field requires version 3 of the U Extensions SDK (current min is 0).
     private static final int RTN_UNREACHABLE =
@@ -5733,6 +5736,53 @@
     }
 
     @Test
+    @SignatureRequiredTest(
+            reason = "NetlinkUtils.sendRtmSetLinkStateRequest requires CAP_NET_ADMIN")
+    public void testSendRtmSetLinkStateMethod() throws Exception {
+        doDualStackProvisioning();
+
+        // Check if the interface has been up.
+        assertTrue(isIfaceUp(mIfaceName));
+
+        // Set the interface down.
+        assertTrue(NetlinkUtils.sendRtmSetLinkStateRequest(mIfaceName, false));
+        assertFalse(isIfaceUp(mIfaceName));
+
+        // Set the interface up again.
+        assertTrue(NetlinkUtils.sendRtmSetLinkStateRequest(mIfaceName, true));
+        assertTrue(isIfaceUp(mIfaceName));
+    }
+
+    private boolean isIfaceUp(@NonNull String ifaceName) {
+        final RtNetlinkLinkMessage msg = NetlinkUtils.getLinkRequest(ifaceName);
+        assertNotNull(msg);
+        return (msg.getIfinfoHeader().flags & IFF_UP) != 0;
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "NetlinkUtils.sendRtmSetLinkNameRequest requires CAP_NET_ADMIN")
+    public void testSendRtmSetLinkNameMethod() {
+        final int ifaceIndex = Os.if_nametoindex(mIfaceName);
+        assertNotEquals(0, ifaceIndex);
+
+        final String newName = findUnusedIfaceName();
+        assertTrue(NetlinkUtils.sendRtmSetLinkNameRequest(mIfaceName, newName));
+        assertEquals(ifaceIndex, Os.if_nametoindex(newName));
+    }
+
+    private String findUnusedIfaceName() {
+        int suffix = 0;
+        while (true) {
+            final String newName = mIfaceName + suffix;
+            assertTrue(newName.length() + 1 <= IFNAMSIZ);
+            if (Os.if_nametoindex(newName) == 0) {
+                return newName;
+            }
+            suffix++;
+        }
+    }
+
+    @Test
     @SignatureRequiredTest(reason = "requires mocked netd to read/write IPv6 sysctl")
     public void testIpv6SysctlsRestAfterStoppingIpClient() throws Exception {
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
diff --git a/tests/unit/src/android/net/util/RawPacketTrackerTest.kt b/tests/unit/src/android/net/util/RawPacketTrackerTest.kt
new file mode 100644
index 0000000..cecd7a0
--- /dev/null
+++ b/tests/unit/src/android/net/util/RawPacketTrackerTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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 android.net.util
+
+import android.net.ip.ConnectivityPacketTracker
+import android.os.HandlerThread
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.FunctionalUtils.ThrowingSupplier
+import com.android.testutils.assertThrows
+import com.android.testutils.visibleOnHandlerThread
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+
+/**
+ * Test for RawPacketTracker.
+ */
+@SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
+class RawPacketTrackerTest {
+    companion object {
+        private const val TEST_TIMEOUT_MS: Long = 1000
+        private const val TEST_MAX_CAPTURE_TIME_MS: Long = 1000
+        private const val TAG = "RawPacketTrackerTest"
+    }
+
+    private val deps = mock(RawPacketTracker.Dependencies::class.java)
+    private val tracker = mock(ConnectivityPacketTracker::class.java)
+    private val ifaceName = "lo"
+    private val handlerThread by lazy {
+        HandlerThread("$TAG-handler-thread").apply { start() }
+    }
+    private lateinit var rawTracker: RawPacketTracker
+
+    @Before
+    fun setUp() {
+        doReturn(handlerThread).`when`(deps).createHandlerThread()
+        doReturn(handlerThread.looper).`when`(deps).getLooper(any())
+        doReturn(tracker).`when`(deps).createPacketTracker(any(), any(), anyInt())
+        rawTracker = RawPacketTracker(deps)
+    }
+
+    @After
+    fun tearDown() {
+        Mockito.framework().clearInlineMocks()
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    @Test
+    fun testStartCapture() {
+        // start capturing
+        startCaptureOnHandler(ifaceName)
+        verifySetCapture(true, 1)
+
+        assertTrue(rawTracker.handler.hasMessages(RawPacketTracker.CMD_STOP_CAPTURE))
+    }
+
+    @Test
+    fun testInvalidStartCapture() {
+        // start capturing with negative timeout
+        assertThrows(IllegalArgumentException::class.java) {
+            startCaptureOnHandler(ifaceName, -1)
+        }
+    }
+
+    @Test
+    fun testStopCapture() {
+        // start capturing
+        startCaptureOnHandler(ifaceName)
+        // simulate capture status for stop capturing
+        verifySetCapture(true, 1)
+
+        // stop capturing
+        stopCaptureOnHandler(ifaceName)
+        verifySetCapture(false, 1)
+        verifyZeroInteractions(tracker)
+    }
+
+    @Test
+    fun testDuplicatedStartAndStop() {
+        // start capture with a long timeout
+        startCaptureOnHandler(ifaceName, 10_000)
+        verifySetCapture(true, 1)
+
+        // start capturing for multiple times
+        for (i in 1..10) {
+            assertThrows(RuntimeException::class.java) {
+                startCaptureOnHandler(ifaceName)
+            }
+        }
+
+        // expect no duplicated start capture
+        verifySetCapture(true, 0)
+
+        // stop capturing for multiple times
+        stopCaptureOnHandler(ifaceName)
+        verifySetCapture(false, 1)
+        for (i in 1..10) {
+            assertThrows(RuntimeException::class.java) {
+                stopCaptureOnHandler(ifaceName)
+            }
+        }
+
+        verifySetCapture(false, 0)
+        verifyZeroInteractions(tracker)
+    }
+
+    @Test
+    fun testMatchedPacketCount() {
+        val matchedPkt = "12345"
+        val notMatchedPkt = "54321"
+
+        // simulate get matched packet count
+        doReturn(1).`when`(tracker).getMatchedPacketCount(matchedPkt)
+        // simulate get not matched packet count
+        doReturn(0).`when`(tracker).getMatchedPacketCount(notMatchedPkt)
+
+        // start capture
+        startCaptureOnHandler(ifaceName)
+
+        assertEquals(1, getMatchedPktCntOnHandler(ifaceName, matchedPkt))
+        assertEquals(0, getMatchedPktCntOnHandler(ifaceName, notMatchedPkt))
+
+        // for non-existed interface
+        val nonExistedIface = "non-existed-iface"
+        assertThrows(RuntimeException::class.java) {
+            getMatchedPktCntOnHandler(nonExistedIface, matchedPkt)
+            getMatchedPktCntOnHandler(nonExistedIface, notMatchedPkt)
+        }
+
+        // stop capture
+        stopCaptureOnHandler(ifaceName)
+
+        // expect no matched packet after stop capturing
+        assertThrows(RuntimeException::class.java) {
+            getMatchedPktCntOnHandler(ifaceName, matchedPkt)
+            getMatchedPktCntOnHandler(ifaceName, notMatchedPkt)
+        }
+    }
+
+    private fun startCaptureOnHandler(
+        ifaceName: String, maxCaptureTime: Long = TEST_MAX_CAPTURE_TIME_MS
+    ) {
+        visibleOnHandlerThread(rawTracker.handler) {
+            rawTracker.startCapture(ifaceName, maxCaptureTime)
+        }
+    }
+
+    private fun stopCaptureOnHandler(ifaceName: String) {
+        visibleOnHandlerThread(rawTracker.handler) {
+            rawTracker.stopCapture(ifaceName)
+        }
+    }
+
+    private fun getMatchedPktCntOnHandler(ifaceName: String, packetPattern: String): Int {
+        return visibleOnHandlerThread(rawTracker.handler, ThrowingSupplier {
+            rawTracker.getMatchedPacketCount(ifaceName, packetPattern)
+        })
+    }
+
+    private fun verifySetCapture(
+        isCapture: Boolean,
+        receiveCnt: Int
+    ) {
+        verify(tracker, timeout(TEST_TIMEOUT_MS).times(receiveCnt)).setCapture(eq(isCapture))
+        clearInvocations<Any>(tracker)
+    }
+}
\ No newline at end of file