Merge "Add bugreport pre-dump functionality"
This commit is contained in:
committed by
Android (Google) Code Review
commit
9b2784ac64
@@ -9276,13 +9276,17 @@ package android.os {
|
||||
}
|
||||
|
||||
public final class BugreportManager {
|
||||
method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void preDumpUiData();
|
||||
method @RequiresPermission(android.Manifest.permission.DUMP) public void requestBugreport(@NonNull android.os.BugreportParams, @Nullable CharSequence, @Nullable CharSequence);
|
||||
method @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void startBugreport(@NonNull android.os.ParcelFileDescriptor, @Nullable android.os.ParcelFileDescriptor, @NonNull android.os.BugreportParams, @NonNull java.util.concurrent.Executor, @NonNull android.os.BugreportManager.BugreportCallback);
|
||||
}
|
||||
|
||||
public final class BugreportParams {
|
||||
ctor public BugreportParams(int);
|
||||
ctor public BugreportParams(int, int);
|
||||
method public int getFlags();
|
||||
method public int getMode();
|
||||
field public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA = 1; // 0x1
|
||||
field public static final int BUGREPORT_MODE_FULL = 0; // 0x0
|
||||
field public static final int BUGREPORT_MODE_INTERACTIVE = 1; // 0x1
|
||||
field public static final int BUGREPORT_MODE_REMOTE = 2; // 0x2
|
||||
|
||||
@@ -147,6 +147,29 @@ public final class BugreportManager {
|
||||
public void onEarlyReportFinished() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speculatively pre-dumps UI data for a bugreport request that might come later.
|
||||
*
|
||||
* <p>Triggers the dump of certain critical UI data, e.g. traces stored in short
|
||||
* ring buffers that might get lost by the time the actual bugreport is requested.
|
||||
*
|
||||
* <p>{@link #startBugreport} will then pick the pre-dumped data if both of the following
|
||||
* conditions are met:
|
||||
* - {@link android.os.BugreportParams#BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA} is specified.
|
||||
* - {@link #preDumpUiData} and {@link #startBugreport} were called by the same UID.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
@RequiresPermission(android.Manifest.permission.DUMP)
|
||||
@WorkerThread
|
||||
public void preDumpUiData() {
|
||||
try {
|
||||
mBinder.preDumpUiData(mContext.getOpPackageName());
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a bugreport.
|
||||
*
|
||||
@@ -198,6 +221,7 @@ public final class BugreportManager {
|
||||
bugreportFd.getFileDescriptor(),
|
||||
screenshotFd.getFileDescriptor(),
|
||||
params.getMode(),
|
||||
params.getFlags(),
|
||||
dsListener,
|
||||
isScreenshotRequested);
|
||||
} catch (RemoteException e) {
|
||||
|
||||
@@ -30,15 +30,45 @@ import java.lang.annotation.RetentionPolicy;
|
||||
@SystemApi
|
||||
public final class BugreportParams {
|
||||
private final int mMode;
|
||||
private final int mFlags;
|
||||
|
||||
/**
|
||||
* Constructs a BugreportParams object to specify what kind of bugreport should be taken.
|
||||
*
|
||||
* @param mode of the bugreport to request
|
||||
*/
|
||||
public BugreportParams(@BugreportMode int mode) {
|
||||
mMode = mode;
|
||||
mFlags = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a BugreportParams object to specify what kind of bugreport should be taken.
|
||||
*
|
||||
* @param mode of the bugreport to request
|
||||
* @param flags to customize the bugreport request
|
||||
*/
|
||||
public BugreportParams(@BugreportMode int mode, @BugreportFlag int flags) {
|
||||
mMode = mode;
|
||||
mFlags = flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mode of the bugreport to request.
|
||||
*/
|
||||
@BugreportMode
|
||||
public int getMode() {
|
||||
return mMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flags to customize the bugreport request.
|
||||
*/
|
||||
@BugreportFlag
|
||||
public int getFlags() {
|
||||
return mFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines acceptable types of bugreports.
|
||||
* @hide
|
||||
@@ -88,4 +118,21 @@ public final class BugreportParams {
|
||||
* Wifi.
|
||||
*/
|
||||
public static final int BUGREPORT_MODE_WIFI = IDumpstate.BUGREPORT_MODE_WIFI;
|
||||
|
||||
/**
|
||||
* Defines acceptable flags for customizing bugreport requests.
|
||||
* @hide
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(flag = true, prefix = { "BUGREPORT_FLAG_" }, value = {
|
||||
BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA
|
||||
})
|
||||
public @interface BugreportFlag {}
|
||||
|
||||
/**
|
||||
* Flag for reusing pre-dumped UI data. The pre-dump and bugreport request calls must be
|
||||
* performed by the same UID, otherwise the flag is ignored.
|
||||
*/
|
||||
public static final int BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA =
|
||||
IDumpstate.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import android.os.HandlerThread;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Process;
|
||||
import android.os.StrictMode;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -48,6 +47,9 @@ 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;
|
||||
@@ -57,11 +59,22 @@ 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.
|
||||
@@ -74,6 +87,7 @@ public class BugreportManagerTest {
|
||||
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);
|
||||
|
||||
|
||||
@@ -89,6 +103,18 @@ public class BugreportManagerTest {
|
||||
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/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/layers_trace.winscope"),
|
||||
Paths.get("/data/misc/wmtrace/transactions_trace.winscope"),
|
||||
};
|
||||
private static final Path[] UI_TRACES_GENERATED_DURING_BUGREPORT = {
|
||||
Paths.get("/data/misc/wmtrace/layers_trace_from_transactions.winscope"),
|
||||
};
|
||||
|
||||
private Handler mHandler;
|
||||
private Executor mExecutor;
|
||||
private BugreportManager mBrm;
|
||||
@@ -124,7 +150,6 @@ public class BugreportManagerTest {
|
||||
FileUtils.closeQuietly(mScreenshotFd);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void normalFlow_wifi() throws Exception {
|
||||
BugreportCallbackImpl callback = new BugreportCallbackImpl();
|
||||
@@ -175,6 +200,66 @@ public class BugreportManagerTest {
|
||||
assertFdsAreClosed(mBugreportFd, mScreenshotFd);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void preDumpUiData_then_fullWithUsePreDumpFlag() throws Exception {
|
||||
startPreDumpedUiTraces();
|
||||
|
||||
mBrm.preDumpUiData();
|
||||
waitTillDumpstateExitedOrTimeout();
|
||||
List<File> expectedPreDumpedTraceFiles = copyFilesAsRoot(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);
|
||||
assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
|
||||
|
||||
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.
|
||||
createFilesWithFakeDataAsRoot(UI_TRACES_PREDUMPED, "system");
|
||||
|
||||
List<File> preDumpedTraceFiles = copyFilesAsRoot(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);
|
||||
assertThatBugreportContainsFiles(UI_TRACES_GENERATED_DURING_BUGREPORT);
|
||||
|
||||
List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
|
||||
assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simultaneousBugreportsNotAllowed() throws Exception {
|
||||
// Start bugreport #1
|
||||
@@ -384,21 +469,151 @@ public class BugreportManagerTest {
|
||||
}
|
||||
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() {
|
||||
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 createFilesWithFakeDataAsRoot(Path[] paths, String owner) 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 611 -o " + owner + " -g " + owner
|
||||
+ " " + src.getAbsolutePath() + " " + path.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<File> copyFilesAsRoot(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();
|
||||
@@ -410,21 +625,16 @@ public class BugreportManagerTest {
|
||||
}
|
||||
|
||||
private static boolean isDumpstateRunning() {
|
||||
String[] output;
|
||||
String output;
|
||||
try {
|
||||
output =
|
||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
.executeShellCommand("ps -A -o NAME | grep dumpstate")
|
||||
.trim()
|
||||
.split("\n");
|
||||
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) {
|
||||
// Check for an exact match since there may be other things that contain "dumpstate" as
|
||||
// a substring (e.g. the dumpstate HAL).
|
||||
if (TextUtils.equals("dumpstate", line)) {
|
||||
for (String line : output.trim().split("\n")) {
|
||||
if (line.matches("^.*\\s+dumpstate:\\s+\\[.*\\]$")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -449,6 +659,17 @@ public class BugreportManagerTest {
|
||||
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()) {
|
||||
@@ -500,6 +721,16 @@ public class BugreportManagerTest {
|
||||
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,
|
||||
|
||||
@@ -199,8 +199,8 @@ public class BugreportReceiverTest {
|
||||
}
|
||||
mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2));
|
||||
return null;
|
||||
}).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), any(),
|
||||
anyBoolean());
|
||||
}).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
|
||||
any(), anyBoolean());
|
||||
|
||||
setWarningState(mContext, STATE_HIDE);
|
||||
|
||||
@@ -543,7 +543,7 @@ public class BugreportReceiverTest {
|
||||
getInstrumentation().waitForIdleSync();
|
||||
|
||||
verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(),
|
||||
anyInt(), any(), anyBoolean());
|
||||
anyInt(), anyInt(), any(), anyBoolean());
|
||||
sendBugreportFinished();
|
||||
}
|
||||
|
||||
@@ -608,7 +608,7 @@ public class BugreportReceiverTest {
|
||||
ArgumentCaptor<IDumpstateListener> listenerCap = ArgumentCaptor.forClass(
|
||||
IDumpstateListener.class);
|
||||
verify(mMockIDumpstate, timeout(TIMEOUT)).startBugreport(anyInt(), any(), any(), any(),
|
||||
anyInt(), listenerCap.capture(), anyBoolean());
|
||||
anyInt(), anyInt(), listenerCap.capture(), anyBoolean());
|
||||
mIDumpstateListener = listenerCap.getValue();
|
||||
assertNotNull("Dumpstate listener should not be null", mIDumpstateListener);
|
||||
mIDumpstateListener.onProgress(0);
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.android.server.SystemConfig;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
/**
|
||||
* Implementation of the service that provides a privileged API to capture and consume bugreports.
|
||||
@@ -60,6 +61,9 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
private final TelephonyManager mTelephonyManager;
|
||||
private final ArraySet<String> mBugreportWhitelistedPackages;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private OptionalInt mPreDumpedDataUid = OptionalInt.empty();
|
||||
|
||||
BugreportManagerServiceImpl(Context context) {
|
||||
mContext = context;
|
||||
mAppOps = context.getSystemService(AppOpsManager.class);
|
||||
@@ -68,15 +72,27 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
SystemConfig.getInstance().getBugreportWhitelistedPackages();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresPermission(android.Manifest.permission.DUMP)
|
||||
public void preDumpUiData(String callingPackage) {
|
||||
enforcePermission(callingPackage, Binder.getCallingUid(), true);
|
||||
|
||||
synchronized (mLock) {
|
||||
preDumpUiDataLocked(callingPackage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresPermission(android.Manifest.permission.DUMP)
|
||||
public void startBugreport(int callingUidUnused, String callingPackage,
|
||||
FileDescriptor bugreportFd, FileDescriptor screenshotFd,
|
||||
int bugreportMode, IDumpstateListener listener, boolean isScreenshotRequested) {
|
||||
int bugreportMode, int bugreportFlags, IDumpstateListener listener,
|
||||
boolean isScreenshotRequested) {
|
||||
Objects.requireNonNull(callingPackage);
|
||||
Objects.requireNonNull(bugreportFd);
|
||||
Objects.requireNonNull(listener);
|
||||
validateBugreportMode(bugreportMode);
|
||||
validateBugreportFlags(bugreportFlags);
|
||||
|
||||
int callingUid = Binder.getCallingUid();
|
||||
enforcePermission(callingPackage, callingUid, bugreportMode
|
||||
@@ -90,7 +106,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
|
||||
synchronized (mLock) {
|
||||
startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd,
|
||||
bugreportMode, listener, isScreenshotRequested);
|
||||
bugreportMode, bugreportFlags, listener, isScreenshotRequested);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,17 +124,14 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
}
|
||||
try {
|
||||
// Note: this may throw SecurityException back out to the caller if they aren't
|
||||
// allowed to cancel the report, in which case we should NOT be setting ctl.stop,
|
||||
// since that would unintentionally kill some other app's bugreport, which we
|
||||
// specifically disallow.
|
||||
// allowed to cancel the report, in which case we should NOT stop the dumpstate
|
||||
// service, since that would unintentionally kill some other app's bugreport, which
|
||||
// we specifically disallow.
|
||||
ds.cancelBugreport(callingUid, callingPackage);
|
||||
} catch (RemoteException e) {
|
||||
Slog.e(TAG, "RemoteException in cancelBugreport", e);
|
||||
}
|
||||
// This tells init to cancel bugreportd service. Note that this is achieved through
|
||||
// setting a system property which is not thread-safe. So the lock here offers
|
||||
// thread-safety only among callers of the API.
|
||||
SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
|
||||
stopDumpstateBinderServiceLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +147,14 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateBugreportFlags(int flags) {
|
||||
flags = clearBugreportFlag(flags, BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
|
||||
if (flags != 0) {
|
||||
Slog.w(TAG, "Unknown bugreport flags: " + flags);
|
||||
throw new IllegalArgumentException("Unknown bugreport flags: " + flags);
|
||||
}
|
||||
}
|
||||
|
||||
private void enforcePermission(
|
||||
String callingPackage, int callingUid, boolean checkCarrierPrivileges) {
|
||||
mAppOps.checkPackage(callingUid, callingPackage);
|
||||
@@ -223,18 +244,63 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
return true;
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void preDumpUiDataLocked(String callingPackage) {
|
||||
mPreDumpedDataUid = OptionalInt.empty();
|
||||
|
||||
if (isDumpstateBinderServiceRunningLocked()) {
|
||||
Slog.e(TAG, "'dumpstate' is already running. "
|
||||
+ "Cannot pre-dump data while another operation is currently in progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
|
||||
if (ds == null) {
|
||||
Slog.e(TAG, "Unable to get bugreport service");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ds.preDumpUiData(callingPackage);
|
||||
} catch (RemoteException e) {
|
||||
return;
|
||||
} finally {
|
||||
// dumpstate service is already started now. We need to kill it to manage the
|
||||
// lifecycle correctly. If we don't subsequent callers will get
|
||||
// BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error.
|
||||
stopDumpstateBinderServiceLocked();
|
||||
}
|
||||
|
||||
|
||||
mPreDumpedDataUid = OptionalInt.of(Binder.getCallingUid());
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void startBugreportLocked(int callingUid, String callingPackage,
|
||||
FileDescriptor bugreportFd, FileDescriptor screenshotFd,
|
||||
int bugreportMode, IDumpstateListener listener, boolean isScreenshotRequested) {
|
||||
int bugreportMode, int bugreportFlags, IDumpstateListener listener,
|
||||
boolean isScreenshotRequested) {
|
||||
if (isDumpstateBinderServiceRunningLocked()) {
|
||||
Slog.w(TAG, "'dumpstate' is already running. Cannot start a new bugreport"
|
||||
+ " while another one is currently in progress.");
|
||||
reportError(listener,
|
||||
IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
|
||||
+ " while another operation is currently in progress.");
|
||||
reportError(listener, IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bugreportFlags & BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA) != 0) {
|
||||
if (mPreDumpedDataUid.isEmpty()) {
|
||||
bugreportFlags = clearBugreportFlag(bugreportFlags,
|
||||
BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
|
||||
Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA."
|
||||
+ " No pre-dumped data is available.");
|
||||
} else if (mPreDumpedDataUid.getAsInt() != callingUid) {
|
||||
bugreportFlags = clearBugreportFlag(bugreportFlags,
|
||||
BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
|
||||
Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA."
|
||||
+ " Data was pre-dumped by a different UID.");
|
||||
}
|
||||
}
|
||||
|
||||
IDumpstate ds = startAndGetDumpstateBinderServiceLocked();
|
||||
if (ds == null) {
|
||||
Slog.w(TAG, "Unable to get bugreport service");
|
||||
@@ -245,10 +311,10 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
// Wrap the listener so we can intercept binder events directly.
|
||||
IDumpstateListener myListener = new DumpstateListener(listener, ds);
|
||||
try {
|
||||
ds.startBugreport(callingUid, callingPackage,
|
||||
bugreportFd, screenshotFd, bugreportMode, myListener, isScreenshotRequested);
|
||||
ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode,
|
||||
bugreportFlags, myListener, isScreenshotRequested);
|
||||
} catch (RemoteException e) {
|
||||
// bugreportd service is already started now. We need to kill it to manage the
|
||||
// dumpstate service is already started now. We need to kill it to manage the
|
||||
// lifecycle correctly. If we don't subsequent callers will get
|
||||
// BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error.
|
||||
// Note that listener will be notified by the death recipient below.
|
||||
@@ -309,6 +375,19 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
|
||||
return ds;
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void stopDumpstateBinderServiceLocked() {
|
||||
// This tells init to cancel bugreportd service. Note that this is achieved through
|
||||
// setting a system property which is not thread-safe. So the lock here offers
|
||||
// thread-safety only among callers of the API.
|
||||
SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
|
||||
}
|
||||
|
||||
private int clearBugreportFlag(int flags, @BugreportParams.BugreportFlag int flag) {
|
||||
flags &= ~flag;
|
||||
return flags;
|
||||
}
|
||||
|
||||
private void reportError(IDumpstateListener listener, int errorCode) {
|
||||
try {
|
||||
listener.onError(errorCode);
|
||||
|
||||
Reference in New Issue
Block a user