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 android.content.Context.RECEIVER_EXPORTED; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static org.junit.Assert.assertNotNull; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assert.fail; 26 27 import android.Manifest; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.os.BugreportManager; 33 import android.os.BugreportManager.BugreportCallback; 34 import android.os.BugreportParams; 35 import android.os.FileUtils; 36 import android.os.Handler; 37 import android.os.HandlerThread; 38 import android.os.ParcelFileDescriptor; 39 import android.os.Process; 40 import android.os.StrictMode; 41 import android.util.Log; 42 43 import androidx.annotation.NonNull; 44 import androidx.test.InstrumentationRegistry; 45 import androidx.test.filters.LargeTest; 46 import androidx.test.uiautomator.By; 47 import androidx.test.uiautomator.BySelector; 48 import androidx.test.uiautomator.UiDevice; 49 import androidx.test.uiautomator.UiObject2; 50 import androidx.test.uiautomator.Until; 51 52 import com.google.common.io.ByteStreams; 53 import com.google.common.io.Files; 54 55 import org.junit.After; 56 import org.junit.Before; 57 import org.junit.Rule; 58 import org.junit.Test; 59 import org.junit.rules.ExternalResource; 60 import org.junit.rules.TestName; 61 import org.junit.runner.RunWith; 62 import org.junit.runners.JUnit4; 63 64 import java.io.BufferedInputStream; 65 import java.io.BufferedOutputStream; 66 import java.io.File; 67 import java.io.FileInputStream; 68 import java.io.FileOutputStream; 69 import java.io.IOException; 70 import java.nio.charset.StandardCharsets; 71 import java.nio.file.Path; 72 import java.nio.file.Paths; 73 import java.util.ArrayList; 74 import java.util.List; 75 import java.util.concurrent.CountDownLatch; 76 import java.util.concurrent.Executor; 77 import java.util.concurrent.TimeUnit; 78 import java.util.zip.ZipEntry; 79 import java.util.zip.ZipInputStream; 80 81 /** 82 * Tests for BugreportManager API. 83 */ 84 @RunWith(JUnit4.class) 85 public class BugreportManagerTest { 86 @Rule public TestName name = new TestName(); 87 @Rule public ExtendedStrictModeVmPolicy mTemporaryVmPolicy = new ExtendedStrictModeVmPolicy(); 88 89 private static final String TAG = "BugreportManagerTest"; 90 private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10); 91 private static final long DUMPSTATE_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 92 private static final long DUMPSTATE_TEARDOWN_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 93 private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 94 95 96 // A small timeout used when waiting for the result of a BugreportCallback to be received. 97 // This value must be at least 1000ms since there is an intentional delay in 98 // BugreportManagerServiceImpl in the error case. 99 private static final long CALLBACK_RESULT_TIMEOUT_MS = 1500; 100 101 // Sent by Shell when its bugreport finishes (contains final bugreport/screenshot file name 102 // associated with the bugreport). 103 private static final String INTENT_BUGREPORT_FINISHED = 104 "com.android.internal.intent.action.BUGREPORT_FINISHED"; 105 private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; 106 private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; 107 108 private static final Path[] UI_TRACES_PREDUMPED = { 109 Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"), 110 Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"), 111 Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"), 112 Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"), 113 Paths.get("/data/misc/wmtrace/wm_trace.winscope"), 114 Paths.get("/data/misc/wmtrace/wm_log.winscope"), 115 }; 116 117 private Handler mHandler; 118 private Executor mExecutor; 119 private BugreportManager mBrm; 120 private File mBugreportFile; 121 private File mScreenshotFile; 122 private ParcelFileDescriptor mBugreportFd; 123 private ParcelFileDescriptor mScreenshotFd; 124 125 @Before setup()126 public void setup() throws Exception { 127 mHandler = createHandler(); 128 mExecutor = (runnable) -> { 129 if (mHandler != null) { 130 mHandler.post(() -> { 131 runnable.run(); 132 }); 133 } 134 }; 135 136 mBrm = getBugreportManager(); 137 mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); 138 mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png"); 139 mBugreportFd = parcelFd(mBugreportFile); 140 mScreenshotFd = parcelFd(mScreenshotFile); 141 142 getPermissions(); 143 } 144 145 @After teardown()146 public void teardown() throws Exception { 147 dropPermissions(); 148 FileUtils.closeQuietly(mBugreportFd); 149 FileUtils.closeQuietly(mScreenshotFd); 150 } 151 152 @Test normalFlow_wifi()153 public void normalFlow_wifi() throws Exception { 154 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 155 // wifi bugreport does not take screenshot 156 mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, wifi(), 157 mExecutor, callback); 158 shareConsentDialog(ConsentReply.ALLOW); 159 waitTillDoneOrTimeout(callback); 160 161 assertThat(callback.isDone()).isTrue(); 162 // Wifi bugreports should not receive any progress. 163 assertThat(callback.hasReceivedProgress()).isFalse(); 164 assertThat(mBugreportFile.length()).isGreaterThan(0L); 165 assertThat(callback.hasEarlyReportFinished()).isTrue(); 166 assertFdsAreClosed(mBugreportFd); 167 } 168 169 @LargeTest 170 @Test normalFlow_interactive()171 public void normalFlow_interactive() throws Exception { 172 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 173 // interactive bugreport does not take screenshot 174 mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, interactive(), 175 mExecutor, callback); 176 shareConsentDialog(ConsentReply.ALLOW); 177 waitTillDoneOrTimeout(callback); 178 179 assertThat(callback.isDone()).isTrue(); 180 // Interactive bugreports show progress updates. 181 assertThat(callback.hasReceivedProgress()).isTrue(); 182 assertThat(mBugreportFile.length()).isGreaterThan(0L); 183 assertThat(callback.hasEarlyReportFinished()).isTrue(); 184 assertFdsAreClosed(mBugreportFd); 185 } 186 187 @LargeTest 188 @Test normalFlow_full()189 public void normalFlow_full() throws Exception { 190 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 191 mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback); 192 shareConsentDialog(ConsentReply.ALLOW); 193 waitTillDoneOrTimeout(callback); 194 195 assertThat(callback.isDone()).isTrue(); 196 // bugreport and screenshot files shouldn't be empty when user consents. 197 assertThat(mBugreportFile.length()).isGreaterThan(0L); 198 assertThat(mScreenshotFile.length()).isGreaterThan(0L); 199 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 200 } 201 202 @LargeTest 203 @Test preDumpUiData_then_fullWithUsePreDumpFlag()204 public void preDumpUiData_then_fullWithUsePreDumpFlag() throws Exception { 205 startPreDumpedUiTraces(); 206 207 mBrm.preDumpUiData(); 208 waitTillDumpstateExitedOrTimeout(); 209 List<File> expectedPreDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED); 210 211 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 212 mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor, 213 callback); 214 shareConsentDialog(ConsentReply.ALLOW); 215 waitTillDoneOrTimeout(callback); 216 217 stopPreDumpedUiTraces(); 218 219 assertThat(callback.isDone()).isTrue(); 220 assertThat(mBugreportFile.length()).isGreaterThan(0L); 221 assertFdsAreClosed(mBugreportFd); 222 223 assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED); 224 225 List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED); 226 assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles); 227 } 228 229 @LargeTest 230 @Test preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump()231 public void preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump() throws Exception { 232 startPreDumpedUiTraces(); 233 234 // Simulate pre-dump, instead of taking a real one. 235 // In some corner cases, data dumped as part of the full bugreport could be the same as the 236 // pre-dumped data and this test would fail. Hence, here we create fake/artificial 237 // pre-dumped data that we know it won't match with the full bugreport data. 238 createFakeTraceFiles(UI_TRACES_PREDUMPED); 239 240 List<File> preDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED); 241 242 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 243 mBrm.startBugreport(mBugreportFd, null, full(), mExecutor, 244 callback); 245 shareConsentDialog(ConsentReply.ALLOW); 246 waitTillDoneOrTimeout(callback); 247 248 stopPreDumpedUiTraces(); 249 250 assertThat(callback.isDone()).isTrue(); 251 assertThat(mBugreportFile.length()).isGreaterThan(0L); 252 assertFdsAreClosed(mBugreportFd); 253 254 assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED); 255 256 List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED); 257 assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles); 258 } 259 260 @LargeTest 261 @Test noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag()262 public void noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag() throws Exception { 263 startPreDumpedUiTraces(); 264 265 mBrm.preDumpUiData(); 266 waitTillDumpstateExitedOrTimeout(); 267 268 // Simulate lost of pre-dumped data. 269 // For example it can happen in this scenario: 270 // 1. Pre-dump data 271 // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK) 272 // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK) 273 removeFilesIfNeeded(UI_TRACES_PREDUMPED); 274 275 // Start bugreport with "use predump" flag. Because the pre-dumped data is not available 276 // the flag will be ignored and data will be dumped as in normal flow. 277 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 278 mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor, 279 callback); 280 shareConsentDialog(ConsentReply.ALLOW); 281 waitTillDoneOrTimeout(callback); 282 283 stopPreDumpedUiTraces(); 284 285 assertThat(callback.isDone()).isTrue(); 286 assertThat(mBugreportFile.length()).isGreaterThan(0L); 287 assertFdsAreClosed(mBugreportFd); 288 289 assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED); 290 } 291 292 @Test simultaneousBugreportsNotAllowed()293 public void simultaneousBugreportsNotAllowed() throws Exception { 294 // Start bugreport #1 295 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 296 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 297 // TODO(b/162389762) Make sure the wait time is reasonable 298 shareConsentDialog(ConsentReply.ALLOW); 299 300 // Before #1 is done, try to start #2. 301 assertThat(callback.isDone()).isFalse(); 302 BugreportCallbackImpl callback2 = new BugreportCallbackImpl(); 303 File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip"); 304 File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png"); 305 ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2); 306 ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2); 307 mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2); 308 Thread.sleep(CALLBACK_RESULT_TIMEOUT_MS); 309 310 // Verify #2 encounters an error. 311 assertThat(callback2.getErrorCode()).isEqualTo( 312 BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); 313 assertFdsAreClosed(bugreportFd2, screenshotFd2); 314 315 // Cancel #1 so we can move on to the next test. 316 mBrm.cancelBugreport(); 317 waitTillDoneOrTimeout(callback); 318 assertThat(callback.isDone()).isTrue(); 319 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 320 } 321 322 @Test cancelBugreport()323 public void cancelBugreport() throws Exception { 324 // Start a bugreport. 325 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 326 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 327 328 // Verify it's not finished yet. 329 assertThat(callback.isDone()).isFalse(); 330 331 // Try to cancel it, but first without DUMP permission. 332 dropPermissions(); 333 try { 334 mBrm.cancelBugreport(); 335 fail("Expected cancelBugreport to throw SecurityException without DUMP permission"); 336 } catch (SecurityException expected) { 337 } 338 assertThat(callback.isDone()).isFalse(); 339 340 // Try again, with DUMP permission. 341 getPermissions(); 342 mBrm.cancelBugreport(); 343 waitTillDoneOrTimeout(callback); 344 assertThat(callback.isDone()).isTrue(); 345 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 346 } 347 348 @Test cancelBugreport_noReportStarted()349 public void cancelBugreport_noReportStarted() throws Exception { 350 // Without the native DumpstateService running, we don't get a SecurityException. 351 mBrm.cancelBugreport(); 352 } 353 354 @LargeTest 355 @Test cancelBugreport_fromDifferentUid()356 public void cancelBugreport_fromDifferentUid() throws Exception { 357 assertThat(Process.myUid()).isNotEqualTo(Process.SHELL_UID); 358 359 // Start a bugreport through ActivityManager's shell command - this starts a BR from the 360 // shell UID rather than our own. 361 BugreportBroadcastReceiver br = new BugreportBroadcastReceiver(); 362 InstrumentationRegistry.getContext() 363 .registerReceiver( 364 br, 365 new IntentFilter(INTENT_BUGREPORT_FINISHED), 366 RECEIVER_EXPORTED); 367 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 368 .executeShellCommand("am bug-report"); 369 370 // The command triggers the report through a broadcast, so wait until dumpstate actually 371 // starts up, which may take a bit. 372 waitTillDumpstateRunningOrTimeout(); 373 374 try { 375 mBrm.cancelBugreport(); 376 fail("Expected cancelBugreport to throw SecurityException when report started by " 377 + "different UID"); 378 } catch (SecurityException expected) { 379 } finally { 380 // Do this in the finally block so that even if this test case fails, we don't break 381 // other test cases unexpectedly due to the still-running shell report. 382 try { 383 // The shell's BR is still running and should complete successfully. 384 br.waitForBugreportFinished(); 385 } finally { 386 // The latch may fail for a number of reasons but we still need to unregister the 387 // BroadcastReceiver. 388 InstrumentationRegistry.getContext().unregisterReceiver(br); 389 } 390 } 391 } 392 393 @Test insufficientPermissions_throwsException()394 public void insufficientPermissions_throwsException() throws Exception { 395 dropPermissions(); 396 397 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 398 try { 399 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 400 fail("Expected startBugreport to throw SecurityException without DUMP permission"); 401 } catch (SecurityException expected) { 402 } 403 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 404 } 405 406 @Test invalidBugreportMode_throwsException()407 public void invalidBugreportMode_throwsException() throws Exception { 408 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 409 410 try { 411 mBrm.startBugreport(mBugreportFd, mScreenshotFd, 412 new BugreportParams(25) /* unknown bugreport mode */, mExecutor, callback); 413 fail("Expected to throw IllegalArgumentException with unknown bugreport mode"); 414 } catch (IllegalArgumentException expected) { 415 } 416 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 417 } 418 createHandler()419 private Handler createHandler() { 420 HandlerThread handlerThread = new HandlerThread("BugreportManagerTest"); 421 handlerThread.start(); 422 return new Handler(handlerThread.getLooper()); 423 } 424 425 /* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */ 426 private static final class BugreportCallbackImpl extends BugreportCallback { 427 private int mErrorCode = -1; 428 private boolean mSuccess = false; 429 private boolean mReceivedProgress = false; 430 private boolean mEarlyReportFinished = false; 431 private final Object mLock = new Object(); 432 433 @Override onProgress(float progress)434 public void onProgress(float progress) { 435 synchronized (mLock) { 436 mReceivedProgress = true; 437 } 438 } 439 440 @Override onError(int errorCode)441 public void onError(int errorCode) { 442 synchronized (mLock) { 443 mErrorCode = errorCode; 444 Log.d(TAG, "bugreport errored."); 445 } 446 } 447 448 @Override onFinished()449 public void onFinished() { 450 synchronized (mLock) { 451 Log.d(TAG, "bugreport finished."); 452 mSuccess = true; 453 } 454 } 455 456 @Override onEarlyReportFinished()457 public void onEarlyReportFinished() { 458 synchronized (mLock) { 459 mEarlyReportFinished = true; 460 } 461 } 462 463 /* Indicates completion; and ended up with a success or error. */ isDone()464 public boolean isDone() { 465 synchronized (mLock) { 466 return (mErrorCode != -1) || mSuccess; 467 } 468 } 469 getErrorCode()470 public int getErrorCode() { 471 synchronized (mLock) { 472 return mErrorCode; 473 } 474 } 475 isSuccess()476 public boolean isSuccess() { 477 synchronized (mLock) { 478 return mSuccess; 479 } 480 } 481 hasReceivedProgress()482 public boolean hasReceivedProgress() { 483 synchronized (mLock) { 484 return mReceivedProgress; 485 } 486 } 487 hasEarlyReportFinished()488 public boolean hasEarlyReportFinished() { 489 synchronized (mLock) { 490 return mEarlyReportFinished; 491 } 492 } 493 } 494 getBugreportManager()495 public static BugreportManager getBugreportManager() { 496 Context context = InstrumentationRegistry.getContext(); 497 BugreportManager bm = 498 (BugreportManager) context.getSystemService(Context.BUGREPORT_SERVICE); 499 if (bm == null) { 500 throw new AssertionError("Failed to get BugreportManager"); 501 } 502 return bm; 503 } createTempFile(String prefix, String extension)504 private static File createTempFile(String prefix, String extension) throws Exception { 505 final File f = File.createTempFile(prefix, extension); 506 f.setReadable(true, true); 507 f.setWritable(true, true); 508 f.deleteOnExit(); 509 return f; 510 } 511 startPreDumpedUiTraces()512 private static void startPreDumpedUiTraces() throws Exception { 513 // Perfetto traces 514 String perfettoConfig = 515 "buffers: {\n" 516 + " size_kb: 2048\n" 517 + " fill_policy: RING_BUFFER\n" 518 + "}\n" 519 + "data_sources: {\n" 520 + " config {\n" 521 + " name: \"android.surfaceflinger.transactions\"\n" 522 + " }\n" 523 + "}\n" 524 + "bugreport_score: 10\n"; 525 File tmp = createTempFile("tmp", ".cfg"); 526 Files.write(perfettoConfig.getBytes(StandardCharsets.UTF_8), tmp); 527 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 528 "install -m 644 -o root -g root " 529 + tmp.getAbsolutePath() + " /data/misc/perfetto-configs/bugreport-manager-test.cfg" 530 ); 531 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 532 "perfetto --background-wait" 533 + " --config /data/misc/perfetto-configs/bugreport-manager-test.cfg --txt" 534 + " --out /data/misc/perfetto-traces/not-used.perfetto-trace" 535 ); 536 537 // Legacy traces 538 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 539 "cmd input_method tracing start" 540 ); 541 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 542 "cmd window tracing start" 543 ); 544 } 545 stopPreDumpedUiTraces()546 private static void stopPreDumpedUiTraces() { 547 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 548 "cmd input_method tracing stop" 549 ); 550 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 551 "cmd window tracing stop" 552 ); 553 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 554 "service call SurfaceFlinger 1025 i32 0" 555 ); 556 } 557 assertThatBugreportContainsFiles(Path[] paths)558 private void assertThatBugreportContainsFiles(Path[] paths) 559 throws IOException { 560 List<Path> entries = listZipArchiveEntries(mBugreportFile); 561 for (Path pathInDevice : paths) { 562 Path pathInArchive = Paths.get("FS" + pathInDevice.toString()); 563 assertThat(entries).contains(pathInArchive); 564 } 565 } 566 extractFilesFromBugreport(Path[] paths)567 private List<File> extractFilesFromBugreport(Path[] paths) throws Exception { 568 List<File> files = new ArrayList<File>(); 569 for (Path pathInDevice : paths) { 570 Path pathInArchive = Paths.get("FS" + pathInDevice.toString()); 571 files.add(extractZipArchiveEntry(mBugreportFile, pathInArchive)); 572 } 573 return files; 574 } 575 listZipArchiveEntries(File archive)576 private static List<Path> listZipArchiveEntries(File archive) throws IOException { 577 ArrayList<Path> entries = new ArrayList<>(); 578 579 ZipInputStream stream = new ZipInputStream( 580 new BufferedInputStream(new FileInputStream(archive))); 581 582 for (ZipEntry entry = stream.getNextEntry(); entry != null; entry = stream.getNextEntry()) { 583 entries.add(Paths.get(entry.toString())); 584 } 585 586 return entries; 587 } 588 extractZipArchiveEntry(File archive, Path entryToExtract)589 private static File extractZipArchiveEntry(File archive, Path entryToExtract) 590 throws Exception { 591 File extractedFile = createTempFile(entryToExtract.getFileName().toString(), ".extracted"); 592 593 ZipInputStream is = new ZipInputStream(new FileInputStream(archive)); 594 boolean hasFoundEntry = false; 595 596 for (ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry()) { 597 if (entry.toString().equals(entryToExtract.toString())) { 598 BufferedOutputStream os = 599 new BufferedOutputStream(new FileOutputStream(extractedFile)); 600 ByteStreams.copy(is, os); 601 os.close(); 602 hasFoundEntry = true; 603 break; 604 } 605 606 ByteStreams.exhaust(is); // skip entry 607 } 608 609 is.closeEntry(); 610 is.close(); 611 612 assertThat(hasFoundEntry).isTrue(); 613 614 return extractedFile; 615 } 616 createFakeTraceFiles(Path[] paths)617 private static void createFakeTraceFiles(Path[] paths) throws Exception { 618 File src = createTempFile("fake", ".data"); 619 Files.write("fake data".getBytes(StandardCharsets.UTF_8), src); 620 621 for (Path path : paths) { 622 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 623 "install -m 644 -o system -g system " 624 + src.getAbsolutePath() + " " + path.toString() 625 ); 626 } 627 628 // Dumpstate executes "perfetto --save-for-bugreport" as shell 629 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 630 "chown shell:shell /data/misc/perfetto-traces/bugreport/systrace.pftrace" 631 ); 632 } 633 copyFiles(Path[] paths)634 private static List<File> copyFiles(Path[] paths) throws Exception { 635 ArrayList<File> files = new ArrayList<File>(); 636 for (Path src : paths) { 637 File dst = createTempFile(src.getFileName().toString(), ".copy"); 638 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 639 "cp " + src.toString() + " " + dst.getAbsolutePath() 640 ); 641 files.add(dst); 642 } 643 return files; 644 } 645 removeFilesIfNeeded(Path[] paths)646 private static void removeFilesIfNeeded(Path[] paths) throws Exception { 647 for (Path path : paths) { 648 InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand( 649 "rm -f " + path.toString() 650 ); 651 } 652 } 653 parcelFd(File file)654 private static ParcelFileDescriptor parcelFd(File file) throws Exception { 655 return ParcelFileDescriptor.open(file, 656 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 657 } 658 assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected)659 private static void assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected) 660 throws IOException { 661 if (actual.size() != expected.size()) { 662 fail("File lists have different size"); 663 } 664 for (int i = 0; i < actual.size(); ++i) { 665 if (!Files.equal(actual.get(i), expected.get(i))) { 666 fail("Contents of " + actual.get(i).toString() 667 + " != " + expected.get(i).toString()); 668 } 669 } 670 } 671 assertThatAllFileContentsAreDifferent(List<File> a, List<File> b)672 private static void assertThatAllFileContentsAreDifferent(List<File> a, List<File> b) 673 throws IOException { 674 if (a.size() != b.size()) { 675 fail("File lists have different size"); 676 } 677 for (int i = 0; i < a.size(); ++i) { 678 if (Files.equal(a.get(i), b.get(i))) { 679 fail("Contents of " + a.get(i).toString() + " == " + b.get(i).toString()); 680 } 681 } 682 } 683 dropPermissions()684 private static void dropPermissions() { 685 InstrumentationRegistry.getInstrumentation().getUiAutomation() 686 .dropShellPermissionIdentity(); 687 } 688 getPermissions()689 private static void getPermissions() { 690 InstrumentationRegistry.getInstrumentation().getUiAutomation() 691 .adoptShellPermissionIdentity(Manifest.permission.DUMP); 692 } 693 isDumpstateRunning()694 private static boolean isDumpstateRunning() { 695 String output; 696 try { 697 output = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 698 .executeShellCommand("service list | grep dumpstate"); 699 } catch (IOException e) { 700 Log.w(TAG, "Failed to check if dumpstate is running", e); 701 return false; 702 } 703 for (String line : output.trim().split("\n")) { 704 if (line.matches("^.*\\s+dumpstate:\\s+\\[.*\\]$")) { 705 return true; 706 } 707 } 708 return false; 709 } 710 assertFdIsClosed(ParcelFileDescriptor pfd)711 private static void assertFdIsClosed(ParcelFileDescriptor pfd) { 712 try { 713 int fd = pfd.getFd(); 714 fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd); 715 } catch (IllegalStateException expected) { 716 } 717 } 718 assertFdsAreClosed(ParcelFileDescriptor... pfds)719 private static void assertFdsAreClosed(ParcelFileDescriptor... pfds) { 720 for (int i = 0; i < pfds.length; i++) { 721 assertFdIsClosed(pfds[i]); 722 } 723 } 724 now()725 private static long now() { 726 return System.currentTimeMillis(); 727 } 728 waitTillDumpstateExitedOrTimeout()729 private static void waitTillDumpstateExitedOrTimeout() throws Exception { 730 long startTimeMs = now(); 731 while (isDumpstateRunning()) { 732 Thread.sleep(500 /* .5s */); 733 if (now() - startTimeMs >= DUMPSTATE_TEARDOWN_TIMEOUT_MS) { 734 break; 735 } 736 Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to exit"); 737 } 738 } 739 waitTillDumpstateRunningOrTimeout()740 private static void waitTillDumpstateRunningOrTimeout() throws Exception { 741 long startTimeMs = now(); 742 while (!isDumpstateRunning()) { 743 Thread.sleep(500 /* .5s */); 744 if (now() - startTimeMs >= DUMPSTATE_STARTUP_TIMEOUT_MS) { 745 break; 746 } 747 Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to start"); 748 } 749 } 750 waitTillDoneOrTimeout(BugreportCallbackImpl callback)751 private static void waitTillDoneOrTimeout(BugreportCallbackImpl callback) throws Exception { 752 long startTimeMs = now(); 753 while (!callback.isDone()) { 754 Thread.sleep(1000 /* 1s */); 755 if (now() - startTimeMs >= BUGREPORT_TIMEOUT_MS) { 756 break; 757 } 758 Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for bugreport to finish"); 759 } 760 } 761 762 /* 763 * Returns a {@link BugreportParams} for wifi only bugreport. 764 * 765 * <p>Wifi bugreports have minimal content and are fast to run. They also suppress progress 766 * updates. 767 */ wifi()768 private static BugreportParams wifi() { 769 return new BugreportParams(BugreportParams.BUGREPORT_MODE_WIFI); 770 } 771 772 /* 773 * Returns a {@link BugreportParams} for interactive bugreport that offers progress updates. 774 * 775 * <p>This is the typical bugreport taken by users. This can take on the order of minutes to 776 * finish. 777 */ interactive()778 private static BugreportParams interactive() { 779 return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE); 780 } 781 782 /* 783 * Returns a {@link BugreportParams} for full bugreport that includes a screenshot. 784 * 785 * <p> This can take on the order of minutes to finish 786 */ full()787 private static BugreportParams full() { 788 return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL); 789 } 790 791 /* 792 * Returns a {@link BugreportParams} for full bugreport that reuses pre-dumped data. 793 * 794 * <p> This can take on the order of minutes to finish 795 */ fullWithUsePreDumpFlag()796 private static BugreportParams fullWithUsePreDumpFlag() { 797 return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL, 798 BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA); 799 } 800 801 /* Allow/deny the consent dialog to sharing bugreport data or check existence only. */ 802 private enum ConsentReply { 803 ALLOW, 804 DENY, 805 TIMEOUT 806 } 807 808 /* 809 * Ensure the consent dialog is shown and take action according to <code>consentReply<code/>. 810 * It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false. 811 */ shareConsentDialog(@onNull ConsentReply consentReply)812 private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception { 813 mTemporaryVmPolicy.permitIncorrectContextUse(); 814 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 815 816 // Unlock before finding/clicking an object. 817 device.wakeUp(); 818 device.executeShellCommand("wm dismiss-keyguard"); 819 820 final BySelector consentTitleObj = By.res("android", "alertTitle"); 821 if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) { 822 fail("The consent dialog is not found"); 823 } 824 if (consentReply.equals(ConsentReply.TIMEOUT)) { 825 return; 826 } 827 final BySelector selector; 828 if (consentReply.equals(ConsentReply.ALLOW)) { 829 selector = By.res("android", "button1"); 830 Log.d(TAG, "Allow the consent dialog"); 831 } else { // ConsentReply.DENY 832 selector = By.res("android", "button2"); 833 Log.d(TAG, "Deny the consent dialog"); 834 } 835 final UiObject2 btnObj = device.findObject(selector); 836 assertNotNull("The button of consent dialog is not found", btnObj); 837 btnObj.click(); 838 839 Log.d(TAG, "Wait for the dialog to be dismissed"); 840 assertTrue(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)); 841 } 842 843 private class BugreportBroadcastReceiver extends BroadcastReceiver { 844 Intent mBugreportFinishedIntent = null; 845 final CountDownLatch mLatch; 846 BugreportBroadcastReceiver()847 BugreportBroadcastReceiver() { 848 mLatch = new CountDownLatch(1); 849 } 850 851 @Override onReceive(Context context, Intent intent)852 public void onReceive(Context context, Intent intent) { 853 setBugreportFinishedIntent(intent); 854 mLatch.countDown(); 855 } 856 setBugreportFinishedIntent(Intent intent)857 private void setBugreportFinishedIntent(Intent intent) { 858 mBugreportFinishedIntent = intent; 859 } 860 getBugreportFinishedIntent()861 public Intent getBugreportFinishedIntent() { 862 return mBugreportFinishedIntent; 863 } 864 waitForBugreportFinished()865 public void waitForBugreportFinished() throws Exception { 866 if (!mLatch.await(BUGREPORT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 867 throw new Exception("Failed to receive BUGREPORT_FINISHED in " 868 + BUGREPORT_TIMEOUT_MS + " ms."); 869 } 870 } 871 } 872 873 /** 874 * A rule to change strict mode vm policy temporarily till test method finished. 875 * 876 * To permit the non-visual context usage in tests while taking bugreports need user consent, 877 * or UiAutomator/BugreportManager.DumpstateListener would run into error. 878 * UiDevice#findObject creates UiObject2, its Gesture object and ViewConfiguration and 879 * UiObject2#click need to know bounds. Both of them access to WindowManager internally without 880 * visual context comes from InstrumentationRegistry and violate the policy. 881 * Also <code>DumpstateListener<code/> violate the policy when onScreenshotTaken is called. 882 * 883 * TODO(b/161201609) Remove this class once violations fixed. 884 */ 885 static class ExtendedStrictModeVmPolicy extends ExternalResource { 886 private boolean mWasVmPolicyChanged = false; 887 private StrictMode.VmPolicy mOldVmPolicy; 888 889 @Override after()890 protected void after() { 891 restoreVmPolicyIfNeeded(); 892 } 893 permitIncorrectContextUse()894 public void permitIncorrectContextUse() { 895 // Allow to call multiple times without losing old policy. 896 if (mOldVmPolicy == null) { 897 mOldVmPolicy = StrictMode.getVmPolicy(); 898 } 899 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 900 .detectAll() 901 .permitIncorrectContextUse() 902 .penaltyLog() 903 .build()); 904 mWasVmPolicyChanged = true; 905 } 906 restoreVmPolicyIfNeeded()907 private void restoreVmPolicyIfNeeded() { 908 if (mWasVmPolicyChanged && mOldVmPolicy != null) { 909 StrictMode.setVmPolicy(mOldVmPolicy); 910 mOldVmPolicy = null; 911 } 912 } 913 } 914 } 915