Merge "Import translations. DO NOT MERGE ANYWHERE" into tm-mainline-prod
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
index ed86854..31990fb 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -222,29 +222,16 @@
 
     @Test
     public void testHttpEngine_EnableQuic() throws Exception {
+        String url = mTestServer.getSuccessUrl();
         mEngine = mEngineBuilder.setEnableQuic(true).addQuicHint(HOST, 443, 443).build();
-        // The hint doesn't guarantee that QUIC will win the race, just that it will race TCP.
-        // We send multiple requests to reduce the flakiness of the test.
-        boolean quicWasUsed = false;
-        for (int i = 0; i < 5; i++) {
-            mCallback = new TestUrlRequestCallback();
-            UrlRequest.Builder builder =
-                    mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
-            mRequest = builder.build();
-            mRequest.start();
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
+        mRequest = builder.build();
+        mRequest.start();
 
-            // This tests uses a non-hermetic server. Instead of asserting, assume the next
-            // callback. This way, if the request were to fail, the test would just be skipped
-            // instead of failing.
-            mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED);
-            UrlResponseInfo info = mCallback.mResponseInfo;
-            assumeOKStatusCode(info);
-            quicWasUsed = isQuic(info.getNegotiatedProtocol());
-            if (quicWasUsed) {
-                break;
-            }
-        }
-        assertTrue(quicWasUsed);
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+        UrlResponseInfo info = mCallback.mResponseInfo;
+        assertOKStatusCode(info);
     }
 
     @Test
@@ -379,34 +366,22 @@
 
     @Test
     public void testHttpEngine_SetQuicOptions_RequestSucceedsWithQuic() throws Exception {
+        String url = mTestServer.getSuccessUrl();
         QuicOptions options = new QuicOptions.Builder().build();
         mEngine = mEngineBuilder
                 .setEnableQuic(true)
                 .addQuicHint(HOST, 443, 443)
                 .setQuicOptions(options)
                 .build();
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
+        mRequest = builder.build();
+        mRequest.start();
 
-        // The hint doesn't guarantee that QUIC will win the race, just that it will race TCP.
-        // We send multiple requests to reduce the flakiness of the test.
-        boolean quicWasUsed = false;
-        for (int i = 0; i < 5; i++) {
-            mCallback = new TestUrlRequestCallback();
-            UrlRequest.Builder builder =
-                    mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
-            mRequest = builder.build();
-            mRequest.start();
-            mCallback.blockForDone();
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+        UrlResponseInfo info = mCallback.mResponseInfo;
+        assertOKStatusCode(info);
 
-            quicWasUsed = isQuic(mCallback.mResponseInfo.getNegotiatedProtocol());
-            if (quicWasUsed) {
-                break;
-            }
-        }
-
-        assertTrue(quicWasUsed);
-        // This tests uses a non-hermetic server. Instead of asserting, assume the next callback.
-        // This way, if the request were to fail, the test would just be skipped instead of failing.
-        assumeOKStatusCode(mCallback.mResponseInfo);
     }
 
     @Test
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 775c36f..18c2171 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -129,9 +129,6 @@
         // Tethered traffic will have the hop limit properly decremented.
         // Consequently, set the hoplimit greater by one than the upstream
         // unicast hop limit.
-        //
-        // TODO: Dynamically pass down the IPV6_UNICAST_HOPS value from the
-        // upstream interface for more correct behaviour.
         static final byte DEFAULT_HOPLIMIT = 65;
 
         public boolean hasDefaultRoute;
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 0b12521..9a4834c 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -43,9 +43,9 @@
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.FastPairManager;
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.BroadcastProviderManager;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
 import com.android.server.nearby.presence.PresenceManager;
-import com.android.server.nearby.provider.BroadcastProviderManager;
-import com.android.server.nearby.provider.DiscoveryProviderManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 import com.android.server.nearby.util.identity.CallerIdentity;
 import com.android.server.nearby.util.permissions.BroadcastPermissions;
@@ -58,10 +58,10 @@
     public static final Boolean MANUAL_TEST = false;
 
     private final Context mContext;
