Implement NetworkRequestsState atom

Bug: 278833398
Test: atest FrameworksNetTests
Change-Id: I95a8c8dddd2d8f6863fe64cd4a65ef48dfb5153a
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 7339d08..3391323 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -328,6 +328,7 @@
 import com.android.server.connectivity.NetworkOffer;
 import com.android.server.connectivity.NetworkPreferenceList;
 import com.android.server.connectivity.NetworkRanker;
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics;
 import com.android.server.connectivity.PermissionMonitor;
 import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
 import com.android.server.connectivity.ProxyTracker;
@@ -941,6 +942,8 @@
 
     private final IpConnectivityLog mMetricsLog;
 
+    @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
     @GuardedBy("mBandwidthRequests")
     private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
 
@@ -1422,6 +1425,19 @@
         }
 
         /**
+         * @see NetworkRequestStateStatsMetrics
+         */
+        public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
+                Context context) {
+            // We currently have network requests metric for Watch devices only
+            if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+                return new NetworkRequestStateStatsMetrics();
+            } else {
+                return null;
+            }
+        }
+
+        /**
          * @see BatteryStatsManager
          */
         public void reportNetworkInterfaceForTransports(Context context, String iface,
@@ -1654,6 +1670,7 @@
                 new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
 
         mMetricsLog = logger;
+        mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
         final NetworkRequest defaultInternetRequest = createDefaultRequest();
         mDefaultRequest = new NetworkRequestInfo(
                 Process.myUid(), defaultInternetRequest, null,
@@ -5324,6 +5341,8 @@
                             updateSignalStrengthThresholds(network, "REGISTER", req);
                         }
                     }
+                } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                    mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
                 }
             }
 
