Put a delay between individual statsd metrics processing.

Also move metrics processing to a JobService.

This is a low-risk change which moves code from
DeferredMetricsReader to ProcessMetricsJobService.

Fixes: 136098269
Test: manual
Change-Id: Ia2f77c911aba5812d7835c3e685ec63581cf0b36
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d123d48..efc8729 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -266,5 +266,9 @@
             android:name=".finalization.FinalScreenActivity"
             android:theme="@style/SudThemeGlifV3.Light"
             android:immersive="true" />
+
+        <service
+            android:name=".analytics.ProcessMetricsJobService"
+            android:permission="android.permission.BIND_JOB_SERVICE"/>
     </application>
 </manifest>
diff --git a/src/com/android/managedprovisioning/analytics/DeferredMetricsReader.java b/src/com/android/managedprovisioning/analytics/DeferredMetricsReader.java
index 82204db..1b7b81b 100644
--- a/src/com/android/managedprovisioning/analytics/DeferredMetricsReader.java
+++ b/src/com/android/managedprovisioning/analytics/DeferredMetricsReader.java
@@ -20,26 +20,25 @@
 import com.android.managedprovisioning.common.ProvisionLogger;
 
 import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.managedprovisioning.analytics.ProcessMetricsJobService.EXTRA_FILE_PATH;
 
