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