Merge "Refactor kitchensink watchdog tests." into sc-v2-dev
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/car_watchdog_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/car_watchdog_test.xml
index 37489ca..c594b13 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/car_watchdog_test.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/car_watchdog_test.xml
@@ -19,15 +19,15 @@
               android:layout_height="match_parent"
               android:orientation="vertical" >
     <Button
-        android:id="@+id/io_overuse_warning_btn"
+        android:id="@+id/non_recurring_io_overuse_btn"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="I/O Overuse Warning"/>
+        android:text="@string/non_recurring_io_overuse"/>
     <Button
-        android:id="@+id/io_overuse_killing_btn"
+        android:id="@+id/recurring_io_overuse_btn"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="I/O Overuse Killing"/>
+        android:text="@string/recurring_io_overuse"/>
     <TextView
         android:id="@+id/io_overuse_textview"
         android:layout_width="wrap_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index c4144e0..16e897c 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -391,4 +391,8 @@
     <string name="remove_metrics_config" translatable="false">Remove MetricsConfig</string>
     <string name="get_report" translatable="false">Get Report</string>
     <string name="show_mem_info" translate="false">Show Memory Info</string>
+
+    <!-- Watchdog Test -->
+    <string name="non_recurring_io_overuse" translate="false">Non-recurring I/O overuse</string>
+    <string name="recurring_io_overuse" translate="false">Recurring I/O overuse</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
index 36a300e..cd5cc3a 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
@@ -16,16 +16,19 @@
 
 package com.google.android.car.kitchensink.watchdog;
 
+import android.annotation.IntDef;
 import android.app.AlertDialog;
 import android.car.watchdog.CarWatchdogManager;
 import android.car.watchdog.IoOveruseStats;
 import android.car.watchdog.ResourceOveruseStats;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.os.Bundle;
 import android.os.FileUtils;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.style.RelativeSizeSpan;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -46,6 +49,8 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.nio.file.Files;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -76,12 +81,35 @@
     private static final double WARN_THRESHOLD_PERCENT = 0.8;
     private static final double EXCEED_WARN_THRESHOLD_PERCENT = 0.9;
 
+    private static final int NOTIFICATION_STATUS_NO = 0;
+    private static final int NOTIFICATION_STATUS_INVALID = 1;
+    private static final int NOTIFICATION_STATUS_VALID = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"NOTIFICATION_STATUS"}, value = {
+            NOTIFICATION_STATUS_NO,
+            NOTIFICATION_STATUS_INVALID,
+            NOTIFICATION_STATUS_VALID
+    })
+    private @interface NotificationStatus{}
+
+    private static final int NOTIFICATION_TYPE_WARNING = 0;
+    private static final int NOTIFICATION_TYPE_OVERUSE = 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"NOTIFICATION_TYPE"}, value = {
+            NOTIFICATION_TYPE_WARNING,
+            NOTIFICATION_TYPE_OVERUSE,
+    })
+    private @interface NotificationType{}
+
     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
     private Context mContext;
     private CarWatchdogManager mCarWatchdogManager;
     private KitchenSinkActivity mActivity;
     private File mTestDir;
     private TextView mOveruseTextView;
+    private TextViewSetter mTextViewSetter;
 
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -104,8 +132,8 @@
         View view = inflater.inflate(R.layout.car_watchdog_test, container, false);
 
         mOveruseTextView = view.findViewById(R.id.io_overuse_textview);
-        Button overuseWarningBtn = view.findViewById(R.id.io_overuse_warning_btn);
-        Button overuseKillingBtn = view.findViewById(R.id.io_overuse_killing_btn);
+        Button nonRecurringIoOveruseButton = view.findViewById(R.id.non_recurring_io_overuse_btn);
+        Button recurringIoOveruseButton = view.findViewById(R.id.recurring_io_overuse_btn);
 
         try {
             mTestDir =
@@ -115,100 +143,145 @@
             mActivity.finish();
         }
 