-import android.annotation.Nullable;
-import android.app.admin.DevicePolicyEventLogger;
-import android.os.AsyncTask;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.PersistableBundle;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
 
 /**
- * Reads the logs from the {@link InputStream} written to by {@link DeferredMetricsWriter}
- * and writes them to another {@link MetricsWriter}.
- *
- * @see DeferredMetricsWriter
+ * Schedules the reading of the metrics written by {@link DeferredMetricsWriter}.
  */
 public class DeferredMetricsReader {
 
-    private final MetricsWriter mMetricsWriter;
+    private static final ComponentName PROCESS_METRICS_SERVICE_COMPONENT = new ComponentName(
+            "com.android.managedprovisioning", ProcessMetricsJobService.class.getName());
+    private static final int JOB_ID = 1;
+    private static final long MINIMUM_LATENCY = 10 * 60 * 1000;
     private final File mFile;
 
     /**
@@ -47,58 +46,22 @@
      *
      * <p>The specified {@link File} is deleted after everything has been read from it.
      */
-    public DeferredMetricsReader(File file, MetricsWriter metricsWriter) {
-        mMetricsWriter = checkNotNull(metricsWriter);
+    public DeferredMetricsReader(File file) {
         mFile = checkNotNull(file);
     }
 
-    /**
-     * Asynchronously reads the logs from the {@link File} specified in the constructor
-     * and writes them to the specified {@link MetricsWriter}.
-     *
-     * <p>The {@link File} will be deleted after they are written to the {@link MetricsWriter}.
-     */
-    public void dumpMetricsAndClearFile() {
-        new ReadDeferredMetricsAsyncTask(mFile, mMetricsWriter).execute();
-    }
-
-    private static class ReadDeferredMetricsAsyncTask extends AsyncTask<Void, Void, Void> {
-        private final MetricsWriter mMetricsWriter;
-        private final File mFile;
-
-        ReadDeferredMetricsAsyncTask(File file,
-                MetricsWriter metricsWriter) {
-            mFile = checkNotNull(file);
-            mMetricsWriter = checkNotNull(metricsWriter);
-        }
-
-        @Override
-        protected Void doInBackground(Void... voids) {
-            try (InputStream inputStream =  new FileInputStream(mFile)) {
-                DevicePolicyEvent event;
-                while ((event = DevicePolicyEvent.parseDelimitedFrom(inputStream)) != null) {
-                    mMetricsWriter.write(devicePolicyEventToLogger(event));
-                }
-            } catch (IOException e) {
-                ProvisionLogger.loge(
-                        "Could not parse DevicePolicyEvent while reading from stream.", e);
-            } finally {
-                mFile.delete();
-            }
-            return null;
-        }
-
-        private DevicePolicyEventLogger devicePolicyEventToLogger(DevicePolicyEvent event) {
-            final DevicePolicyEventLogger eventLogger = DevicePolicyEventLogger
-                    .createEvent(event.getEventId())
-                    .setAdmin(event.getAdminPackageName())
-                    .setInt(event.getIntegerValue())
-                    .setBoolean(event.getBooleanValue())
-                    .setTimePeriod(event.getTimePeriodMillis());
-            if (event.getStringListValueCount() > 0) {
-                eventLogger.setStrings(event.getStringListValueList().toArray(new String[0]));
-            }
-            return eventLogger;
+    public void scheduleDumpMetrics(Context context) {
+        final JobInfo jobInfo = new JobInfo.Builder(JOB_ID, PROCESS_METRICS_SERVICE_COMPONENT)
+                .setExtras(PersistableBundle.forPair(EXTRA_FILE_PATH, mFile.getAbsolutePath()))
+                .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
+                .setMinimumLatency(MINIMUM_LATENCY)
+                .setPersisted(true)
+                .build();
+        final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        if (jobScheduler != null) {
+            jobScheduler.schedule(jobInfo);
+        } else {
+            ProvisionLogger.logv("JobScheduler is null.");
         }
     }
 }
diff --git a/src/com/android/managedprovisioning/analytics/ProcessMetricsJobService.java b/src/com/android/managedprovisioning/analytics/ProcessMetricsJobService.java
new file mode 100644
index 0000000..fd144a2
--- /dev/null
+++ b/src/com/android/managedprovisioning/analytics/ProcessMetricsJobService.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.managedprovisioning.analytics;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.AsyncTask;
+import android.os.PersistableBundle;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.managedprovisioning.DevicePolicyProtos.DevicePolicyEvent;
+import com.android.managedprovisioning.common.ProvisionLogger;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link JobService} that reads the logs from the {@link InputStream} written to by
+ * {@link DeferredMetricsWriter} and writes them using another {@link MetricsWriter}.
+ *
+ * @see DeferredMetricsWriter
+ */
+public class ProcessMetricsJobService extends JobService {
+
+    static String EXTRA_FILE_PATH = "extra_file_path";
+
+    private final MetricsWriter mMetricsWriter;
+
+    @VisibleForTesting
+    ProcessMetricsJobService(MetricsWriter metricsWriter) {
+        mMetricsWriter = metricsWriter;
+    }
+
+    public ProcessMetricsJobService() {
+        this(new InstantMetricsWriter());
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        final PersistableBundle extras = params.getExtras();
+        if (extras == null || !extras.containsKey(EXTRA_FILE_PATH)) {
+            return false;
+        }
+        final File metrics = new File(extras.getString(EXTRA_FILE_PATH));
+        if (!metrics.exists()) {
+            return false;
+        }
+        executeReadDeferredMetrics(params, metrics);
+        return true;
+    }
+
+    @VisibleForTesting
+    void executeReadDeferredMetrics(JobParameters params,
+            File metricsFile) {
+        new ReadDeferredMetricsAsyncTask(params, metricsFile, mMetricsWriter).execute();
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+
+    /**
+     * An {@link AsyncTask} which reads the logs from the {@link File} specified in the constructor
+     * and writes them to the specified {@link MetricsWriter}.
+     *
+     * <p>The {@link File} will be deleted after they are written to the {@link MetricsWriter}.
+     */
+    private class ReadDeferredMetricsAsyncTask extends AsyncTask<Void, Void, Void> {
+        private static final int METRICS_INTERVAL_MILLIS = 10;
+        private final MetricsWriter mMetricsWriter;
+        private final File mFile;
+        private final JobParameters mJobParameters;
+
+        ReadDeferredMetricsAsyncTask(JobParameters params,
+                File file,
+                MetricsWriter metricsWriter) {
+            mFile = checkNotNull(file);
+            mMetricsWriter = metricsWriter;
+            mJobParameters = params;
+        }
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            try (InputStream inputStream =  new FileInputStream(mFile)) {
+                DevicePolicyEvent event;
+                while ((event = DevicePolicyEvent.parseDelimitedFrom(inputStream)) != null) {
+                    delayProcessMetric();
+                    mMetricsWriter.write(devicePolicyEventToLogger(event));
+                }
+            } catch (IOException e) {
+                ProvisionLogger.loge(
+                        "Could not parse DevicePolicyEvent while reading from stream.", e);
+            } finally {
+                mFile.delete();
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            jobFinished(mJobParameters, false);
+        }
+
+        /**
+         * Waits for {@link #METRICS_INTERVAL_MILLIS}.
+         * <p>statsd cannot handle too many metrics at once, so we must wait between each
+         * {@link MetricsWriter#write(DevicePolicyEventLogger...)} call.
+         */
+        private void delayProcessMetric() {
+            try {
+                Thread.sleep(METRICS_INTERVAL_MILLIS);
+            } catch (InterruptedException e) {
+                ProvisionLogger.loge(
+                        "Thread interrupted while waiting to log metric.", e);
+            }
+        }
+
+        private DevicePolicyEventLogger devicePolicyEventToLogger(DevicePolicyEvent event) {
+            final DevicePolicyEventLogger eventLogger = DevicePolicyEventLogger
+                    .createEvent(event.getEventId())
+                    .setAdmin(event.getAdminPackageName())
+                    .setInt(event.getIntegerValue())
+                    .setBoolean(event.getBooleanValue())
+                    .setTimePeriod(event.getTimePeriodMillis());
+            if (event.getStringListValueCount() > 0) {
+                eventLogger.setStrings(event.getStringListValueList().toArray(new String[0]));
+            }
+            return eventLogger;
+        }
+    }
+}
diff --git a/src/com/android/managedprovisioning/finalization/FinalizationController.java b/src/com/android/managedprovisioning/finalization/FinalizationController.java
index 2948d2f..1ea806e 100644
--- a/src/com/android/managedprovisioning/finalization/FinalizationController.java
+++ b/src/com/android/managedprovisioning/finalization/FinalizationController.java
@@ -31,10 +31,7 @@
 import android.os.UserHandle;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.managedprovisioning.R;
 import com.android.managedprovisioning.analytics.DeferredMetricsReader;
-import com.android.managedprovisioning.analytics.InstantMetricsWriter;
-import com.android.managedprovisioning.analytics.MetricsWriterFactory;
 import com.android.managedprovisioning.common.NotificationHelper;
 import com.android.managedprovisioning.common.ProvisionLogger;
 import com.android.managedprovisioning.common.SettingsFacade;
@@ -43,8 +40,6 @@
 import com.android.managedprovisioning.provisioning.Constants;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 
 /**
  * Controller for the finalization of managed provisioning.
@@ -74,8 +69,7 @@
                 userProvisioningStateHelper,
                 new NotificationHelper(context),
                 new DeferredMetricsReader(
-                        Constants.getDeferredMetricsFile(context),
-                        new InstantMetricsWriter()));
+                        Constants.getDeferredMetricsFile(context)));
     }
 
     public FinalizationController(Context context) {
@@ -86,8 +80,7 @@
                 new UserProvisioningStateHelper(context),
                 new NotificationHelper(context),
                 new DeferredMetricsReader(
-                        Constants.getDeferredMetricsFile(context),
-                        new InstantMetricsWriter()));
+                        Constants.getDeferredMetricsFile(context)));
     }
 
     @VisibleForTesting
@@ -181,7 +174,7 @@
      * provisioning and sets the right user provisioning states.</p>
      */
     void provisioningFinalized() {
-        mDeferredMetricsReader.dumpMetricsAndClearFile();
+        mDeferredMetricsReader.scheduleDumpMetrics(mContext);
 
         if (mUserProvisioningStateHelper.isStateUnmanagedOrFinalized()) {
             ProvisionLogger.logw("provisioningInitiallyDone called, but state is finalized or "
diff --git a/tests/instrumentation/src/com/android/managedprovisioning/finalization/FinalizationControllerTest.java b/tests/instrumentation/src/com/android/managedprovisioning/finalization/FinalizationControllerTest.java
index 579050b..9611a62 100644
--- a/tests/instrumentation/src/com/android/managedprovisioning/finalization/FinalizationControllerTest.java
+++ b/tests/instrumentation/src/com/android/managedprovisioning/finalization/FinalizationControllerTest.java
@@ -33,8 +33,6 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
-import android.app.Activity;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -122,7 +120,7 @@
         mController.provisioningFinalized();
 
         // THEN deferred metrics are written
-        verify(mDeferredMetricsReader).dumpMetricsAndClearFile();
+        verify(mDeferredMetricsReader).scheduleDumpMetrics(any(Context.class));
         verifyNoMoreInteractions(mDeferredMetricsReader);
 
         // THEN nothing should happen
@@ -139,7 +137,7 @@
         mController.provisioningFinalized();
 
         // THEN deferred metrics are written
-        verify(mDeferredMetricsReader).dumpMetricsAndClearFile();
+        verify(mDeferredMetricsReader).scheduleDumpMetrics(any(Context.class));
         verifyNoMoreInteractions(mDeferredMetricsReader);
 
         // THEN nothing should happen
@@ -198,7 +196,7 @@
         mController.provisioningFinalized();
 
         // THEN deferred metrics are written
-        verify(mDeferredMetricsReader).dumpMetricsAndClearFile();
+        verify(mDeferredMetricsReader).scheduleDumpMetrics(any(Context.class));
         verifyNoMoreInteractions(mDeferredMetricsReader);
 
         // THEN the user provisioning state is finalized
@@ -232,7 +230,7 @@
         mController.provisioningFinalized();
 
         // THEN deferred metrics are written
-        verify(mDeferredMetricsReader).dumpMetricsAndClearFile();
+        verify(mDeferredMetricsReader).scheduleDumpMetrics(any(Context.class));
         verifyNoMoreInteractions(mDeferredMetricsReader);
 
         // THEN the user provisioning state is finalized
diff --git a/tests/robotests/src/com/android/managedprovisioning/analytics/DeferredMetricsWriteReadRoboTest.java b/tests/robotests/src/com/android/managedprovisioning/analytics/DeferredMetricsWriteReadRoboTest.java
index 4bff123..5a6ee5a 100644
--- a/tests/robotests/src/com/android/managedprovisioning/analytics/DeferredMetricsWriteReadRoboTest.java
+++ b/tests/robotests/src/com/android/managedprovisioning/analytics/DeferredMetricsWriteReadRoboTest.java
@@ -20,17 +20,12 @@
 
 import android.app.admin.DevicePolicyEventLogger;
 
-import com.android.managedprovisioning.DevicePolicyProtos.DevicePolicyEvent;
-
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -71,9 +66,9 @@
 
     private DevicePolicyEventLogger[] readMetricsFromFile(File file) {
         final List<DevicePolicyEventLogger> eventsList = new ArrayList<>();
-        final DeferredMetricsReader reader = new DeferredMetricsReader(file,
+        final ProcessMetricsJobService processMetricsJobService = new ProcessMetricsJobService(
                 loggers -> eventsList.addAll(Arrays.asList(loggers)));
-        reader.dumpMetricsAndClearFile();
+        processMetricsJobService.executeReadDeferredMetrics(/* params */ null, file);
         Robolectric.flushBackgroundThreadScheduler();
         return eventsList.toArray(new DevicePolicyEventLogger[0]);
     }
diff --git a/tests/robotests/src/com/android/managedprovisioning/analytics/InstantMetricsWriterTest.java b/tests/robotests/src/com/android/managedprovisioning/analytics/InstantMetricsWriterTest.java
index ceab313..0c30508 100644
--- a/tests/robotests/src/com/android/managedprovisioning/analytics/InstantMetricsWriterTest.java
+++ b/tests/robotests/src/com/android/managedprovisioning/analytics/InstantMetricsWriterTest.java
@@ -36,7 +36,7 @@
         final MetricsWriter writer = new InstantMetricsWriter();
         final DevicePolicyEventLogger devicePolicyEventLogger = mock(DevicePolicyEventLogger.class);
 
-        writer.write();
+        writer.write(devicePolicyEventLogger);
 
         verify(devicePolicyEventLogger).write();
         verifyNoMoreInteractions(devicePolicyEventLogger);