blob: 37a499adf6824e296bd80ad08e546824bad73c43 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.os.bugreports.tests;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BugreportManager;
import android.os.BugreportManager.BugreportCallback;
import android.os.BugreportParams;
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.StrictMode;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Tests for BugreportManager API.
*/
@RunWith(JUnit4.class)
public class BugreportManagerTest {
@Rule public TestName name = new TestName();
@Rule public ExtendedStrictModeVmPolicy mTemporaryVmPolicy = new ExtendedStrictModeVmPolicy();
private static final String TAG = "BugreportManagerTest";
private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
private static final long DUMPSTATE_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long DUMPSTATE_TEARDOWN_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
// A small timeout used when waiting for the result of a BugreportCallback to be received.
// This value must be at least 1000ms since there is an intentional delay in
// BugreportManagerServiceImpl in the error case.
private static final long CALLBACK_RESULT_TIMEOUT_MS = 1500;
// Sent by Shell when its bugreport finishes (contains final bugreport/screenshot file name
// associated with the bugreport).
private static final String INTENT_BUGREPORT_FINISHED =
"com.android.internal.intent.action.BUGREPORT_FINISHED";
private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
private static final Path[] UI_TRACES_PREDUMPED = {
Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"),
Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
Paths.get("/data/misc/wmtrace/wm_log.winscope"),
Paths.get("/data/misc/wmtrace/wm_transition_trace.winscope"),
Paths.get("/data/misc/wmtrace/shell_transition_trace.winscope"),
};
private Handler mHandler;
private Executor mExecutor;
private BugreportManager mBrm;
private File mBugreportFile;
private File mScreenshotFile;
private ParcelFileDescriptor mBugreportFd;
private ParcelFileDescriptor mScreenshotFd;
@Before
public void setup() throws Exception {
mHandler = createHandler();
mExecutor = (runnable) -> {
if (mHandler != null) {
mHandler.post(() -> {
runnable.run();
});
}
};
mBrm = getBugreportManager();
mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip");
mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png");
mBugreportFd = parcelFd(mBugreportFile);
mScreenshotFd = parcelFd(mScreenshotFile);
getPermissions();
}
@After
public void teardown() throws Exception {
dropPermissions();
FileUtils.closeQuietly(mBugreportFd);
FileUtils.closeQuietly(mScreenshotFd);
}
@Test
public void normalFlow_wifi() throws Exception {
BugreportCallbackImpl callback = new BugreportCallbackImpl();
// wifi bugreport does not take screenshot
mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, wifi(),
mExecutor, callback);
shareConsentDialog(ConsentReply.ALLOW);
waitTillDoneOrTimeout(callback);
assertThat(callback.isDone()).isTrue();
// Wifi bugreports should not receive any progress.
assertThat(callback.hasReceivedProgress()).isFalse();
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertThat(callback.hasEarlyReportFinished()).isTrue();
assertFdsAreClosed(mBugreportFd);
}
@LargeTest
@Test
public void normalFlow_interactive() throws Exception {
BugreportCallbackImpl callback = new BugreportCallbackImpl();
// interactive bugreport does not take screenshot
mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, interactive(),
mExecutor, callback);
shareConsentDialog(ConsentReply.ALLOW);
waitTillDoneOrTimeout(callback);
assertThat(callback.isDone()).isTrue();
// Interactive bugreports show progress updates.
assertThat(callback.hasReceivedProgress()).isTrue();
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertThat(callback.hasEarlyReportFinished()).isTrue();
assertFdsAreClosed(mBugreportFd);
}
@LargeTest
@Test
public void normalFlow_full() throws Exception {
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback);
shareConsentDialog(ConsentReply.ALLOW);
waitTillDoneOrTimeout(callback);
assertThat(callback.isDone()).isTrue();
// bugreport and screenshot files shouldn't be empty when user consents.
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertThat(mScreenshotFile.length()).isGreaterThan(0L);
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
}
@LargeTest
@Test
public void preDumpUiData_then_fullWithUsePreDumpFlag() throws Exception {
startPreDumpedUiTraces();
mBrm.preDumpUiData();
waitTillDumpstateExitedOrTimeout();
List<File> expectedPreDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
callback);
shareConsentDialog(ConsentReply.ALLOW);
waitTillDoneOrTimeout(callback);
stopPreDumpedUiTraces();
assertThat(callback.isDone()).isTrue();
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertFdsAreClosed(mBugreportFd);
assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
}
@LargeTest
@Test
public void preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump() throws Exception {
startPreDumpedUiTraces();
// Simulate pre-dump, instead of taking a real one.
// In some corner cases, data dumped as part of the full bugreport could be the same as the
// pre-dumped data and this test would fail. Hence, here we create fake/artificial
// pre-dumped data that we know it won't match with the full bugreport data.
createFakeTraceFiles(UI_TRACES_PREDUMPED);
List<File> preDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
callback);
shareConsentDialog(ConsentReply.ALLOW);
waitTillDoneOrTimeout(callback);
stopPreDumpedUiTraces();
assertThat(callback.isDone()).isTrue();
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertFdsAreClosed(mBugreportFd);
assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
}
@Test
public void simultaneousBugreportsNotAllowed() throws Exception {
// Start bugreport #1
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
// TODO(b/162389762) Make sure the wait time is reasonable
shareConsentDialog(ConsentReply.ALLOW);
// Before #1 is done, try to start #2.
assertThat(callback.isDone()).isFalse();
BugreportCallbackImpl callback2 = new BugreportCallbackImpl();
File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip");
File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png");
ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2);
ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2);
mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2);
Thread.sleep(CALLBACK_RESULT_TIMEOUT_MS);
// Verify #2 encounters an error.
assertThat(callback2.getErrorCode()).isEqualTo(
BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
assertFdsAreClosed(bugreportFd2, screenshotFd2);
// Cancel #1 so we can move on to the next test.
mBrm.cancelBugreport();
waitTillDoneOrTimeout(callback);
assertThat(callback.isDone()).isTrue();
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
}
@Test
public void cancelBugreport() throws Exception {
// Start a bugreport.
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
// Verify it's not finished yet.
assertThat(callback.isDone()).isFalse();
// Try to cancel it, but first without DUMP permission.
dropPermissions();
try {
mBrm.cancelBugreport();
fail("Expected cancelBugreport to throw SecurityException without DUMP permission");
} catch (SecurityException expected) {
}
assertThat(callback.isDone()).isFalse();
// Try again, with DUMP permission.
getPermissions();
mBrm.cancelBugreport();
waitTillDoneOrTimeout(callback);
assertThat(callback.isDone()).isTrue();
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
}
@Test
public void cancelBugreport_noReportStarted() throws Exception {
// Without the native DumpstateService running, we don't get a SecurityException.
mBrm.cancelBugreport();
}
@LargeTest
@Test
public void cancelBugreport_fromDifferentUid() throws Exception {
assertThat(Process.myUid()).isNotEqualTo(Process.SHELL_UID);
// Start a bugreport through ActivityManager's shell command - this starts a BR from the
// shell UID rather than our own.
BugreportBroadcastReceiver br = new BugreportBroadcastReceiver();
InstrumentationRegistry.getContext()
.registerReceiver(br, new IntentFilter(INTENT_BUGREPORT_FINISHED));
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand("am bug-report");
// The command triggers the report through a broadcast, so wait until dumpstate actually
// starts up, which may take a bit.
waitTillDumpstateRunningOrTimeout();
try {
mBrm.cancelBugreport();
fail("Expected cancelBugreport to throw SecurityException when report started by "
+ "different UID");
} catch (SecurityException expected) {
} finally {
// Do this in the finally block so that even if this test case fails, we don't break
// other test cases unexpectedly due to the still-running shell report.
try {
// The shell's BR is still running and should complete successfully.
br.waitForBugreportFinished();
} finally {
// The latch may fail for a number of reasons but we still need to unregister the
// BroadcastReceiver.
InstrumentationRegistry.getContext().unregisterReceiver(br);
}
}
}
@Test
public void insufficientPermissions_throwsException() throws Exception {
dropPermissions();
BugreportCallbackImpl callback = new BugreportCallbackImpl();
try {
mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
fail("Expected startBugreport to throw SecurityException without DUMP permission");
} catch (SecurityException expected) {
}
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
}
@Test
public void invalidBugreportMode_throwsException() throws Exception {
BugreportCallbackImpl callback = new BugreportCallbackImpl();
try {
mBrm.startBugreport(mBugreportFd, mScreenshotFd,
new BugreportParams(25) /* unknown bugreport mode */, mExecutor, callback);
fail("Expected to throw IllegalArgumentException with unknown bugreport mode");
} catch (IllegalArgumentException expected) {
}
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
}
private Handler createHandler() {
HandlerThread handlerThread = new HandlerThread("BugreportManagerTest");
handlerThread.start();
return new Handler(handlerThread.getLooper());
}
/* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */
private static final class BugreportCallbackImpl extends BugreportCallback {
private int mErrorCode = -1;
private boolean mSuccess = false;
private boolean mReceivedProgress = false;
private boolean mEarlyReportFinished = false;
private final Object mLock = new Object();
@Override
public void onProgress(float progress) {
synchronized (mLock) {
mReceivedProgress = true;
}
}
@Override
public void onError(int errorCode) {
synchronized (mLock) {
mErrorCode = errorCode;
Log.d(TAG, "bugreport errored.");
}
}
@Override
public void onFinished() {
synchronized (mLock) {
Log.d(TAG, "bugreport finished.");
mSuccess = true;
}
}
@Override
public void onEarlyReportFinished() {
synchronized (mLock) {
mEarlyReportFinished = true;
}
}
/* Indicates completion; and ended up with a success or error. */
public boolean isDone() {
synchronized (mLock) {
return (mErrorCode != -1) || mSuccess;
}
}
public int getErrorCode() {
synchronized (mLock) {
return mErrorCode;
}
}
public boolean isSuccess() {
synchronized (mLock) {
return mSuccess;
}
}
public boolean hasReceivedProgress() {
synchronized (mLock) {
return mReceivedProgress;
}
}
public boolean hasEarlyReportFinished() {
synchronized (mLock) {
return mEarlyReportFinished;
}
}
}
public static BugreportManager getBugreportManager() {
Context context = InstrumentationRegistry.getContext();
BugreportManager bm =
(BugreportManager) context.getSystemService(Context.BUGREPORT_SERVICE);
if (bm == null) {
throw new AssertionError("Failed to get BugreportManager");
}
return bm;
}
private static File createTempFile(String prefix, String extension) throws Exception {
final File f = File.createTempFile(prefix, extension);
f.setReadable(true, true);
f.setWritable(true, true);
f.deleteOnExit();
return f;
}
private static void startPreDumpedUiTraces() throws Exception {
// Perfetto traces
String perfettoConfig =
"buffers: {\n"
+ " size_kb: 2048\n"
+ " fill_policy: RING_BUFFER\n"
+ "}\n"
+ "data_sources: {\n"
+ " config {\n"
+ " name: \"android.surfaceflinger.transactions\"\n"
+ " }\n"
+ "}\n"
+ "bugreport_score: 10\n";
File tmp = createTempFile("tmp", ".cfg");
Files.write(perfettoConfig.getBytes(StandardCharsets.UTF_8), tmp);
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"install -m 644 -o root -g root "
+ tmp.getAbsolutePath() + " /data/misc/perfetto-configs/bugreport-manager-test.cfg"
);
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"perfetto --background-wait"
+ " --config /data/misc/perfetto-configs/bugreport-manager-test.cfg --txt"
+ " --out /data/misc/perfetto-traces/not-used.perfetto-trace"
);
// Legacy traces
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"cmd input_method tracing start"
);
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"cmd window tracing start"
);
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"service call SurfaceFlinger 1025 i32 1"
);
}
private static void stopPreDumpedUiTraces() {
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"cmd input_method tracing stop"
);
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"cmd window tracing stop"
);
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"service call SurfaceFlinger 1025 i32 0"
);
}
private void assertThatBugreportContainsFiles(Path[] paths)
throws IOException {
List<Path> entries = listZipArchiveEntries(mBugreportFile);
for (Path pathInDevice : paths) {
Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
assertThat(entries).contains(pathInArchive);
}
}
private List<File> extractFilesFromBugreport(Path[] paths) throws Exception {
List<File> files = new ArrayList<File>();
for (Path pathInDevice : paths) {
Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
files.add(extractZipArchiveEntry(mBugreportFile, pathInArchive));
}
return files;
}
private static List<Path> listZipArchiveEntries(File archive) throws IOException {
ArrayList<Path> entries = new ArrayList<>();
ZipInputStream stream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(archive)));
for (ZipEntry entry = stream.getNextEntry(); entry != null; entry = stream.getNextEntry()) {
entries.add(Paths.get(entry.toString()));
}
return entries;
}
private static File extractZipArchiveEntry(File archive, Path entryToExtract)
throws Exception {
File extractedFile = createTempFile(entryToExtract.getFileName().toString(), ".extracted");
ZipInputStream is = new ZipInputStream(new FileInputStream(archive));
boolean hasFoundEntry = false;
for (ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry()) {
if (entry.toString().equals(entryToExtract.toString())) {
BufferedOutputStream os =
new BufferedOutputStream(new FileOutputStream(extractedFile));
ByteStreams.copy(is, os);
os.close();
hasFoundEntry = true;
break;
}
ByteStreams.exhaust(is); // skip entry
}
is.closeEntry();
is.close();
assertThat(hasFoundEntry).isTrue();
return extractedFile;
}
private static void createFakeTraceFiles(Path[] paths) throws Exception {
File src = createTempFile("fake", ".data");
Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
for (Path path : paths) {
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"install -m 644 -o system -g system "
+ src.getAbsolutePath() + " " + path.toString()
);
}
// Dumpstate executes "perfetto --save-for-bugreport" as shell
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"chown shell:shell /data/misc/perfetto-traces/bugreport/systrace.pftrace"
);
}
private static List<File> copyFiles(Path[] paths) throws Exception {
ArrayList<File> files = new ArrayList<File>();
for (Path src : paths) {
File dst = createTempFile(src.getFileName().toString(), ".copy");
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"cp " + src.toString() + " " + dst.getAbsolutePath()
);
files.add(dst);
}
return files;
}
private static ParcelFileDescriptor parcelFd(File file) throws Exception {
return ParcelFileDescriptor.open(file,
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
}
private static void assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected)
throws IOException {
if (actual.size() != expected.size()) {
fail("File lists have different size");
}
for (int i = 0; i < actual.size(); ++i) {
if (!Files.equal(actual.get(i), expected.get(i))) {
fail("Contents of " + actual.get(i).toString()
+ " != " + expected.get(i).toString());
}
}
}
private static void assertThatAllFileContentsAreDifferent(List<File> a, List<File> b)
throws IOException {
if (a.size() != b.size()) {
fail("File lists have different size");
}
for (int i = 0; i < a.size(); ++i) {
if (Files.equal(a.get(i), b.get(i))) {
fail("Contents of " + a.get(i).toString() + " == " + b.get(i).toString());
}
}
}
private static void dropPermissions() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
private static void getPermissions() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(Manifest.permission.DUMP);
}
private static boolean isDumpstateRunning() {
String output;
try {
output = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand("service list | grep dumpstate");
} catch (IOException e) {
Log.w(TAG, "Failed to check if dumpstate is running", e);
return false;
}
for (String line : output.trim().split("\n")) {
if (line.matches("^.*\\s+dumpstate:\\s+\\[.*\\]$")) {
return true;
}
}
return false;
}
private static void assertFdIsClosed(ParcelFileDescriptor pfd) {
try {
int fd = pfd.getFd();
fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd);
} catch (IllegalStateException expected) {
}
}
private static void assertFdsAreClosed(ParcelFileDescriptor... pfds) {
for (int i = 0; i < pfds.length; i++) {
assertFdIsClosed(pfds[i]);
}
}
private static long now() {
return System.currentTimeMillis();
}
private static void waitTillDumpstateExitedOrTimeout() throws Exception {
long startTimeMs = now();
while (isDumpstateRunning()) {
Thread.sleep(500 /* .5s */);
if (now() - startTimeMs >= DUMPSTATE_TEARDOWN_TIMEOUT_MS) {
break;
}
Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to exit");
}
}
private static void waitTillDumpstateRunningOrTimeout() throws Exception {
long startTimeMs = now();
while (!isDumpstateRunning()) {
Thread.sleep(500 /* .5s */);
if (now() - startTimeMs >= DUMPSTATE_STARTUP_TIMEOUT_MS) {
break;
}
Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to start");
}
}
private static void waitTillDoneOrTimeout(BugreportCallbackImpl callback) throws Exception {
long startTimeMs = now();
while (!callback.isDone()) {
Thread.sleep(1000 /* 1s */);
if (now() - startTimeMs >= BUGREPORT_TIMEOUT_MS) {
break;
}
Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for bugreport to finish");
}
}
/*
* Returns a {@link BugreportParams} for wifi only bugreport.
*
* <p>Wifi bugreports have minimal content and are fast to run. They also suppress progress
* updates.
*/
private static BugreportParams wifi() {
return new BugreportParams(BugreportParams.BUGREPORT_MODE_WIFI);
}
/*
* Returns a {@link BugreportParams} for interactive bugreport that offers progress updates.
*
* <p>This is the typical bugreport taken by users. This can take on the order of minutes to
* finish.
*/
private static BugreportParams interactive() {
return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE);
}
/*
* Returns a {@link BugreportParams} for full bugreport that includes a screenshot.
*
* <p> This can take on the order of minutes to finish
*/
private static BugreportParams full() {
return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL);
}
/*
* Returns a {@link BugreportParams} for full bugreport that reuses pre-dumped data.
*
* <p> This can take on the order of minutes to finish
*/
private static BugreportParams fullWithUsePreDumpFlag() {
return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL,
BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
}
/* Allow/deny the consent dialog to sharing bugreport data or check existence only. */
private enum ConsentReply {
ALLOW,
DENY,
TIMEOUT
}
/*
* Ensure the consent dialog is shown and take action according to <code>consentReply<code/>.
* It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false.
*/
private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception {
mTemporaryVmPolicy.permitIncorrectContextUse();
final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
// Unlock before finding/clicking an object.
device.wakeUp();
device.executeShellCommand("wm dismiss-keyguard");
final BySelector consentTitleObj = By.res("android", "alertTitle");
if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) {
fail("The consent dialog is not found");
}
if (consentReply.equals(ConsentReply.TIMEOUT)) {
return;
}
final BySelector selector;
if (consentReply.equals(ConsentReply.ALLOW)) {
selector = By.res("android", "button1");
Log.d(TAG, "Allow the consent dialog");
} else { // ConsentReply.DENY
selector = By.res("android", "button2");
Log.d(TAG, "Deny the consent dialog");
}
final UiObject2 btnObj = device.findObject(selector);
assertNotNull("The button of consent dialog is not found", btnObj);
btnObj.click();
Log.d(TAG, "Wait for the dialog to be dismissed");
assertTrue(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS));
}
private class BugreportBroadcastReceiver extends BroadcastReceiver {
Intent mBugreportFinishedIntent = null;
final CountDownLatch mLatch;
BugreportBroadcastReceiver() {
mLatch = new CountDownLatch(1);
}
@Override
public void onReceive(Context context, Intent intent) {
setBugreportFinishedIntent(intent);
mLatch.countDown();
}
private void setBugreportFinishedIntent(Intent intent) {
mBugreportFinishedIntent = intent;
}
public Intent getBugreportFinishedIntent() {
return mBugreportFinishedIntent;
}
public void waitForBugreportFinished() throws Exception {
if (!mLatch.await(BUGREPORT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
throw new Exception("Failed to receive BUGREPORT_FINISHED in "
+ BUGREPORT_TIMEOUT_MS + " ms.");
}
}
}
/**
* A rule to change strict mode vm policy temporarily till test method finished.
*
* To permit the non-visual context usage in tests while taking bugreports need user consent,
* or UiAutomator/BugreportManager.DumpstateListener would run into error.
* UiDevice#findObject creates UiObject2, its Gesture object and ViewConfiguration and
* UiObject2#click need to know bounds. Both of them access to WindowManager internally without
* visual context comes from InstrumentationRegistry and violate the policy.
* Also <code>DumpstateListener<code/> violate the policy when onScreenshotTaken is called.
*
* TODO(b/161201609) Remove this class once violations fixed.
*/
static class ExtendedStrictModeVmPolicy extends ExternalResource {
private boolean mWasVmPolicyChanged = false;
private StrictMode.VmPolicy mOldVmPolicy;
@Override
protected void after() {
restoreVmPolicyIfNeeded();
}
public void permitIncorrectContextUse() {
// Allow to call multiple times without losing old policy.
if (mOldVmPolicy == null) {
mOldVmPolicy = StrictMode.getVmPolicy();
}
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.permitIncorrectContextUse()
.penaltyLog()
.build());
mWasVmPolicyChanged = true;
}
private void restoreVmPolicyIfNeeded() {
if (mWasVmPolicyChanged && mOldVmPolicy != null) {
StrictMode.setVmPolicy(mOldVmPolicy);
mOldVmPolicy = null;
}
}
}
}