[RESTRICT AUTOMERGE] Add tests and add AutoValue.

- Add BugStorageUtilsTest
- Migrate MetaBugReport pojo to AutoValue.
  This removed 40 lines of extra code.
  It is useful for testing and debugging,
  it automatically creates equals(), toString(),
  hash() methods.
- Add completeDelete - destructively deletes the bugreport from db.
- Add expire bugreport - set status to expired and
  delete zip file.

Test: atest packages/services/Car/tests/BugReportApp/tests
Test: manually on a hawk bench
Bug: 144523228
Change-Id: Ice23b105a71ed2021d28943d31e26ec5c839f637
(cherry picked from commit 08d0edea03bd94a9abcb79f947824b452a8b946a)
diff --git a/tests/BugReportApp/Android.mk b/tests/BugReportApp/Android.mk
index f948b82..186fe4b 100644
--- a/tests/BugReportApp/Android.mk
+++ b/tests/BugReportApp/Android.mk
@@ -37,7 +37,8 @@
 LOCAL_DEX_PREOPT := false
 
 LOCAL_JAVA_LIBRARIES += \
-    android.car
+    android.car \
+    br_google_auto_value_target
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     androidx.recyclerview_recyclerview \
@@ -56,6 +57,11 @@
 
 LOCAL_REQUIRED_MODULES := privapp_whitelist_com.google.android.car.bugreport
 
+# Explicitly define annotation processors even if javac can find them from
+# LOCAL_STATIC_JAVA_LIBRARIES.
+LOCAL_ANNOTATION_PROCESSORS := br_google_auto_value
+LOCAL_ANNOTATION_PROCESSOR_CLASSES := com.google.auto.value.processor.AutoValueProcessor
+
 include $(BUILD_PACKAGE)
 
 # ====  prebuilt library  ========================
@@ -76,3 +82,28 @@
     br_apache_commons:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar
 
 include $(BUILD_MULTI_PREBUILT)
+
+# Following shenanigans are needed for LOCAL_ANNOTATION_PROCESSORS.
+
+# ====  prebuilt host libraries  ========================
+include $(CLEAR_VARS)
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+    br_google_auto_value:../../../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar
+
+include $(BUILD_HOST_PREBUILT)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_MODULE := br_google_auto_value_target
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := ../../../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar
+LOCAL_UNINSTALLABLE_MODULE := true
+
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
index 5d60672..5463813 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
@@ -48,8 +48,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.Random;
@@ -77,9 +75,6 @@
     private static final int VOICE_MESSAGE_MAX_DURATION_MILLIS = 60 * 1000;
     private static final int AUDIO_PERMISSIONS_REQUEST_ID = 1;
 
-    private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT =
-            new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
-
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
     private TextView mInProgressTitleText;
@@ -323,7 +318,7 @@
         // Delete the bugreport from database, otherwise pressing "Show Bugreports" button will
         // create unnecessary cancelled bugreports.
         if (mMetaBugReport != null) {
-            BugStorageUtils.deleteBugReport(this, mMetaBugReport.getId());
+            BugStorageUtils.completeDeleteBugReport(this, mMetaBugReport.getId());
         }
         Intent intent = new Intent(this, BugReportInfoActivity.class);
         startActivity(intent);
@@ -456,10 +451,9 @@
      * @param type bug report type, {@link MetaBugReport.BugReportType}.
      */
     private static MetaBugReport createBugReport(Context context, int type) {
-        Date initiatedAt = new Date();
-        String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
+        String timestamp = MetaBugReport.toBugReportTimestamp(new Date());
         String username = getCurrentUserName(context);
-        String title = BugReportTitleGenerator.generateBugReportTitle(initiatedAt, username);
+        String title = BugReportTitleGenerator.generateBugReportTitle(timestamp, username);
         return BugStorageUtils.createBugReport(context, title, timestamp, username, type);
     }
 
@@ -477,10 +471,9 @@
          *
          * <p>Example: "[A45E8] Feedback from user Driver at 2019-09-21_12:00:00"
          */
