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