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