-        static String generateBugReportTitle(Date initiatedAt, String username) {
+        static String generateBugReportTitle(String timestamp, String username) {
             // Lookup string is used to search a bug in Buganizer (see b/130915969).
             String lookupString = generateRandomString(LOOKUP_STRING_LENGTH);
-            String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
             return "[" + lookupString + "] Feedback from user " + username + " at " + timestamp;
         }
 
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 ceba655..341ab6b 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -372,11 +372,13 @@
 
     private void zipDirectoryAndScheduleForUpload() {
         try {
+            // All the generated zip files, images and audio messages are located in this dir.
+            // This is located under the current user.
+            File bugReportTempDir = FileUtils.createTempDir(this, mMetaBugReport.getTimestamp());
             // When OutputStream from openBugReportFile is closed, BugStorageProvider automatically
             // schedules an upload job.
             zipDirectoryToOutputStream(
-                    FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()),
-                    BugStorageUtils.openBugReportFile(this, mMetaBugReport));
+                    bugReportTempDir, BugStorageUtils.openBugReportFile(this, mMetaBugReport));
             showBugReportFinishedNotification();
         } catch (IOException e) {
             Log.e(TAG, "Failed to zip files", e);
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
index 2809495..51d5a4c 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
@@ -52,6 +52,7 @@
     private static final String AUTHORITY = "com.google.android.car.bugreport";
     private static final String BUG_REPORTS_TABLE = "bugreports";
     private static final String URL_SEGMENT_DELETE_ZIP_FILE = "deleteZipFile";
+    private static final String URL_SEGMENT_COMPLETE_DELETE = "completeDelete";
 
     static final Uri BUGREPORT_CONTENT_URI =
             Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE);
@@ -71,6 +72,7 @@
     private static final int URL_MATCHED_BUG_REPORTS_URI = 1;
     private static final int URL_MATCHED_BUG_REPORT_ID_URI = 2;
     private static final int URL_MATCHED_DELETE_ZIP_FILE = 3;
+    private static final int URL_MATCHED_COMPLETE_DELETE = 4;
 
     private Handler mHandler;
 
@@ -131,6 +133,12 @@
         return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/" + bugReportId);
     }
 
+    /** Builds {@link Uri} that completely deletes the bugreport from DB and files. */
+    static Uri buildUriCompleteDelete(int bugReportId) {
+        return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/"
+                + URL_SEGMENT_COMPLETE_DELETE + "/" + bugReportId);
+    }
+
     /** Builds {@link Uri} that deletes a zip file for given bugreport id. */
     static Uri buildUriDeleteZipFile(int bugReportId) {
         return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/"
@@ -144,6 +152,9 @@
         mUriMatcher.addURI(
                 AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_DELETE_ZIP_FILE + "/#",
                 URL_MATCHED_DELETE_ZIP_FILE);
+        mUriMatcher.addURI(
+                AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_COMPLETE_DELETE + "/#",
+                URL_MATCHED_COMPLETE_DELETE);
     }
 
     @Override
