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

Commit d053302e authored by Evan Laird's avatar Evan Laird
Browse files

Add a command line interface to SystemUI

This CL adds some basic support for a command line interface in
SystemUI, using `adb shell cmd` as the jump-off point.

The basic design is kind of as follows:

[ shell ]  -> [ system_server ] -> [ StatusBarService ] -> [ SystemUI ]
                                                                  |
             [ shell output ] <- [ sbm service ] <- [ CommandRegistry ]

Where StatusBarManagerService implements a new `passthrough` interface and
forwards all remaining arguments to SystemUI. SystemUI then spawns a new
thread and sends the commands off to the CommandRegistry. Commands are
parsed and executed from there, and all output is written to the given
ParcelFileDescriptor, which is piped back to SBMService using a
TransferPipe.

Commands will by default run on the main executor, but can register with
an executor in order to send work off to any other thread. The command
registry thread will block on the completion of the command.

Implementing new commands as of now is trivial: create a new concrete
implementation of CommandLine and add it to the command map inside of
CommandRegistry.

Test: `adb shell cmd statusbar prefs list-prefs` should have output

Change-Id: I189fa55df6caabe25d693c2241a4d994e7473a2c
parent f1897983
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -242,4 +242,10 @@ oneway interface IStatusBar
     * @param connect {@code true} if needs connection, otherwise set the connection to null.
     */
    void requestWindowMagnificationConnection(boolean connect);

    /**
     * Allow for pass-through arguments from `adb shell cmd statusbar <args>`, and write to the
     * file descriptor passed in.
     */
     void passThroughShellCommand(in String[] args, in ParcelFileDescriptor pfd);
}
+3 −1
Original line number Diff line number Diff line
@@ -75,8 +75,10 @@ public final class Prefs {
            Key.HAS_SEEN_BUBBLES_EDUCATION,
            Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION,
            Key.HAS_SEEN_REVERSE_BOTTOM_SHEET,
            Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT
            Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT,
            Key.HAS_SEEN_PRIORITY_ONBOARDING
    })
    // TODO: annotate these with their types so {@link PrefsCommandLine} can know how to set them
    public @interface Key {
        @Deprecated
        String OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME = "OverviewLastStackTaskActiveTime";
+35 −3
Original line number Diff line number Diff line
@@ -58,12 +58,14 @@ import com.android.internal.statusbar.IStatusBar;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.policy.CallbackController;
import com.android.systemui.tracing.ProtoTracer;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * This class takes the functions from IStatusBar that come in on
@@ -159,6 +161,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController<
     */
    private int mLastUpdatedImeDisplayId = INVALID_DISPLAY;
    private ProtoTracer mProtoTracer;
    private final @Nullable CommandRegistry mRegistry;

    /**
     * These methods are called back on the main thread.
@@ -368,11 +371,12 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController<
    }

    public CommandQueue(Context context) {
        this(context, null);
        this(context, null, null);
    }

    public CommandQueue(Context context, ProtoTracer protoTracer) {
    public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) {
        mProtoTracer = protoTracer;
        mRegistry = registry;
        context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler);
        // We always have default display.
        setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE);
@@ -1013,6 +1017,34 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController<
        }
    }

    @Override
    public void passThroughShellCommand(String[] args, ParcelFileDescriptor pfd) {
        final FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor());
        final PrintWriter pw = new PrintWriter(fos);
        // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible
        // to just throw this work onto the handler just like the other messages
        Thread thr = new Thread("Sysui.passThroughShellCommand") {
            public void run() {
                try {
                    if (mRegistry == null) {
                        return;
                    }

                    // Registry blocks this thread until finished
                    mRegistry.onShellCommand(pw, args);
                } finally {
                    pw.flush();
                    try {
                        // Close the file descriptor so the TransferPipe finishes its thread
                        pfd.close();
                    } catch (Exception e) {
                    }
                }
            }
        };
        thr.start();
    }

    private final class H extends Handler {
        private H(Looper l) {
            super(l);
+204 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.statusbar.commandline

import android.content.Context

import com.android.systemui.Prefs
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main

import java.io.PrintWriter
import java.lang.IllegalStateException
import java.util.concurrent.Executor
import java.util.concurrent.FutureTask

import javax.inject.Inject

/**
 * Registry / dispatcher for incoming shell commands. See [StatusBarManagerService] and
 * [StatusBarShellCommand] for how things are set up. Commands come in here by way of the service
 * like so:
 *
 * `adb shell cmd statusbar <command>`
 *
 * Where `cmd statusbar` send the shell command through to StatusBarManagerService, and
 * <command> is either processed in system server, or sent through to IStatusBar (CommandQueue)
 */
@SysUISingleton
class CommandRegistry @Inject constructor(
    val context: Context,
    @Main val mainExecutor: Executor
) {
    // To keep the command line parser hermetic, create a new one for every shell command
    private val commandMap = mutableMapOf<String, CommandWrapper>()
    private var initialized = false

    /**
     * Register a [Command] for a given name. The name here is the top-level namespace for
     * the registered command. A command could look like this for instance:
     *
     * `adb shell cmd statusbar notifications list`
     *
     * Where `notifications` is the command that signifies which receiver to send the remaining args
     * to.
     *
     * @param command String name of the command to register. Currently does not support aliases
     * @param receiverFactory Creates an instance of the receiver on every command
     * @param executor Pass an executor to offload your `receive` to another thread
     */
    @Synchronized
    fun registerCommand(
        name: String,
        commandFactory: () -> Command,
        executor: Executor
    ) {
        if (commandMap[name] != null) {
            throw IllegalStateException("A command is already registered for ($name)")
        }
        commandMap[name] = CommandWrapper(commandFactory, executor)
    }

    /**
     * Register a [Command] for a given name, to be executed on the main thread.
     */
    @Synchronized
    fun registerCommand(name: String, commandFactory: () -> Command) {
        registerCommand(name, commandFactory, mainExecutor)
    }

    /** Unregister a receiver */
    @Synchronized
    fun unregisterCommand(command: String) {
        commandMap.remove(command)
    }

    private fun initializeCommands() {
        initialized = true
        // TODO: Might want a dedicated place for commands without a home. Currently
        // this is here because Prefs.java is just an interface
        registerCommand("prefs") { PrefsCommand(context) }
    }

    /**
     * Receive a shell command and dispatch to the appropriate [Command]. Blocks until finished.
     */
    fun onShellCommand(pw: PrintWriter, args: Array<String>) {
        if (!initialized) initializeCommands()

        if (args.isEmpty()) {
            help(pw)
            return
        }

        val commandName = args[0]
        val wrapper = commandMap[commandName]

        if (wrapper == null) {
            help(pw)
            return
        }

        // Create a new instance of the command
        val command = wrapper.commandFactory()

        // Wrap the receive command in a task so that we can wait for its completion
        val task = FutureTask<Unit> {
            command.execute(pw, args.drop(1))
        }

        wrapper.executor.execute {
            task.run()
        }

        // Wait for the future to complete
        task.get()
    }

    private fun help(pw: PrintWriter) {
        pw.println("Usage: adb shell cmd statusbar <command>")
        pw.println("  known commands:")
        for (k in commandMap.keys) {
            pw.println("   $k")
        }
    }
}

private const val TAG = "CommandRegistry"

interface Command {
    fun execute(pw: PrintWriter, args: List<String>)
    fun help(pw: PrintWriter)
}

// Wrap commands in an executor package
private data class CommandWrapper(val commandFactory: () -> Command, val executor: Executor)

// Commands can go here for now, but they should move outside

private class PrefsCommand(val context: Context) : Command {
    override fun help(pw: PrintWriter) {
        pw.println("usage: prefs <command> [args]")
        pw.println("Available commands:")
        pw.println("  list-prefs")
        pw.println("  set-pref <pref name> <value>")
    }

    override fun execute(pw: PrintWriter, args: List<String>) {
        if (args.isEmpty()) {
            help(pw)
            return
        }

        val topLevel = args[0]

        when (topLevel) {
            "list-prefs" -> listPrefs(pw)
            "set-pref" -> setPref(pw, args.drop(1))
            else -> help(pw)
        }
    }

    private fun listPrefs(pw: PrintWriter) {
        pw.println("Available keys:")
        for (field in Prefs.Key::class.java.declaredFields) {
            pw.print("  ")
            pw.println(field.get(Prefs.Key::class.java))
        }
    }

    /**
     * Sets a preference from [Prefs]
     */
    private fun setPref(pw: PrintWriter, args: List<String>) {
        if (args.isEmpty()) {
            pw.println("invalid arguments: $args")
            return
        }
        val pref = args[0]

        when (pref) {
            Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING -> {
                val value = Integer.parseInt(args[1])
                Prefs.putBoolean(context, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, value != 0)
            }
            else -> {
                pw.println("Cannot set pref ($pref)")
            }
        }
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.commandline.CommandRegistry;
import com.android.systemui.statusbar.notification.AssistantFeedbackController;
import com.android.systemui.statusbar.notification.DynamicChildBindController;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
@@ -190,8 +191,11 @@ public interface StatusBarDependenciesModule {
     */
    @Provides
    @SysUISingleton
    static CommandQueue provideCommandQueue(Context context, ProtoTracer protoTracer) {
        return new CommandQueue(context, protoTracer);
    static CommandQueue provideCommandQueue(
            Context context,
            ProtoTracer protoTracer,
            CommandRegistry registry) {
        return new CommandQueue(context, protoTracer, registry);
    }

    /**
Loading