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