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

Commit a13e078b authored by Stefan Andonian's avatar Stefan Andonian Committed by Android (Google) Code Review
Browse files

Merge "Output ViewCapture data via Shell command." into udc-qpr-dev

parents 5f5a3f57 20e0c901
Loading
Loading
Loading
Loading
+109 −24
Original line number Diff line number Diff line
@@ -86,7 +86,10 @@ import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -127,6 +130,9 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Service that manages requests and callbacks for launchers that support
@@ -215,7 +221,8 @@ public class LauncherAppsService extends SystemService {

        final LauncherAppsServiceInternal mInternal;

        private RemoteCallbackList<IDumpCallback> mDumpCallbacks = new RemoteCallbackList<>();
        @NonNull
        private final RemoteCallbackList<IDumpCallback> mDumpCallbacks = new RemoteCallbackList<>();

        public LauncherAppsImpl(Context context) {
            mContext = context;
@@ -1462,46 +1469,124 @@ public class LauncherAppsService extends SystemService {
                    getActivityOptionsForLauncher(opts), user.getIdentifier());
        }

        @Override
        public void onShellCommand(FileDescriptor in, @NonNull FileDescriptor out,
                @NonNull FileDescriptor err, @Nullable String[] args, ShellCallback cb,
                @Nullable ResultReceiver receiver) {
            final int callingUid = injectBinderCallingUid();
            if (!(callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID)) {
                throw new SecurityException("Caller must be shell");
            }

            final long token = injectClearCallingIdentity();
            try {
                int status = (new LauncherAppsShellCommand())
                        .exec(this, in, out, err, args, cb, receiver);
                if (receiver != null) {
                    receiver.send(status, null);
                }
            } finally {
                injectRestoreCallingIdentity(token);
            }
        }

        /** Handles Shell commands for LauncherAppsService */
        private class LauncherAppsShellCommand extends ShellCommand {
            @Override
            public int onCommand(@Nullable String cmd) {
                if ("dump-view-hierarchies".equals(cmd)) {
                    dumpViewCaptureDataToShell();
                    return 0;
                } else {
                    return handleDefaultCommands(cmd);
                }
            }

            private void dumpViewCaptureDataToShell() {
                try (ZipOutputStream zipOs = new ZipOutputStream(getRawOutputStream())) {
                    forEachViewCaptureWindow((fileName, is) -> {
                        try {
                            zipOs.putNextEntry(new ZipEntry("FS" + fileName));
                            is.transferTo(zipOs);
                            zipOs.closeEntry();
                        } catch (IOException e) {
                            getErrPrintWriter().write("Failed to output " + fileName
                                    + " data to shell: " + e.getMessage());
                        }
                    });
                } catch (IOException e) {
                    getErrPrintWriter().write("Failed to create or close zip output stream: "
                            + e.getMessage());
                }
            }

            @Override
            public void onHelp() {
                final PrintWriter pw = getOutPrintWriter();
                pw.println("Usage: cmd launcherapps COMMAND [options ...]");
                pw.println();
                pw.println("cmd launcherapps dump-view-hierarchies");
                pw.println("    Output captured view hierarchies. Files will be generated in ");
                pw.println("    `"  + WM_TRACE_DIR + "`. After pulling the data to your device,");
                pw.println("     you can upload / visualize it at `go/winscope`.");
                pw.println();
            }
        }

        /**
         * Using a pipe, outputs view capture data to the wmtrace dir
         */
        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
                @Nullable String[] args) {
            super.dump(fd, pw, args);

            // Before the wmtrace directory is picked up by dumpstate service, some processes need
            // to write their data to that location. They can do that via these dumpCallbacks.
            int i = mDumpCallbacks.beginBroadcast();
            while (i > 0) {
                i--;
                dumpDataToWmTrace((String) mDumpCallbacks.getBroadcastCookie(i) + "_" + i,
                        mDumpCallbacks.getBroadcastItem(i));
            forEachViewCaptureWindow(this::dumpViewCaptureDataToWmTrace);
        }

        private void dumpViewCaptureDataToWmTrace(@NonNull String fileName,
                @NonNull InputStream is) {
            Path outPath = Paths.get(fileName);
            try {
                Files.copy(is, outPath, StandardCopyOption.REPLACE_EXISTING);
                Files.setPosixFilePermissions(outPath, WM_TRACE_FILE_PERMISSIONS);
            } catch (IOException e) {
                Log.d(TAG, "failed to write data to " + fileName + " in wmtrace dir", e);
            }
            mDumpCallbacks.finishBroadcast();
        }

        private void dumpDataToWmTrace(String name, IDumpCallback cb) {
            ParcelFileDescriptor[] pipe;
        /**
         * IDumpCallback.onDump alerts the in-process ViewCapture instance to start sending data
         * to LauncherAppsService via the pipe's input provided. This data (as well as an output
         * file name) is provided to the consumer via an InputStream to output where it wants (for
         * example, the winscope trace directory or the shell's stdout).
         */
        private void forEachViewCaptureWindow(
                @NonNull BiConsumer<String, InputStream> outputtingConsumer) {
            for (int i = mDumpCallbacks.beginBroadcast() - 1; i >= 0; i--) {
                String packageName = (String) mDumpCallbacks.getBroadcastCookie(i);
                String fileName = WM_TRACE_DIR + packageName + "_" + i + VC_FILE_SUFFIX;

                try {
                pipe = ParcelFileDescriptor.createPipe();
                cb.onDump(pipe[1]);
            } catch (IOException | RemoteException e) {
                    // Order is important here. OnDump needs to be called before the BiConsumer
                    // accepts & starts blocking on reading the input stream.
                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                    mDumpCallbacks.getBroadcastItem(i).onDump(pipe[1]);

                    InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pipe[0]);
                    outputtingConsumer.accept(fileName, is);
                    is.close();
                } catch (Exception e) {
                    Log.d(TAG, "failed to pipe view capture data", e);
                return;
                }

            Path path = Paths.get(WM_TRACE_DIR + Paths.get(name + VC_FILE_SUFFIX).getFileName());
            try (InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pipe[0])) {
                Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
                Files.setPosixFilePermissions(path, WM_TRACE_FILE_PERMISSIONS);
            } catch (IOException e) {
                Log.d(TAG, "failed to write data to file in wmtrace dir", e);
            }
            mDumpCallbacks.finishBroadcast();
        }

        @RequiresPermission(READ_FRAME_BUFFER)
        @Override
        public void registerDumpCallback(IDumpCallback cb) {
        public void registerDumpCallback(@NonNull IDumpCallback cb) {
            int status = checkCallingOrSelfPermissionForPreflight(mContext, READ_FRAME_BUFFER);
            if (PERMISSION_GRANTED == status) {
                String name = mContext.getPackageManager().getNameForUid(Binder.getCallingUid());
@@ -1513,7 +1598,7 @@ public class LauncherAppsService extends SystemService {

        @RequiresPermission(READ_FRAME_BUFFER)
        @Override
        public void unRegisterDumpCallback(IDumpCallback cb) {
        public void unRegisterDumpCallback(@NonNull IDumpCallback cb) {
            int status = checkCallingOrSelfPermissionForPreflight(mContext, READ_FRAME_BUFFER);
            if (PERMISSION_GRANTED == status) {
                mDumpCallbacks.unregister(cb);