-        overuseWarningBtn.setOnClickListener(
-                new View.OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        mExecutor.execute(
-                                () -> {
-                                    IoOveruseListener listener = addResourceOveruseListener();
+        nonRecurringIoOveruseButton.setOnClickListener(
+                v -> mExecutor.execute(
+                        () -> {
+                            mTextViewSetter = new TextViewSetter(mOveruseTextView, mActivity);
+                            mTextViewSetter.set("Starting non-recurring I/O overuse test.");
+                            IoOveruseListener listener = addResourceOveruseListener();
 
-                                    if (!writeToDisk(TEN_MEGABYTES)) {
-                                        mCarWatchdogManager.removeResourceOveruseListener(listener);
-                                        return;
-                                    }
+                            if (overuseDiskIo(listener)) {
+                                showAlert("Non-recurring I/O overuse test",
+                                        "Test completed successfully.", 0);
+                            } else {
+                                mTextViewSetter.setPermanent(
+                                        "Non-recurring I/O overuse test failed.");
+                            }
 
-                                    long remainingBytes = fetchRemainingBytes(TEN_MEGABYTES);
-                                    if (remainingBytes == 0) {
-                                        mCarWatchdogManager.removeResourceOveruseListener(listener);
-                                        return;
-                                    }
+                            finishTest(listener);
+                            Log.d(TAG, "Non-recurring I/O overuse test completed.");
+                        }));
 
-                                    /*
-                                     * CarService notifies applications on exceeding 80% of the
-                                     * threshold. The app maybe notified before completing the
-                                     * following write. Ergo, the minimum expected written bytes
-                                     * should be the warn threshold rather than the actual amount
-                                     * of bytes written by the app.
-                                     */
-                                    long bytesToWarnThreshold = (long) Math.ceil(
-                                            (remainingBytes + TEN_MEGABYTES)
-                                                    * WARN_THRESHOLD_PERCENT);
+        recurringIoOveruseButton.setOnClickListener(
+                v -> mExecutor.execute(
+                        () -> {
+                            mTextViewSetter = new TextViewSetter(mOveruseTextView, mActivity);
+                            mTextViewSetter.set("Starting recurring I/O overuse test.");
+                            IoOveruseListener listener = addResourceOveruseListener();
 
-                                    listener.setExpectedMinWrittenBytes(bytesToWarnThreshold);
+                            if (!overuseDiskIo(listener)) {
+                                mTextViewSetter.setPermanent("First disk I/O overuse failed.");
+                                finishTest(listener);
+                                return;
+                            }
+                            mTextViewSetter.setPermanent(
+                                    "First disk I/O overuse completed successfully."
+                                            + System.lineSeparator());
 
-                                    long bytesToExceedWarnThreshold =
-                                            (long) Math.ceil(remainingBytes
-                                                    * EXCEED_WARN_THRESHOLD_PERCENT);
+                            if (!overuseDiskIo(listener)) {
+                                mTextViewSetter.setPermanent("Second disk I/O overuse failed.");
+                                finishTest(listener);
+                                return;
+                            }
+                            mTextViewSetter.setPermanent(
+                                    "Second disk I/O overuse completed successfully."
+                                            + System.lineSeparator());
 
-                                    if (!writeToDisk(bytesToExceedWarnThreshold)) {
-                                        mCarWatchdogManager.removeResourceOveruseListener(listener);
-                                        return;
-                                    }
+                            if (!overuseDiskIo(listener)) {
+                                mTextViewSetter.setPermanent("Third disk I/O overuse failed.");
+                                finishTest(listener);
+                                return;
+                            }
+                            mTextViewSetter.setPermanent(
+                                    "Third disk I/O overuse completed successfully.");
 
-                                    listener.checkIsNotified();
-
-                                    mCarWatchdogManager.removeResourceOveruseListener(listener);
-                                    Log.d(TAG, "Test finished.");
-                                });
-                    }
-                });
-
-        overuseKillingBtn.setOnClickListener(
-                new View.OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        // TODO(b/185807690): Implement test
-                    }
-                });
+                            finishTest(listener);
+                            showAlert("Recurring I/O overuse test", "Test completed successfully.",
+                                    0);
+                            Log.d(TAG, "Recurring I/O overuse test completed.");
+                        }));
 
         return view;
     }
 
