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