@@ -244,18 +255,6 @@
             @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
         SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
         switch (mUriMatcher.match(uri)) {
-            //  returns the bugreport that match the id.
-            case URL_MATCHED_BUG_REPORT_ID_URI:
-                if (selection != null || selectionArgs != null) {
-                    throw new IllegalArgumentException("selection is not allowed for "
-                            + URL_MATCHED_BUG_REPORT_ID_URI);
-                }
-                selection = COLUMN_ID + " = ?";
-                selectionArgs = new String[]{uri.getLastPathSegment()};
-                // Ignore the results of zip file deletion, likelihood of failure is too small.
-                deleteZipFile(Integer.parseInt(uri.getLastPathSegment()));
-                getContext().getContentResolver().notifyChange(uri, null);
-                return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
             case URL_MATCHED_DELETE_ZIP_FILE:
                 if (selection != null || selectionArgs != null) {
                     throw new IllegalArgumentException("selection is not allowed for "
@@ -266,6 +265,17 @@
                     return 1;
                 }
                 return 0;
+            case URL_MATCHED_COMPLETE_DELETE:
+                if (selection != null || selectionArgs != null) {
+                    throw new IllegalArgumentException("selection is not allowed for "
+                            + URL_MATCHED_COMPLETE_DELETE);
+                }
+                selection = COLUMN_ID + " = ?";
+                selectionArgs = new String[]{uri.getLastPathSegment()};
+                // Ignore the results of zip file deletion, possibly it wasn't even created.
+                deleteZipFile(Integer.parseInt(uri.getLastPathSegment()));
+                getContext().getContentResolver().notifyChange(uri, null);
+                return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
             default:
                 throw new IllegalArgumentException("Unknown URL " + uri);
         }
@@ -334,7 +344,7 @@
                     Log.e(TAG, "Only read-only or write-only mode supported; mode=" + mode);
                     return;
                 }
-                Log.i(TAG, "File " + path + " opened in write-only mode.");
+                Log.i(TAG, "File " + path + " opened in write-only mode, scheduling for upload.");
                 Status status;
                 if (e == null) {
                     // success writing the file. Update the field to indicate bugreport
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
index 4cdc7cd..6069a8e 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
@@ -34,11 +34,13 @@
 import android.util.Log;
 
 import com.google.api.client.auth.oauth2.TokenResponseException;
+import com.google.common.base.Strings;
 
 import java.io.FileNotFoundException;
 import java.io.OutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -88,20 +90,7 @@
 
         ContentResolver r = context.getContentResolver();
         Uri uri = r.insert(BugStorageProvider.BUGREPORT_CONTENT_URI, values);
-
-        Cursor c = r.query(uri, new String[]{COLUMN_ID}, null, null, null);
-        int count = (c == null) ? 0 : c.getCount();
-        if (count != 1) {
-            throw new RuntimeException("Could not create a bug report entry.");
-        }
-        c.moveToFirst();
-        int id = getInt(c, COLUMN_ID);
-        c.close();
-        return new MetaBugReport.Builder(id, timestamp)
-                .setTitle(title)
-                .setUserName(username)
-                .setType(type)
-                .build();
+        return findBugReport(context, Integer.parseInt(uri.getLastPathSegment())).get();
     }
 
     /**
@@ -125,16 +114,17 @@
     }
 
     /**
-     * Deletes {@link MetaBugReport} record from a local database. Returns true if the record was
-     * deleted.
+     * Deletes {@link MetaBugReport} record from a local database and deletes the associated file.
+     *
+     * <p>WARNING: destructive operation.
      *
      * @param context     - an application context.
      * @param bugReportId - a bug report id.
      * @return true if the record was deleted.
      */
-    static boolean deleteBugReport(@NonNull Context context, int bugReportId) {
+    static boolean completeDeleteBugReport(@NonNull Context context, int bugReportId) {
         ContentResolver r = context.getContentResolver();
-        return r.delete(BugStorageProvider.buildUriWithBugId(bugReportId), null, null) == 1;
+        return r.delete(BugStorageProvider.buildUriCompleteDelete(bugReportId), null, null) == 1;
     }
 
     /** Deletes zip file for given bugreport id; doesn't delete sqlite3 record. */
@@ -144,10 +134,10 @@
     }
 
     /**
-     * Returns bugreports that are waiting to be uploaded.
+     * Returns all the bugreports that are waiting to be uploaded.
      */
     @NonNull