+    private boolean overuseDiskIo(IoOveruseListener listener) {
+        DiskIoStats diskIoStats = fetchInitialDiskIoStats();
+        if (diskIoStats == null) {
+            return false;
+        }
+        Log.i(TAG, "Fetched initial disk I/O status: " + diskIoStats);
+
+        /*
+         * CarService notifies applications on exceeding 80% of the overuse threshold. The app maybe
+         * notified before completing the following write. Ergo, the minimum expected written bytes
+         * should be the warn threshold rather than the actual amount of bytes written by the app.
+         */
+        long minBytesWritten =
+                (long) Math.ceil((diskIoStats.totalBytesWritten + diskIoStats.remainingBytes)
+                        * WARN_THRESHOLD_PERCENT);
+        listener.expectNewNotification(minBytesWritten, diskIoStats.totalOveruses,
+                NOTIFICATION_TYPE_WARNING);
+        long bytesToExceedWarnThreshold =
+                (long) Math.ceil(diskIoStats.remainingBytes * EXCEED_WARN_THRESHOLD_PERCENT);
+        if (!writeToDisk(bytesToExceedWarnThreshold) || !listener.isValidNotificationReceived()) {
+            return false;
+        }
+        mTextViewSetter.setPermanent(
+                "80% exceeding I/O overuse notification received successfully.");
+
+        long remainingBytes = listener.getNotifiedRemainingBytes();
+        listener.expectNewNotification(remainingBytes, diskIoStats.totalOveruses + 1,
+                NOTIFICATION_TYPE_OVERUSE);
+        if (!writeToDisk(remainingBytes) || !listener.isValidNotificationReceived()) {
+            return false;
+        }
+        mTextViewSetter.setPermanent(
+                "100% exceeding I/O overuse notification received successfully.");
+
+        return true;
+    }
+
     @Override
     public void onDestroyView() {
         FileUtils.deleteContentsAndDir(mTestDir);
         super.onDestroyView();
     }
 
