Add progressbar to BugReportApp.

When BugReport is initiated when dumpstate is in progress
by the BugReport app, it will show a dialog with a progress
bar and numeric value of the progress.

When dumpstate finishes, it changes the title to "bug report collected",
it doesn't close the dialog.

Bug: 134530860
Change-Id: I2e768d47b8122e3cd67e7872c5373d901cc8e3d6
Test: tested on a hawk rig - took bugreport, uploaded,
      clicked start bugreport shortcut when it's running,
      it shows a progress.
(cherry picked from commit ca85bfd52e652589f1fbf7dfecc50a6c003a0242)
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
index b0c754a..cce327d 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -17,6 +17,7 @@
 
 import static com.google.android.car.bugreport.PackageUtils.getPackageVersion;
 
+import android.annotation.FloatRange;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
 import android.app.Notification;
@@ -32,11 +33,14 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
 import android.view.Display;
 import android.widget.Toast;
 
+import com.google.common.util.concurrent.AtomicDouble;
+
 import libcore.io.IoUtils;
 
 import java.io.BufferedOutputStream;
@@ -93,22 +97,40 @@
     private static final String MESSAGE_FAILURE_DUMPSTATE = "Failed to grab dumpstate";
     private static final String MESSAGE_FAILURE_ZIP = "Failed to zip files";
 
-    // Binder given to clients
+    private static final int PROGRESS_HANDLER_EVENT_PROGRESS = 1;
+    private static final String PROGRESS_HANDLER_DATA_PROGRESS = "progress";
+
+    static final float MAX_PROGRESS_VALUE = 100f;
+
+    /** Binder given to clients. */
     private final IBinder mBinder = new ServiceBinder();
 
+    private final AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false);
+    private final AtomicDouble mBugReportProgress = new AtomicDouble(0);
+
     private MetaBugReport mMetaBugReport;
     private NotificationManager mNotificationManager;
     private NotificationChannel mNotificationChannel;
-    private AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false);
-    private Handler mHandler;
     private ScheduledExecutorService mSingleThreadExecutor;
+    private BugReportProgressListener mBugReportProgressListener;
     private Car mCar;
     private CarBugreportManager mBugreportManager;
     private CarBugreportManager.CarBugreportManagerCallback mCallback;
 
-    /**
-     * Client binder.
-     */
+    /** A handler on the main thread. */
+    private Handler mHandler;
+
+    /** A listener that's notified when bugreport progress changes. */
+    interface BugReportProgressListener {
+        /**
+         * Called when bug report progress changes.
+         *
+         * @param progress - a bug report progress in [0.0, 100.0].
+         */
+        void onProgress(float progress);
+    }
+
+    /** Client binder. */
     public class ServiceBinder extends Binder {
         BugReportService getService() {
             // Return this instance of LocalService so clients can call public methods
@@ -116,6 +138,23 @@
         }
     }
 
+    /** A handler on a main thread. */
+    private class BugReportHandler extends Handler {
+        @Override
+        public void handleMessage(Message message) {
+            switch (message.what) {
+                case PROGRESS_HANDLER_EVENT_PROGRESS:
+                    if (mBugReportProgressListener != null) {
+                        float progress = message.getData().getFloat(PROGRESS_HANDLER_DATA_PROGRESS);
+                        mBugReportProgressListener.onProgress(progress);
+                    }
+                    break;
+                default:
+                    Log.d(TAG, "Unknown event " + message.what + ", ignoring.");
+            }
+        }
+    }
+
     @Override
     public void onCreate() {
         mNotificationManager = getSystemService(NotificationManager.class);
@@ -124,8 +163,8 @@
                 getString(R.string.notification_bugreport_channel_name),
                 NotificationManager.IMPORTANCE_MIN);
         mNotificationManager.createNotificationChannel(mNotificationChannel);
-        mHandler = new Handler();
         mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
