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