-    private Injector mInjector;
     private final FastPairManager mFastPairManager;
     private final PresenceManager mPresenceManager;
     private final NearbyConfiguration mNearbyConfiguration;
+    private Injector mInjector;
     private final BroadcastReceiver mBluetoothReceiver =
             new BroadcastReceiver() {
                 @Override
@@ -79,8 +79,8 @@
                     }
                 }
             };
-    private DiscoveryProviderManager mProviderManager;
-    private BroadcastProviderManager mBroadcastProviderManager;
+    private final DiscoveryProviderManager mProviderManager;
+    private final BroadcastProviderManager mBroadcastProviderManager;
 
     public NearbyService(Context context) {
         mContext = context;
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
similarity index 96%
rename from nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
rename to nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index bc69631..024bff8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -31,6 +31,7 @@
 import com.android.server.nearby.presence.Advertisement;
 import com.android.server.nearby.presence.ExtendedAdvertisement;
 import com.android.server.nearby.presence.FastAdvertisement;
+import com.android.server.nearby.provider.BleBroadcastProvider;
 import com.android.server.nearby.util.ForegroundThread;
 
 import java.util.concurrent.Executor;
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
similarity index 96%
rename from nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
rename to nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index cd97bb2..59db5d8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
 
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -39,6 +39,11 @@
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
 import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.PrivacyFilter;
 import com.android.server.nearby.util.identity.CallerIdentity;
 import com.android.server.nearby.util.permissions.DiscoveryPermissions;
 
@@ -386,7 +391,10 @@
         return false;
     }
 
-    class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
+    /**
+     * Class to make listener unregister after the binder is dead.
+     */
+    public class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
         public IScanListener listener;
 
         ScanListenerDeathRecipient(IScanListener listener) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
index 21b4d7c..64c55f0 100644
--- a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
@@ -41,14 +41,6 @@
     protected final Executor mExecutor;
     protected Listener mListener;
 
