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