Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9b2784ac authored by Kean Mariotti's avatar Kean Mariotti Committed by Android (Google) Code Review
Browse files

Merge "Add bugreport pre-dump functionality"

parents 5e8d4040 615fb23f
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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
+24 −0
Original line number Diff line number Diff line
@@ -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) {
+47 −0
Original line number Diff line number Diff line
@@ -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;
}
+245 −14
Original line number Diff line number Diff line
@@ -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,
+4 −4
Original line number Diff line number Diff line
@@ -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);
Loading