+        mHandler = new BugReportHandler();
         mCar = Car.createCar(this);
         try {
             mBugreportManager = (CarBugreportManager) mCar.getCarManager(Car.CAR_BUGREPORT_SERVICE);
@@ -144,6 +183,7 @@
         Log.i(TAG, String.format("Will start collecting bug report, version=%s",
                 getPackageVersion(this)));
         mIsCollectingBugReport.set(true);
+        mBugReportProgress.set(0);
 
         Notification notification =
                 new Notification.Builder(this, NOTIFICATION_STATUS_CHANNEL_ID)
@@ -161,16 +201,32 @@
         return START_NOT_STICKY;
     }
 
+    /** Returns true if bugreporting is in progress. */
     public boolean isCollectingBugReport() {
         return mIsCollectingBugReport.get();
     }
 
+    /** Returns current bugreport progress. */
+    public float getBugReportProgress() {
+        return (float) mBugReportProgress.get();
+    }
+
+    /** Sets a bugreport progress listener. The listener is called on a main thread. */
+    public void setBugReportProgressListener(BugReportProgressListener listener) {
+        mBugReportProgressListener = listener;
+    }
+
+    /** Removes the bugreport progress listener. */
+    public void removeBugReportProgressListener() {
+        mBugReportProgressListener = null;
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
         return mBinder;
     }
 
-    private void sendStatusInformation(@StringRes int resId) {
+    private void showToast(@StringRes int resId) {
         // run on ui thread.
         mHandler.post(() -> Toast.makeText(this, getText(resId), Toast.LENGTH_LONG).show());
     }
@@ -213,7 +269,7 @@
             return result;
         } catch (IOException | InterruptedException e) {
             Log.e(TAG, "screencap process failed: ", e);
-            sendStatusInformation(R.string.toast_status_screencap_failed);
+            showToast(R.string.toast_status_screencap_failed);
         }
         return null;
     }
@@ -257,10 +313,17 @@
             Log.e(TAG, "Failed to grab dump state", e);
             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
                     MESSAGE_FAILURE_DUMPSTATE);
-            sendStatusInformation(R.string.toast_status_dump_state_failed);
+            showToast(R.string.toast_status_dump_state_failed);
         }
     }
 
+    private void sendProgressEventToHandler(float progress) {
+        Message message = new Message();
+        message.what = PROGRESS_HANDLER_EVENT_PROGRESS;
+        message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress);
+        mHandler.sendMessage(message);
+    }
+
     private void requestBugReport(ParcelFileDescriptor outFd) {
         if (DEBUG) {
             Log.d(TAG, "Requesting a bug report from CarBugReportManager.");
@@ -269,20 +332,27 @@
             @Override
             public void onError(int errorCode) {
                 Log.e(TAG, "Bugreport failed " + errorCode);
-                sendStatusInformation(R.string.toast_status_failed);
+                showToast(R.string.toast_status_failed);
                 // TODO(b/133520419): show this error on Info page or add to zip file.
                 scheduleZipTask();
+                // We let the UI know that bug reporting is finished, because the next step is to
+                // zip everything and upload.
+                mBugReportProgress.set(MAX_PROGRESS_VALUE);
+                sendProgressEventToHandler(MAX_PROGRESS_VALUE);
             }
 
             @Override
-            public void onProgress(float progress) {
-                Log.d(TAG, "bugreport progress received " + progress);
+            public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) {
+                mBugReportProgress.set(progress);
+                sendProgressEventToHandler(progress);
             }
 
             @Override
             public void onFinished() {
                 Log.i(TAG, "Bugreport finished");
                 scheduleZipTask();
+                mBugReportProgress.set(MAX_PROGRESS_VALUE);
+                sendProgressEventToHandler(MAX_PROGRESS_VALUE);
             }
         };
         mBugreportManager.requestZippedBugreport(outFd, mCallback);
@@ -303,10 +373,10 @@
             Log.e(TAG, "Failed to zip files", e);
             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
                     MESSAGE_FAILURE_ZIP);
-            sendStatusInformation(R.string.toast_status_failed);
+            showToast(R.string.toast_status_failed);
         }
         mIsCollectingBugReport.set(false);
-        sendStatusInformation(R.string.toast_status_finished);
+        showToast(R.string.toast_status_finished);
     }
 
     @Override