-    /** Interface for listening to discovery providers. */
-    public interface Listener {
-        /**
-         * Called when a provider has a new nearby device available. May be invoked from any thread.
-         */
-        void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
-    }
-
     protected AbstractDiscoveryProvider(Context context, Executor executor) {
         mContext = context;
         mExecutor = executor;
@@ -86,10 +78,18 @@
      * as a discovery provider should not be controlling itself. Using this method from subclasses
      * could also result in deadlock.
      */
-    protected DiscoveryProviderController getController() {
+    public DiscoveryProviderController getController() {
         return mController;
     }
 
+    /** Interface for listening to discovery providers. */
+    public interface Listener {
+        /**
+         * Called when a provider has a new nearby device available. May be invoked from any thread.
+         */
+        void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
+    }
+
     private class Controller implements DiscoveryProviderController {
 
         private boolean mStarted = false;
@@ -104,7 +104,6 @@
         public boolean isStarted() {
             return mStarted;
         }
-
         @Override
         public void start() {
             if (mStarted) {
@@ -125,6 +124,12 @@
             mExecutor.execute(AbstractDiscoveryProvider.this::onStop);
         }
 
+        @ScanRequest.ScanMode
+        @Override
+        public int getProviderScanMode() {
+            return mScanMode;
+        }
+
         @Override
         public void setProviderScanMode(@ScanRequest.ScanMode int scanMode) {
             if (mScanMode == scanMode) {
@@ -135,12 +140,6 @@
             mExecutor.execute(AbstractDiscoveryProvider.this::invalidateScanMode);
         }
 
-        @ScanRequest.ScanMode
-        @Override
-        public int getProviderScanMode() {
-            return mScanMode;
-        }
-
         @Override
         public void setProviderScanFilters(List<ScanFilter> filters) {
             mExecutor.execute(() -> onSetScanFilters(filters));
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 2b9fdb9..5632ab5 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -45,7 +45,7 @@
     /**
      * Listener for Broadcast status changes.
      */
-    interface BroadcastListener {
+    public interface BroadcastListener {
         void onStatusChanged(int status);
     }
 
@@ -56,13 +56,16 @@
     private boolean mIsAdvertising;
     @VisibleForTesting
     AdvertisingSetCallback mAdvertisingSetCallback;
-    BleBroadcastProvider(Injector injector, Executor executor) {
+    public BleBroadcastProvider(Injector injector, Executor executor) {
         mInjector = injector;
         mExecutor = executor;
         mAdvertisingSetCallback = getAdvertisingSetCallback();
     }
 
-    void start(@BroadcastRequest.BroadcastVersion int version, byte[] advertisementPackets,
+    /**
+     * Starts to broadcast with given bytes.
+     */
+    public void start(@BroadcastRequest.BroadcastVersion int version, byte[] advertisementPackets,
             BroadcastListener listener) {
         if (mIsAdvertising) {
             stop();
@@ -113,7 +116,10 @@
         }
     }
 
-    void stop() {
+    /**
+     * Stops current advertisement.
+     */
+    public void stop() {
         if (mIsAdvertising) {
             BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
             if (adapter != null) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index 98cac59..1b82af3 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -142,7 +142,7 @@
     }
 
     @VisibleForTesting
-    List<ScanFilter> getFiltersLocked() {
+    public List<ScanFilter> getFiltersLocked() {
         synchronized (mLock) {
             return mScanFilters == null ? null : List.copyOf(mScanFilters);
         }
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
index fa1a874..71ffda5 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
@@ -23,7 +23,7 @@
 import java.util.List;
 
 /** Interface for controlling discovery providers. */
-interface DiscoveryProviderController {
+public interface DiscoveryProviderController {
 
     /**
      * Sets the listener which can expect to receive all state updates from after this point. May be
diff --git a/nearby/tests/multidevices/README.md b/nearby/tests/multidevices/README.md
index b64667c..9d086de 100644
--- a/nearby/tests/multidevices/README.md
+++ b/nearby/tests/multidevices/README.md
@@ -43,14 +43,24 @@
 *   Adjust Bluetooth profile configurations. \
     The Fast Pair provider simulator is an opposite role to the seeker. It needs
     to enable/disable the following Bluetooth profile:
-    *   Disable A2DP (profile_supported_a2dp)
-    *   Disable the AVRCP controller (profile_supported_avrcp_controller)
-    *   Enable A2DP sink (profile_supported_a2dp_sink)
-    *   Enable the HFP client connection service (profile_supported_hfpclient,
-        hfp_client_connection_service_enabled)
-    *   Enable the AVRCP target (profile_supported_avrcp_target)
-    *   Enable the automatic audio focus request
-        (a2dp_sink_automatically_request_audio_focus)
+    *   Disable A2DP source (bluetooth.profile.a2dp.source.enabled)
+    *   Enable A2DP sink (bluetooth.profile.a2dp.sink.enabled)
+    *   Disable the AVRCP controller (bluetooth.profile.avrcp.controller.enabled)
+    *   Enable the AVRCP target (bluetooth.profile.avrcp.target.enabled)
+    *   Enable the HFP service (bluetooth.profile.hfp.ag.enabled, bluetooth.profile.hfp.hf.enabled)
+
+```makefile
+# The Bluetooth profiles that Fast Pair provider simulator expect to have enabled.
+PRODUCT_PRODUCT_PROPERTIES += \
+    bluetooth.device.default_name=FastPairProviderSimulator \
+    bluetooth.profile.a2dp.source.enabled=false \
+    bluetooth.profile.a2dp.sink.enabled=true \
+    bluetooth.profile.avrcp.controller.enabled=false \
+    bluetooth.profile.avrcp.target.enabled=true \
+    bluetooth.profile.hfp.ag.enabled=true \
+    bluetooth.profile.hfp.hf.enabled=true
+```
+
 *   Adjust Bluetooth TX power limitation in Bluetooth module and disable the
     Fast Pair in Google Play service (aka GMS)
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
similarity index 94%
rename from nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
rename to nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index 58b8e07..d0008f5 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
 
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
@@ -40,6 +40,8 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.nearby.provider.BleBroadcastProvider;
+
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.Before;
@@ -52,7 +54,7 @@
 import java.util.Collections;
 
 /**
- * Unit test for {@link BroadcastProviderManager}.
+ * Unit test for {@link com.android.server.nearby.managers.BroadcastProviderManager}.
  */
 public class BroadcastProviderManagerTest {
     private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
@@ -84,7 +86,8 @@
                 "true", false);
 
         mContext = ApplicationProvider.getApplicationContext();
-        mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+        mBroadcastProviderManager = new BroadcastProviderManager(
+                MoreExecutors.directExecutor(),
                 mBleBroadcastProvider);
 
         PrivateCredential privateCredential =
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
similarity index 90%
rename from nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
rename to nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 5ffaffb..542ceed 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
 
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
@@ -37,6 +37,10 @@
 import android.os.IBinder;
 
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
 import com.android.server.nearby.util.identity.CallerIdentity;
 
 import org.junit.Before;
@@ -56,257 +60,33 @@
     private static final int UID = 1234;
     private static final int PID = 5678;
     private static final String PACKAGE_NAME = "android.nearby.test";
-
-    @Mock Injector mInjector;
-    @Mock Context mContext;
-    @Mock AppOpsManager mAppOpsManager;
-    @Mock BleDiscoveryProvider mBleDiscoveryProvider;
-    @Mock ChreDiscoveryProvider mChreDiscoveryProvider;
-    @Mock DiscoveryProviderController mBluetoothController;
-    @Mock DiscoveryProviderController mChreController;
-    @Mock IScanListener mScanListener;
-    @Mock CallerIdentity mCallerIdentity;
-    @Mock DiscoveryProviderManager.ScanListenerDeathRecipient mScanListenerDeathRecipient;
-    @Mock IBinder mIBinder;
-
+    private static final int RSSI = -60;
+    @Mock
+    Injector mInjector;
+    @Mock
+    Context mContext;
+    @Mock
+    AppOpsManager mAppOpsManager;
+    @Mock
+    BleDiscoveryProvider mBleDiscoveryProvider;
+    @Mock
+    ChreDiscoveryProvider mChreDiscoveryProvider;
+    @Mock
+    DiscoveryProviderController mBluetoothController;
+    @Mock
+    DiscoveryProviderController mChreController;
+    @Mock
+    IScanListener mScanListener;
+    @Mock
+    CallerIdentity mCallerIdentity;
+    @Mock
+    DiscoveryProviderManager.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+    @Mock
+    IBinder mIBinder;
     private DiscoveryProviderManager mDiscoveryProviderManager;
     private Map<IBinder, DiscoveryProviderManager.ScanListenerRecord>
             mScanTypeScanListenerRecordMap;
 
-    private static final int RSSI = -60;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
-        when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
-        when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
-
-        mScanTypeScanListenerRecordMap = new HashMap<>();
-        mDiscoveryProviderManager =
-                new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
-                        mChreDiscoveryProvider,
-                        mScanTypeScanListenerRecordMap);
-        mCallerIdentity = CallerIdentity
-                .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
-    }
-
-    @Test
-    public void testOnNearbyDeviceDiscovered() {
-        NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .build();
-        mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
-    }
-
-    @Test
-    public void testInvalidateProviderScanMode() {
-        mDiscoveryProviderManager.invalidateProviderScanMode();
-    }
-
-    @Test
-    public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(true);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, never()).start();
-        assertThat(start).isTrue();
-    }
-
-    @Test
-    public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(true);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getChreOnlyPresenceScanFilter())
-                .addScanFilter(getPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, never()).start();
-        assertThat(start).isTrue();
-    }
-
-    @Test
-    public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(false);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, never()).start();
-        assertThat(start).isFalse();
-    }
-
-    @Test
-    public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(true);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, never()).start();
-        assertThat(start).isTrue();
-    }
-
-    @Test
-    public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(false);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, atLeastOnce()).start();
-        assertThat(start).isTrue();
-    }
-
-    @Test
-    public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(null);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, never()).start();
-        assertThat(start).isNull();
-    }
-
-    @Test
-    public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
-        when(mChreDiscoveryProvider.available()).thenReturn(null);
-
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-
-        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
-        verify(mBluetoothController, atLeastOnce()).start();
-        assertThat(start).isTrue();
-    }
-
-    @Test
-    public void test_stopChreProvider_clearFilters() throws Exception {
-        // Cannot use mocked ChreDiscoveryProvider,
-        // so we cannot use class variable mDiscoveryProviderManager
-        ExecutorService executor = Executors.newSingleThreadExecutor();
-        DiscoveryProviderManager manager =
-                new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
-                        new ChreDiscoveryProvider(
-                                mContext,
-                                new ChreCommunication(mInjector, mContext, executor), executor),
-                mScanTypeScanListenerRecordMap);
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-        manager.startChreProvider(List.of(getPresenceScanFilter()));
-        // This is an asynchronized process. The filters will be set in executor thread. So we need
-        // to wait for some time to get the correct result.
-        Thread.sleep(200);
-
-        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
-                .isTrue();
-        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
-
-        manager.stopChreProvider();
-        Thread.sleep(200);
-        // The filters should be cleared right after.
-        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
-                .isFalse();
-        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
-    }
-
-    @Test
-    public void test_restartChreProvider() throws Exception {
-        // Cannot use mocked ChreDiscoveryProvider,
-        // so we cannot use class variable mDiscoveryProviderManager
-        ExecutorService executor = Executors.newSingleThreadExecutor();
-        DiscoveryProviderManager manager =
-                new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
-                        new ChreDiscoveryProvider(
-                                mContext,
-                                new ChreCommunication(mInjector, mContext, executor), executor),
-                        mScanTypeScanListenerRecordMap);
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
-                .addScanFilter(getPresenceScanFilter()).build();
-        DiscoveryProviderManager.ScanListenerRecord record =
-                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
-                        mCallerIdentity, mScanListenerDeathRecipient);
-        mScanTypeScanListenerRecordMap.put(mIBinder, record);
-        manager.startChreProvider(List.of(getPresenceScanFilter()));
-        // This is an asynchronized process. The filters will be set in executor thread. So we need
-        // to wait for some time to get the correct result.
-        Thread.sleep(200);
-
-        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
-                .isTrue();
-        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
-
-        // We want to make sure quickly restart the provider the filters should
-        // be reset correctly.
-        // See b/255922206, there can be a race condition that filters get cleared because onStop()
-        // get executed after onStart() if they are called from different threads.
-        manager.stopChreProvider();
-        manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
-                List.of(getPresenceScanFilter()));
-        manager.startChreProvider(List.of(getPresenceScanFilter()));
-        Thread.sleep(200);
-        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
-                .isTrue();
-        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
-
-        // Wait for enough time
-        Thread.sleep(1000);
-
-        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
-                .isTrue();
-        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
-    }
-
     private static PresenceScanFilter getPresenceScanFilter() {
         final byte[] secretId = new byte[]{1, 2, 3, 4};
         final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
@@ -349,4 +129,247 @@
                 .addExtendedProperty(scanModeElement)
                 .build();
     }
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+        when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+
+        mScanTypeScanListenerRecordMap = new HashMap<>();
+        mDiscoveryProviderManager =
+                new DiscoveryProviderManager(mContext, mInjector,
+                        mBleDiscoveryProvider,
+                        mChreDiscoveryProvider,
+                        mScanTypeScanListenerRecordMap);
+        mCallerIdentity = CallerIdentity
+                .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+    }
+
+    @Test
+    public void testOnNearbyDeviceDiscovered() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .build();
+        mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+    }
+
+    @Test
+    public void testInvalidateProviderScanMode() {
+        mDiscoveryProviderManager.invalidateProviderScanMode();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter())
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isFalse();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isNull();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void test_stopChreProvider_clearFilters() throws Exception {
+        // Cannot use mocked ChreDiscoveryProvider,
+        // so we cannot use class variable mDiscoveryProviderManager
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        DiscoveryProviderManager manager =
+                new DiscoveryProviderManager(mContext, mInjector,
+                        mBleDiscoveryProvider,
+                        new ChreDiscoveryProvider(
+                                mContext,
+                                new ChreCommunication(mInjector, mContext, executor), executor),
+                        mScanTypeScanListenerRecordMap);
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        // This is an asynchronized process. The filters will be set in executor thread. So we need
+        // to wait for some time to get the correct result.
+        Thread.sleep(200);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        manager.stopChreProvider();
+        Thread.sleep(200);
+        // The filters should be cleared right after.
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isFalse();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+    }
+
+    @Test
+    public void test_restartChreProvider() throws Exception {
+        // Cannot use mocked ChreDiscoveryProvider,
+        // so we cannot use class variable mDiscoveryProviderManager
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        DiscoveryProviderManager manager =
+                new DiscoveryProviderManager(mContext, mInjector,
+                        mBleDiscoveryProvider,
+                        new ChreDiscoveryProvider(
+                                mContext,
+                                new ChreCommunication(mInjector, mContext, executor), executor),
+                        mScanTypeScanListenerRecordMap);
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        // This is an asynchronized process. The filters will be set in executor thread. So we need
+        // to wait for some time to get the correct result.
+        Thread.sleep(200);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        // We want to make sure quickly restart the provider the filters should
+        // be reset correctly.
+        // See b/255922206, there can be a race condition that filters get cleared because onStop()
+        // get executed after onStart() if they are called from different threads.
+        manager.stopChreProvider();
+        manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+                List.of(getPresenceScanFilter()));
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        Thread.sleep(200);
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        // Wait for enough time
+        Thread.sleep(1000);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+    }
 }
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..290d201
--- /dev/null
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -0,0 +1,191 @@
+/*
+ * 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.DailykeepaliveInfoReported;
+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 DailykeepaliveInfoReported proto.
+     */
+    public DailykeepaliveInfoReported buildKeepaliveMetrics() {
+        final long timeNow = mDependencies.getUptimeMillis();
+        updateDurationsPerNumOfKeepalive(timeNow);
+
+        final DurationPerNumOfKeepalive.Builder durationPerNumOfKeepalive =
+                DurationPerNumOfKeepalive.newBuilder();
+
+        mDurationPerNumOfKeepalive.forEach(
+                durationForNumOfKeepalive ->
+                        durationPerNumOfKeepalive.addDurationForNumOfKeepalive(
+                                durationForNumOfKeepalive));
+
+        final DailykeepaliveInfoReported.Builder dailyKeepaliveInfoReported =
+                DailykeepaliveInfoReported.newBuilder();
+
+        // TODO(b/273451360): fill all the other values and write to ConnectivityStatsLog.
+        dailyKeepaliveInfoReported.setDurationPerNumOfKeepalive(durationPerNumOfKeepalive);
+
+        return dailyKeepaliveInfoReported.build();
+    }
+
+    /** Resets the stored metrics but maintains the state of keepalives */
+    public void resetMetrics() {
+        mDurationPerNumOfKeepalive.clear();
+        ensureDurationPerNumOfKeepaliveSize();
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index b535a8f..869562b 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -108,17 +108,6 @@
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertThrows
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.argThat
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
 import java.io.Closeable
 import java.io.IOException
 import java.net.DatagramSocket
@@ -136,6 +125,17 @@
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
 
 // This test doesn't really have a constraint on how fast the methods should return. If it's
 // going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
diff --git a/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java b/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
index f0c87673..4854901 100644
--- a/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
@@ -23,12 +23,14 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Instrumentation;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.Network;
 import android.net.PacProxyManager;
@@ -150,6 +152,9 @@
     @AppModeFull(reason = "Instant apps can't bind sockets to localhost for a test proxy server")
     @Test
     public void testSetCurrentProxyScriptUrl() throws Exception {
+        // Devices without WebView/JavaScript cannot support PAC proxies
+        assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WEBVIEW));
+
         // Register a PacProxyInstalledListener
         final TestPacProxyInstalledListener listener = new TestPacProxyInstalledListener();
         final Executor executor = (Runnable r) -> r.run();
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 36b3356..8b286a0 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -26,7 +26,6 @@
         "libandroid_net_frameworktests_util_jni",
         "libbase",
         "libbinder",