-    public static List<MetaBugReport> getPendingBugReports(@NonNull Context context) {
+    public static List<MetaBugReport> getUploadPendingBugReports(@NonNull Context context) {
         String selection = COLUMN_STATUS + "=?";
         String[] selectionArgs = new String[]{
                 Integer.toString(Status.STATUS_UPLOAD_PENDING.getValue())};
@@ -195,11 +185,12 @@
 
         if (count > 0) c.moveToFirst();
         for (int i = 0; i < count; i++) {
-            MetaBugReport meta = new MetaBugReport.Builder(getInt(c, COLUMN_ID),
-                    getString(c, COLUMN_TIMESTAMP))
+            MetaBugReport meta = MetaBugReport.builder()
+                    .setId(getInt(c, COLUMN_ID))
+                    .setTimestamp(getString(c, COLUMN_TIMESTAMP))
                     .setUserName(getString(c, COLUMN_USERNAME))
                     .setTitle(getString(c, COLUMN_TITLE))
-                    .setFilepath(getString(c, COLUMN_FILEPATH))
+                    .setFilePath(getString(c, COLUMN_FILEPATH))
                     .setStatus(getInt(c, COLUMN_STATUS))
                     .setStatusMessage(getString(c, COLUMN_STATUS_MESSAGE))
                     .setType(getInt(c, COLUMN_TYPE))
@@ -232,7 +223,7 @@
             Log.w(TAG, "Column " + colName + " not found.");
             return "";
         }
-        return c.getString(colIndex);
+        return Strings.nullToEmpty(c.getString(colIndex));
     }
 
     /**
@@ -262,6 +253,22 @@
         setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_PENDING, msg);
     }
 
+    /**
+     * Sets {@link MetaBugReport} status {@link Status#STATUS_EXPIRED}.
+     * Deletes the associated zip file from disk.
+     *
+     * @return true if succeeded.
+     */
+    static boolean expireBugReport(@NonNull Context context,
+            @NonNull MetaBugReport metaBugReport, @NonNull Instant expiredAt) {
+        metaBugReport = setBugReportStatus(
+                context, metaBugReport, Status.STATUS_EXPIRED, "Expired on " + expiredAt);
+        if (metaBugReport.getStatus() != Status.STATUS_EXPIRED.getValue()) {
+            return false;
+        }
+        return deleteBugReportZipfile(context, metaBugReport.getId());
+    }
+
     /** Gets the root cause of the error. */
     @NonNull
     private static String getRootCauseMessage(@Nullable Throwable t) {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
index a20c6c6..eac8b9a 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
@@ -31,7 +31,7 @@
     private static final int RETRY_DELAY_IN_MS = 5_000;
 
     /**
-     * Schedules an upload job under the current user.
+     * Schedules {@link UploadJob} under the current user.
      *
      * <p>Make sure this method is called under the primary user.
      *
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
index 6d5e89b..5157d38 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
@@ -20,13 +20,22 @@
 import android.annotation.IntDef;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.Log;
 
-import com.google.common.base.Strings;
+import com.google.auto.value.AutoValue;
 
 import java.lang.annotation.Retention;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 
 /** Represents the information that a bugreport can contain. */
-public final class MetaBugReport implements Parcelable {
+@AutoValue
+abstract class MetaBugReport implements Parcelable {
+
+    private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
 
     /** Contains {@link #TYPE_SILENT} and audio message. */
     static final int TYPE_INTERACTIVE = 0;
@@ -44,109 +53,91 @@
     @IntDef({TYPE_INTERACTIVE, TYPE_SILENT})
     @interface BugReportType {};
 
-    private final int mId;
-    private final String mTimestamp;
-    private final String mTitle;
-    private final String mUsername;
-    private final String mFilePath;
-    private final int mStatus;
-    private final String mStatusMessage;
-    /** One of {@link BugReportType}. */
-    private final int mType;
-
-    private MetaBugReport(Builder builder) {
-        mId = builder.mId;
-        mTimestamp = builder.mTimestamp;
-        mTitle = builder.mTitle;
-        mUsername = builder.mUsername;
-        mFilePath = builder.mFilePath;
-        mStatus = builder.mStatus;
-        mStatusMessage = builder.mStatusMessage;
-        mType = builder.mType;
-    }
-
     /**
      * @return Id of the bug report. Bug report id monotonically increases and is unique.
      */
-    public int getId() {
-        return mId;
-    }
+    public abstract int getId();
 
     /**
      * @return Username (LDAP) that created this bugreport
      */
-    public String getUsername() {
-        return Strings.nullToEmpty(mUsername);
-    }
+    public abstract String getUserName();
 
     /**
      * @return Title of the bug.
      */
-    public String getTitle() {
-        return Strings.nullToEmpty(mTitle);
-    }
+    public abstract String getTitle();
 
     /**
      * @return Timestamp when the bug report is initialized.
      */
-    public String getTimestamp() {
-        return Strings.nullToEmpty(mTimestamp);
+    public abstract String getTimestamp();
+
+    /**
+     * @return Timestamp converted to {@link Date}.
+     */
+    public Date getTimestampDate() {
+        try {
+            return BUG_REPORT_TIMESTAMP_DATE_FORMAT.parse(getTimestamp());
+        } catch (ParseException e) {
+            Log.e(this.getClass().getSimpleName(), "Failed to parse timestamp", e);
+            return new Date(0); // Return "January 1, 1970, 00:00:00 GMT".
+        }
     }
 
     /**
      * @return path to the zip file
      */
-    public String getFilePath() {
-        return Strings.nullToEmpty(mFilePath);
-    }
+    public abstract String getFilePath();
 
     /**
      * @return {@link Status} of the bug upload.
      */
-    public int getStatus() {
-        return mStatus;
-    }
+    public abstract int getStatus();
 
     /**
      * @return StatusMessage of the bug upload.
      */
-    public String getStatusMessage() {
-        return Strings.nullToEmpty(mStatusMessage);
-    }
+    public abstract String getStatusMessage();
 
     /**
      * @return {@link BugReportType}.
      */
-    public int getType() {
-        return mType;
-    }
+    public abstract int getType();
+
+    /** @return {@link Builder} from the meta bug report. */
+    public abstract Builder toBuilder();
 
     @Override
     public int describeContents() {
         return 0;
     }
 
-    /** Returns {@link Builder} from the meta bug report. */
-    public Builder toBuilder() {
-        return new Builder(mId, mTimestamp)
-                .setFilepath(mFilePath)
-                .setStatus(mStatus)
-                .setStatusMessage(mStatusMessage)
-                .setTitle(mTitle)
-                .setUserName(mUsername)
-                .setType(mType);
-    }
-
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(mId);
-        dest.writeString(mTimestamp);
-        dest.writeString(mTitle);
-        dest.writeString(mUsername);
-        dest.writeString(mFilePath);
-        dest.writeInt(mStatus);
-        dest.writeString(mStatusMessage);
-        dest.writeInt(mType);
+        dest.writeInt(getId());
+        dest.writeString(getTimestamp());
+        dest.writeString(getTitle());
+        dest.writeString(getUserName());
+        dest.writeString(getFilePath());
+        dest.writeInt(getStatus());
+        dest.writeString(getStatusMessage());
+        dest.writeInt(getType());
+    }
+
+    /** Converts {@link Date} to bugreport timestamp. */
+    static String toBugReportTimestamp(Date date) {
+        return BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(date);
+    }
+
+    /** Creates a {@link Builder} with default, non-null values. */
+    static Builder builder() {
+        return new AutoValue_MetaBugReport.Builder()
+                .setTimestamp("")
+                .setFilePath("")
+                .setStatusMessage("")
+                .setTitle("")
+                .setUserName("");
     }
 
     /** A creator that's used by Parcelable. */
@@ -161,10 +152,12 @@
                     int status = in.readInt();
                     String statusMessage = in.readString();
                     int type = in.readInt();
-                    return new Builder(id, timestamp)
+                    return MetaBugReport.builder()
+                            .setId(id)
+                            .setTimestamp(timestamp)
                             .setTitle(title)
                             .setUserName(username)
-                            .setFilepath(filePath)
+                            .setFilePath(filePath)
                             .setStatus(status)
                             .setStatusMessage(statusMessage)
                             .setType(type)
@@ -177,66 +170,32 @@
             };
 
     /** Builder for MetaBugReport. */
-    public static class Builder {
-        private final int mId;
-        private final String mTimestamp;
-        private String mTitle;
-        private String mUsername;
-        private String mFilePath;
-        private int mStatus;
-        private String mStatusMessage;
-        private int mType;
+    @AutoValue.Builder
+    abstract static class Builder {
+        /** Sets id. */
+        public abstract Builder setId(int id);
 
-        /**
-         * Initializes MetaBugReport.Builder.
-         *
-         * @param id        - mandatory bugreport id
-         * @param timestamp - mandatory timestamp when bugreport initialized.
-         */
-        public Builder(int id, String timestamp) {
-            mId = id;
-            mTimestamp = timestamp;
-        }
+        /** Sets timestamp. */
+        public abstract Builder setTimestamp(String timestamp);
 
         /** Sets title. */
-        public Builder setTitle(String title) {
-            mTitle = title;
-            return this;
-        }
+        public abstract Builder setTitle(String title);
 
         /** Sets username. */
-        public Builder setUserName(String username) {
-            mUsername = username;
-            return this;
-        }
+        public abstract Builder setUserName(String username);
 
         /** Sets filepath. */
-        public Builder setFilepath(String filePath) {
-            mFilePath = filePath;
-            return this;
-        }
+        public abstract Builder setFilePath(String filePath);
 
         /** Sets {@link Status}. */
-        public Builder setStatus(int status) {
-            mStatus = status;
-            return this;
-        }
+        public abstract Builder setStatus(int status);
 
         /** Sets statusmessage. */
-        public Builder setStatusMessage(String statusMessage) {
-            mStatusMessage = statusMessage;
-            return this;
-        }
+        public abstract Builder setStatusMessage(String statusMessage);
 
         /** Sets the {@link BugReportType}. */
-        public Builder setType(@BugReportType int type) {
-            mType = type;
-            return this;
-        }
+        public abstract Builder setType(@BugReportType int type);
 
-        /** Returns a {@link MetaBugReport}. */
-        public MetaBugReport build() {
-            return new MetaBugReport(this);
-        }
+        public abstract MetaBugReport build();
     }
 }
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java b/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
index 779750c..82c573f 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
@@ -131,7 +131,7 @@
     /** Returns true is there are more files to upload. */
     @Override
     protected Boolean doInBackground(Void... voids) {
-        List<MetaBugReport> bugReports = BugStorageUtils.getPendingBugReports(mContext);
+        List<MetaBugReport> bugReports = BugStorageUtils.getUploadPendingBugReports(mContext);
 
         for (MetaBugReport bugReport : bugReports) {
             try {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
index 67aa0ca..30179a1 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
@@ -45,7 +45,10 @@
     STATUS_MOVE_FAILED(8),
 
     // Bugreport is moving to USB drive.
-    STATUS_MOVE_IN_PROGRESS(9);
+    STATUS_MOVE_IN_PROGRESS(9),
+
+    // Bugreport is expired. Associated file is deleted from the disk.
+    STATUS_EXPIRED(10);
 
     private final int mValue;
 
@@ -81,6 +84,8 @@
                 return "Move failed";
             case 9:
                 return "Move in progress";
+            case 10:
+                return "Expired";
         }
         return "unknown";
     }
diff --git a/tests/BugReportApp/tests/Android.mk b/tests/BugReportApp/tests/Android.mk
new file mode 100644
index 0000000..2a6ab88
--- /dev/null
+++ b/tests/BugReportApp/tests/Android.mk
@@ -0,0 +1,40 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := BugReportAppTest
+LOCAL_INSTRUMENTATION_FOR := BugReportApp
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := platform
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_JAVA_LIBRARIES := \
+    android.test.base \
+    android.test.mock \
+    android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    truth-prebuilt
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
diff --git a/tests/BugReportApp/tests/AndroidManifest.xml b/tests/BugReportApp/tests/AndroidManifest.xml
new file mode 100644
index 0000000..e6a8537
--- /dev/null
+++ b/tests/BugReportApp/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.car.bugreport.tests" >
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:label="BugReportAppTest"
+            android:targetPackage="com.google.android.car.bugreport" />
+</manifest>
diff --git a/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java b/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java
new file mode 100644
index 0000000..6c4a805
--- /dev/null
+++ b/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.google.android.car.bugreport;
+
+import static com.google.android.car.bugreport.MetaBugReport.TYPE_INTERACTIVE;
+import static com.google.android.car.bugreport.Status.STATUS_PENDING_USER_ACTION;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.util.Date;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class BugStorageUtilsTest {
+    private static final String TIMESTAMP_TODAY = MetaBugReport.toBugReportTimestamp(new Date());
+    private static final int BUGREPORT_ZIP_FILE_CONTENT = 1;
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    @Test
+    public void test_createBugReport_createsAndReturnsMetaBugReport() throws Exception {
+        MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+                STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createFile= */ true);
+
+        assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).get()).isEqualTo(bug);
+    }
+
+    @Test
+    public void test_deleteBugreport_marksBugReportDeletedAndDeletesZip() throws Exception {
+        MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+                STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createFile= */ true);
+        try (InputStream in = mContext.getContentResolver()
+                .openInputStream(BugStorageProvider.buildUriWithBugId(bug.getId()))) {
+            assertThat(in).isNotNull();
+        }
+        Instant now = Instant.now();
+
+        boolean deleteResult = BugStorageUtils.expireBugReport(mContext, bug, now);
+
+        assertThat(deleteResult).isTrue();
+        assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).get())
+                .isEqualTo(bug.toBuilder()
+                        .setStatus(Status.STATUS_EXPIRED.getValue())
+                        .setStatusMessage("Expired on " + now).build());
+        try (InputStream in = mContext.getContentResolver()
+                .openInputStream(BugStorageProvider.buildUriWithBugId(bug.getId()))) {
+            assertThat(in).isNull();
+        }
+    }
+
+    @Test
+    public void test_completeDeleteBugReport_removesBugReportRecordFromDb() throws Exception {
+        MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+                STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createFile= */ true);
+        try (InputStream in = mContext.getContentResolver()
+                .openInputStream(BugStorageProvider.buildUriWithBugId(bug.getId()))) {
+            assertThat(in).isNotNull();
+        }
+
+        boolean deleteResult = BugStorageUtils.completeDeleteBugReport(mContext, bug.getId());
+
+        assertThat(deleteResult).isTrue();
+        assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).isPresent()).isFalse();
+        assertThrows(FileNotFoundException.class, () -> {
+            mContext.getContentResolver()
+                .openInputStream(BugStorageProvider.buildUriWithBugId(bug.getId()));
+        });
+    }
+
+    private MetaBugReport createBugReportWithStatus(
+            String timestamp, Status status, int type, boolean createFile) throws IOException {
+        MetaBugReport bugReport = BugStorageUtils.createBugReport(
+                mContext, "sample title", timestamp, "driver", type);
+        if (createFile) {
+            try (OutputStream out = BugStorageUtils.openBugReportFile(mContext, bugReport)) {
+                out.write(BUGREPORT_ZIP_FILE_CONTENT);
+            }
+        }
+        return BugStorageUtils.setBugReportStatus(mContext, bugReport, status, "");
+    }
+
+    private static void assertThrows(Class<? extends Throwable> exceptionClass,
+            ExceptionRunnable r) {
+        try {
+            r.run();
+        } catch (Throwable e) {
+            assertTrue("Expected exception type " + exceptionClass.getName() + " but got "
+                    + e.getClass().getName(), exceptionClass.isAssignableFrom(e.getClass()));
+            return;
+        }
+        fail("Expected exception type " + exceptionClass.getName()
+                + ", but no exception was thrown");
+    }
+
+    private interface ExceptionRunnable {
+        void run() throws Exception;
+    }
+}