-    private long fetchRemainingBytes(long minWrittenBytes) {
-        ResourceOveruseStats stats =
-                mCarWatchdogManager.getResourceOveruseStats(
-                        CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
-                        CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
-
-        IoOveruseStats ioOveruseStats = stats.getIoOveruseStats();
-        if (ioOveruseStats == null) {
-            showErrorAlert(
-                    "No I/O overuse stats available for the application after writing "
-                            + minWrittenBytes
-                            + " bytes.");
-            return 0;
+    private @Nullable DiskIoStats fetchInitialDiskIoStats() {
+        if (!writeToDisk(TEN_MEGABYTES)) {
+            return null;
         }
-        if (ioOveruseStats.getTotalBytesWritten() < minWrittenBytes) {
-            showErrorAlert(
-                    "Actual written bytes to disk '"
-                            + minWrittenBytes
-                            + "' don't match written bytes '"
-                            + ioOveruseStats.getTotalBytesWritten()
-                            + "' returned by get request");
-            return 0;
+
+        ResourceOveruseStats resourceOveruseStats = mCarWatchdogManager.getResourceOveruseStats(
+                CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+                CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+        Log.d(TAG, "Stats fetched from watchdog manager: " + resourceOveruseStats);
+
+        IoOveruseStats ioOveruseStats = resourceOveruseStats.getIoOveruseStats();
+        if (ioOveruseStats == null) {
+            showErrorAlert("No I/O overuse stats available for the application after writing "
+                    + TEN_MEGABYTES + " bytes." + System.lineSeparator() + "Note: Start custom "
+                    + "perf collection with 1 second interval before running the test.");
+            return null;
+        }
+        if (ioOveruseStats.getTotalBytesWritten() < TEN_MEGABYTES) {
+            showErrorAlert("Actual written bytes to disk '" + TEN_MEGABYTES
+                    + "' is greater than total bytes written '"
+                    + ioOveruseStats.getTotalBytesWritten() + "' returned by get request.");
+            return null;
         }
         /*
          * Check for foreground mode bytes given kitchensink app is running in the foreground
          * during manual testing.
          */
-        return ioOveruseStats.getRemainingWriteBytes().getForegroundModeBytes();
+        long remainingBytes = ioOveruseStats.getRemainingWriteBytes().getForegroundModeBytes();
+        if (remainingBytes == 0) {
+            showErrorAlert("Zero remaining bytes reported." + System.lineSeparator()
+                    + "Note: Reset resource overuse stats before running the test.");
+            return null;
+        }
+        return new DiskIoStats(ioOveruseStats.getTotalBytesWritten(), remainingBytes,
+                ioOveruseStats.getTotalOveruses());
     }
 
     private IoOveruseListener addResourceOveruseListener() {
@@ -218,88 +291,118 @@
         return listener;
     }
 
+    private void finishTest(IoOveruseListener listener) {
+        if (FileUtils.deleteContents(mTestDir)) {
+            Log.i(TAG, "Deleted contents of the test directory " + mTestDir.getAbsolutePath());
+        } else {
+            Log.e(TAG, "Failed to delete contents of the test directory "
+                    + mTestDir.getAbsolutePath());
+        }
+        mCarWatchdogManager.removeResourceOveruseListener(listener);
+    }
+
+    private boolean writeToDisk(long bytes) {
+        File uniqueFile = new File(mTestDir, Long.toString(System.nanoTime()));
+        boolean result = writeToFile(uniqueFile, bytes);
+        if (uniqueFile.delete()) {
+            Log.i(TAG, "Deleted file: " + uniqueFile.getAbsolutePath());
+        } else {
+            Log.e(TAG, "Failed to delete file: " + uniqueFile.getAbsolutePath());
+        }
+        return result;
+    }
+
+    private boolean writeToFile(File uniqueFile, long bytes) {
+        long writtenBytes = 0;
+        try (FileOutputStream fos = new FileOutputStream(uniqueFile)) {
+            Log.d(TAG, "Attempting to write " + bytes + " bytes");
+            writtenBytes = writeToFos(fos, bytes);
+            if (writtenBytes < bytes) {
+                showErrorAlert("Failed to write '" + bytes + "' bytes to disk. '"
+                        + writtenBytes + "' bytes were successfully written, while '"
+                        + (bytes - writtenBytes)
+                        + "' bytes were pending at the moment the exception occurred."
+                        + System.lineSeparator()
+                        + "Note: Clear the app's storage and rerun the test.");
+                return false;
+            }
+            fos.getFD().sync();
+            mTextViewSetter.set("Wrote " + bytes + " bytes to disk. Waiting "
+                    + (DISK_DELAY_MS / 1000) + " seconds for the disk I/O activity to be detected "
+                    + "by the watchdog service...");
+            Thread.sleep(DISK_DELAY_MS);
+            return true;
+        } catch (IOException | InterruptedException e) {
+            String reason;
+            if (e instanceof IOException) {
+                reason = "I/O exception";
+            } else {
+                reason = "Thread interrupted";
+                Thread.currentThread().interrupt();
+            }
+            String message = reason + " after successfully writing to disk.";
+            Log.e(TAG, message, e);
+            showErrorAlert(message + System.lineSeparator() + System.lineSeparator()
+                    + e.getMessage());
+        }
+        return false;
+    }
+
+    private long writeToFos(FileOutputStream fos, long remainingBytes) {
+        long totalBytesWritten = 0;
+        while (remainingBytes != 0) {
+            int writeBytes =
+                    (int) Math.min(Integer.MAX_VALUE,
+                                    Math.min(Runtime.getRuntime().freeMemory(), remainingBytes));
+            try {
+                fos.write(new byte[writeBytes]);
+            }  catch (InterruptedIOException e) {
+                Thread.currentThread().interrupt();
+                continue;
+            } catch (IOException e) {
+                Log.e(TAG, "I/O exception while writing " + writeBytes + " to disk", e);
+                return totalBytesWritten;
+            }
+            totalBytesWritten += writeBytes;
+            remainingBytes -= writeBytes;
+            if (writeBytes > 0 && remainingBytes > 0) {
+                Log.i(TAG, "Total bytes written: " + totalBytesWritten + "/"
+                        + (totalBytesWritten + remainingBytes));
+                mTextViewSetter.set("Wrote (" + totalBytesWritten + " / "
+                        + (totalBytesWritten + remainingBytes) + ") bytes. Writing to disk...");
+            }
+        }
+        Log.i(TAG, "Write completed.");
+        return totalBytesWritten;
+    }
+
     private void showErrorAlert(String message) {
+        mTextViewSetter.setPermanent("Error: " + message);
         showAlert("Error", message, android.R.drawable.ic_dialog_alert);
     }
 
     private void showAlert(String title, String message, int iconDrawable) {
         mActivity.runOnUiThread(
                 () -> {
+                    SpannableString messageSpan = new SpannableString(message);
+                    messageSpan.setSpan(new RelativeSizeSpan(1.3f), 0, message.length(), 0);
                     new AlertDialog.Builder(mContext)
                             .setTitle(title)
-                            .setMessage(message)
-                            .setPositiveButton(
-                                    android.R.string.yes,
-                                    new DialogInterface.OnClickListener() {
-                                        public void onClick(DialogInterface dialog, int which) {
-                                            // do nothing
-                                        }
-                                    })
+                            .setMessage(messageSpan)
+                            .setPositiveButton(android.R.string.ok, null)
                             .setIcon(iconDrawable)
                             .show();
                 });
     }
 
-    private void onDiskWrite(long writtenBytes, long pendingBytes) {
-        mActivity.runOnUiThread(() -> {
-            if (pendingBytes != 0) {
-                mOveruseTextView.setText("Writing to disk... (" + writtenBytes + " / "
-                        + (writtenBytes + pendingBytes) + ") bytes");
-            } else {
-                mOveruseTextView.setText("Disk write completed!");
-            }
-        });
-    }
-
-    private boolean writeToDisk(long bytes) {
-        long writtenBytes = 0;
-        File uniqueFile = new File(mTestDir, Long.toString(System.nanoTime()));
-        try (FileOutputStream fos = new FileOutputStream(uniqueFile)) {
-            Log.d(TAG, "Attempting to write " + bytes + " bytes");
-            writtenBytes = writeToFos(fos, bytes);
-            if (writtenBytes < bytes) {
-                showErrorAlert("Failed to write '" + bytes + "' bytes to disk. '" + writtenBytes
-                                + "' bytes were successfully written, while '"
-                                + (bytes - writtenBytes)
-                                + "' bytes were pending at the moment the exception occurred.");
-                return false;
-            }
-            fos.getFD().sync();
-            // Wait for the IO event to propagate to the system
-            Thread.sleep(DISK_DELAY_MS);
-            return true;
-        } catch (IOException | InterruptedException e) {
-            String reason = e instanceof IOException ? "I/O exception" : "Thread interrupted";
-            showErrorAlert(
-                    reason + " after successfully writing to disk.\n\n" + e.getMessage());
-            return false;
+    private static String toNotificationTypeString(@NotificationType int type) {
+        switch (type) {
+            case NOTIFICATION_TYPE_WARNING:
+                return "I/O overuse warning notification";
+            case NOTIFICATION_TYPE_OVERUSE:
+                return "I/O overuse exceeding notification";
         }
-    }
-
-    private long writeToFos(FileOutputStream fos, long maxSize) {
-        long writtenSize = 0;
-        while (maxSize != 0) {
-            int writeSize =
-                    (int) Math.min(Integer.MAX_VALUE,
-                                    Math.min(Runtime.getRuntime().freeMemory(), maxSize));
-            try {
-                fos.write(new byte[writeSize]);
-            } catch (InterruptedIOException e) {
-                Thread.currentThread().interrupt();
-                continue;
-            } catch (IOException e) {
-                e.printStackTrace();
-                return writtenSize;
-            }
-            writtenSize += writeSize;
-            maxSize -= writeSize;
-            if (writeSize > 0) {
-                Log.i(TAG, "writeSize:" + writeSize);
-                onDiskWrite(writtenSize, maxSize);
-            }
-        }
-        Log.i(TAG, "Write completed.");
-        return writtenSize;
+        return "Unknown notification type";
     }
 
     private final class IoOveruseListener
@@ -308,46 +411,86 @@
 
         private final Object mLock = new Object();
         @GuardedBy("mLock")
-        private boolean mNotificationReceived;
-
-        private long mExpectedMinWrittenBytes;
+        private @NotificationStatus int mNotificationStatus;
+        @GuardedBy("mLock")
+        private long mNotifiedRemainingBytes;
+        @GuardedBy("mLock")
+        private long mExpectedMinBytesWritten;
+        @GuardedBy("mLock")
+        private long mExceptedTotalOveruses;
+        @GuardedBy("mLock")
+        private @NotificationType int mExpectedNotificationType;
 
         @Override
         public void onOveruse(@NonNull ResourceOveruseStats resourceOveruseStats) {
             synchronized (mLock) {
-                mNotificationReceived = true;
                 mLock.notifyAll();
+                mNotificationStatus = NOTIFICATION_STATUS_INVALID;
+                Log.d(TAG, "Stats received in the "
+                        + toNotificationTypeString(mExpectedNotificationType) + ": "
+                        + resourceOveruseStats);
+                IoOveruseStats ioOveruseStats = resourceOveruseStats.getIoOveruseStats();
+                if (ioOveruseStats == null) {
+                    showErrorAlert("No I/O overuse stats reported for the application in the "
+                            + toNotificationTypeString(mExpectedNotificationType) + '.');
+                    return;
+                }
+                long totalBytesWritten = ioOveruseStats.getTotalBytesWritten();
+                if (totalBytesWritten < mExpectedMinBytesWritten) {
+                    showErrorAlert("Expected minimum bytes written '" + mExpectedMinBytesWritten
+                            + "' is greater than total bytes written '" + totalBytesWritten
+                            + "' reported in the "
+                            + toNotificationTypeString(mExpectedNotificationType) + '.');
+                    return;
+                }
+                mNotifiedRemainingBytes = ioOveruseStats.getRemainingWriteBytes()
+                        .getForegroundModeBytes();
+                if (mExpectedNotificationType == NOTIFICATION_TYPE_WARNING
+                        && mNotifiedRemainingBytes == 0) {
+                    showErrorAlert("Expected non-zero remaining write bytes in the "
+                            + toNotificationTypeString(mExpectedNotificationType) + '.');
+                    return;
+                } else if (mExpectedNotificationType == NOTIFICATION_TYPE_OVERUSE
+                        && mNotifiedRemainingBytes != 0) {
+                    showErrorAlert("Expected zero remaining write bytes doesn't match remaining "
+                            + "write bytes " + mNotifiedRemainingBytes + " reported in the "
+                            + toNotificationTypeString(mExpectedNotificationType) + ".");
+                    return;
+                }
+                long totalOveruses = ioOveruseStats.getTotalOveruses();
+                if (totalOveruses != mExceptedTotalOveruses) {
+                    showErrorAlert("Expected total overuses " + mExceptedTotalOveruses
+                            + "doesn't match total overuses " + totalOveruses + " reported in the "
+                            + toNotificationTypeString(mExpectedNotificationType) + '.');
+                    return;
+                }
+                mNotificationStatus = NOTIFICATION_STATUS_VALID;
             }
-            Log.d(TAG, resourceOveruseStats.toString());
-            if (resourceOveruseStats.getIoOveruseStats() == null) {
-                showErrorAlert(
-                        "No I/O overuse stats reported for the application in the overuse"
-                                + " notification.");
-                return;
-            }
-            long reportedWrittenBytes =
-                    resourceOveruseStats.getIoOveruseStats().getTotalBytesWritten();
-            if (reportedWrittenBytes < mExpectedMinWrittenBytes) {
-                showErrorAlert(
-                        "Actual written bytes to disk '"
-                                + mExpectedMinWrittenBytes
-                                + "' don't match written bytes '"
-                                + reportedWrittenBytes
-                                + "' reported in overuse notification");
-                return;
-            }
-            showAlert("I/O Overuse", "Overuse notification received!", 0);
         }
 
-        public void setExpectedMinWrittenBytes(long expectedMinWrittenBytes) {
-            mExpectedMinWrittenBytes = expectedMinWrittenBytes;
+        public long getNotifiedRemainingBytes() {
+            synchronized (mLock) {
+                return mNotifiedRemainingBytes;
+            }
         }
 
-        private void checkIsNotified() {
+        public void expectNewNotification(long expectedMinBytesWritten, long expectedTotalOveruses,
+                @NotificationType int notificationType) {
+            synchronized (mLock) {
+                mNotificationStatus = NOTIFICATION_STATUS_NO;
+                mExpectedMinBytesWritten = expectedMinBytesWritten;
+                mExceptedTotalOveruses = expectedTotalOveruses;
+                mExpectedNotificationType = notificationType;
+            }
+        }
+
+        private boolean isValidNotificationReceived() {
             synchronized (mLock) {
                 long now = SystemClock.uptimeMillis();
                 long deadline = now + NOTIFICATION_DELAY_MS;
-                while (!mNotificationReceived && now < deadline) {
+                mTextViewSetter.set("Waiting " + (NOTIFICATION_DELAY_MS / 1000)
+                                + " seconds to be notified of disk I/O overuse...");
+                while (mNotificationStatus == NOTIFICATION_STATUS_NO && now < deadline) {
                     try {
                         mLock.wait(deadline - now);
                     } catch (InterruptedException e) {
@@ -358,10 +501,59 @@
                     }
                     break;
                 }
-                if (!mNotificationReceived) {
-                    showErrorAlert("I/O Overuse notification not received.");
+                mTextViewSetter.set("");
+                if (mNotificationStatus == NOTIFICATION_STATUS_NO) {
+                    showErrorAlert("No " + toNotificationTypeString(mExpectedNotificationType)
+                            + " received.");
                 }
+                return mNotificationStatus == NOTIFICATION_STATUS_VALID;
             }
         }
     }
+
+    private static final class DiskIoStats {
+        public final long totalBytesWritten;
+        public final long remainingBytes;
+        public final long totalOveruses;
+
+        DiskIoStats(long totalBytesWritten, long remainingBytes, long totalOveruses) {
+            this.totalBytesWritten = totalBytesWritten;
+            this.remainingBytes = remainingBytes;
+            this.totalOveruses = totalOveruses;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("DiskIoStats{TotalBytesWritten: ").append(totalBytesWritten)
+                    .append(", RemainingBytes: ").append(remainingBytes)
+                    .append(", TotalOveruses: ").append(totalOveruses)
+                    .append("}").toString();
+        }
+    }
+
+    private static final class TextViewSetter {
+        private final SpannableStringBuilder mPermanentSpannableString =
+                new SpannableStringBuilder();
+        private final TextView mTextView;
+        private final KitchenSinkActivity mActivity;
+
+        TextViewSetter(TextView textView, KitchenSinkActivity activity) {
+            mTextView = textView;
+            mActivity = activity;
+        }
+
+        private void setPermanent(CharSequence charSequence) {
+            mPermanentSpannableString.append(System.lineSeparator()).append(charSequence);
+            mActivity.runOnUiThread(() ->
+                    mTextView.setText(new SpannableStringBuilder(mPermanentSpannableString)));
+        }
+
+        private void set(CharSequence charSequence) {
+            mActivity.runOnUiThread(() ->
+                    mTextView.setText(new SpannableStringBuilder(mPermanentSpannableString)
+                            .append(System.lineSeparator())
+                            .append(charSequence)));
+        }
+    }
 }