-        "libbpf_bcc",
         "libc++",
         "libcrypto",
         "libcutils",
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 1cc0c89..3188c9c 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -502,7 +502,7 @@
     // complete before callbacks are verified.
     private static final int TEST_REQUEST_TIMEOUT_MS = 150;
 
-    private static final int UNREASONABLY_LONG_ALARM_WAIT_MS = 1000;
+    private static final int UNREASONABLY_LONG_ALARM_WAIT_MS = 2_000;
 
     private static final long TIMESTAMP = 1234L;
 
@@ -3369,8 +3369,10 @@
         // This test would be flaky with the default 120ms timer: that is short enough that
         // lingered networks are torn down before assertions can be run. We don't want to mock the
         // lingering timer to keep the WakeupMessage logic realistic: this has already proven useful
-        // in detecting races.
-        mService.mLingerDelayMs = 300;
+        // in detecting races. Furthermore, sometimes the test is running while Phenotype is running
+        // so hot that the test doesn't get the CPU for multiple hundreds of milliseconds, so this
+        // needs to be suitably long.
+        mService.mLingerDelayMs = 2_000;
 
         NetworkRequest request = new NetworkRequest.Builder()
                 .clearCapabilities().addCapability(NET_CAPABILITY_NOT_METERED)
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..d262255
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -0,0 +1,504 @@
+/*
+ * 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.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.metrics.DailykeepaliveInfoReported;
+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
[email protected](Build.VERSION_CODES.TIRAMISU)
+public class KeepaliveStatsTrackerTest {
+    private static final int TEST_UID = 1234;
+
+    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());
+        }
+    }
+
+    private void assertDailyKeepaliveInfoReported(
+            DailykeepaliveInfoReported dailyKeepaliveInfoReported,
+            int[] expectRegisteredDurations,
+            int[] expectActiveDurations) {
+        // TODO(b/273451360) Assert these values when they are filled.
+        assertFalse(dailyKeepaliveInfoReported.hasKeepaliveLifetimePerCarrier());
+        assertFalse(dailyKeepaliveInfoReported.hasKeepaliveRequests());
+        assertFalse(dailyKeepaliveInfoReported.hasAutomaticKeepaliveRequests());
+        assertFalse(dailyKeepaliveInfoReported.hasDistinctUserCount());
+        assertTrue(dailyKeepaliveInfoReported.getUidList().isEmpty());
+
+        final DurationPerNumOfKeepalive resultDurations =
+                dailyKeepaliveInfoReported.getDurationPerNumOfKeepalive();
+        assertDurationMetrics(expectRegisteredDurations, expectActiveDurations, resultDurations);
+    }
+
+    @Test
+    public void testNoKeepalive() {
+        final int writeTime = 5000;
+
+        setUptimeMillis(writeTime);
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // Expect that the durations are all in numOfKeepalive = 0.
+        final int[] expectRegisteredDurations = new int[] {writeTime};
+        final int[] expectActiveDurations = new int[] {writeTime};
+
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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};
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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};
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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)
+                };
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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)
+                };
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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)};
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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])
+                };
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                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)
+                };
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+    }
+
+    /*
+     * 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 DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        // Same expect as testOneKeepalive_startOnly
+        final int[] expectRegisteredDurations = new int[] {startTime, writeTime - startTime};
+        final int[] expectActiveDurations = new int[] {startTime, writeTime - startTime};
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                expectRegisteredDurations,
+                expectActiveDurations);
+
+        // Reset metrics
+        mKeepaliveStatsTracker.resetMetrics();
+
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported2 =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+        // Expect the stored durations to be 0 but still contain the number of keepalive = 1.
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported2,
+                /* expectRegisteredDurations= */ new int[] {0, 0},
+                /* expectActiveDurations= */ new int[] {0, 0});
+
+        // Expect that the keepalive is still registered after resetting so it can be stopped.
+        setUptimeMillis(stopTime);
+        mKeepaliveStatsTracker.onStopKeepalive(/* wasActive= */ true);
+
+        setUptimeMillis(writeTime2);
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported3 =
+                mKeepaliveStatsTracker.buildKeepaliveMetrics();
+
+        final int[] expectRegisteredDurations2 =
+                new int[] {writeTime2 - stopTime, stopTime - writeTime};
+        final int[] expectActiveDurations2 =
+                new int[] {writeTime2 - stopTime, stopTime - writeTime};
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported3,
+                expectRegisteredDurations2,
+                expectActiveDurations2);
+    }
+}