1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.car.kitchensink.watchdog;
18 
19 import android.annotation.IntDef;
20 import android.app.AlertDialog;
21 import android.car.watchdog.CarWatchdogManager;
22 import android.car.watchdog.IoOveruseStats;
23 import android.car.watchdog.ResourceOveruseStats;
24 import android.content.Context;
25 import android.os.Bundle;
26 import android.os.FileUtils;
27 import android.os.Handler;
28 import android.os.SystemClock;
29 import android.text.SpannableString;
30 import android.text.SpannableStringBuilder;
31 import android.text.style.RelativeSizeSpan;
32 import android.util.Log;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.Button;
37 import android.widget.TextView;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.fragment.app.Fragment;
42 
43 import com.android.internal.annotations.GuardedBy;
44 
45 import com.google.android.car.kitchensink.KitchenSinkActivity;
46 import com.google.android.car.kitchensink.R;
47 
48 import java.io.File;
49 import java.io.FileOutputStream;
50 import java.io.IOException;
51 import java.io.InterruptedIOException;
52 import java.lang.annotation.Retention;
53 import java.lang.annotation.RetentionPolicy;
54 import java.nio.file.Files;
55 import java.util.concurrent.ExecutorService;
56 import java.util.concurrent.Executors;
57 import java.util.concurrent.atomic.AtomicBoolean;
58 
59 /**
60  * Fragment to test the I/O monitoring of Car Watchdog.
61  *
62  * <p>Before running the tests, start a custom performance collection, this enables the watchdog
63  * daemon to read proc stats more frequently and reduces the test wait time. Then run the dumpsys
64  * command to reset I/O overuse counters in the adb shell, which clears any previous stats saved by
65  * watchdog. After the test is finished, stop the custom performance collection, this resets
66  * watchdog's I/O stat collection to the default interval.
67  *
68  * <p>Commands:
69  *
70  * <p>adb shell dumpsys android.automotive.watchdog.ICarWatchdog/default --start_perf \
71  * --max_duration 600 --interval 1
72  *
73  * <p>adb shell dumpsys android.automotive.watchdog.ICarWatchdog/default \
74  * --reset_resource_overuse_stats shared:com.google.android.car.uid.kitchensink
75  *
76  * <p>adb shell dumpsys android.automotive.watchdog.ICarWatchdog/default --stop_perf /dev/null
77  */
78 public class CarWatchdogTestFragment extends Fragment {
79     private static final long TEN_MEGABYTES = 1024 * 1024 * 10;
80     private static final int WATCHDOG_IO_EVENT_SYNC_SHORT_DELAY_MS = 3_000;
81     // By default, watchdog daemon syncs the disk I/O events with the CarService once every
82     // 2 minutes unless it is manually changed with the aforementioned `--start_perf` watchdog
83     // daemon's dumpsys command.
84     private static final int WATCHDOG_IO_EVENT_SYNC_LONG_DELAY_MS = 240_000;
85     private static final int USER_APP_SWITCHING_TIMEOUT_MS = 30_000;
86     private static final String TAG = "CarWatchdogTestFragment";
87     private static final double WARN_THRESHOLD_PERCENT = 0.8;
88     private static final double EXCEED_WARN_THRESHOLD_PERCENT = 0.9;
89 
90     private static final int NOTIFICATION_STATUS_NO = 0;
91     private static final int NOTIFICATION_STATUS_INVALID = 1;
92     private static final int NOTIFICATION_STATUS_VALID = 2;
93 
94     @Retention(RetentionPolicy.SOURCE)
95     @IntDef(prefix = {"NOTIFICATION_STATUS"}, value = {
96             NOTIFICATION_STATUS_NO,
97             NOTIFICATION_STATUS_INVALID,
98             NOTIFICATION_STATUS_VALID
99     })
100     private @interface NotificationStatus{}
101 
102     private static final int NOTIFICATION_TYPE_WARNING = 0;
103     private static final int NOTIFICATION_TYPE_OVERUSE = 1;
104 
105     @Retention(RetentionPolicy.SOURCE)
106     @IntDef(prefix = {"NOTIFICATION_TYPE"}, value = {
107             NOTIFICATION_TYPE_WARNING,
108             NOTIFICATION_TYPE_OVERUSE,
109     })
110     private @interface NotificationType{}
111 
112     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
113     private final AtomicBoolean mIsAppInForeground = new AtomicBoolean(true);
114     private Context mContext;
115     private CarWatchdogManager mCarWatchdogManager;
116     private KitchenSinkActivity mActivity;
117     private File mTestDir;
118     private TextView mOveruseTextView;
119     private TextViewSetter mTextViewSetter;
120 
121     @Override
onCreate(@ullable Bundle savedInstanceState)122     public void onCreate(@Nullable Bundle savedInstanceState) {
123         mContext = getContext();
124         mActivity = (KitchenSinkActivity) getActivity();
125         mActivity.requestRefreshManager(
126                 () -> {
127                     mCarWatchdogManager = mActivity.getCarWatchdogManager();
128                 },
129                 new Handler(mContext.getMainLooper()));
130         super.onCreate(savedInstanceState);
131     }
132 
133     @Nullable
134     @Override
onCreateView( @onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)135     public View onCreateView(
136             @NonNull LayoutInflater inflater,
137             @Nullable ViewGroup container,
138             @Nullable Bundle savedInstanceState) {
139         mIsAppInForeground.set(true);
140 
141         View view = inflater.inflate(R.layout.car_watchdog_test, container, false);
142 
143         mOveruseTextView = view.findViewById(R.id.io_overuse_textview);
144         Button nonRecurringIoOveruseButton = view.findViewById(R.id.non_recurring_io_overuse_btn);
145         Button recurringIoOveruseButton = view.findViewById(R.id.recurring_io_overuse_btn);
146         Button longRunningRecurringIoOveruseButton =
147                 view.findViewById(R.id.long_running_recurring_io_overuse_btn);
148 
149         try {
150             mTestDir =
151                     Files.createTempDirectory(mActivity.getFilesDir().toPath(), "testDir").toFile();
152         } catch (IOException e) {
153             e.printStackTrace();
154             mActivity.finish();
155         }
156 
157         nonRecurringIoOveruseButton.setOnClickListener(
158                 v -> mExecutor.execute(
159                         () -> {
160                             mTextViewSetter = new TextViewSetter(mOveruseTextView, mActivity);
161                             mTextViewSetter.setPermanent("Note: Keep the app in the foreground "
162                                     + "until the test completes." + System.lineSeparator());
163                             mTextViewSetter.set("Starting non-recurring I/O overuse test.");
164                             IoOveruseListener listener = addResourceOveruseListener();
165 
166                             if (overuseDiskIo(listener, WATCHDOG_IO_EVENT_SYNC_SHORT_DELAY_MS)) {
167                                 showAlert("Non-recurring I/O overuse test",
168                                         "Test completed successfully.", 0);
169                             } else {
170                                 mTextViewSetter.setPermanent(
171                                         "Non-recurring I/O overuse test failed.");
172                             }
173 
174                             finishTest(listener);
175                             Log.d(TAG, "Non-recurring I/O overuse test completed.");
176                         }));
177 
178         recurringIoOveruseButton.setOnClickListener(v -> mExecutor.execute(
179                 () -> {
180                     mTextViewSetter = new TextViewSetter(mOveruseTextView, mActivity);
181                     mTextViewSetter.setPermanent("Note: Keep the app in the foreground "
182                             + "until the test completes." + System.lineSeparator());
183                     recurringIoOveruseTest(WATCHDOG_IO_EVENT_SYNC_SHORT_DELAY_MS);
184                 }));
185 
186         // Long-running recurring I/O overuse test is helpful to trigger recurring I/O overuse
187         // behavior in environments where shell access is limited.
188         longRunningRecurringIoOveruseButton.setOnClickListener(v -> mExecutor.execute(
189                 () -> {
190                     mTextViewSetter = new TextViewSetter(mOveruseTextView, mActivity);
191                     mTextViewSetter.setPermanent("Note: Please switch the app to the background "
192                             + "and don't bring the app to the foreground until the test completes."
193                             + System.lineSeparator());
194 
195                     waitFor(USER_APP_SWITCHING_TIMEOUT_MS,
196                             "user to switch the app to the background");
197                     recurringIoOveruseTest(WATCHDOG_IO_EVENT_SYNC_LONG_DELAY_MS);
198                 }));
199 
200         return view;
201     }
202 
203     @Override
onPause()204     public void onPause() {
205         super.onPause();
206         Log.d(TAG, "App switched to background");
207         mIsAppInForeground.set(false);
208     }
209 
210     @Override
onResume()211     public void onResume() {
212         super.onResume();
213         Log.d(TAG, "App switched to foreground");
214         mIsAppInForeground.set(true);
215     }
216 
recurringIoOveruseTest(int watchdogSyncDelayMs)217     private void recurringIoOveruseTest(int watchdogSyncDelayMs) {
218         mTextViewSetter.set("Starting recurring I/O overuse test.");
219         IoOveruseListener listener = addResourceOveruseListener();
220 
221         if (!overuseDiskIo(listener, watchdogSyncDelayMs)) {
222             mTextViewSetter.setPermanent("First disk I/O overuse failed.");
223             finishTest(listener);
224             return;
225         }
226         mTextViewSetter.setPermanent("First disk I/O overuse completed successfully."
227                 + System.lineSeparator());
228 
229         if (!overuseDiskIo(listener, watchdogSyncDelayMs)) {
230             mTextViewSetter.setPermanent("Second disk I/O overuse failed.");
231             finishTest(listener);
232             return;
233         }
234         mTextViewSetter.setPermanent("Second disk I/O overuse completed successfully."
235                 + System.lineSeparator());
236 
237         if (!overuseDiskIo(listener, watchdogSyncDelayMs)) {
238             mTextViewSetter.setPermanent("Third disk I/O overuse failed.");
239             finishTest(listener);
240             return;
241         }
242         mTextViewSetter.setPermanent("Third disk I/O overuse completed successfully.");
243 
244         finishTest(listener);
245         showAlert("Recurring I/O overuse test", "Test completed successfully.", 0);
246         Log.d(TAG, "Recurring I/O overuse test completed.");
247     }
248 
overuseDiskIo(IoOveruseListener listener, int watchdogSyncDelayMs)249     private boolean overuseDiskIo(IoOveruseListener listener, int watchdogSyncDelayMs) {
250         DiskIoStats diskIoStats = fetchInitialDiskIoStats(watchdogSyncDelayMs);
251         if (diskIoStats == null) {
252             return false;
253         }
254         Log.i(TAG, "Fetched initial disk I/O status: " + diskIoStats);
255 
256         /*
257          * CarService notifies applications on exceeding 80% of the overuse threshold. The app maybe
258          * notified before completing the following write. Ergo, the minimum expected written bytes
259          * should be the warn threshold rather than the actual amount of bytes written by the app.
260          */
261         long minBytesWritten =
262                 (long) Math.ceil((diskIoStats.totalBytesWritten + diskIoStats.remainingBytes)
263                         * WARN_THRESHOLD_PERCENT);
264         listener.expectNewNotification(minBytesWritten, diskIoStats.totalOveruses,
265                 NOTIFICATION_TYPE_WARNING);
266         long bytesToExceedWarnThreshold =
267                 (long) Math.ceil(diskIoStats.remainingBytes * EXCEED_WARN_THRESHOLD_PERCENT);
268         if (!writeToDisk(bytesToExceedWarnThreshold)
269                 || !listener.isValidNotificationReceived(watchdogSyncDelayMs)) {
270             return false;
271         }
272         mTextViewSetter.setPermanent(
273                 "80% exceeding I/O overuse notification received successfully.");
274 
275         long remainingBytes = listener.getNotifiedRemainingBytes();
276         listener.expectNewNotification(remainingBytes, diskIoStats.totalOveruses + 1,
277                 NOTIFICATION_TYPE_OVERUSE);
278         if (!writeToDisk(remainingBytes)
279                 || !listener.isValidNotificationReceived(watchdogSyncDelayMs)) {
280             return false;
281         }
282         mTextViewSetter.setPermanent(
283                 "100% exceeding I/O overuse notification received successfully.");
284 
285         return true;
286     }
287 
288     @Override
onDestroyView()289     public void onDestroyView() {
290         FileUtils.deleteContentsAndDir(mTestDir);
291         super.onDestroyView();
292     }
293 
fetchInitialDiskIoStats(int watchdogSyncDelayMs)294     private @Nullable DiskIoStats fetchInitialDiskIoStats(int watchdogSyncDelayMs) {
295         if (!writeToDisk(TEN_MEGABYTES)) {
296             return null;
297         }
298         waitFor(watchdogSyncDelayMs,
299                 "the disk I/O activity to be detected by the watchdog service...");
300 
301         ResourceOveruseStats resourceOveruseStats = mCarWatchdogManager.getResourceOveruseStats(
302                 CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
303                 CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
304         Log.d(TAG, "Stats fetched from watchdog manager: " + resourceOveruseStats);
305 
306         IoOveruseStats ioOveruseStats = resourceOveruseStats.getIoOveruseStats();
307         if (ioOveruseStats == null) {
308             showErrorAlert("No I/O overuse stats available for the application after writing "
309                     + TEN_MEGABYTES + " bytes." + System.lineSeparator() + "Note: Start custom "
310                     + "perf collection with 1 second interval before running the test.");
311             return null;
312         }
313         if (ioOveruseStats.getTotalBytesWritten() < TEN_MEGABYTES) {
314             showErrorAlert("Actual written bytes to disk '" + TEN_MEGABYTES
315                     + "' is greater than total bytes written '"
316                     + ioOveruseStats.getTotalBytesWritten() + "' returned by get request.");
317             return null;
318         }
319 
320         long remainingBytes = mIsAppInForeground.get()
321                 ? ioOveruseStats.getRemainingWriteBytes().getForegroundModeBytes()
322                 : ioOveruseStats.getRemainingWriteBytes().getBackgroundModeBytes();
323         if (remainingBytes == 0) {
324             showErrorAlert("Zero remaining bytes reported." + System.lineSeparator()
325                     + "Note: Reset resource overuse stats before running the test.");
326             return null;
327         }
328         return new DiskIoStats(ioOveruseStats.getTotalBytesWritten(), remainingBytes,
329                 ioOveruseStats.getTotalOveruses());
330     }
331 
addResourceOveruseListener()332     private IoOveruseListener addResourceOveruseListener() {
333         IoOveruseListener listener = new IoOveruseListener();
334         mCarWatchdogManager.addResourceOveruseListener(
335                 mActivity.getMainExecutor(), CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, listener);
336         return listener;
337     }
338 
finishTest(IoOveruseListener listener)339     private void finishTest(IoOveruseListener listener) {
340         if (FileUtils.deleteContents(mTestDir)) {
341             Log.i(TAG, "Deleted contents of the test directory " + mTestDir.getAbsolutePath());
342         } else {
343             Log.e(TAG, "Failed to delete contents of the test directory "
344                     + mTestDir.getAbsolutePath());
345         }
346         mCarWatchdogManager.removeResourceOveruseListener(listener);
347     }
348 
writeToDisk(long bytes)349     private boolean writeToDisk(long bytes) {
350         File uniqueFile = new File(mTestDir, Long.toString(System.nanoTime()));
351         boolean result = writeToFile(uniqueFile, bytes);
352         if (uniqueFile.delete()) {
353             Log.i(TAG, "Deleted file: " + uniqueFile.getAbsolutePath());
354         } else {
355             Log.e(TAG, "Failed to delete file: " + uniqueFile.getAbsolutePath());
356         }
357         return result;
358     }
359 
writeToFile(File uniqueFile, long bytes)360     private boolean writeToFile(File uniqueFile, long bytes) {
361         long writtenBytes = 0;
362         try (FileOutputStream fos = new FileOutputStream(uniqueFile)) {
363             Log.d(TAG, "Attempting to write " + bytes + " bytes");
364             writtenBytes = writeToFos(fos, bytes);
365             if (writtenBytes < bytes) {
366                 showErrorAlert("Failed to write '" + bytes + "' bytes to disk. '"
367                         + writtenBytes + "' bytes were successfully written, while '"
368                         + (bytes - writtenBytes)
369                         + "' bytes were pending at the moment the exception occurred."
370                         + System.lineSeparator()
371                         + "Note: Clear the app's storage and rerun the test.");
372                 return false;
373             }
374             fos.getFD().sync();
375             mTextViewSetter.set("Wrote " + bytes + " bytes to disk.");
376             return true;
377         } catch (IOException e) {
378             String message = "I/O exception after successfully writing to disk.";
379             Log.e(TAG, message, e);
380             showErrorAlert(message + System.lineSeparator() + System.lineSeparator()
381                     + e.getMessage());
382         }
383         return false;
384     }
385 
writeToFos(FileOutputStream fos, long remainingBytes)386     private long writeToFos(FileOutputStream fos, long remainingBytes) {
387         long totalBytesWritten = 0;
388         while (remainingBytes != 0) {
389             int writeBytes =
390                     (int) Math.min(Integer.MAX_VALUE,
391                                     Math.min(Runtime.getRuntime().freeMemory(), remainingBytes));
392             try {
393                 fos.write(new byte[writeBytes]);
394             }  catch (InterruptedIOException e) {
395                 Thread.currentThread().interrupt();
396                 continue;
397             } catch (IOException e) {
398                 Log.e(TAG, "I/O exception while writing " + writeBytes + " to disk", e);
399                 return totalBytesWritten;
400             }
401             totalBytesWritten += writeBytes;
402             remainingBytes -= writeBytes;
403             if (writeBytes > 0 && remainingBytes > 0) {
404                 Log.i(TAG, "Total bytes written: " + totalBytesWritten + "/"
405                         + (totalBytesWritten + remainingBytes));
406                 mTextViewSetter.set("Wrote (" + totalBytesWritten + " / "
407                         + (totalBytesWritten + remainingBytes) + ") bytes. Writing to disk...");
408             }
409         }
410         Log.i(TAG, "Write completed.");
411         return totalBytesWritten;
412     }
413 
waitFor(int waitMs, String reason)414     private void waitFor(int waitMs, String reason) {
415         try {
416             mTextViewSetter.set("Waiting " + (waitMs / 1000) + " seconds for " + reason);
417             Thread.sleep(waitMs);
418         } catch (InterruptedException e) {
419             Thread.currentThread().interrupt();
420             String message = "Thread interrupted while waiting for " + reason;
421             Log.e(TAG, message, e);
422             showErrorAlert(message + System.lineSeparator() + System.lineSeparator()
423                     + e.getMessage());
424         }
425     }
426 
showErrorAlert(String message)427     private void showErrorAlert(String message) {
428         mTextViewSetter.setPermanent("Error: " + message);
429         showAlert("Error", message, android.R.drawable.ic_dialog_alert);
430     }
431 
showAlert(String title, String message, int iconDrawable)432     private void showAlert(String title, String message, int iconDrawable) {
433         mActivity.runOnUiThread(
434                 () -> {
435                     SpannableString messageSpan = new SpannableString(message);
436                     messageSpan.setSpan(new RelativeSizeSpan(1.3f), 0, message.length(), 0);
437                     new AlertDialog.Builder(mContext)
438                             .setTitle(title)
439                             .setMessage(messageSpan)
440                             .setPositiveButton(android.R.string.ok, null)
441                             .setIcon(iconDrawable)
442                             .show();
443                 });
444     }
445 
toNotificationTypeString(@otificationType int type)446     private static String toNotificationTypeString(@NotificationType int type) {
447         switch (type) {
448             case NOTIFICATION_TYPE_WARNING:
449                 return "I/O overuse warning notification";
450             case NOTIFICATION_TYPE_OVERUSE:
451                 return "I/O overuse exceeding notification";
452         }
453         return "Unknown notification type";
454     }
455 
456     private final class IoOveruseListener
457             implements CarWatchdogManager.ResourceOveruseListener {
458         private static final int NOTIFICATION_DELAY_MS = 10_000;
459 
460         private final Object mLock = new Object();
461         @GuardedBy("mLock")
462         private @NotificationStatus int mNotificationStatus;
463         @GuardedBy("mLock")
464         private long mNotifiedRemainingBytes;
465         @GuardedBy("mLock")
466         private long mExpectedMinBytesWritten;
467         @GuardedBy("mLock")
468         private long mExceptedTotalOveruses;
469         @GuardedBy("mLock")
470         private @NotificationType int mExpectedNotificationType;
471 
472         @Override
onOveruse(@onNull ResourceOveruseStats resourceOveruseStats)473         public void onOveruse(@NonNull ResourceOveruseStats resourceOveruseStats) {
474             synchronized (mLock) {
475                 mLock.notifyAll();
476                 mNotificationStatus = NOTIFICATION_STATUS_INVALID;
477                 Log.d(TAG, "Stats received in the "
478                         + toNotificationTypeString(mExpectedNotificationType) + ": "
479                         + resourceOveruseStats);
480                 IoOveruseStats ioOveruseStats = resourceOveruseStats.getIoOveruseStats();
481                 if (ioOveruseStats == null) {
482                     showErrorAlert("No I/O overuse stats reported for the application in the "
483                             + toNotificationTypeString(mExpectedNotificationType) + '.');
484                     return;
485                 }
486                 long totalBytesWritten = ioOveruseStats.getTotalBytesWritten();
487                 if (totalBytesWritten < mExpectedMinBytesWritten) {
488                     showErrorAlert("Expected minimum bytes written '" + mExpectedMinBytesWritten
489                             + "' is greater than total bytes written '" + totalBytesWritten
490                             + "' reported in the "
491                             + toNotificationTypeString(mExpectedNotificationType) + '.');
492                     return;
493                 }
494                 mNotifiedRemainingBytes = mIsAppInForeground.get()
495                         ? ioOveruseStats.getRemainingWriteBytes().getForegroundModeBytes()
496                         : ioOveruseStats.getRemainingWriteBytes().getBackgroundModeBytes();
497                 if (mExpectedNotificationType == NOTIFICATION_TYPE_WARNING
498                         && mNotifiedRemainingBytes == 0) {
499                     showErrorAlert("Expected non-zero remaining write bytes in the "
500                             + toNotificationTypeString(mExpectedNotificationType) + '.');
501                     return;
502                 } else if (mExpectedNotificationType == NOTIFICATION_TYPE_OVERUSE
503                         && mNotifiedRemainingBytes != 0) {
504                     showErrorAlert("Expected zero remaining write bytes doesn't match remaining "
505                             + "write bytes " + mNotifiedRemainingBytes + " reported in the "
506                             + toNotificationTypeString(mExpectedNotificationType) + ".");
507                     return;
508                 }
509                 long totalOveruses = ioOveruseStats.getTotalOveruses();
510                 if (totalOveruses != mExceptedTotalOveruses) {
511                     showErrorAlert("Expected total overuses " + mExceptedTotalOveruses
512                             + "doesn't match total overuses " + totalOveruses + " reported in the "
513                             + toNotificationTypeString(mExpectedNotificationType) + '.');
514                     return;
515                 }
516                 mNotificationStatus = NOTIFICATION_STATUS_VALID;
517             }
518         }
519 
getNotifiedRemainingBytes()520         public long getNotifiedRemainingBytes() {
521             synchronized (mLock) {
522                 return mNotifiedRemainingBytes;
523             }
524         }
525 
expectNewNotification(long expectedMinBytesWritten, long expectedTotalOveruses, @NotificationType int notificationType)526         public void expectNewNotification(long expectedMinBytesWritten, long expectedTotalOveruses,
527                 @NotificationType int notificationType) {
528             synchronized (mLock) {
529                 mNotificationStatus = NOTIFICATION_STATUS_NO;
530                 mExpectedMinBytesWritten = expectedMinBytesWritten;
531                 mExceptedTotalOveruses = expectedTotalOveruses;
532                 mExpectedNotificationType = notificationType;
533             }
534         }
535 
isValidNotificationReceived(int watchdogSyncDelayMs)536         private boolean isValidNotificationReceived(int watchdogSyncDelayMs) {
537             synchronized (mLock) {
538                 long now = SystemClock.uptimeMillis();
539                 long deadline = now + NOTIFICATION_DELAY_MS + watchdogSyncDelayMs;
540                 mTextViewSetter.set("Waiting "
541                         + ((NOTIFICATION_DELAY_MS + watchdogSyncDelayMs) / 1000)
542                         + " seconds to be notified of disk I/O overuse...");
543                 while (mNotificationStatus == NOTIFICATION_STATUS_NO && now < deadline) {
544                     try {
545                         mLock.wait(deadline - now);
546                     } catch (InterruptedException e) {
547                         Thread.currentThread().interrupt();
548                         continue;
549                     } finally {
550                         now = SystemClock.uptimeMillis();
551                     }
552                     break;
553                 }
554                 mTextViewSetter.set("");
555                 if (mNotificationStatus == NOTIFICATION_STATUS_NO) {
556                     showErrorAlert("No " + toNotificationTypeString(mExpectedNotificationType)
557                             + " received.");
558                 }
559                 return mNotificationStatus == NOTIFICATION_STATUS_VALID;
560             }
561         }
562     }
563 
564     private static final class DiskIoStats {
565         public final long totalBytesWritten;
566         public final long remainingBytes;
567         public final long totalOveruses;
568 
DiskIoStats(long totalBytesWritten, long remainingBytes, long totalOveruses)569         DiskIoStats(long totalBytesWritten, long remainingBytes, long totalOveruses) {
570             this.totalBytesWritten = totalBytesWritten;
571             this.remainingBytes = remainingBytes;
572             this.totalOveruses = totalOveruses;
573         }
574 
575         @Override
toString()576         public String toString() {
577             return new StringBuilder()
578                     .append("DiskIoStats{TotalBytesWritten: ").append(totalBytesWritten)
579                     .append(", RemainingBytes: ").append(remainingBytes)
580                     .append(", TotalOveruses: ").append(totalOveruses)
581                     .append("}").toString();
582         }
583     }
584 
585     private static final class TextViewSetter {
586         private final SpannableStringBuilder mPermanentSpannableString =
587                 new SpannableStringBuilder();
588         private final TextView mTextView;
589         private final KitchenSinkActivity mActivity;
590 
TextViewSetter(TextView textView, KitchenSinkActivity activity)591         TextViewSetter(TextView textView, KitchenSinkActivity activity) {
592             mTextView = textView;
593             mActivity = activity;
594         }
595 
setPermanent(CharSequence charSequence)596         private void setPermanent(CharSequence charSequence) {
597             mPermanentSpannableString.append(System.lineSeparator()).append(charSequence);
598             mActivity.runOnUiThread(() ->
599                     mTextView.setText(new SpannableStringBuilder(mPermanentSpannableString)));
600         }
601 
set(CharSequence charSequence)602         private void set(CharSequence charSequence) {
603             mActivity.runOnUiThread(() ->
604                     mTextView.setText(new SpannableStringBuilder(mPermanentSpannableString)
605                             .append(System.lineSeparator())
606                             .append(charSequence)));
607         }
608     }
609 }
610