@@ -5541,6 +5560,8 @@
             }
             if (req.isListen()) {
                 removeListenRequestFromNetworks(req);
+            } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+                mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
             }
         }
         nri.unlinkDeathRecipient();
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
new file mode 100644
index 0000000..ab3d315
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateInfo.java
@@ -0,0 +1,108 @@
+/*
+ * 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.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.SystemClock;
+
+import com.android.net.module.util.BitUtils;
+
+
+class NetworkRequestStateInfo {
+    private final NetworkRequest mNetworkRequest;
+    private final long mNetworkRequestReceivedTime;
+
+    private enum NetworkRequestState {
+        RECEIVED,
+        REMOVED
+    }
+    private NetworkRequestState mNetworkRequestState;
+    private int mNetworkRequestDurationMillis;
+    private final Dependencies mDependencies;
+
+    NetworkRequestStateInfo(NetworkRequest networkRequest,
+            Dependencies deps) {
+        mDependencies = deps;
+        mNetworkRequest = networkRequest;
+        mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
+        mNetworkRequestDurationMillis = 0;
+        mNetworkRequestState = NetworkRequestState.RECEIVED;
+    }
+
+    NetworkRequestStateInfo(NetworkRequestStateInfo anotherNetworkRequestStateInfo) {
+        mDependencies = anotherNetworkRequestStateInfo.mDependencies;
+        mNetworkRequest = new NetworkRequest(anotherNetworkRequestStateInfo.mNetworkRequest);
+        mNetworkRequestReceivedTime = anotherNetworkRequestStateInfo.mNetworkRequestReceivedTime;
+        mNetworkRequestDurationMillis =
+                anotherNetworkRequestStateInfo.mNetworkRequestDurationMillis;
+        mNetworkRequestState = anotherNetworkRequestStateInfo.mNetworkRequestState;
+    }
+
+    public void setNetworkRequestRemoved() {
+        mNetworkRequestState = NetworkRequestState.REMOVED;
+        mNetworkRequestDurationMillis = (int) (
+                mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
+    }
+
+    public int getNetworkRequestStateStatsType() {
+        if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
+            return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+        } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
+            return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+        } else {
+            return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+        }
+    }
+
+    public int getRequestId() {
+        return mNetworkRequest.requestId;
+    }
+
+    public int getPackageUid() {
+        return mNetworkRequest.networkCapabilities.getRequestorUid();
+    }
+
+    public int getTransportTypes() {
+        return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
+    }
+
+    public boolean getNetCapabilityNotMetered() {
+        return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+    }
+
+    public boolean getNetCapabilityInternet() {
+        return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    public int getNetworkRequestDurationMillis() {
+        return mNetworkRequestDurationMillis;
+    }
+
+    /** Dependency class */
+    public static class Dependencies {
+        // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
+        // relative to start time and avoid timezone change, including time spent in deep sleep.
+        public long getElapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
new file mode 100644
index 0000000..1bc654a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRequestStateStatsMetrics.java
@@ -0,0 +1,228 @@
+/*
+ * 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.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
+
+import android.annotation.NonNull;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.util.ArrayDeque;
+
+/**
+ * A Connectivity Service helper class to push atoms capturing network requests have been received
+ * and removed and its metadata.
+ *
+ * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
+ * hashmap is stored to calculate network request duration when it is removed.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+public class NetworkRequestStateStatsMetrics {
+
+    private static final String TAG = "NetworkRequestStateStatsMetrics";
+    private static final int CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC = 0;
+    private static final int CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC = 1;
+
+    @VisibleForTesting
+    static final int MAX_QUEUED_REQUESTS = 20;
+
+    // Stats logging frequency is limited to 10 ms at least, 500ms are taken as a safely margin
+    // for cases of longer periods of frequent network requests.
+    private static final int ATOM_INTERVAL_MS = 500;
+    private final StatsLoggingHandler mStatsLoggingHandler;
+
+    private final Dependencies mDependencies;
+
+    private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+    private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+
+    public NetworkRequestStateStatsMetrics() {
+        this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
+    }
+
+    @VisibleForTesting
+    NetworkRequestStateStatsMetrics(Dependencies deps,
+            NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
+        mNetworkRequestsActive = new SparseArray<>();
+        mDependencies = deps;
+        mNRStateInfoDeps = nrStateInfoDeps;
+        HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
+        handlerThread.start();
+        mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
+    }
+
+    /**
+     * Register network request receive event, push RECEIVE atom
+     *
+     * @param networkRequest network request received
+     */
+    public void onNetworkRequestReceived(NetworkRequest networkRequest) {
+        if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
+            Log.w(TAG, "Received already registered network request, id = "
+                    + networkRequest.requestId);
+        } else {
+            Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
+                    + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
+            NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+                    networkRequest, mNRStateInfoDeps);
+            mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
+            mStatsLoggingHandler.sendMessage(Message.obtain(
+                    mStatsLoggingHandler,
+                    CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+                    networkRequestStateInfo));
+        }
+    }
+
+    /**
+     * Register network request remove event, push REMOVE atom
+     *
+     * @param networkRequest network request removed
+     */
+    public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
+        NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
+                networkRequest.requestId);
+        if (networkRequestStateInfo == null) {
+            Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
+        } else {
+            Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
+            mNetworkRequestsActive.remove(networkRequest.requestId);
+            networkRequestStateInfo = new NetworkRequestStateInfo(networkRequestStateInfo);
+            networkRequestStateInfo.setNetworkRequestRemoved();
+            mStatsLoggingHandler.sendMessage(Message.obtain(
+                    mStatsLoggingHandler,
+                    CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC,
+                    networkRequestStateInfo));
+        }
+    }
+
+    /** Dependency class */
+    public static class Dependencies {
+        /**
+         * Creates a thread with provided tag.
+         *
+         * @param tag for the thread.
+         */
+        public HandlerThread makeHandlerThread(@NonNull final String tag) {
+            return new HandlerThread(tag);
+        }
+
+        /**
+         * @see Handler#sendMessageDelayed(Message, long)
+         */
+        public void sendMessageDelayed(@NonNull Handler handler, int what, long delayMillis) {
+            handler.sendMessageDelayed(Message.obtain(handler, what), delayMillis);
+        }
+
+        /**
+         * Gets number of millis since event.
+         *
+         * @param eventTimeMillis long timestamp in millis when the event occurred.
+         */
+        public long getMillisSinceEvent(long eventTimeMillis) {
+            return SystemClock.elapsedRealtime() - eventTimeMillis;
+        }
+
+        /**
+         * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
+         *
+         * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
+         */
+        public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
+            ConnectivityStatsLog.write(
+                    NETWORK_REQUEST_STATE_CHANGED,
+                    networkRequestStateInfo.getPackageUid(),
+                    networkRequestStateInfo.getTransportTypes(),
+                    networkRequestStateInfo.getNetCapabilityNotMetered(),
+                    networkRequestStateInfo.getNetCapabilityInternet(),
+                    networkRequestStateInfo.getNetworkRequestStateStatsType(),
+                    networkRequestStateInfo.getNetworkRequestDurationMillis());
+        }
+    }
+
+    private class StatsLoggingHandler extends Handler {
+        private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
+
+        private final ArrayDeque<NetworkRequestStateInfo> mPendingState = new ArrayDeque<>();
+
+        private long mLastLogTime = 0;
+
+        StatsLoggingHandler(Looper looper) {
+            super(looper);
+        }
+
+        private void maybeEnqueueStatsMessage(NetworkRequestStateInfo networkRequestStateInfo) {
+            if (mPendingState.size() < MAX_QUEUED_REQUESTS) {
+                mPendingState.add(networkRequestStateInfo);
+            } else {
+                Log.w(TAG, "Too many network requests received within last " + ATOM_INTERVAL_MS
+                        + " ms, dropping the last network request (id = "
+                        + networkRequestStateInfo.getRequestId() + ") event");
+                return;
+            }
+            if (hasMessages(CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC)) {
+                return;
+            }
+            long millisSinceLastLog = mDependencies.getMillisSinceEvent(mLastLogTime);
+
+            if (millisSinceLastLog >= ATOM_INTERVAL_MS) {
+                sendMessage(
+                        Message.obtain(this, CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC));
+            } else {
+                mDependencies.sendMessageDelayed(
+                        this,
+                        CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+                        ATOM_INTERVAL_MS - millisSinceLastLog);
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            NetworkRequestStateInfo loggingInfo;
+            switch (msg.what) {
+                case CMD_SEND_MAYBE_ENQUEUE_NETWORK_REQUEST_STATE_METRIC:
+                    maybeEnqueueStatsMessage((NetworkRequestStateInfo) msg.obj);
+                    break;
+                case CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC:
+                    mLastLogTime = SystemClock.elapsedRealtime();
+                    if (!mPendingState.isEmpty()) {
+                        loggingInfo = mPendingState.remove();
+                        mDependencies.writeStats(loggingInfo);
+                        if (!mPendingState.isEmpty()) {
+                            mDependencies.sendMessageDelayed(
+                                    this,
+                                    CMD_SEND_PENDING_NETWORK_REQUEST_STATE_METRIC,
+                                    ATOM_INTERVAL_MS);
+                        }
+                    }
+                    break;
+                default: // fall out
+            }
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
new file mode 100644
index 0000000..44a645a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateInfoTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class NetworkRequestStateInfoTest {
+
+    @Mock
+    private NetworkRequestStateInfo.Dependencies mDependencies;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+    @Test
+    public void testSetNetworkRequestRemoved() {
+        final long nrStartTime = 1L;
+        final long nrEndTime = 101L;
+
+        NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
+                new NetworkCapabilities()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                        .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
+                0, 1, NetworkRequest.Type.REQUEST
+        );
+
+        // This call will be used to calculate NR received time
+        Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
+        NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+                notMeteredWifiNetworkRequest, mDependencies);
+
+        // This call will be used to calculate NR removed time
+        Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
+        networkRequestStateInfo.setNetworkRequestRemoved();
+        assertEquals(
+                nrEndTime - nrStartTime,
+                networkRequestStateInfo.getNetworkRequestDurationMillis());
+        assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+                NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
+    }
+
+    @Test
+    public void testCheckInitialState() {
+        NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+                new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
+                mDependencies);
+        assertEquals(networkRequestStateInfo.getNetworkRequestStateStatsType(),
+                NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
new file mode 100644
index 0000000..e8b62f8
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRequestStateStatsMetricsTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.server.connectivity;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestStateStatsMetricsTest {
+    @Mock
+    private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
+    @Mock
+    private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+    @Captor
+    private ArgumentCaptor<Handler> mHandlerCaptor;
+    @Captor
+    private ArgumentCaptor<Integer> mMessageWhatCaptor;
+
+    private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+    private HandlerThread mHandlerThread;
+    private static final int TEST_REQUEST_ID = 10;
+    private static final int TEST_PACKAGE_UID = 20;
+    private static final int TIMEOUT_MS = 30_000;
+    private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
+            new NetworkCapabilities()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
+                    .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
+                    .setRequestorUid(TEST_PACKAGE_UID),
+            0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
+    );
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
+        Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
+                .thenReturn(mHandlerThread);
+        Mockito.when(mNRStateStatsDeps.getMillisSinceEvent(anyLong())).thenReturn(0L);
+        Mockito.doAnswer(invocation -> {
+            mHandlerCaptor.getValue().sendMessage(
+                    Message.obtain(mHandlerCaptor.getValue(), mMessageWhatCaptor.getValue()));
+            return null;
+        }).when(mNRStateStatsDeps).sendMessageDelayed(
+                mHandlerCaptor.capture(), mMessageWhatCaptor.capture(), anyLong());
+        mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
+                mNRStateStatsDeps, mNRStateInfoDeps);
+    }
+
+    @Test
+    public void testNetworkRequestReceivedRemoved() {
+        final long nrStartTime = 1L;
+        final long nrEndTime = 101L;
+        // This call will be used to calculate NR received time
+        Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+
+        ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+                ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+        verify(mNRStateStatsDeps, times(1))
+                .writeStats(networkRequestStateInfoCaptor.capture());
+
+        NetworkRequestStateInfo nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+        assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
+                nrStateInfoSent.getNetworkRequestStateStatsType());
+        assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+        assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+        assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+        assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+        assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+        assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
+
+        clearInvocations(mNRStateStatsDeps);
+        // This call will be used to calculate NR removed time
+        Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
+        mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+
+        verify(mNRStateStatsDeps, times(1))
+                .writeStats(networkRequestStateInfoCaptor.capture());
+
+        nrStateInfoSent = networkRequestStateInfoCaptor.getValue();
+        assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+                nrStateInfoSent.getNetworkRequestStateStatsType());
+        assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+        assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+        assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+        assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+        assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+        assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
+    }
+
+    @Test
+    public void testUnreceivedNetworkRequestRemoved() {
+        mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+        verify(mNRStateStatsDeps, never())
+                .writeStats(any(NetworkRequestStateInfo.class));
+    }
+
+    @Test
+    public void testNoMessagesWhenNetworkRequestReceived() {
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+        verify(mNRStateStatsDeps, times(1))
+                .writeStats(any(NetworkRequestStateInfo.class));
+
+        clearInvocations(mNRStateStatsDeps);
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+        verify(mNRStateStatsDeps, never())
+                .writeStats(any(NetworkRequestStateInfo.class));
+    }
+
+    @Test
+    public void testMessageQueueSizeLimitNotExceeded() {
+        // Imitate many events (MAX_QUEUED_REQUESTS) are coming together at once while
+        // the other event is being processed.
+        final ConditionVariable cv = new ConditionVariable();
+        mHandlerThread.getThreadHandler().post(() -> cv.block());
+        for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS / 2; i++) {
+            mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+                    new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+                    0, i + 1, NetworkRequest.Type.REQUEST));
+            mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(new NetworkRequest(
+                    new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+                    0, i + 1, NetworkRequest.Type.REQUEST));
+        }
+
+        // When event queue is full, all other events should be dropped.
+        mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(new NetworkRequest(
+                new NetworkCapabilities().setRequestorUid(TEST_PACKAGE_UID),
+                0, 2 * NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS + 1,
+                NetworkRequest.Type.REQUEST));
+
+        cv.open();
+
+        // Check only first MAX_QUEUED_REQUESTS events are logged.
+        ArgumentCaptor<NetworkRequestStateInfo> networkRequestStateInfoCaptor =
+                ArgumentCaptor.forClass(NetworkRequestStateInfo.class);
+        verify(mNRStateStatsDeps, timeout(100).times(
+                NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS))
+                .writeStats(networkRequestStateInfoCaptor.capture());
+        for (int i = 0; i < NetworkRequestStateStatsMetrics.MAX_QUEUED_REQUESTS; i++) {
+            NetworkRequestStateInfo nrStateInfoSent =
+                    networkRequestStateInfoCaptor.getAllValues().get(i);
+            assertEquals(i / 2 + 1, nrStateInfoSent.getRequestId());
+            assertEquals(
+                    (i % 2 == 0)
+                            ? NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED
+                            : NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+                    nrStateInfoSent.getNetworkRequestStateStatsType());
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+    private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+            .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+    private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+            .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+    @Before
+    fun setup() {
+        waitForIdle()
+        clearInvocations(networkRequestStateStatsMetrics)
+    }
+
+    @Test
+    fun testRequestTypeNRProduceMetrics() {
+        cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+        waitForIdle()
+
+        verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+                argThat{req -> req.networkCapabilities.equals(
+                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+    }
+
+    @Test
+    fun testListenTypeNRProduceNoMetrics() {
+        cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+        waitForIdle()
+        verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+    }
+
+    @Test
+    fun testRemoveRequestTypeNRProduceMetrics() {
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+        waitForIdle()
+        clearInvocations(networkRequestStateStatsMetrics)
+
+        cm.unregisterNetworkCallback(cb)
+        waitForIdle()
+        verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+                argThat{req -> req.networkCapabilities.equals(
+                        CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 958c4f2..21396f2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -63,6 +63,7 @@
 import com.android.server.connectivity.ConnectivityFlags
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
+import com.android.server.connectivity.NetworkRequestStateStatsMetrics
 import com.android.server.connectivity.ProxyTracker
 import com.android.testutils.visibleOnHandlerThread
 import com.android.testutils.waitForIdle
@@ -157,6 +158,7 @@
     val netd = mock<INetd>()
     val bpfNetMaps = mock<BpfNetMaps>()
     val clatCoordinator = mock<ClatCoordinator>()
+    val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
     val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
     val alarmManager = makeMockAlarmManager()
     val systemConfigManager = makeMockSystemConfigManager()
@@ -197,6 +199,9 @@
                 MultinetworkPolicyTracker(c, h, r,
                         MultinetworkPolicyTrackerTestDependencies(connResources.get()))
 
+        override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+                this@CSTest.networkRequestStateStatsMetrics
+
         // All queried features must be mocked, because the test cannot hold the
         // READ_DEVICE_CONFIG permission and device config utils use static methods for
         // checking permissions.