Create KeepaliveStatsTracker for Duration metrics.

Create a new class to store duration metrics of AutomaticOnOffKeepalives
for the DurationPerNumOfKeepalive proto.
This tracks the duration of time that the AutomaticOnOffKeepaliveTracker
contains a given number of concurrent keepalives and the duration of
time that it contains a given number of active concurrent keepalives.

Bug: 273451360
Test: atest FrameworksNetTests
Change-Id: If31ccbc7b02e6ad3ff1646e92e55ca7c8662e47c
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 881c92d..ee8ab68 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -177,6 +177,7 @@
     private static final int MAX_EVENTS_LOGS = 40;
     private final LocalLog mEventLog = new LocalLog(MAX_EVENTS_LOGS);
 
+    private final KeepaliveStatsTracker mKeepaliveStatsTracker = new KeepaliveStatsTracker();
     /**
      * Information about a managed keepalive.
      *
@@ -421,6 +422,7 @@
     public void handleStartKeepalive(Message message) {
         final AutomaticOnOffKeepalive autoKi = (AutomaticOnOffKeepalive) message.obj;
         mEventLog.log("Start keepalive " + autoKi.mCallback + " on " + autoKi.getNetwork());
+        mKeepaliveStatsTracker.onStartKeepalive();
         mKeepaliveTracker.handleStartKeepalive(autoKi.mKi);
 
         // Add automatic on/off request into list to track its life cycle.
@@ -438,12 +440,14 @@
     }
 
     private void handleResumeKeepalive(@NonNull final KeepaliveTracker.KeepaliveInfo ki) {
+        mKeepaliveStatsTracker.onResumeKeepalive();
         mKeepaliveTracker.handleStartKeepalive(ki);
         mEventLog.log("Resumed successfully keepalive " + ki.mCallback + " on " + ki.mNai);
     }
 
     private void handlePauseKeepalive(@NonNull final KeepaliveTracker.KeepaliveInfo ki) {
         mEventLog.log("Suspend keepalive " + ki.mCallback + " on " + ki.mNai);
+        mKeepaliveStatsTracker.onPauseKeepalive();
         // TODO : mKT.handleStopKeepalive should take a KeepaliveInfo instead
         mKeepaliveTracker.handleStopKeepalive(ki.getNai(), ki.getSlot(), SUCCESS_PAUSED);
     }
@@ -467,6 +471,7 @@
 
     private void cleanupAutoOnOffKeepalive(@NonNull final AutomaticOnOffKeepalive autoKi) {
         ensureRunningOnHandlerThread();
+        mKeepaliveStatsTracker.onStopKeepalive(autoKi.mAutomaticOnOffState != STATE_SUSPENDED);
         autoKi.close();
         if (null != autoKi.mAlarmListener) mAlarmManager.cancel(autoKi.mAlarmListener);
 
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
new file mode 100644
index 0000000..c461809
--- /dev/null
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -0,0 +1,186 @@
+/*
+ * 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 android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.metrics.DurationForNumOfKeepalive;
+import com.android.metrics.DurationPerNumOfKeepalive;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// TODO(b/273451360): Also track KeepaliveLifetimeForCarrier and DailykeepaliveInfoReported
+/**
+ * Tracks carrier and duration metrics of automatic on/off keepalives.
+ *
+ * <p>This class follows AutomaticOnOffKeepaliveTracker closely and its on*Keepalive methods needs
+ * to be called in a timely manner to keep the metrics accurate. It is also not thread-safe and all
+ * public methods must be called by the same thread, namely the ConnectivityService handler thread.
+ */
+public class KeepaliveStatsTracker {
+    private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
+
+    private final Dependencies mDependencies;
+    // List of duration stats metric where the index is the number of concurrent keepalives.
+    // Each DurationForNumOfKeepalive message stores a registered duration and an active duration.
+    // Registered duration is the total time spent with mNumRegisteredKeepalive == index.
+    // Active duration is the total time spent with mNumActiveKeepalive == index.
+    private final List<DurationForNumOfKeepalive.Builder> mDurationPerNumOfKeepalive =
+            new ArrayList<>();
+
+    private int mNumRegisteredKeepalive = 0;
+    private int mNumActiveKeepalive = 0;
+
+    // A timestamp of the most recent time the duration metrics was updated.
+    private long mTimestampSinceLastUpdateDurations;
+
+    /** Dependency class */
+    @VisibleForTesting
+    public static class Dependencies {
+        // Returns a timestamp with the time base of SystemClock.uptimeMillis to keep durations
+        // relative to start time and avoid timezone change.
+        public long getUptimeMillis() {
+            return SystemClock.uptimeMillis();
+        }
+    }
+
+    public KeepaliveStatsTracker() {
+        this(new Dependencies());
+    }
+
+    @VisibleForTesting
+    public KeepaliveStatsTracker(Dependencies dependencies) {
+        mDependencies = dependencies;
+        mTimestampSinceLastUpdateDurations = mDependencies.getUptimeMillis();
+    }
+
+    /** Ensures the list of duration metrics is large enough for number of registered keepalives. */
+    private void ensureDurationPerNumOfKeepaliveSize() {
+        if (mNumActiveKeepalive < 0 || mNumRegisteredKeepalive < 0) {
+            throw new IllegalStateException(
+                    "Number of active or registered keepalives is negative");
+        }
+        if (mNumActiveKeepalive > mNumRegisteredKeepalive) {
+            throw new IllegalStateException(
+                    "Number of active keepalives greater than registered keepalives");
+        }
+
+        while (mDurationPerNumOfKeepalive.size() <= mNumRegisteredKeepalive) {
+            final DurationForNumOfKeepalive.Builder durationForNumOfKeepalive =
+                    DurationForNumOfKeepalive.newBuilder();
+            durationForNumOfKeepalive.setNumOfKeepalive(mDurationPerNumOfKeepalive.size());
+            durationForNumOfKeepalive.setKeepaliveRegisteredDurationsMsec(0);
+            durationForNumOfKeepalive.setKeepaliveActiveDurationsMsec(0);
+
+            mDurationPerNumOfKeepalive.add(durationForNumOfKeepalive);
+        }
+    }
+
+    /**
+     * Updates the durations metrics to the given time. This should always be called before making a
+     * change to mNumRegisteredKeepalive or mNumActiveKeepalive to keep the duration metrics
+     * correct.
+     *
+     * @param timeNow a timestamp obtained using Dependencies.getUptimeMillis
+     */
+    private void updateDurationsPerNumOfKeepalive(long timeNow) {
+        if (mDurationPerNumOfKeepalive.size() < mNumRegisteredKeepalive) {
+            Log.e(TAG, "Unexpected jump in number of registered keepalive");
+        }
+        ensureDurationPerNumOfKeepaliveSize();
+
+        final int durationIncrease = (int) (timeNow - mTimestampSinceLastUpdateDurations);
+        final DurationForNumOfKeepalive.Builder durationForNumOfRegisteredKeepalive =
+                mDurationPerNumOfKeepalive.get(mNumRegisteredKeepalive);
+
+        durationForNumOfRegisteredKeepalive.setKeepaliveRegisteredDurationsMsec(
+                durationForNumOfRegisteredKeepalive.getKeepaliveRegisteredDurationsMsec()
+                        + durationIncrease);
+
+        final DurationForNumOfKeepalive.Builder durationForNumOfActiveKeepalive =
+                mDurationPerNumOfKeepalive.get(mNumActiveKeepalive);
+
+        durationForNumOfActiveKeepalive.setKeepaliveActiveDurationsMsec(
+                durationForNumOfActiveKeepalive.getKeepaliveActiveDurationsMsec()
+                        + durationIncrease);
+
+        mTimestampSinceLastUpdateDurations = timeNow;
+    }
+
+    /** Inform the KeepaliveStatsTracker a keepalive has just started and is active. */
+    public void onStartKeepalive() {
+        final long timeNow = mDependencies.getUptimeMillis();
+        updateDurationsPerNumOfKeepalive(timeNow);
+
+        mNumRegisteredKeepalive++;
+        mNumActiveKeepalive++;
+    }
+
+    /** Inform the KeepaliveStatsTracker a keepalive has just been paused. */
+    public void onPauseKeepalive() {
+        final long timeNow = mDependencies.getUptimeMillis();
+        updateDurationsPerNumOfKeepalive(timeNow);
+
+        mNumActiveKeepalive--;
+    }
+
+    /** Inform the KeepaliveStatsTracker a keepalive has just been resumed. */
+    public void onResumeKeepalive() {
+        final long timeNow = mDependencies.getUptimeMillis();
+        updateDurationsPerNumOfKeepalive(timeNow);
+
+        mNumActiveKeepalive++;
+    }
+
+    /** Inform the KeepaliveStatsTracker a keepalive has just been stopped. */
+    public void onStopKeepalive(boolean wasActive) {
+        final long timeNow = mDependencies.getUptimeMillis();
+        updateDurationsPerNumOfKeepalive(timeNow);
+
+        mNumRegisteredKeepalive--;
+        if (wasActive) mNumActiveKeepalive--;
+    }
+
+    /**
+     * Builds and returns DurationPerNumOfKeepalive proto
+     *
+     * <p>TODO(b/273451360): Write DailykeepaliveInfoReported metrics to statsd
+     */
+    public DurationPerNumOfKeepalive buildKeepaliveMetrics() {
+        final long timeNow = mDependencies.getUptimeMillis();
+        updateDurationsPerNumOfKeepalive(timeNow);
+
+        final DurationPerNumOfKeepalive.Builder durationPerNumOfKeepalive =
+                DurationPerNumOfKeepalive.newBuilder();
+
+        mDurationPerNumOfKeepalive.forEach(
+                durationForNumOfKeepalive ->
+                        durationPerNumOfKeepalive.addDurationForNumOfKeepalive(
+                                durationForNumOfKeepalive));
+
+        return durationPerNumOfKeepalive.build();
+    }
+
+    /** Resets the stored metrics but maintains the state of keepalives */
+    public void resetMetrics() {
+        mDurationPerNumOfKeepalive.clear();
+        ensureDurationPerNumOfKeepaliveSize();
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
new file mode 100644
index 0000000..8837ba1
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2022 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 org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.metrics.DurationForNumOfKeepalive;
+import com.android.metrics.DurationPerNumOfKeepalive;
+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.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+public class KeepaliveStatsTrackerTest {
+
+    private KeepaliveStatsTracker mKeepaliveStatsTracker;
+    @Mock KeepaliveStatsTracker.Dependencies mDependencies;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        setUptimeMillis(0);
+        mKeepaliveStatsTracker = new KeepaliveStatsTracker(mDependencies);
+    }
+
+    private void setUptimeMillis(long time) {
+        doReturn(time).when(mDependencies).getUptimeMillis();
+    }
+
+    /**
+     * Asserts that a DurationPerNumOfKeepalive contains expected values
+     *
+     * @param expectRegisteredDurations integer array where the index is the number of concurrent
+     *     keepalives and the value is the expected duration of time that the tracker is in a state
+     *     with the given number of keepalives registered.
+     * @param expectActiveDurations integer array where the index is the number of concurrent
+     *     keepalives and the value is the expected duration of time that the tracker is in a state
+     *     with the given number of keepalives active.
+     * @param resultDurationsPerNumOfKeepalive the DurationPerNumOfKeepalive message to assert.
+     */
+    private void assertDurationMetrics(
+            int[] expectRegisteredDurations,
+            int[] expectActiveDurations,
+            DurationPerNumOfKeepalive resultDurationsPerNumOfKeepalive) {
+        final int maxNumOfKeepalive = expectRegisteredDurations.length;
+        assertEquals(maxNumOfKeepalive, expectActiveDurations.length);
+        assertEquals(
+                maxNumOfKeepalive,
+                resultDurationsPerNumOfKeepalive.getDurationForNumOfKeepaliveCount());
+        for (int numOfKeepalive = 0; numOfKeepalive < maxNumOfKeepalive; numOfKeepalive++) {
+            final DurationForNumOfKeepalive resultDurations =
+                    resultDurationsPerNumOfKeepalive.getDurationForNumOfKeepalive(numOfKeepalive);
+
+            assertEquals(numOfKeepalive, resultDurations.getNumOfKeepalive());
+            assertEquals(
+                    expectRegisteredDurations[numOfKeepalive],
+                    resultDurations.getKeepaliveRegisteredDurationsMsec());
+            assertEquals(
+                    expectActiveDurations[numOfKeepalive],
+                    resultDurations.getKeepaliveActiveDurationsMsec());
+        }
+    }
+
+    @Test
+    public void testNoKeepalive() {
+        final int writeTime = 5000;
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // Expect that the durations are all in numOfKeepalive = 0.
+        final int[] expectRegisteredDurations = new int[] {writeTime};
+        final int[] expectActiveDurations = new int[] {writeTime};
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S                          W
+     * Timeline  |------------------------------|
+     */
+    @Test
+    public void testOneKeepalive_startOnly() {
+        final int startTime = 1000;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // The keepalive is never stopped, expect the duration for numberOfKeepalive of 1 to range
+        // from startTime to writeTime.
+        final int[] expectRegisteredDurations = new int[] {startTime, writeTime - startTime};
+        final int[] expectActiveDurations = new int[] {startTime, writeTime - startTime};
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S       P                  W
+     * Timeline  |------------------------------|
+     */
+    @Test
+    public void testOneKeepalive_paused() {
+        final int startTime = 1000;
+        final int pauseTime = 2030;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(pauseTime);
+        mKeepaliveStatsTracker.onPauseKeepalive();
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // The keepalive is paused but not stopped, expect the registered duration for
+        // numberOfKeepalive of 1 to still range from startTime to writeTime while the active
+        // duration stops at pauseTime.
+        final int[] expectRegisteredDurations = new int[] {startTime, writeTime - startTime};
+        final int[] expectActiveDurations =
+                new int[] {startTime + (writeTime - pauseTime), pauseTime - startTime};
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S       P        R         W
+     * Timeline  |------------------------------|
+     */
+    @Test
+    public void testOneKeepalive_resumed() {
+        final int startTime = 1000;
+        final int pauseTime = 2030;
+        final int resumeTime = 3450;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(pauseTime);
+        mKeepaliveStatsTracker.onPauseKeepalive();
+
+        setUptimeMillis(resumeTime);
+        mKeepaliveStatsTracker.onResumeKeepalive();
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // The keepalive is paused and resumed but not stopped, expect the registered duration for
+        // numberOfKeepalive of 1 to still range from startTime to writeTime while the active
+        // duration stops at pauseTime but resumes at resumeTime and stops at writeTime.
+        final int[] expectRegisteredDurations = new int[] {startTime, writeTime - startTime};
+        final int[] expectActiveDurations =
+                new int[] {
+                    startTime + (resumeTime - pauseTime),
+                    (pauseTime - startTime) + (writeTime - resumeTime)
+                };
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S       P      R     S     W
+     * Timeline  |------------------------------|
+     */
+    @Test
+    public void testOneKeepalive_stopped() {
+        final int startTime = 1000;
+        final int pauseTime = 2930;
+        final int resumeTime = 3452;
+        final int stopTime = 4157;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(pauseTime);
+        mKeepaliveStatsTracker.onPauseKeepalive();
+
+        setUptimeMillis(resumeTime);
+        mKeepaliveStatsTracker.onResumeKeepalive();
+
+        setUptimeMillis(stopTime);
+        mKeepaliveStatsTracker.onStopKeepalive(/* wasActive= */ true);
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // The keepalive is now stopped, expect the registered duration for numberOfKeepalive of 1
+        // to now range from startTime to stopTime while the active duration stops at pauseTime but
+        // resumes at resumeTime and stops again at stopTime.
+        final int[] expectRegisteredDurations =
+                new int[] {startTime + (writeTime - stopTime), stopTime - startTime};
+        final int[] expectActiveDurations =
+                new int[] {
+                    startTime + (resumeTime - pauseTime) + (writeTime - stopTime),
+                    (pauseTime - startTime) + (stopTime - resumeTime)
+                };
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S       P            S     W
+     * Timeline  |------------------------------|
+     */
+    @Test
+    public void testOneKeepalive_pausedStopped() {
+        final int startTime = 1000;
+        final int pauseTime = 2930;
+        final int stopTime = 4157;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(pauseTime);
+        mKeepaliveStatsTracker.onPauseKeepalive();
+
+        setUptimeMillis(stopTime);
+        mKeepaliveStatsTracker.onStopKeepalive(/* wasActive= */ false);
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // The keepalive is stopped while paused, expect the registered duration for
+        // numberOfKeepalive of 1 to range from startTime to stopTime while the active duration
+        // simply stops at pauseTime.
+        final int[] expectRegisteredDurations =
+                new int[] {startTime + (writeTime - stopTime), stopTime - startTime};
+        final int[] expectActiveDurations =
+                new int[] {startTime + (writeTime - pauseTime), (pauseTime - startTime)};
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S  P R P R P R       S     W
+     * Timeline  |------------------------------|
+     */
+    @Test
+    public void testOneKeepalive_multiplePauses() {
+        final int startTime = 1000;
+        // Alternating timestamps of pause and resume
+        final int[] pauseResumeTimes = new int[] {1200, 1400, 1700, 2000, 2400, 2800};
+        final int stopTime = 4000;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        for (int i = 0; i < pauseResumeTimes.length; i++) {
+            setUptimeMillis(pauseResumeTimes[i]);
+            if (i % 2 == 0) {
+                mKeepaliveStatsTracker.onPauseKeepalive();
+            } else {
+                mKeepaliveStatsTracker.onResumeKeepalive();
+            }
+        }
+
+        setUptimeMillis(stopTime);
+        mKeepaliveStatsTracker.onStopKeepalive(/* wasActive= */ true);
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        final int[] expectRegisteredDurations =
+                new int[] {startTime + (writeTime - stopTime), stopTime - startTime};
+        final int[] expectActiveDurations =
+                new int[] {
+                    startTime + /* sum of (Resume - Pause) */ (900) + (writeTime - stopTime),
+                    (pauseResumeTimes[0] - startTime)
+                            + /* sum of (Pause - Resume) */ (700)
+                            + (stopTime - pauseResumeTimes[5])
+                };
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive1    S1  P1     R1         S1    W
+     * Keepalive2           S2     P2   R2       W
+     * Timeline   |------------------------------|
+     */
+    @Test
+    public void testTwoKeepalives() {
+        // The suffix 1/2 indicates which keepalive it is referring to.
+        final int startTime1 = 1000;
+        final int pauseTime1 = 1500;
+        final int startTime2 = 2000;
+        final int resumeTime1 = 2500;
+        final int pauseTime2 = 3000;
+        final int resumeTime2 = 3500;
+        final int stopTime1 = 4157;
+        final int writeTime = 5000;
+
+        setUptimeMillis(startTime1);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(pauseTime1);
+        mKeepaliveStatsTracker.onPauseKeepalive();
+
+        setUptimeMillis(startTime2);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(resumeTime1);
+        mKeepaliveStatsTracker.onResumeKeepalive();
+
+        setUptimeMillis(pauseTime2);
+        mKeepaliveStatsTracker.onPauseKeepalive();
+
+        setUptimeMillis(resumeTime2);
+        mKeepaliveStatsTracker.onResumeKeepalive();
+
+        setUptimeMillis(stopTime1);
+        mKeepaliveStatsTracker.onStopKeepalive(/* wasActive= */ true);
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // With two keepalives, the number of concurrent keepalives can vary from 0-2 depending on
+        // both keepalive states.
+        final int[] expectRegisteredDurations =
+                new int[] {
+                    startTime1,
+                    // 1 registered keepalive before keepalive2 starts and after keepalive1 stops.
+                    (startTime2 - startTime1) + (writeTime - stopTime1),
+                    // 2 registered keepalives between keepalive2 start and keepalive1 stop.
+                    stopTime1 - startTime2
+                };
+        final int[] expectActiveDurations =
+                new int[] {
+                    // 0 active keepalives when keepalive1 is paused before keepalive2 starts.
+                    startTime1 + (startTime2 - pauseTime1),
+                    // 1 active keepalive before keepalive1 is paused.
+                    (pauseTime1 - startTime1)
+                            // before keepalive1 is resumed and after keepalive2 starts.
+                            + (resumeTime1 - startTime2)
+                            // during keepalive2 is paused since keepalive1 has been resumed.
+                            + (resumeTime2 - pauseTime2)
+                            // after keepalive1 stops since keepalive2 has been resumed.
+                            + (writeTime - stopTime1),
+                    // 2 active keepalives before keepalive2 is paused and before keepalive1 stops.
+                    (pauseTime2 - resumeTime1) + (stopTime1 - resumeTime2)
+                };
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    /*
+     * Diagram of test (not to scale):
+     * Key: S - Start/Stop, P - Pause, R - Resume, W - Write
+     *
+     * Keepalive     S   W(reset+W)         S    W
+     * Timeline   |------------------------------|
+     */
+    @Test
+    public void testResetMetrics() {
+        final int startTime = 1000;
+        final int writeTime = 5000;
+        final int stopTime = 7000;
+        final int writeTime2 = 10000;
+
+        setUptimeMillis(startTime);
+        mKeepaliveStatsTracker.onStartKeepalive();
+
+        setUptimeMillis(writeTime);
+        final DurationPerNumOfKeepalive resultDurations =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // Same expect as testOneKeepalive_startOnly
+        final int[] expectRegisteredDurations = new int[] {startTime, writeTime - startTime};
+        final int[] expectActiveDurations = new int[] {startTime, writeTime - startTime};
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+
+        // Reset metrics
+        mKeepaliveStatsTracker.resetMetrics();
+
+        final DurationPerNumOfKeepalive resultDurations2 =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+        // Expect the stored durations to be 0 but still contain the number of keepalive = 1.
+        assertDurationMetrics(new int[] {0, 0}, new int[] {0, 0}, resultDurations2);
+
+        // Expect that the keepalive is still registered after resetting so it can be stopped.
+        setUptimeMillis(stopTime);
+        mKeepaliveStatsTracker.onStopKeepalive(/* wasActive= */ true);
+
+        setUptimeMillis(writeTime2);
+        final DurationPerNumOfKeepalive resultDurations3 =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        final int[] expectRegisteredDurations2 =
+                new int[] {writeTime2 - stopTime, stopTime - writeTime};
+        final int[] expectActiveDurations2 =
+                new int[] {writeTime2 - stopTime, stopTime - writeTime};
+        assertDurationMetrics(expectRegisteredDurations2, expectActiveDurations2, resultDurations3);
+    }
+}