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

Commit 9920baf4 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add a command line interface to SystemUI"

parents 98a53309 d053302e
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;
@@ -192,8 +193,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