1 /* 2 * Copyright (C) 2015 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.android.shell; 18 19 import static android.test.MoreAsserts.assertContainsRegex; 20 21 import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; 22 import static com.android.shell.BugreportPrefs.getWarningState; 23 import static com.android.shell.BugreportPrefs.setWarningState; 24 import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_REQUESTED; 25 import static com.android.shell.BugreportProgressService.PROPERTY_LAST_ID; 26 import static com.android.shell.BugreportProgressService.SCREENSHOT_DELAY_SECONDS; 27 28 import static org.junit.Assert.assertEquals; 29 import static org.junit.Assert.assertFalse; 30 import static org.junit.Assert.assertNotEquals; 31 import static org.junit.Assert.assertNotNull; 32 import static org.junit.Assert.assertNull; 33 import static org.junit.Assert.assertTrue; 34 import static org.junit.Assert.fail; 35 import static org.mockito.ArgumentMatchers.any; 36 import static org.mockito.ArgumentMatchers.anyBoolean; 37 import static org.mockito.ArgumentMatchers.anyInt; 38 import static org.mockito.Mockito.timeout; 39 import static org.mockito.Mockito.times; 40 import static org.mockito.Mockito.verify; 41 42 import android.app.ActivityManager; 43 import android.app.ActivityManager.RunningServiceInfo; 44 import android.app.Instrumentation; 45 import android.app.NotificationManager; 46 import android.content.Context; 47 import android.content.Intent; 48 import android.net.Uri; 49 import android.os.BugreportManager; 50 import android.os.Build; 51 import android.os.Bundle; 52 import android.os.IDumpstate; 53 import android.os.IDumpstateListener; 54 import android.os.ParcelFileDescriptor; 55 import android.os.SystemClock; 56 import android.os.SystemProperties; 57 import android.service.notification.StatusBarNotification; 58 import android.text.TextUtils; 59 import android.text.format.DateUtils; 60 import android.util.Log; 61 62 import androidx.test.InstrumentationRegistry; 63 import androidx.test.filters.LargeTest; 64 import androidx.test.rule.ServiceTestRule; 65 import androidx.test.runner.AndroidJUnit4; 66 import androidx.test.uiautomator.UiDevice; 67 import androidx.test.uiautomator.UiObject; 68 import androidx.test.uiautomator.UiObject2; 69 import androidx.test.uiautomator.UiObjectNotFoundException; 70 71 import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener; 72 73 import libcore.io.IoUtils; 74 import libcore.io.Streams; 75 76 import org.junit.After; 77 import org.junit.Before; 78 import org.junit.Rule; 79 import org.junit.Test; 80 import org.junit.rules.TestName; 81 import org.junit.runner.RunWith; 82 import org.mockito.ArgumentCaptor; 83 import org.mockito.Mock; 84 import org.mockito.Mockito; 85 import org.mockito.MockitoAnnotations; 86 87 import java.io.BufferedOutputStream; 88 import java.io.BufferedWriter; 89 import java.io.ByteArrayOutputStream; 90 import java.io.FileOutputStream; 91 import java.io.IOException; 92 import java.io.InputStream; 93 import java.io.OutputStreamWriter; 94 import java.io.Writer; 95 import java.util.ArrayList; 96 import java.util.List; 97 import java.util.SortedSet; 98 import java.util.TreeSet; 99 import java.util.zip.ZipEntry; 100 import java.util.zip.ZipInputStream; 101 import java.util.zip.ZipOutputStream; 102 103 /** 104 * Integration tests for {@link BugreportProgressService}. 105 * <p> 106 * These tests rely on external UI components (like the notificatio bar and activity chooser), 107 * which can make them unreliable and slow. 108 * <p> 109 * The general workflow is: 110 * <ul> 111 * <li>creates the bug report files 112 * <li>generates the BUGREPORT_FINISHED intent 113 * <li>emulate user actions to share the intent with a custom activity 114 * <li>asserts the extras received by the custom activity 115 * </ul> 116 * <p> 117 * <strong>NOTE</strong>: these tests only work if the device is unlocked. 118 */ 119 @LargeTest 120 @RunWith(AndroidJUnit4.class) 121 public class BugreportReceiverTest { 122 private static final String TAG = "BugreportReceiverTest"; 123 124 // Timeout for UI operations, in milliseconds. 125 private static final int TIMEOUT = (int) (5 * DateUtils.SECOND_IN_MILLIS); 126 127 // The default timeout is too short to verify the notification button state. Using a longer 128 // timeout in the tests. 129 private static final int SCREENSHOT_DELAY_SECONDS = 5; 130 131 // Timeout for when waiting for a screenshot to finish. 132 private static final int SAFE_SCREENSHOT_DELAY = SCREENSHOT_DELAY_SECONDS + 10; 133 134 private static final String BUGREPORT_FILE = "test_bugreport.txt"; 135 private static final String SCREENSHOT_FILE = "test_screenshot.png"; 136 private static final String BUGREPORT_CONTENT = "Dump, might as well dump!\n"; 137 private static final String SCREENSHOT_CONTENT = "A picture is worth a thousand words!\n"; 138 139 private static final String NAME = "BUG, Y U NO REPORT?"; 140 private static final String NEW_NAME = "Bug_Forrest_Bug"; 141 private static final String TITLE = "Wimbugdom Champion 2015"; 142 143 private static final String NO_DESCRIPTION = null; 144 private static final String NO_NAME = null; 145 private static final String NO_SCREENSHOT = null; 146 private static final String NO_TITLE = null; 147 148 private String mDescription; 149 private String mProgressTitle; 150 private int mBugreportId; 151 152 private Context mContext; 153 private UiBot mUiBot; 154 private CustomActionSendMultipleListener mListener; 155 private BugreportProgressService mService; 156 private IDumpstateListener mIDumpstateListener; 157 private ParcelFileDescriptor mBugreportFd; 158 private ParcelFileDescriptor mScreenshotFd; 159 160 @Mock private IDumpstate mMockIDumpstate; 161 162 @Rule public TestName mName = new TestName(); 163 @Rule public ServiceTestRule mServiceRule = new ServiceTestRule(); 164 165 @Before setUp()166 public void setUp() throws Exception { 167 Log.i(TAG, getName() + ".setup()"); 168 MockitoAnnotations.initMocks(this); 169 Instrumentation instrumentation = getInstrumentation(); 170 mContext = instrumentation.getTargetContext(); 171 mUiBot = new UiBot(instrumentation, TIMEOUT); 172 mListener = ActionSendMultipleConsumerActivity.getListener(mContext); 173 174 cancelExistingNotifications(); 175 176 mBugreportId = getBugreportId(); 177 mProgressTitle = getBugreportInProgress(mBugreportId); 178 // Creates a multi-line description. 179 StringBuilder sb = new StringBuilder(); 180 for (int i = 1; i <= 20; i++) { 181 sb.append("All work and no play makes Shell a dull app!\n"); 182 } 183 mDescription = sb.toString(); 184 185 // Mocks BugreportManager and updates tests value to the service 186 mService = ((BugreportProgressService.LocalBinder) mServiceRule.bindService( 187 new Intent(mContext, BugreportProgressService.class))).getService(); 188 mService.mBugreportManager = new BugreportManager(mContext, mMockIDumpstate); 189 mService.mScreenshotDelaySec = SCREENSHOT_DELAY_SECONDS; 190 // Dup the fds which are passing to startBugreport function. 191 Mockito.doAnswer(invocation -> { 192 final boolean isScreenshotRequested = invocation.getArgument(7); 193 if (isScreenshotRequested) { 194 mScreenshotFd = ParcelFileDescriptor.dup(invocation.getArgument(3)); 195 } 196 mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2)); 197 return null; 198 }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(), 199 any(), anyBoolean(), anyBoolean()); 200 int bugreportStateHide = mContext.getResources().getInteger( 201 com.android.internal.R.integer.bugreport_state_hide); 202 setWarningState(mContext, bugreportStateHide); 203 204 mUiBot.turnScreenOn(); 205 } 206 207 @After tearDown()208 public void tearDown() throws Exception { 209 Log.i(TAG, getName() + ".tearDown()"); 210 if (mBugreportFd != null) { 211 IoUtils.closeQuietly(mBugreportFd); 212 } 213 if (mScreenshotFd != null) { 214 IoUtils.closeQuietly(mScreenshotFd); 215 } 216 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 217 try { 218 cancelExistingNotifications(); 219 } finally { 220 // Collapses just in case, so a failure here does not compromise tests on other classes. 221 mUiBot.collapseStatusBar(); 222 } 223 } 224 225 /* 226 * TODO: this test is incomplete because: 227 * - the assertProgressNotification() is not really asserting the progress because the 228 * UI automation API doesn't provide a way to check the notification progress bar value 229 * - it should use the binder object instead of SystemProperties to update progress 230 */ 231 @Test testProgress()232 public void testProgress() throws Exception { 233 sendBugreportStarted(); 234 waitForScreenshotButtonEnabled(true); 235 assertProgressNotification(mProgressTitle, 0f); 236 237 mIDumpstateListener.onProgress(10); 238 assertProgressNotification(mProgressTitle, 10); 239 240 mIDumpstateListener.onProgress(95); 241 assertProgressNotification(mProgressTitle, 95.00f); 242 243 // ...but never more than the capped value. 244 mIDumpstateListener.onProgress(200); 245 assertProgressNotification(mProgressTitle, 99); 246 247 mIDumpstateListener.onProgress(300); 248 assertProgressNotification(mProgressTitle, 99); 249 250 Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId, 1); 251 assertActionSendMultiple(extras); 252 253 assertServiceNotRunning(); 254 } 255 256 @Test testStressProgress()257 public void testStressProgress() throws Exception { 258 sendBugreportStarted(); 259 waitForScreenshotButtonEnabled(true); 260 261 for (int i = 0; i <= 1000; i++) { 262 mIDumpstateListener.onProgress(i); 263 } 264 sendBugreportFinished(); 265 Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 1); 266 assertActionSendMultiple(extras); 267 268 assertServiceNotRunning(); 269 } 270 271 @Test testProgress_cancel()272 public void testProgress_cancel() throws Exception { 273 sendBugreportStarted(); 274 waitForScreenshotButtonEnabled(true); 275 276 assertProgressNotification(mProgressTitle, 00.00f); 277 278 cancelFromNotification(mProgressTitle); 279 280 assertServiceNotRunning(); 281 } 282 283 @Test testProgress_takeExtraScreenshot()284 public void testProgress_takeExtraScreenshot() throws Exception { 285 sendBugreportStarted(); 286 287 waitForScreenshotButtonEnabled(true); 288 takeScreenshot(); 289 assertScreenshotButtonEnabled(false); 290 waitForScreenshotButtonEnabled(true); 291 292 Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId, 2); 293 assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 1); 294 295 assertServiceNotRunning(); 296 } 297 298 @Test testScreenshotFinishesAfterBugreport()299 public void testScreenshotFinishesAfterBugreport() throws Exception { 300 sendBugreportStarted(); 301 waitForScreenshotButtonEnabled(true); 302 takeScreenshot(); 303 sendBugreportFinished(); 304 waitShareNotification(mBugreportId); 305 306 // There's no indication in the UI about the screenshot finish, so just sleep like a baby... 307 sleep(SAFE_SCREENSHOT_DELAY * DateUtils.SECOND_IN_MILLIS); 308 309 Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 2); 310 assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 1); 311 312 assertServiceNotRunning(); 313 } 314 315 @Test testProgress_changeDetailsInvalidInput()316 public void testProgress_changeDetailsInvalidInput() throws Exception { 317 sendBugreportStarted(); 318 waitForScreenshotButtonEnabled(true); 319 320 DetailsUi detailsUi = new DetailsUi(mBugreportId); 321 322 // Change name 323 detailsUi.focusOnName(); 324 detailsUi.nameField.setText(NEW_NAME); 325 detailsUi.focusAwayFromName(); 326 detailsUi.clickOk(); 327 328 // Now try to set an invalid name. 329 detailsUi.reOpen(NEW_NAME); 330 detailsUi.nameField.setText("/etc/passwd"); 331 detailsUi.clickOk(); 332 333 // Finally, make the real changes. 334 detailsUi.reOpen("_etc_passwd"); 335 detailsUi.nameField.setText(NEW_NAME); 336 detailsUi.titleField.setText(TITLE); 337 detailsUi.descField.setText(mDescription); 338 339 detailsUi.clickOk(); 340 341 assertProgressNotification(NEW_NAME, 00.00f); 342 343 Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE, 1); 344 assertActionSendMultiple(extras, NEW_NAME, TITLE, mDescription, 0); 345 346 assertServiceNotRunning(); 347 } 348 349 @Test testProgress_cancelBugClosesDetailsDialog()350 public void testProgress_cancelBugClosesDetailsDialog() throws Exception { 351 sendBugreportStarted(); 352 waitForScreenshotButtonEnabled(true); 353 354 cancelFromNotification(mProgressTitle); 355 mUiBot.collapseStatusBar(); 356 357 assertDetailsUiClosed(); 358 assertServiceNotRunning(); 359 } 360 361 @Test testProgress_changeDetailsTest()362 public void testProgress_changeDetailsTest() throws Exception { 363 sendBugreportStarted(); 364 waitForScreenshotButtonEnabled(true); 365 366 DetailsUi detailsUi = new DetailsUi(mBugreportId); 367 368 // Change fields. 369 detailsUi.reOpen(mProgressTitle); 370 detailsUi.nameField.setText(NEW_NAME); 371 detailsUi.titleField.setText(TITLE); 372 detailsUi.descField.setText(mDescription); 373 374 detailsUi.clickOk(); 375 376 assertProgressNotification(NEW_NAME, 00.00f); 377 378 Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE, 1); 379 assertActionSendMultiple(extras, NEW_NAME, TITLE, mDescription, 0); 380 381 assertServiceNotRunning(); 382 } 383 384 @Test testProgress_changeJustDetailsTouchingDetails()385 public void testProgress_changeJustDetailsTouchingDetails() throws Exception { 386 changeJustDetailsTest(true); 387 } 388 389 @Test testProgress_changeJustDetailsTouchingNotification()390 public void testProgress_changeJustDetailsTouchingNotification() throws Exception { 391 changeJustDetailsTest(false); 392 } 393 changeJustDetailsTest(boolean touchDetails)394 private void changeJustDetailsTest(boolean touchDetails) throws Exception { 395 sendBugreportStarted(); 396 waitForScreenshotButtonEnabled(true); 397 398 DetailsUi detailsUi = new DetailsUi(mBugreportId, touchDetails); 399 400 detailsUi.nameField.setText(""); 401 detailsUi.titleField.setText(""); 402 detailsUi.descField.setText(mDescription); 403 detailsUi.clickOk(); 404 405 Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId, 1); 406 assertActionSendMultiple(extras, NO_NAME, NO_TITLE, mDescription, 0); 407 408 assertServiceNotRunning(); 409 } 410 411 /** 412 * Tests the scenario where the initial screenshot and dumpstate are finished while the user 413 * is changing the info in the details screen. 414 */ 415 @Test testProgress_bugreportAndScreenshotFinishedWhileChangingDetails()416 public void testProgress_bugreportAndScreenshotFinishedWhileChangingDetails() throws Exception { 417 bugreportFinishedWhileChangingDetailsTest(false); 418 } 419 420 /** 421 * Tests the scenario where dumpstate is finished while the user is changing the info in the 422 * details screen, but the initial screenshot finishes afterwards. 423 */ 424 @Test testProgress_bugreportFinishedWhileChangingDetails()425 public void testProgress_bugreportFinishedWhileChangingDetails() throws Exception { 426 bugreportFinishedWhileChangingDetailsTest(true); 427 } 428 bugreportFinishedWhileChangingDetailsTest(boolean waitScreenshot)429 private void bugreportFinishedWhileChangingDetailsTest(boolean waitScreenshot) throws Exception { 430 sendBugreportStarted(); 431 if (waitScreenshot) { 432 waitForScreenshotButtonEnabled(true); 433 } 434 435 DetailsUi detailsUi = new DetailsUi(mBugreportId); 436 437 // Finish the bugreport while user's still typing the name. 438 detailsUi.nameField.setText(NEW_NAME); 439 sendBugreportFinished(); 440 441 // Wait until the share notification is received... 442 waitShareNotification(mBugreportId); 443 // ...then close notification bar. 444 mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 445 446 // Make sure UI was updated properly. 447 assertFalse("didn't disable name on UI", detailsUi.nameField.isEnabled()); 448 assertNotEquals("didn't revert name on UI", NAME, detailsUi.nameField.getText()); 449 450 // Finish changing other fields. 451 detailsUi.titleField.setText(TITLE); 452 detailsUi.descField.setText(mDescription); 453 detailsUi.clickOk(); 454 455 // Finally, share bugreport. 456 Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 1); 457 assertActionSendMultiple(extras, NO_NAME, TITLE, mDescription, 0); 458 459 assertServiceNotRunning(); 460 } 461 462 @Test testBugreportFinished_withWarningFirstTime()463 public void testBugreportFinished_withWarningFirstTime() throws Exception { 464 bugreportFinishedWithWarningTest(null); 465 } 466 467 @Test testBugreportFinished_withWarningUnknownState()468 public void testBugreportFinished_withWarningUnknownState() throws Exception { 469 int bugreportStateUnknown = mContext.getResources().getInteger( 470 com.android.internal.R.integer.bugreport_state_unknown); 471 bugreportFinishedWithWarningTest(bugreportStateUnknown); 472 } 473 474 @Test testBugreportFinished_withWarningShowAgain()475 public void testBugreportFinished_withWarningShowAgain() throws Exception { 476 int bugreportStateShow = mContext.getResources().getInteger( 477 com.android.internal.R.integer.bugreport_state_show); 478 bugreportFinishedWithWarningTest(bugreportStateShow); 479 } 480 bugreportFinishedWithWarningTest(Integer propertyState)481 private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception { 482 int bugreportStateUnknown = mContext.getResources().getInteger( 483 com.android.internal.R.integer.bugreport_state_unknown); 484 int bugreportStateHide = mContext.getResources().getInteger( 485 com.android.internal.R.integer.bugreport_state_hide); 486 if (propertyState == null) { 487 // Clear properties 488 mContext.getSharedPreferences( 489 mContext.getResources().getString(com.android.internal.R.string.prefs_bugreport) 490 , Context.MODE_PRIVATE).edit().clear().commit(); 491 // Confidence check... 492 assertEquals("Did not reset properties", bugreportStateUnknown, 493 getWarningState(mContext, bugreportStateUnknown)); 494 } else { 495 setWarningState(mContext, propertyState); 496 } 497 498 // Send notification and click on share. 499 sendBugreportStarted(); 500 waitForScreenshotButtonEnabled(true); 501 sendBugreportFinished(); 502 mUiBot.clickOnNotification(mContext.getString( 503 R.string.bugreport_finished_title, mBugreportId)); 504 505 // Handle the warning 506 mUiBot.getObject(mContext.getString(R.string.bugreport_confirm)); 507 // TODO: get ok and dontShowAgain from the dialog reference above 508 UiObject dontShowAgain = 509 mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat)); 510 final boolean firstTime = 511 propertyState == null || propertyState == bugreportStateUnknown; 512 if (firstTime) { 513 if (Build.IS_USER) { 514 assertFalse("Checkbox should NOT be checked by default on user builds", 515 dontShowAgain.isChecked()); 516 mUiBot.click(dontShowAgain, "dont-show-again"); 517 } else { 518 assertTrue("Checkbox should be checked by default on build type " + Build.TYPE, 519 dontShowAgain.isChecked()); 520 } 521 } else { 522 assertFalse("Checkbox should not be checked", dontShowAgain.isChecked()); 523 mUiBot.click(dontShowAgain, "dont-show-again"); 524 } 525 UiObject ok = mUiBot.getVisibleObject(mContext.getString(com.android.internal.R.string.ok)); 526 mUiBot.click(ok, "ok"); 527 528 // Share the bugreport. 529 mUiBot.chooseActivity(UI_NAME, mContext, 1); 530 Bundle extras = mListener.getExtras(); 531 assertActionSendMultiple(extras); 532 533 // Make sure it's hidden now. 534 int newState = getWarningState(mContext, bugreportStateUnknown); 535 assertEquals("Didn't change state", bugreportStateHide, newState); 536 } 537 538 @Test testBugreportFinished_withEmptyBugreportFile()539 public void testBugreportFinished_withEmptyBugreportFile() throws Exception { 540 sendBugreportStarted(); 541 542 IoUtils.closeQuietly(mBugreportFd); 543 mBugreportFd = null; 544 sendBugreportFinished(); 545 546 assertServiceNotRunning(); 547 } 548 549 @Test testShareBugreportAfterServiceDies()550 public void testShareBugreportAfterServiceDies() throws Exception { 551 sendBugreportStarted(); 552 waitForScreenshotButtonEnabled(true); 553 sendBugreportFinished(); 554 killService(); 555 assertServiceNotRunning(); 556 Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId, 1); 557 assertActionSendMultiple(extras); 558 } 559 560 @Test testBugreportRequestTwice_oneStartBugreportInvoked()561 public void testBugreportRequestTwice_oneStartBugreportInvoked() throws Exception { 562 sendBugreportStarted(); 563 new BugreportRequestedReceiver().onReceive(mContext, 564 new Intent(INTENT_BUGREPORT_REQUESTED)); 565 getInstrumentation().waitForIdleSync(); 566 567 verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(), 568 anyInt(), anyInt(), any(), anyBoolean(), anyBoolean()); 569 sendBugreportFinished(); 570 } 571 cancelExistingNotifications()572 private void cancelExistingNotifications() { 573 // Must kill service first, because notifications from a foreground service cannot be 574 // canceled. 575 killService(); 576 577 NotificationManager nm = NotificationManager.from(mContext); 578 StatusBarNotification[] activeNotifications = nm.getActiveNotifications(); 579 if (activeNotifications.length == 0) { 580 return; 581 } 582 583 Log.w(TAG, getName() + ": " + activeNotifications.length + " active notifications"); 584 585 nm.cancelAll(); 586 587 // Wait a little bit... 588 for (int i = 1; i < 5; i++) { 589 int total = nm.getActiveNotifications().length; 590 if (total == 0) { 591 return; 592 } 593 Log.d(TAG, total + "notifications are still active; sleeping "); 594 nm.cancelAll(); 595 sleep(1000); 596 } 597 assertEquals("old notifications were not cancelled", 0, nm.getActiveNotifications().length); 598 } 599 cancelFromNotification(String name)600 private void cancelFromNotification(String name) { 601 openProgressNotification(name); 602 UiObject cancelButton = mUiBot.getObject(mContext.getString( 603 com.android.internal.R.string.cancel)); 604 mUiBot.click(cancelButton, "cancel_button"); 605 } 606 assertProgressNotification(String name, float percent)607 private void assertProgressNotification(String name, float percent) { 608 openProgressNotification(name); 609 // TODO: need a way to get the ProgresBar from the "android:id/progress" UIObject... 610 } 611 openProgressNotification(String title)612 private void openProgressNotification(String title) { 613 Log.v(TAG, "Looking for progress notification for '" + title + "'"); 614 UiObject2 notification = mUiBot.getNotification2(title); 615 if (notification != null) { 616 mUiBot.expandNotification(notification); 617 } 618 } 619 620 /** 621 * Sends a "bugreport requested" intent with the default values. 622 */ sendBugreportStarted()623 private void sendBugreportStarted() throws Exception { 624 Intent intent = new Intent(INTENT_BUGREPORT_REQUESTED); 625 // Ideally, we should invoke BugreportRequestedReceiver by sending 626 // INTENT_BUGREPORT_REQUESTED. But the intent has been protected broadcast by the system 627 // starting from S. 628 new BugreportRequestedReceiver().onReceive(mContext, intent); 629 630 ArgumentCaptor<IDumpstateListener> listenerCap = ArgumentCaptor.forClass( 631 IDumpstateListener.class); 632 verify(mMockIDumpstate, timeout(TIMEOUT)).startBugreport(anyInt(), any(), any(), any(), 633 anyInt(), anyInt(), listenerCap.capture(), anyBoolean(), anyBoolean()); 634 mIDumpstateListener = listenerCap.getValue(); 635 assertNotNull("Dumpstate listener should not be null", mIDumpstateListener); 636 mIDumpstateListener.onProgress(0); 637 } 638 639 /** 640 * Sends a "bugreport finished" event and waits for the result. 641 * 642 * @param id The bugreport id for finished notification string title substitution. 643 * @param count Number of files to be shared 644 * @return extras sent in the shared intent. 645 */ sendBugreportFinishedAndGetSharedIntent(int id, int count)646 private Bundle sendBugreportFinishedAndGetSharedIntent(int id, int count) throws Exception { 647 sendBugreportFinished(); 648 return acceptBugreportAndGetSharedIntent(id, count); 649 } 650 651 /** 652 * Sends a "bugreport finished" event and waits for the result. 653 * 654 * @param notificationTitle The title of finished notification. 655 * @param count Number of files to be shared 656 * @return extras sent in the shared intent. 657 */ sendBugreportFinishedAndGetSharedIntent(String notificationTitle, int count)658 private Bundle sendBugreportFinishedAndGetSharedIntent(String notificationTitle, int count) 659 throws Exception { 660 sendBugreportFinished(); 661 return acceptBugreportAndGetSharedIntent(notificationTitle, count); 662 } 663 664 /** 665 * Accepts the notification to share the finished bugreport and waits for the result. 666 * 667 * @param id The bugreport id for finished notification string title substitution. 668 * @param count Number of files to be shared 669 * @return extras sent in the shared intent. 670 */ acceptBugreportAndGetSharedIntent(int id, int count)671 private Bundle acceptBugreportAndGetSharedIntent(int id, int count) { 672 final String notificationTitle = mContext.getString(R.string.bugreport_finished_title, id); 673 return acceptBugreportAndGetSharedIntent(notificationTitle, count); 674 } 675 676 /** 677 * Accepts the notification to share the finished bugreport and waits for the result. 678 * 679 * @param notificationTitle The title of finished notification. 680 * @param count Number of files to be shared 681 * @return extras sent in the shared intent. 682 */ acceptBugreportAndGetSharedIntent(String notificationTitle, int count)683 private Bundle acceptBugreportAndGetSharedIntent(String notificationTitle, int count) { 684 mUiBot.clickOnNotification(notificationTitle); 685 mUiBot.chooseActivity(UI_NAME, mContext, count); 686 return mListener.getExtras(); 687 } 688 689 /** 690 * Waits for the notification to share the finished bugreport. 691 */ waitShareNotification(int id)692 private void waitShareNotification(int id) { 693 mUiBot.getNotification(mContext.getString(R.string.bugreport_finished_title, id)); 694 } 695 696 /** 697 * Callbacks to service to finish the bugreport. 698 */ sendBugreportFinished()699 private void sendBugreportFinished() throws Exception { 700 if (mBugreportFd != null) { 701 writeZipFile(mBugreportFd, BUGREPORT_FILE, BUGREPORT_CONTENT); 702 } 703 if (mScreenshotFd != null) { 704 writeScreenshotFile(mScreenshotFd, SCREENSHOT_CONTENT); 705 } 706 mIDumpstateListener.onFinished(""); 707 getInstrumentation().waitForIdleSync(); 708 } 709 710 /** 711 * Asserts the proper {@link Intent#ACTION_SEND_MULTIPLE} intent was sent. 712 */ assertActionSendMultiple(Bundle extras)713 private void assertActionSendMultiple(Bundle extras) throws IOException { 714 assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 0); 715 } 716 717 /** 718 * Asserts the proper {@link Intent#ACTION_SEND_MULTIPLE} intent was sent. 719 * 720 * @param extras extras received in the intent 721 * @param name bugreport name as provided by the user (or received by dumpstate) 722 * @param title bugreport name as provided by the user 723 * @param description bugreport description as provided by the user 724 * @param numberScreenshots expected number of screenshots taken by Shell. 725 */ assertActionSendMultiple(Bundle extras, String name, String title, String description, int numberScreenshots)726 private void assertActionSendMultiple(Bundle extras, String name, String title, 727 String description, int numberScreenshots) 728 throws IOException { 729 String body = extras.getString(Intent.EXTRA_TEXT); 730 assertContainsRegex("missing build info", 731 SystemProperties.get("ro.build.description"), body); 732 assertContainsRegex("missing serial number", 733 SystemProperties.get("ro.serialno"), body); 734 if (description != null) { 735 assertContainsRegex("missing description", description, body); 736 } 737 738 final String extrasSubject = extras.getString(Intent.EXTRA_SUBJECT); 739 if (title != null) { 740 assertEquals("wrong subject", title, extrasSubject); 741 } else { 742 if (name != null) { 743 assertEquals("wrong subject", getBugreportName(name), extrasSubject); 744 } else { 745 assertTrue("wrong subject", extrasSubject.startsWith( 746 getBugreportPrefixName())); 747 } 748 } 749 750 List<Uri> attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM); 751 int expectedNumberScreenshots = numberScreenshots; 752 if (getScreenshotContent() != null) { 753 expectedNumberScreenshots ++; // Add screenshot received by dumpstate 754 } 755 int expectedSize = expectedNumberScreenshots + 1; // All screenshots plus the bugreport file 756 assertEquals("wrong number of attachments (" + attachments + ")", 757 expectedSize, attachments.size()); 758 759 // Need to interact through all attachments, since order is not guaranteed. 760 Uri zipUri = null; 761 List<Uri> screenshotUris = new ArrayList<>(expectedNumberScreenshots); 762 for (Uri attachment : attachments) { 763 if (attachment.getPath().endsWith(".zip")) { 764 zipUri = attachment; 765 } 766 if (attachment.getPath().endsWith(".png")) { 767 screenshotUris.add(attachment); 768 } 769 } 770 assertNotNull("did not get .zip attachment", zipUri); 771 assertZipContent(zipUri, BUGREPORT_FILE, BUGREPORT_CONTENT); 772 if (!TextUtils.isEmpty(title)) { 773 assertZipContent(zipUri, "title.txt", title); 774 } 775 if (!TextUtils.isEmpty(description)) { 776 assertZipContent(zipUri, "description.txt", description); 777 } 778 779 // URI of the screenshot taken by dumpstate. 780 Uri externalScreenshotUri = null; 781 SortedSet<String> internalScreenshotNames = new TreeSet<>(); 782 for (Uri screenshotUri : screenshotUris) { 783 String screenshotName = screenshotUri.getLastPathSegment(); 784 if (screenshotName.endsWith(SCREENSHOT_FILE)) { 785 externalScreenshotUri = screenshotUri; 786 } else { 787 internalScreenshotNames.add(screenshotName); 788 } 789 } 790 // Check external screenshot 791 if (getScreenshotContent() != null) { 792 assertNotNull("did not get .png attachment for external screenshot", 793 externalScreenshotUri); 794 assertContent(externalScreenshotUri, SCREENSHOT_CONTENT); 795 } else { 796 assertNull("should not have .png attachment for external screenshot", 797 externalScreenshotUri); 798 } 799 // Check internal screenshots' file names. 800 if (name != null) { 801 SortedSet<String> expectedNames = new TreeSet<>(); 802 for (int i = 1; i <= numberScreenshots; i++) { 803 String expectedName = "screenshot-" + name + "-" + i + ".png"; 804 expectedNames.add(expectedName); 805 } 806 // Ideally we should use MoreAsserts, but the error message in case of failure is not 807 // really useful. 808 assertEquals("wrong names for internal screenshots", 809 expectedNames, internalScreenshotNames); 810 } 811 } 812 assertContent(Uri uri, String expectedContent)813 private void assertContent(Uri uri, String expectedContent) throws IOException { 814 Log.v(TAG, "assertContents(uri=" + uri); 815 try (InputStream is = mContext.getContentResolver().openInputStream(uri)) { 816 String actualContent = new String(Streams.readFully(is)); 817 assertEquals("wrong content for '" + uri + "'", expectedContent, actualContent); 818 } 819 } 820 assertZipContent(Uri uri, String entryName, String expectedContent)821 private void assertZipContent(Uri uri, String entryName, String expectedContent) 822 throws IOException, IOException { 823 Log.v(TAG, "assertZipEntry(uri=" + uri + ", entryName=" + entryName); 824 try (ZipInputStream zis = new ZipInputStream(mContext.getContentResolver().openInputStream( 825 uri))) { 826 ZipEntry entry; 827 while ((entry = zis.getNextEntry()) != null) { 828 Log.v(TAG, "Zip entry: " + entry.getName()); 829 if (entry.getName().equals(entryName)) { 830 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 831 Streams.copy(zis, bos); 832 String actualContent = new String(bos.toByteArray(), "UTF-8"); 833 bos.close(); 834 assertEquals("wrong content for zip entry'" + entryName + "' on '" + uri + "'", 835 expectedContent, actualContent); 836 return; 837 } 838 } 839 } 840 fail("Did not find entry '" + entryName + "' on file '" + uri + "'"); 841 } 842 assertServiceNotRunning()843 private void assertServiceNotRunning() { 844 mServiceRule.unbindService(); 845 waitForService(false); 846 } 847 isServiceRunning(String name)848 private boolean isServiceRunning(String name) { 849 ActivityManager manager = (ActivityManager) mContext 850 .getSystemService(Context.ACTIVITY_SERVICE); 851 for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 852 if (service.service.getClassName().equals(name)) { 853 return true; 854 } 855 } 856 return false; 857 } 858 waitForService(boolean expectRunning)859 private void waitForService(boolean expectRunning) { 860 String service = BugreportProgressService.class.getName(); 861 boolean actualRunning; 862 for (int i = 1; i <= 5; i++) { 863 actualRunning = isServiceRunning(service); 864 Log.d(TAG, "Attempt " + i + " to check status of service '" 865 + service + "': expected=" + expectRunning + ", actual= " + actualRunning); 866 if (actualRunning == expectRunning) { 867 return; 868 } 869 sleep(DateUtils.SECOND_IN_MILLIS); 870 } 871 872 fail("Service status didn't change to " + expectRunning); 873 } 874 killService()875 private void killService() { 876 String service = BugreportProgressService.class.getName(); 877 mServiceRule.unbindService(); 878 if (!isServiceRunning(service)) return; 879 880 Log.w(TAG, "Service '" + service + "' is still running, killing it"); 881 silentlyExecuteShellCommand("am stopservice com.android.shell/.BugreportProgressService"); 882 883 waitForService(false); 884 } 885 silentlyExecuteShellCommand(String cmd)886 private void silentlyExecuteShellCommand(String cmd) { 887 Log.w(TAG, "silentlyExecuteShellCommand: '" + cmd + "'"); 888 try { 889 UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd); 890 } catch (IOException e) { 891 Log.w(TAG, "error executing shell comamand '" + cmd + "'", e); 892 } 893 } 894 writeScreenshotFile(ParcelFileDescriptor fd, String content)895 private void writeScreenshotFile(ParcelFileDescriptor fd, String content) throws IOException { 896 Log.v(TAG, "writeScreenshotFile(" + fd + ")"); 897 try (Writer writer = new BufferedWriter(new OutputStreamWriter( 898 new FileOutputStream(fd.getFileDescriptor())))) { 899 writer.write(content); 900 } 901 } 902 writeZipFile(ParcelFileDescriptor fd, String entryName, String content)903 private void writeZipFile(ParcelFileDescriptor fd, String entryName, String content) 904 throws IOException { 905 Log.v(TAG, "writeZipFile(" + fd + ", " + entryName + ")"); 906 try (ZipOutputStream zos = new ZipOutputStream( 907 new BufferedOutputStream(new FileOutputStream(fd.getFileDescriptor())))) { 908 ZipEntry entry = new ZipEntry(entryName); 909 zos.putNextEntry(entry); 910 byte[] data = content.getBytes(); 911 zos.write(data, 0, data.length); 912 zos.closeEntry(); 913 } 914 } 915 916 /** 917 * Gets the notification button used to take a screenshot. 918 */ getScreenshotButton()919 private UiObject getScreenshotButton() { 920 openProgressNotification(mProgressTitle); 921 return mUiBot.getObject( 922 mContext.getString(R.string.bugreport_screenshot_action)); 923 } 924 925 /** 926 * Takes a screenshot using the system notification. 927 */ takeScreenshot()928 private void takeScreenshot() throws Exception { 929 UiObject screenshotButton = getScreenshotButton(); 930 mUiBot.click(screenshotButton, "screenshot_button"); 931 } 932 waitForScreenshotButtonEnabled(boolean expectedEnabled)933 private UiObject waitForScreenshotButtonEnabled(boolean expectedEnabled) throws Exception { 934 UiObject screenshotButton = getScreenshotButton(); 935 int maxAttempts = SAFE_SCREENSHOT_DELAY; 936 int i = 0; 937 do { 938 boolean enabled = screenshotButton.isEnabled(); 939 if (enabled == expectedEnabled) { 940 return screenshotButton; 941 } 942 i++; 943 Log.v(TAG, "Sleeping for 1 second while waiting for screenshot.enable to be " 944 + expectedEnabled + " (attempt " + i + ")"); 945 Thread.sleep(DateUtils.SECOND_IN_MILLIS); 946 } while (i <= maxAttempts); 947 fail("screenshot.enable didn't change to " + expectedEnabled + " in " + maxAttempts + "s"); 948 return screenshotButton; 949 } 950 assertScreenshotButtonEnabled(boolean expectedEnabled)951 private void assertScreenshotButtonEnabled(boolean expectedEnabled) throws Exception { 952 UiObject screenshotButton = getScreenshotButton(); 953 assertEquals("wrong state for screenshot button ", expectedEnabled, 954 screenshotButton.isEnabled()); 955 } 956 assertDetailsUiClosed()957 private void assertDetailsUiClosed() { 958 // TODO: unhardcode resource ids 959 mUiBot.assertNotVisibleById("android:id/alertTitle"); 960 } 961 getName()962 private String getName() { 963 return mName.getMethodName(); 964 } 965 getInstrumentation()966 private Instrumentation getInstrumentation() { 967 return InstrumentationRegistry.getInstrumentation(); 968 } 969 sleep(long ms)970 private static void sleep(long ms) { 971 Log.d(TAG, "sleeping for " + ms + "ms"); 972 SystemClock.sleep(ms); 973 Log.d(TAG, "woke up"); 974 } 975 getBugreportId()976 private int getBugreportId() { 977 return SystemProperties.getInt(PROPERTY_LAST_ID, 1); 978 } 979 getBugreportInProgress(int bugreportId)980 private String getBugreportInProgress(int bugreportId) { 981 return mContext.getString(R.string.bugreport_in_progress_title, bugreportId); 982 } 983 getBugreportPrefixName()984 private String getBugreportPrefixName() { 985 String buildId = SystemProperties.get("ro.build.id", "UNKNOWN_BUILD"); 986 String deviceName = SystemProperties.get("ro.product.name", "UNKNOWN_DEVICE"); 987 return String.format("bugreport-%s-%s", deviceName, buildId); 988 } 989 getBugreportName(String name)990 private String getBugreportName(String name) { 991 return String.format("%s-%s.zip", getBugreportPrefixName(), name); 992 } 993 getScreenshotContent()994 private String getScreenshotContent() { 995 if (mScreenshotFd == null) { 996 return NO_SCREENSHOT; 997 } 998 return SCREENSHOT_CONTENT; 999 } 1000 1001 /** 1002 * Helper class containing the UiObjects present in the bugreport info dialog. 1003 */ 1004 private final class DetailsUi { 1005 1006 final UiObject nameField; 1007 final UiObject titleField; 1008 final UiObject descField; 1009 final UiObject okButton; 1010 final UiObject cancelButton; 1011 1012 /** 1013 * Gets the UI objects by opening the progress notification and clicking on DETAILS. 1014 * 1015 * @param id bugreport id 1016 */ DetailsUi(int id)1017 DetailsUi(int id) throws UiObjectNotFoundException { 1018 this(id, true); 1019 } 1020 1021 /** 1022 * Gets the UI objects by opening the progress notification and clicking on DETAILS or in 1023 * the notification itself. 1024 * 1025 * @param id bugreport id 1026 */ DetailsUi(int id, boolean clickDetails)1027 DetailsUi(int id, boolean clickDetails) throws UiObjectNotFoundException { 1028 openProgressNotification(mProgressTitle); 1029 final UiObject notification = mUiBot.getObject(mProgressTitle); 1030 final UiObject detailsButton = mUiBot.getObject(mContext.getString( 1031 R.string.bugreport_info_action)); 1032 1033 if (clickDetails) { 1034 mUiBot.click(detailsButton, "details_button"); 1035 } else { 1036 mUiBot.click(notification, "notification"); 1037 } 1038 // TODO: unhardcode resource ids 1039 UiObject dialogTitle = mUiBot.getVisibleObjectById("android:id/alertTitle"); 1040 assertEquals("Wrong title", mContext.getString(R.string.bugreport_info_dialog_title, 1041 id), dialogTitle.getText().toString()); 1042 nameField = mUiBot.getVisibleObjectById("com.android.shell:id/name"); 1043 titleField = mUiBot.getVisibleObjectById("com.android.shell:id/title"); 1044 descField = mUiBot.getVisibleObjectById("com.android.shell:id/description"); 1045 okButton = mUiBot.getObjectById("android:id/button1"); 1046 cancelButton = mUiBot.getObjectById("android:id/button2"); 1047 } 1048 1049 /** 1050 * Set focus on the name field so it can be validated once focus is lost. 1051 */ focusOnName()1052 void focusOnName() throws UiObjectNotFoundException { 1053 mUiBot.click(nameField, "name_field"); 1054 assertTrue("name_field not focused", nameField.isFocused()); 1055 } 1056 1057 /** 1058 * Takes focus away from the name field so it can be validated. 1059 */ focusAwayFromName()1060 void focusAwayFromName() throws UiObjectNotFoundException { 1061 mUiBot.click(titleField, "title_field"); // Change focus. 1062 assertFalse("name_field is focused", nameField.isFocused()); 1063 } 1064 reOpen(String name)1065 void reOpen(String name) { 1066 openProgressNotification(name); 1067 final UiObject detailsButton = mUiBot.getObject(mContext.getString( 1068 R.string.bugreport_info_action)); 1069 mUiBot.click(detailsButton, "details_button"); 1070 } 1071 clickOk()1072 void clickOk() { 1073 mUiBot.click(okButton, "details_ok_button"); 1074 } 1075 clickCancel()1076 void clickCancel() { 1077 mUiBot.click(cancelButton, "details_cancel_button"); 1078 } 1079 } 1080 } 1081