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