Add periodically pulling data from StatsD

- Use a provided Handler to schedule periodic
  pulling reports from StatsD.
- The period is currently hard-coded, but
  it can be simply refactored into config
  in the future.
- Add FakeHandlerWrapper to simplify working
  with Handler and running delayed callbacks.

Bug: 189143813
Test: atest CarServiceUnitTest:StatsPublisherTest
Change-Id: Ib3d342fff3b6e10e8ce1e4fc0aac46a3a8ffe1ac
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
index b55e038..79ca177 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
@@ -23,18 +23,30 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+
+import com.android.car.telemetry.StatsLogProto;
 import com.android.car.telemetry.StatsdConfigProto;
 import com.android.car.telemetry.TelemetryProto;
 import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
 import com.android.car.test.FakeSharedPreferences;
 
+import com.google.common.collect.Range;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -74,14 +86,22 @@
                     .addAllowedLogSource("AID_SYSTEM")
                     .build();
 
+    private static final StatsLogProto.ConfigMetricsReportList EMPTY_STATS_REPORT =
+            StatsLogProto.ConfigMetricsReportList.newBuilder().build();
+
     private StatsPublisher mPublisher;  // subject
     private final FakeSharedPreferences mFakeSharedPref = new FakeSharedPreferences();
+    private final FakeHandlerWrapper mFakeHandlerWrapper =
+            new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.QUEUEING);
     @Mock private DataSubscriber mMockDataSubscriber;
     @Mock private StatsManagerProxy mStatsManager;
 
+    @Captor private ArgumentCaptor<Bundle> mBundleCaptor;
+
     @Before
     public void setUp() throws Exception {
-        mPublisher = new StatsPublisher(mStatsManager, mFakeSharedPref);
+        mPublisher = new StatsPublisher(
+                mStatsManager, mFakeSharedPref, mFakeHandlerWrapper.getMockHandler());
         when(mMockDataSubscriber.getPublisherParam()).thenReturn(STATS_PUBLISHER_PARAMS_1);
         when(mMockDataSubscriber.getMetricsConfig()).thenReturn(METRICS_CONFIG);
         when(mMockDataSubscriber.getSubscriber()).thenReturn(SUBSCRIBER_1);
@@ -92,7 +112,8 @@
      * stays the same.
      */
     private StatsPublisher createRestartedPublisher() {
-        return new StatsPublisher(mStatsManager, mFakeSharedPref);
+        return new StatsPublisher(
+                mStatsManager, mFakeSharedPref, mFakeHandlerWrapper.getMockHandler());
     }
 
     @Test
@@ -161,6 +182,72 @@
         assertThat(publisher2.hasDataSubscriber(mMockDataSubscriber)).isFalse();
     }
 
+    @Test
+    public void testAddDataSubscriber_queuesPeriodicTaskInTheHandler() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1);
+        Message msg = mFakeHandlerWrapper.getQueuedMessages().get(0);
+        long expectedPullPeriodMillis = 10 * 60 * 1000;  // 10 minutes
+        assertThatMessageIsScheduledWithGivenDelay(msg, expectedPullPeriodMillis);
+    }
+
+    @Test
+    public void testRemoveDataSubscriber_removesPeriodicStatsdReportPull() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mPublisher.removeDataSubscriber(mMockDataSubscriber);
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+    }
+
+    @Test
+    public void testRemoveAllDataSubscriber_removesPeriodicStatsdReportPull() {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+        mPublisher.removeAllDataSubscribers();
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+    }
+
+    @Test
+    public void testAfterDispatchItSchedulesANewPullReportTask() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        Message firstMessage = mFakeHandlerWrapper.getQueuedMessages().get(0);
+        when(mStatsManager.getReports(anyLong())).thenReturn(EMPTY_STATS_REPORT.toByteArray());
+
+        mFakeHandlerWrapper.dispatchQueuedMessages();
+
+        assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1);
+        Message newMessage = mFakeHandlerWrapper.getQueuedMessages().get(0);
+        assertThat(newMessage).isNotEqualTo(firstMessage);
+        long expectedPullPeriodMillis = 10 * 60 * 1000;  // 10 minutes
+        assertThatMessageIsScheduledWithGivenDelay(newMessage, expectedPullPeriodMillis);
+    }
+
+    @Test
+    public void testPullsStatsdReport() throws Exception {
+        mPublisher.addDataSubscriber(mMockDataSubscriber);
+        when(mStatsManager.getReports(anyLong())).thenReturn(
+                StatsLogProto.ConfigMetricsReportList.newBuilder()
+                        // add 2 empty reports
+                        .addReports(StatsLogProto.ConfigMetricsReport.newBuilder())
+                        .addReports(StatsLogProto.ConfigMetricsReport.newBuilder())
+                        .build().toByteArray());
+
+        mFakeHandlerWrapper.dispatchQueuedMessages();
+
+        verify(mMockDataSubscriber).push(mBundleCaptor.capture());
+        assertThat(mBundleCaptor.getValue().getInt("reportsCount")).isEqualTo(2);
+    }
+
     // TODO(b/189142577): add test cases when connecting to Statsd fails
     // TODO(b/189142577): add test cases for handling config version upgrades
+
+    private static void assertThatMessageIsScheduledWithGivenDelay(Message msg, long delayMillis) {
+        long expectedTimeMillis = SystemClock.uptimeMillis() + delayMillis;
+        long deltaMillis = 1000;  // +/- 1 seconds is good enough for testing
+        assertThat(msg.getWhen()).isIn(Range
+                .closed(expectedTimeMillis - deltaMillis, expectedTimeMillis + deltaMillis));
+    }
 }