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