1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.os.bugreports.tests; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assert.fail; 24 25 import android.Manifest; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.os.BugreportManager; 31 import android.os.BugreportManager.BugreportCallback; 32 import android.os.BugreportParams; 33 import android.os.FileUtils; 34 import android.os.Handler; 35 import android.os.HandlerThread; 36 import android.os.ParcelFileDescriptor; 37 import android.os.Process; 38 import android.os.StrictMode; 39 import android.text.TextUtils; 40 import android.util.Log; 41 42 import androidx.annotation.NonNull; 43 import androidx.test.InstrumentationRegistry; 44 import androidx.test.filters.LargeTest; 45 import androidx.test.uiautomator.By; 46 import androidx.test.uiautomator.BySelector; 47 import androidx.test.uiautomator.UiDevice; 48 import androidx.test.uiautomator.UiObject2; 49 import androidx.test.uiautomator.Until; 50 51 import org.junit.After; 52 import org.junit.Before; 53 import org.junit.Rule; 54 import org.junit.Test; 55 import org.junit.rules.ExternalResource; 56 import org.junit.rules.TestName; 57 import org.junit.runner.RunWith; 58 import org.junit.runners.JUnit4; 59 60 import java.io.File; 61 import java.io.IOException; 62 import java.util.concurrent.CountDownLatch; 63 import java.util.concurrent.Executor; 64 import java.util.concurrent.TimeUnit; 65 66 /** 67 * Tests for BugreportManager API. 68 */ 69 @RunWith(JUnit4.class) 70 public class BugreportManagerTest { 71 @Rule public TestName name = new TestName(); 72 @Rule public ExtendedStrictModeVmPolicy mTemporaryVmPolicy = new ExtendedStrictModeVmPolicy(); 73 74 private static final String TAG = "BugreportManagerTest"; 75 private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10); 76 private static final long DUMPSTATE_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 77 private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 78 79 80 // A small timeout used when waiting for the result of a BugreportCallback to be received. 81 // This value must be at least 1000ms since there is an intentional delay in 82 // BugreportManagerServiceImpl in the error case. 83 private static final long CALLBACK_RESULT_TIMEOUT_MS = 1500; 84 85 // Sent by Shell when its bugreport finishes (contains final bugreport/screenshot file name 86 // associated with the bugreport). 87 private static final String INTENT_BUGREPORT_FINISHED = 88 "com.android.internal.intent.action.BUGREPORT_FINISHED"; 89 private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; 90 private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; 91 92 private Handler mHandler; 93 private Executor mExecutor; 94 private BugreportManager mBrm; 95 private File mBugreportFile; 96 private File mScreenshotFile; 97 private ParcelFileDescriptor mBugreportFd; 98 private ParcelFileDescriptor mScreenshotFd; 99 100 @Before setup()101 public void setup() throws Exception { 102 mHandler = createHandler(); 103 mExecutor = (runnable) -> { 104 if (mHandler != null) { 105 mHandler.post(() -> { 106 runnable.run(); 107 }); 108 } 109 }; 110 111 mBrm = getBugreportManager(); 112 mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); 113 mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png"); 114 mBugreportFd = parcelFd(mBugreportFile); 115 mScreenshotFd = parcelFd(mScreenshotFile); 116 117 getPermissions(); 118 } 119 120 @After teardown()121 public void teardown() throws Exception { 122 dropPermissions(); 123 FileUtils.closeQuietly(mBugreportFd); 124 FileUtils.closeQuietly(mScreenshotFd); 125 } 126 127 128 @Test normalFlow_wifi()129 public void normalFlow_wifi() throws Exception { 130 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 131 // wifi bugreport does not take screenshot 132 mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, wifi(), 133 mExecutor, callback); 134 shareConsentDialog(ConsentReply.ALLOW); 135 waitTillDoneOrTimeout(callback); 136 137 assertThat(callback.isDone()).isTrue(); 138 // Wifi bugreports should not receive any progress. 139 assertThat(callback.hasReceivedProgress()).isFalse(); 140 assertThat(mBugreportFile.length()).isGreaterThan(0L); 141 assertThat(callback.hasEarlyReportFinished()).isTrue(); 142 assertFdsAreClosed(mBugreportFd); 143 } 144 145 @LargeTest 146 @Test normalFlow_interactive()147 public void normalFlow_interactive() throws Exception { 148 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 149 // interactive bugreport does not take screenshot 150 mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, interactive(), 151 mExecutor, callback); 152 shareConsentDialog(ConsentReply.ALLOW); 153 waitTillDoneOrTimeout(callback); 154 155 assertThat(callback.isDone()).isTrue(); 156 // Interactive bugreports show progress updates. 157 assertThat(callback.hasReceivedProgress()).isTrue(); 158 assertThat(mBugreportFile.length()).isGreaterThan(0L); 159 assertThat(callback.hasEarlyReportFinished()).isTrue(); 160 assertFdsAreClosed(mBugreportFd); 161 } 162 163 @LargeTest 164 @Test normalFlow_full()165 public void normalFlow_full() throws Exception { 166 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 167 mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback); 168 shareConsentDialog(ConsentReply.ALLOW); 169 waitTillDoneOrTimeout(callback); 170 171 assertThat(callback.isDone()).isTrue(); 172 // bugreport and screenshot files shouldn't be empty when user consents. 173 assertThat(mBugreportFile.length()).isGreaterThan(0L); 174 assertThat(mScreenshotFile.length()).isGreaterThan(0L); 175 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 176 } 177 178 @Test simultaneousBugreportsNotAllowed()179 public void simultaneousBugreportsNotAllowed() throws Exception { 180 // Start bugreport #1 181 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 182 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 183 // TODO(b/162389762) Make sure the wait time is reasonable 184 shareConsentDialog(ConsentReply.ALLOW); 185 186 // Before #1 is done, try to start #2. 187 assertThat(callback.isDone()).isFalse(); 188 BugreportCallbackImpl callback2 = new BugreportCallbackImpl(); 189 File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip"); 190 File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png"); 191 ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2); 192 ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2); 193 mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2); 194 Thread.sleep(CALLBACK_RESULT_TIMEOUT_MS); 195 196 // Verify #2 encounters an error. 197 assertThat(callback2.getErrorCode()).isEqualTo( 198 BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); 199 assertFdsAreClosed(bugreportFd2, screenshotFd2); 200 201 // Cancel #1 so we can move on to the next test. 202 mBrm.cancelBugreport(); 203 waitTillDoneOrTimeout(callback); 204 assertThat(callback.isDone()).isTrue(); 205 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 206 } 207 208 @Test cancelBugreport()209 public void cancelBugreport() throws Exception { 210 // Start a bugreport. 211 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 212 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 213 214 // Verify it's not finished yet. 215 assertThat(callback.isDone()).isFalse(); 216 217 // Try to cancel it, but first without DUMP permission. 218 dropPermissions(); 219 try { 220 mBrm.cancelBugreport(); 221 fail("Expected cancelBugreport to throw SecurityException without DUMP permission"); 222 } catch (SecurityException expected) { 223 } 224 assertThat(callback.isDone()).isFalse(); 225 226 // Try again, with DUMP permission. 227 getPermissions(); 228 mBrm.cancelBugreport(); 229 waitTillDoneOrTimeout(callback); 230 assertThat(callback.isDone()).isTrue(); 231 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 232 } 233 234 @Test cancelBugreport_noReportStarted()235 public void cancelBugreport_noReportStarted() throws Exception { 236 // Without the native DumpstateService running, we don't get a SecurityException. 237 mBrm.cancelBugreport(); 238 } 239 240 @LargeTest 241 @Test cancelBugreport_fromDifferentUid()242 public void cancelBugreport_fromDifferentUid() throws Exception { 243 assertThat(Process.myUid()).isNotEqualTo(Process.SHELL_UID); 244 245 // Start a bugreport through ActivityManager's shell command - this starts a BR from the 246 // shell UID rather than our own. 247 BugreportBroadcastReceiver br = new BugreportBroadcastReceiver(); 248 InstrumentationRegistry.getContext() 249 .registerReceiver(br, new IntentFilter(INTENT_BUGREPORT_FINISHED)); 250 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 251 .executeShellCommand("am bug-report"); 252 253 // The command triggers the report through a broadcast, so wait until dumpstate actually 254 // starts up, which may take a bit. 255 waitTillDumpstateRunningOrTimeout(); 256 257 try { 258 mBrm.cancelBugreport(); 259 fail("Expected cancelBugreport to throw SecurityException when report started by " 260 + "different UID"); 261 } catch (SecurityException expected) { 262 } finally { 263 // Do this in the finally block so that even if this test case fails, we don't break 264 // other test cases unexpectedly due to the still-running shell report. 265 try { 266 // The shell's BR is still running and should complete successfully. 267 br.waitForBugreportFinished(); 268 } finally { 269 // The latch may fail for a number of reasons but we still need to unregister the 270 // BroadcastReceiver. 271 InstrumentationRegistry.getContext().unregisterReceiver(br); 272 } 273 } 274 } 275 276 @Test insufficientPermissions_throwsException()277 public void insufficientPermissions_throwsException() throws Exception { 278 dropPermissions(); 279 280 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 281 try { 282 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 283 fail("Expected startBugreport to throw SecurityException without DUMP permission"); 284 } catch (SecurityException expected) { 285 } 286 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 287 } 288 289 @Test invalidBugreportMode_throwsException()290 public void invalidBugreportMode_throwsException() throws Exception { 291 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 292 293 try { 294 mBrm.startBugreport(mBugreportFd, mScreenshotFd, 295 new BugreportParams(25) /* unknown bugreport mode */, mExecutor, callback); 296 fail("Expected to throw IllegalArgumentException with unknown bugreport mode"); 297 } catch (IllegalArgumentException expected) { 298 } 299 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 300 } 301 createHandler()302 private Handler createHandler() { 303 HandlerThread handlerThread = new HandlerThread("BugreportManagerTest"); 304 handlerThread.start(); 305 return new Handler(handlerThread.getLooper()); 306 } 307 308 /* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */ 309 private static final class BugreportCallbackImpl extends BugreportCallback { 310 private int mErrorCode = -1; 311 private boolean mSuccess = false; 312 private boolean mReceivedProgress = false; 313 private boolean mEarlyReportFinished = false; 314 private final Object mLock = new Object(); 315 316 @Override onProgress(float progress)317 public void onProgress(float progress) { 318 synchronized (mLock) { 319 mReceivedProgress = true; 320 } 321 } 322 323 @Override onError(int errorCode)324 public void onError(int errorCode) { 325 synchronized (mLock) { 326 mErrorCode = errorCode; 327 Log.d(TAG, "bugreport errored."); 328 } 329 } 330 331 @Override onFinished()332 public void onFinished() { 333 synchronized (mLock) { 334 Log.d(TAG, "bugreport finished."); 335 mSuccess = true; 336 } 337 } 338 339 @Override onEarlyReportFinished()340 public void onEarlyReportFinished() { 341 synchronized (mLock) { 342 mEarlyReportFinished = true; 343 } 344 } 345 346 /* Indicates completion; and ended up with a success or error. */ isDone()347 public boolean isDone() { 348 synchronized (mLock) { 349 return (mErrorCode != -1) || mSuccess; 350 } 351 } 352 getErrorCode()353 public int getErrorCode() { 354 synchronized (mLock) { 355 return mErrorCode; 356 } 357 } 358 isSuccess()359 public boolean isSuccess() { 360 synchronized (mLock) { 361 return mSuccess; 362 } 363 } 364 hasReceivedProgress()365 public boolean hasReceivedProgress() { 366 synchronized (mLock) { 367 return mReceivedProgress; 368 } 369 } 370 hasEarlyReportFinished()371 public boolean hasEarlyReportFinished() { 372 synchronized (mLock) { 373 return mEarlyReportFinished; 374 } 375 } 376 } 377 getBugreportManager()378 public static BugreportManager getBugreportManager() { 379 Context context = InstrumentationRegistry.getContext(); 380 BugreportManager bm = 381 (BugreportManager) context.getSystemService(Context.BUGREPORT_SERVICE); 382 if (bm == null) { 383 throw new AssertionError("Failed to get BugreportManager"); 384 } 385 return bm; 386 } 387 createTempFile(String prefix, String extension)388 private static File createTempFile(String prefix, String extension) throws Exception { 389 final File f = File.createTempFile(prefix, extension); 390 f.setReadable(true, true); 391 f.setWritable(true, true); 392 393 f.deleteOnExit(); 394 return f; 395 } 396 parcelFd(File file)397 private static ParcelFileDescriptor parcelFd(File file) throws Exception { 398 return ParcelFileDescriptor.open(file, 399 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 400 } 401 dropPermissions()402 private static void dropPermissions() { 403 InstrumentationRegistry.getInstrumentation().getUiAutomation() 404 .dropShellPermissionIdentity(); 405 } 406 getPermissions()407 private static void getPermissions() { 408 InstrumentationRegistry.getInstrumentation().getUiAutomation() 409 .adoptShellPermissionIdentity(Manifest.permission.DUMP); 410 } 411 isDumpstateRunning()412 private static boolean isDumpstateRunning() { 413 String[] output; 414 try { 415 output = 416 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 417 .executeShellCommand("ps -A -o NAME | grep dumpstate") 418 .trim() 419 .split("\n"); 420 } catch (IOException e) { 421 Log.w(TAG, "Failed to check if dumpstate is running", e); 422 return false; 423 } 424 for (String line : output) { 425 // Check for an exact match since there may be other things that contain "dumpstate" as 426 // a substring (e.g. the dumpstate HAL). 427 if (TextUtils.equals("dumpstate", line)) { 428 return true; 429 } 430 } 431 return false; 432 } 433 assertFdIsClosed(ParcelFileDescriptor pfd)434 private static void assertFdIsClosed(ParcelFileDescriptor pfd) { 435 try { 436 int fd = pfd.getFd(); 437 fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd); 438 } catch (IllegalStateException expected) { 439 } 440 } 441 assertFdsAreClosed(ParcelFileDescriptor... pfds)442 private static void assertFdsAreClosed(ParcelFileDescriptor... pfds) { 443 for (int i = 0; i < pfds.length; i++) { 444 assertFdIsClosed(pfds[i]); 445 } 446 } 447 now()448 private static long now() { 449 return System.currentTimeMillis(); 450 } 451 waitTillDumpstateRunningOrTimeout()452 private static void waitTillDumpstateRunningOrTimeout() throws Exception { 453 long startTimeMs = now(); 454 while (!isDumpstateRunning()) { 455 Thread.sleep(500 /* .5s */); 456 if (now() - startTimeMs >= DUMPSTATE_STARTUP_TIMEOUT_MS) { 457 break; 458 } 459 Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to start"); 460 } 461 } 462 waitTillDoneOrTimeout(BugreportCallbackImpl callback)463 private static void waitTillDoneOrTimeout(BugreportCallbackImpl callback) throws Exception { 464 long startTimeMs = now(); 465 while (!callback.isDone()) { 466 Thread.sleep(1000 /* 1s */); 467 if (now() - startTimeMs >= BUGREPORT_TIMEOUT_MS) { 468 break; 469 } 470 Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for bugreport to finish"); 471 } 472 } 473 474 /* 475 * Returns a {@link BugreportParams} for wifi only bugreport. 476 * 477 * <p>Wifi bugreports have minimal content and are fast to run. They also suppress progress 478 * updates. 479 */ wifi()480 private static BugreportParams wifi() { 481 return new BugreportParams(BugreportParams.BUGREPORT_MODE_WIFI); 482 } 483 484 /* 485 * Returns a {@link BugreportParams} for interactive bugreport that offers progress updates. 486 * 487 * <p>This is the typical bugreport taken by users. This can take on the order of minutes to 488 * finish. 489 */ interactive()490 private static BugreportParams interactive() { 491 return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE); 492 } 493 494 /* 495 * Returns a {@link BugreportParams} for full bugreport that includes a screenshot. 496 * 497 * <p> This can take on the order of minutes to finish 498 */ full()499 private static BugreportParams full() { 500 return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL); 501 } 502 503 /* Allow/deny the consent dialog to sharing bugreport data or check existence only. */ 504 private enum ConsentReply { 505 ALLOW, 506 DENY, 507 TIMEOUT 508 } 509 510 /* 511 * Ensure the consent dialog is shown and take action according to <code>consentReply<code/>. 512 * It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false. 513 */ shareConsentDialog(@onNull ConsentReply consentReply)514 private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception { 515 mTemporaryVmPolicy.permitIncorrectContextUse(); 516 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 517 518 // Unlock before finding/clicking an object. 519 device.wakeUp(); 520 device.executeShellCommand("wm dismiss-keyguard"); 521 522 final BySelector consentTitleObj = By.res("android", "alertTitle"); 523 if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) { 524 fail("The consent dialog is not found"); 525 } 526 if (consentReply.equals(ConsentReply.TIMEOUT)) { 527 return; 528 } 529 final BySelector selector; 530 if (consentReply.equals(ConsentReply.ALLOW)) { 531 selector = By.res("android", "button1"); 532 Log.d(TAG, "Allow the consent dialog"); 533 } else { // ConsentReply.DENY 534 selector = By.res("android", "button2"); 535 Log.d(TAG, "Deny the consent dialog"); 536 } 537 final UiObject2 btnObj = device.findObject(selector); 538 assertNotNull("The button of consent dialog is not found", btnObj); 539 btnObj.click(); 540 541 Log.d(TAG, "Wait for the dialog to be dismissed"); 542 assertTrue(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)); 543 } 544 545 private class BugreportBroadcastReceiver extends BroadcastReceiver { 546 Intent mBugreportFinishedIntent = null; 547 final CountDownLatch mLatch; 548 BugreportBroadcastReceiver()549 BugreportBroadcastReceiver() { 550 mLatch = new CountDownLatch(1); 551 } 552 553 @Override onReceive(Context context, Intent intent)554 public void onReceive(Context context, Intent intent) { 555 setBugreportFinishedIntent(intent); 556 mLatch.countDown(); 557 } 558 setBugreportFinishedIntent(Intent intent)559 private void setBugreportFinishedIntent(Intent intent) { 560 mBugreportFinishedIntent = intent; 561 } 562 getBugreportFinishedIntent()563 public Intent getBugreportFinishedIntent() { 564 return mBugreportFinishedIntent; 565 } 566 waitForBugreportFinished()567 public void waitForBugreportFinished() throws Exception { 568 if (!mLatch.await(BUGREPORT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 569 throw new Exception("Failed to receive BUGREPORT_FINISHED in " 570 + BUGREPORT_TIMEOUT_MS + " ms."); 571 } 572 } 573 } 574 575 /** 576 * A rule to change strict mode vm policy temporarily till test method finished. 577 * 578 * To permit the non-visual context usage in tests while taking bugreports need user consent, 579 * or UiAutomator/BugreportManager.DumpstateListener would run into error. 580 * UiDevice#findObject creates UiObject2, its Gesture object and ViewConfiguration and 581 * UiObject2#click need to know bounds. Both of them access to WindowManager internally without 582 * visual context comes from InstrumentationRegistry and violate the policy. 583 * Also <code>DumpstateListener<code/> violate the policy when onScreenshotTaken is called. 584 * 585 * TODO(b/161201609) Remove this class once violations fixed. 586 */ 587 static class ExtendedStrictModeVmPolicy extends ExternalResource { 588 private boolean mWasVmPolicyChanged = false; 589 private StrictMode.VmPolicy mOldVmPolicy; 590 591 @Override after()592 protected void after() { 593 restoreVmPolicyIfNeeded(); 594 } 595 permitIncorrectContextUse()596 public void permitIncorrectContextUse() { 597 // Allow to call multiple times without losing old policy. 598 if (mOldVmPolicy == null) { 599 mOldVmPolicy = StrictMode.getVmPolicy(); 600 } 601 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 602 .detectAll() 603 .permitIncorrectContextUse() 604 .penaltyLog() 605 .build()); 606 mWasVmPolicyChanged = true; 607 } 608 restoreVmPolicyIfNeeded()609 private void restoreVmPolicyIfNeeded() { 610 if (mWasVmPolicyChanged && mOldVmPolicy != null) { 611 StrictMode.setVmPolicy(mOldVmPolicy); 612 mOldVmPolicy = null; 613 } 614 } 615 } 616 } 617