Introduce DumpManager

Introduces DumpManager, a unified dumping system that supports dumping
at different priority levels.

Currently, when a bug report gets taken, SystemUI is only dumped during
the CRITICAL section. This has its advantages (we get to go first!) but
also imposes a strict limit on how much we can dump. To get around this
restriction, we need to *also* dump SystemUI during the NORMAL section,
which has much more forgiving constraints.

This CL simply creates the mechanism for systemUI to dump at different
priority levels, but doesn't actually cause us to participate in the
NORMAL section (yet, see later CLs).

It introduces the DumpManager, unified replacement for DumpController &
various logic in SystemUIService and Dependency.java. See kdoc in
DumpManager for usage notes.

Migration of current users of DumpController coming in a later CL.

Test: atest, manual
Change-Id: If4f41ed496c0c64024a83aad812b77f60fe27555
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 69bc259..362a9e8 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -42,6 +42,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -128,8 +129,6 @@
 import com.android.systemui.wm.DisplayImeController;
 import com.android.systemui.wm.SystemWindows;
 
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -211,6 +210,8 @@
     private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>();
     private final ArrayMap<Object, LazyDependencyCreator> mProviders = new ArrayMap<>();
 
+    @Inject DumpManager mDumpManager;
+
     @Inject Lazy<ActivityStarter> mActivityStarter;
     @Inject Lazy<BroadcastDispatcher> mBroadcastDispatcher;
     @Inject Lazy<AsyncSensorManager> mAsyncSensorManager;
@@ -534,34 +535,6 @@
         sDependency = this;
     }
 
-    static void staticDump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        sDependency.dump(fd, pw, args);
-    }
-
-    /**
-     * {@see SystemUI.dump}
-     */
-    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        // Make sure that the DumpController gets added to mDependencies, as they are only added
-        // with Dependency#get.
-        getDependency(DumpController.class);
-        getDependency(BroadcastDispatcher.class);
-
-        // If an arg is specified, try to dump the dependency
-        String controller = args != null && args.length > 1
-                ? args[1].toLowerCase()
-                : null;
-        if (controller != null) {
-            pw.println("Dumping controller=" + controller + ":");
-        } else {
-            pw.println("Dumping existing controllers:");
-        }
-        mDependencies.values().stream()
-                .filter(obj -> obj instanceof Dumpable && (controller == null
-                        || obj.getClass().getName().toLowerCase().endsWith(controller)))
-                .forEach(o -> ((Dumpable) o).dump(fd, pw, args));
-    }
-
     protected final <T> T getDependency(Class<T> cls) {
         return getDependencyInner(cls);
     }
@@ -576,6 +549,11 @@
         if (obj == null) {
             obj = createDependency(key);
             mDependencies.put(key, obj);
+
+            // TODO: Get dependencies to register themselves instead
+            if (autoRegisterModulesForDump() && obj instanceof Dumpable) {
+                mDumpManager.registerDumpable(obj.getClass().getName(), (Dumpable) obj);
+            }
         }
         return obj;
     }
@@ -593,6 +571,17 @@
         return provider.createDependency();
     }
 
+    // Currently, there are situations in tests where we might create more than one instance of a
+    // thing that should be a singleton: the "real" one (created by Dagger, usually as a result of
+    // inflating a view), and a mocked one (injected into Dependency). If we register the mocked
+    // one, the DumpManager will throw an exception complaining (rightly) that we have too many
+    // things registered with that name. So in tests, we disable the auto-registration until the
+    // root cause is fixed, i.e. inflated views in tests with Dagger dependencies.
+    @VisibleForTesting
+    protected boolean autoRegisterModulesForDump() {
+        return true;
+    }
+
     private static Dependency sDependency;
 
     /**
@@ -605,6 +594,9 @@
 
     private <T> void destroyDependency(Class<T> cls, Consumer<T> destroy) {
         T dep = (T) mDependencies.remove(cls);
+        if (dep instanceof Dumpable) {
+            mDumpManager.unregisterDumpable(dep.getClass().getName());
+        }
         if (dep != null && destroy != null) {
             destroy.accept(dep);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUI.java b/packages/SystemUI/src/com/android/systemui/SystemUI.java
index f795faf..e880cc8 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUI.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUI.java
@@ -21,10 +21,18 @@
 import android.content.res.Configuration;
 import android.os.Bundle;
 
+import androidx.annotation.NonNull;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 
-public abstract class SystemUI {
+/**
+ * A top-level module of system UI code (sometimes called "system UI services" elsewhere in code).
+ * Which SystemUI modules are loaded can be controlled via a config resource.
+ *
+ * @see SystemUIApplication#startServicesIfNeeded()
+ */
+public abstract class SystemUI implements Dumpable {
     protected final Context mContext;
 
     public SystemUI(Context context) {
@@ -36,7 +44,8 @@
     protected void onConfigurationChanged(Configuration newConfig) {
     }
 
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    @Override
+    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
     }
 
     protected void onBootCompleted() {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index 1315152..cbdae4e 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -33,6 +33,7 @@
 
 import com.android.systemui.dagger.ContextComponentHelper;
 import com.android.systemui.dagger.SystemUIRootComponent;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.util.NotificationChannels;
 
 import java.lang.reflect.Constructor;
@@ -171,6 +172,8 @@
             }
         }
 
+        final DumpManager dumpManager = mRootComponent.createDumpManager();
+
         Log.v(TAG, "Starting SystemUI services for user " +
                 Process.myUserHandle().getIdentifier() + ".");
         TimingsTraceLog log = new TimingsTraceLog("SystemUIBootTiming",
@@ -209,6 +212,8 @@
             if (mBootCompleteCache.isBootComplete()) {
                 mServices[i].onBootCompleted();
             }
+
+            dumpManager.registerDumpable(mServices[i].getClass().getName(), mServices[i]);
         }
         mRootComponent.getInitController().executePostInitTasks();
         log.traceEnd();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
index 2d2d91d..f4ec6f7 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java
@@ -20,9 +20,6 @@
 import android.content.Intent;
 import android.os.IBinder;
 
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
 public class SystemUISecondaryUserService extends Service {
 
     @Override
@@ -35,11 +32,4 @@
     public IBinder onBind(Intent intent) {
         return null;
     }
-
-    @Override
-    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        SystemUIService.dumpServices(
-                ((SystemUIApplication) getApplication()).getServices(), fd, pw, args);
-    }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 41d8314..e65fccd 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui;
 
-import android.annotation.NonNull;
 import android.app.Service;
 import android.content.Intent;
 import android.os.Build;
@@ -28,8 +27,7 @@
 
 import com.android.internal.os.BinderInternal;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.shared.plugins.PluginManagerImpl;
+import com.android.systemui.dump.DumpManager;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -39,11 +37,15 @@
 public class SystemUIService extends Service {
 
     private final Handler mMainHandler;
+    private final DumpManager mDumpManager;
 
     @Inject
-    public SystemUIService(@Main Handler mainHandler) {
+    public SystemUIService(
+            @Main Handler mainHandler,
+            DumpManager dumpManager) {
         super();
         mMainHandler = mainHandler;
+        mDumpManager = dumpManager;
     }
 
     @Override
@@ -79,62 +81,16 @@
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        dumpServices(((SystemUIApplication) getApplication()).getServices(), fd, pw, args);
-
-        if (args == null || args.length == 0 || args[0].equals("--config")) {
-            dumpConfig(pw);
+        // If no args are passed, assume we're being dumped as part of a bug report (sadly, we have
+        // no better way to guess whether this is taking place). Set the appropriate dump priority
+        // (CRITICAL) to reflect that this is taking place.
+        String[] massagedArgs = args;
+        if (args.length == 0) {
+            massagedArgs = new String[] {
+                    DumpManager.PRIORITY_ARG,
+                    DumpManager.PRIORITY_ARG_CRITICAL};
         }
-    }
 
-    static void dumpServices(
-            SystemUI[] services, FileDescriptor fd, PrintWriter pw, String[] args) {
-        if (args == null || args.length == 0) {
-            pw.println("dumping service: " + Dependency.class.getName());
-            Dependency.staticDump(fd, pw, args);
-            for (SystemUI ui: services) {
-                pw.println("dumping service: " + ui.getClass().getName());
-                ui.dump(fd, pw, args);
-            }
-            if (Build.IS_DEBUGGABLE) {
-                pw.println("dumping plugins:");
-                ((PluginManagerImpl) Dependency.get(PluginManager.class)).dump(fd, pw, args);
-            }
-        } else {
-            String svc = args[0].toLowerCase();
-            if (Dependency.class.getName().toLowerCase().endsWith(svc)) {
-                Dependency.staticDump(fd, pw, args);
-            }
-            for (SystemUI ui: services) {
-                String name = ui.getClass().getName().toLowerCase();
-                if (name.endsWith(svc)) {
-                    ui.dump(fd, pw, args);
-                }
-            }
-        }
-    }
-
-    private void dumpConfig(@NonNull PrintWriter pw) {
-        pw.println("SystemUiServiceComponents configuration:");
-
-        pw.print("vendor component: ");
-        pw.println(getResources().getString(R.string.config_systemUIVendorServiceComponent));
-
-        dumpConfig(pw, "global", R.array.config_systemUIServiceComponents);
-        dumpConfig(pw, "per-user", R.array.config_systemUIServiceComponentsPerUser);
-    }
-
-    private void dumpConfig(@NonNull PrintWriter pw, @NonNull String type, int resId) {
-        final String[] services = getResources().getStringArray(resId);
-        pw.print(type); pw.print(": ");
-        if (services == null) {
-            pw.println("N/A");
-            return;
-        }
-        pw.print(services.length);
-        pw.println(" services");
-        for (int i = 0; i < services.length; i++) {
-            pw.print("  "); pw.print(i); pw.print(": "); pw.println(services[i]);
-        }
+        mDumpManager.dump(fd, pw, massagedArgs);
     }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
index cedf7c3..bd803fa 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
 import java.io.FileDescriptor
 import java.io.PrintWriter
 import java.util.concurrent.Executor
@@ -65,12 +66,18 @@
 open class BroadcastDispatcher @Inject constructor (
     private val context: Context,
     @Main private val mainHandler: Handler,
-    @Background private val bgLooper: Looper
+    @Background private val bgLooper: Looper,
+    dumpManager: DumpManager
 ) : Dumpable {
 
     // Only modify in BG thread
     private val receiversByUser = SparseArray<UserBroadcastDispatcher>(20)
 
+    init {
+        // TODO: Don't do this in the constructor
+        dumpManager.registerDumpable(javaClass.name, this)
+    }
+
     /**
      * Register a receiver for broadcast with the dispatcher
      *
@@ -112,10 +119,10 @@
      */
     @JvmOverloads
     fun registerReceiver(
-            receiver: BroadcastReceiver,
-            filter: IntentFilter,
-            executor: Executor? = context.mainExecutor,
-            user: UserHandle = context.user
+        receiver: BroadcastReceiver,
+        filter: IntentFilter,
+        executor: Executor? = context.mainExecutor,
+        user: UserHandle = context.user
     ) {
         checkFilter(filter)
         this.handler
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIRootComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIRootComponent.java
index 12b9be1..18c3eac 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIRootComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIRootComponent.java
@@ -25,6 +25,7 @@
 import com.android.systemui.InitController;
 import com.android.systemui.SystemUIAppComponentFactory;
 import com.android.systemui.SystemUIFactory;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
 import com.android.systemui.pip.phone.dagger.PipModule;
@@ -76,6 +77,10 @@
     @Singleton
     Dependency.DependencyInjector createDependency();
 
+    /** */
+    @Singleton
+    DumpManager createDumpManager();
+
     /**
      * FragmentCreator generates all Fragments that need injection.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
new file mode 100644
index 0000000..59a7a32
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -0,0 +1,367 @@
+/*
+ * 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.dump
+
+import android.content.Context
+import android.os.SystemClock
+import android.os.Trace
+import android.util.ArrayMap
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_CRITICAL
+import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_HIGH
+import com.android.systemui.dump.DumpManager.Companion.PRIORITY_ARG_NORMAL
+import com.android.systemui.log.LogBuffer
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Oversees SystemUI's output during bug reports (and dumpsys in general)
+ *
+ * When a bug report is taken, SystemUI dumps various diagnostic information that we hope will be
+ * useful for the eventual readers of the bug report. Code that wishes to participate in this dump
+ * should register itself here.
+ *
+ * Dump output is split into two sections, CRITICAL and NORMAL. All dumpables registered via
+ * [registerDumpable] appear in the CRITICAL section, while all [LogBuffer]s appear in the NORMAL
+ * section (due to their length).
+ *
+ * The CRITICAL and NORMAL sections can be found within a bug report by searching for
+ * "SERVICE com.android.systemui/.SystemUIService" and
+ * "SERVICE com.android.systemui/.dump.SystemUIAuxiliaryDumpService", respectively.
+ *
+ * Finally, some or all of the dump can be triggered on-demand via adb (see below).
+ *
+ * ```
+ * # For the following, let <invocation> be:
+ * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService
+ *
+ * # To dump specific target(s), specify one or more registered names:
+ * $ <invocation> NotifCollection
+ * $ <invocation> StatusBar FalsingManager BootCompleteCacheImpl
+ *
+ * # Log buffers can be dumped in the same way (and can even be mixed in with other dump targets,
+ * # although it's not clear why one would want such a thing):
+ * $ <invocation> NotifLog
+ * $ <invocation> StatusBar NotifLog BootCompleteCacheImpl
+ *
+ * # If passing -t or --tail, shows only the last N lines of any log buffers:
+ * $ <invocation> NotifLog --tail 100
+ *
+ * # Dump targets are matched using String.endsWith(), so dumpables that register using their
+ * # fully-qualified class name can still be dumped using their short name:
+ * $ <invocation> com.android.keyguard.KeyguardUpdateMonitor
+ * $ <invocation> keyguard.KeyguardUpdateMonitor
+ * $ <invocation> KeyguardUpdateMonitor
+ *
+ * # To dump all dumpables or all buffers:
+ * $ <invocation> dumpables
+ * $ <invocation> buffers
+ *
+ * Finally, the following will simulate what we dump during the CRITICAL and NORMAL sections of a
+ * bug report:
+ * $ <invocation> bugreport-critical
+ * $ <invocation> bugreport-normal
+ * ```
+ */
+@Singleton
+class DumpManager @Inject constructor(
+    private val context: Context
+) {
+    private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = ArrayMap()
+    private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = ArrayMap()
+
+    /**
+     * Register a dumpable to be called during a bug report. The dumpable will be called during the
+     * CRITICAL section of the bug report, so don't dump an excessive amount of stuff here.
+     *
+     * @param name The name to register the dumpable under. This is typically the qualified class
+     * name of the thing being dumped (getClass().getName()), but can be anything as long as it
+     * doesn't clash with an existing registration.
+     */
+    @Synchronized
+    fun registerDumpable(name: String, module: Dumpable) {
+        if (RESERVED_NAMES.contains(name)) {
+            throw IllegalArgumentException("'$name' is reserved")
+        }
+
+        if (!canAssignToNameLocked(name, module)) {
+            throw IllegalArgumentException("'$name' is already registered")
+        }
+
+        dumpables[name] = RegisteredDumpable(name, module)
+    }
+
+    /**
+     * Unregisters a previously-registered dumpable.
+     */
+    @Synchronized
+    fun unregisterDumpable(name: String) {
+        dumpables.remove(name)
+    }
+
+    /**
+     * Register a [LogBuffer] to be dumped during a bug report.
+     */
+    @Synchronized
+    fun registerBuffer(name: String, buffer: LogBuffer) {
+        if (!canAssignToNameLocked(name, buffer)) {
+            throw IllegalArgumentException("'$name' is already registered")
+        }
+        buffers[name] = RegisteredDumpable(name, buffer)
+    }
+
+    /**
+     * Dump the diagnostics! Behavior can be controlled via [args].
+     */
+    @Synchronized
+    fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
+        Trace.beginSection("DumpManager#dump()")
+        val start = SystemClock.uptimeMillis()
+
+        val parsedArgs = try {
+            parseArgs(args)
+        } catch (e: ArgParseException) {
+            pw.println(e.message)
+            return
+        }
+
+        when (parsedArgs.dumpPriority) {
+            PRIORITY_ARG_CRITICAL -> dumpCriticalLocked(fd, pw, parsedArgs)
+            PRIORITY_ARG_NORMAL -> dumpNormalLocked(pw, parsedArgs)
+            else -> dumpParameterizedLocked(fd, pw, parsedArgs)
+        }
+
+        pw.println()
+        pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
+        Trace.endSection()
+    }
+
+    private fun dumpCriticalLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+        dumpDumpablesLocked(fd, pw, args)
+        dumpConfig(pw)
+    }
+
+    private fun dumpNormalLocked(pw: PrintWriter, args: ParsedArgs) {
+        dumpBuffersLocked(pw, args)
+    }
+
+    private fun dumpParameterizedLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+        when (args.command) {
+            "bugreport-critical" -> dumpCriticalLocked(fd, pw, args)
+            "bugreport-normal" -> dumpNormalLocked(pw, args)
+            "dumpables" -> dumpDumpablesLocked(fd, pw, args)
+            "buffers" -> dumpBuffersLocked(pw, args)
+            else -> dumpTargetsLocked(args.nonFlagArgs, fd, pw, args)
+        }
+    }
+
+    private fun dumpTargetsLocked(
+        targets: List<String>,
+        fd: FileDescriptor,
+        pw: PrintWriter,
+        args: ParsedArgs
+    ) {
+        if (targets.isEmpty()) {
+            pw.println("Nothing to dump :(")
+        } else {
+            for (target in targets) {
+                dumpTarget(target, fd, pw, args)
+            }
+        }
+    }
+
+    private fun dumpTarget(
+        target: String,
+        fd: FileDescriptor,
+        pw: PrintWriter,
+        args: ParsedArgs
+    ) {
+        if (target == "config") {
+            dumpConfig(pw)
+            return
+        }
+
+        for (dumpable in dumpables.values) {
+            if (dumpable.name.endsWith(target)) {
+                dumpDumpable(dumpable, fd, pw, args)
+                return
+            }
+        }
+
+        for (buffer in buffers.values) {
+            if (buffer.name.endsWith(target)) {
+                dumpBuffer(buffer, pw, args)
+                return
+            }
+        }
+    }
+
+    private fun dumpDumpablesLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+        for (module in dumpables.values) {
+            dumpDumpable(module, fd, pw, args)
+        }
+    }
+
+    private fun dumpBuffersLocked(pw: PrintWriter, args: ParsedArgs) {
+        for (buffer in buffers.values) {
+            dumpBuffer(buffer, pw, args)
+        }
+    }
+
+    private fun dumpDumpable(
+        dumpable: RegisteredDumpable<Dumpable>,
+        fd: FileDescriptor,
+        pw: PrintWriter,
+        args: ParsedArgs
+    ) {
+        pw.println()
+        pw.println("${dumpable.name}:")
+        pw.println("----------------------------------------------------------------------------")
+        dumpable.dumpable.dump(fd, pw, args.rawArgs)
+    }
+
+    private fun dumpBuffer(
+        buffer: RegisteredDumpable<LogBuffer>,
+        pw: PrintWriter,
+        args: ParsedArgs
+    ) {
+        pw.println()
+        pw.println()
+        pw.println("BUFFER ${buffer.name}:")
+        pw.println("============================================================================")
+        buffer.dumpable.dump(pw, args.tailLength)
+    }
+
+    private fun dumpConfig(pw: PrintWriter) {
+        pw.println("SystemUiServiceComponents configuration:")
+        pw.print("vendor component: ")
+        pw.println(context.resources.getString(R.string.config_systemUIVendorServiceComponent))
+        dumpServiceList(pw, "global", R.array.config_systemUIServiceComponents)
+        dumpServiceList(pw, "per-user", R.array.config_systemUIServiceComponentsPerUser)
+    }
+
+    private fun dumpServiceList(pw: PrintWriter, type: String, resId: Int) {
+        val services: Array<String>? = context.resources.getStringArray(resId)
+        pw.print(type)
+        pw.print(": ")
+        if (services == null) {
+            pw.println("N/A")
+            return
+        }
+        pw.print(services.size)
+        pw.println(" services")
+        for (i in services.indices) {
+            pw.print("  ")
+            pw.print(i)
+            pw.print(": ")
+            pw.println(services[i])
+        }
+    }
+
+    private fun parseArgs(args: Array<String>): ParsedArgs {
+        val mutArgs = args.toMutableList()
+        val pArgs = ParsedArgs(args, mutArgs)
+
+        val iterator = mutArgs.iterator()
+        while (iterator.hasNext()) {
+            val arg = iterator.next()
+            if (arg.startsWith("-")) {
+                iterator.remove()
+                when (arg) {
+                    PRIORITY_ARG -> {
+                        pArgs.dumpPriority = readArgument(iterator, PRIORITY_ARG) {
+                            if (PRIORITY_OPTIONS.contains(it)) {
+                                it
+                            } else {
+                                throw IllegalArgumentException()
+                            }
+                        }
+                    }
+                    "-t", "--tail" -> {
+                        pArgs.tailLength = readArgument(iterator, "--tail") {
+                            it.toInt()
+                        }
+                    }
+                    else -> {
+                        throw ArgParseException("Unknown flag: $arg")
+                    }
+                }
+            }
+        }
+
+        if (mutArgs.isNotEmpty() && COMMANDS.contains(mutArgs[0])) {
+            pArgs.command = mutArgs.removeAt(0)
+        }
+
+        return pArgs
+    }
+
+    private fun <T> readArgument(
+        iterator: MutableIterator<String>,
+        flag: String,
+        parser: (arg: String) -> T
+    ): T {
+        if (!iterator.hasNext()) {
+            throw ArgParseException("Missing argument for $flag")
+        }
+        val value = iterator.next()
+
+        return try {
+            parser(value).also { iterator.remove() }
+        } catch (e: Exception) {
+            throw ArgParseException("Invalid argument '$value' for flag $flag")
+        }
+    }
+
+    private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
+        val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
+        return existingDumpable == null || newDumpable == existingDumpable
+    }
+
+    companion object {
+        const val PRIORITY_ARG = "--dump-priority"
+        const val PRIORITY_ARG_CRITICAL = "CRITICAL"
+        const val PRIORITY_ARG_HIGH = "HIGH"
+        const val PRIORITY_ARG_NORMAL = "NORMAL"
+    }
+}
+
+private val PRIORITY_OPTIONS =
+        arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)
+
+private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")
+
+private val RESERVED_NAMES = arrayOf("config", *COMMANDS)
+
+private data class RegisteredDumpable<T>(
+    val name: String,
+    val dumpable: T
+)
+
+private class ParsedArgs(
+    val rawArgs: Array<String>,
+    val nonFlagArgs: List<String>
+) {
+    var dumpPriority: String? = null
+    var tailLength: Int = 0
+    var command: String? = null
+}
+
+class ArgParseException(message: String) : Exception(message)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
index 18c7bae..7defef9 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.log
 
 import android.util.Log
-import com.android.systemui.DumpController
-import com.android.systemui.Dumpable
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.dagger.LogModule
+import java.io.PrintWriter
 import java.text.SimpleDateFormat
 import java.util.ArrayDeque
 import java.util.Locale
@@ -35,11 +35,10 @@
  * You can dump the entire buffer at any time by running:
  *
  * ```
- * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService \
- *      dependency DumpController <bufferName>
+ * $ adb shell dumpsys activity service com.android.systemui/.SystemUIService <bufferName>
  * ```
  *
- * where `bufferName` is the (case-sensitive) [name] passed to the constructor.
+ * ...where `bufferName` is the (case-sensitive) [name] passed to the constructor.
  *
  * By default, only messages of WARN level or higher are echoed to logcat, but this can be adjusted
  * locally (usually for debugging purposes).
@@ -75,8 +74,8 @@
 ) {
     private val buffer: ArrayDeque<LogMessageImpl> = ArrayDeque()
 
-    fun attach(dumpController: DumpController) {
-        dumpController.registerDumpable(name, onDump)
+    fun attach(dumpManager: DumpManager) {
+        dumpManager.registerBuffer(name, this)
     }
 
     /**
@@ -174,22 +173,26 @@
     }
 
     /** Converts the entire buffer to a newline-delimited string */
-    fun dump(): String {
+    fun dump(pw: PrintWriter, tailLength: Int) {
         synchronized(buffer) {
-            val sb = StringBuilder()
-            for (message in buffer) {
-                dumpMessage(message, sb)
+            val start = if (tailLength <= 0) { 0 } else { buffer.size - tailLength }
+
+            for ((i, message) in buffer.withIndex()) {
+                if (i >= start) {
+                    dumpMessage(message, pw)
+                }
             }
-            return sb.toString()
         }
     }
 
-    private fun dumpMessage(message: LogMessage, sb: StringBuilder) {
-        sb.append(DATE_FORMAT.format(message.timestamp))
-                .append(" ").append(message.level)
-                .append(" ").append(message.tag)
-                .append(" ").append(message.printer(message))
-                .append("\n")
+    private fun dumpMessage(message: LogMessage, pw: PrintWriter) {
+        pw.print(DATE_FORMAT.format(message.timestamp))
+        pw.print(" ")
+        pw.print(message.level)
+        pw.print(" ")
+        pw.print(message.tag)
+        pw.print(" ")
+        pw.println(message.printer(message))
     }
 
     private fun echoToLogcat(message: LogMessage) {
@@ -203,10 +206,6 @@
             LogLevel.WTF -> Log.wtf(message.tag, strMessage)
         }
     }
-
-    private val onDump = Dumpable { _, pw, _ ->
-        pw.println(dump())
-    }
 }
 
 private const val TAG = "LogBuffer"
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 4a7469c..b1d972e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -20,8 +20,8 @@
 import android.os.Build;
 import android.os.Looper;
 
-import com.android.systemui.DumpController;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.LogcatEchoTracker;
 import com.android.systemui.log.LogcatEchoTrackerDebug;
@@ -43,9 +43,9 @@
     @DozeLog
     public static LogBuffer provideDozeLogBuffer(
             LogcatEchoTracker bufferFilter,
-            DumpController dumpController) {
+            DumpManager dumpManager) {
         LogBuffer buffer = new LogBuffer("DozeLog", 100, 10, bufferFilter);
-        buffer.attach(dumpController);
+        buffer.attach(dumpManager);
         return buffer;
     }
 
@@ -55,9 +55,9 @@
     @NotificationLog
     public static LogBuffer provideNotificationsLogBuffer(
             LogcatEchoTracker bufferFilter,
-            DumpController dumpController) {
+            DumpManager dumpManager) {
         LogBuffer buffer = new LogBuffer("NotifLog2", 1000, 10, bufferFilter);
-        buffer.attach(dumpController);
+        buffer.attach(dumpManager);
         return buffer;
     }
 
@@ -67,9 +67,9 @@
     @QSLog
     public static LogBuffer provideQuickSettingsLogBuffer(
             LogcatEchoTracker bufferFilter,
-            DumpController dumpController) {
+            DumpManager dumpManager) {
         LogBuffer buffer = new LogBuffer("QSLog", 500, 10, bufferFilter);
-        buffer.attach(dumpController);
+        buffer.attach(dumpManager);
         return buffer;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
index bf2d4cd..475ddc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java
@@ -15,29 +15,20 @@
 package com.android.systemui;
 
 import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
 import android.os.Looper;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.systemui.Dependency.DependencyKey;
 import com.android.systemui.statusbar.policy.FlashlightController;
 
 import org.junit.Assert;
 import org.junit.Test;
 
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
 @SmallTest
 public class DependencyTest extends SysuiTestCase {
 
-    public static final DependencyKey<Dumpable> DUMPABLE = new DependencyKey<>("dumpable");
-
     @Test
     public void testClassDependency() {
         FlashlightController f = mock(FlashlightController.class);
@@ -53,17 +44,6 @@
     }
 
     @Test
-    public void testDump() {
-        Dumpable d = mock(Dumpable.class);
-        String[] args = new String[0];
-        FileDescriptor fd = mock(FileDescriptor.class);
-        mDependency.injectTestDependency(DUMPABLE, d);
-        Dependency.get(DUMPABLE);
-        mDependency.dump(fd, mock(PrintWriter.class), args);
-        verify(d).dump(eq(fd), any(), eq(args));
-    }
-
-    @Test
     public void testInitDependency() {
         Dependency.clearDependencies();
         Dependency dependency = new Dependency();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java b/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
index b3071f9..a7f4fa5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
@@ -60,6 +60,11 @@
         return super.createDependency(key);
     }
 
+    @Override
+    protected boolean autoRegisterModulesForDump() {
+        return false;
+    }
+
     public <T> boolean hasInstantiatedDependency(Class<T> key) {
         return mObjs.containsKey(key) || mInstantiatedObjects.contains(key);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt
index 22b1837..3357c58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt
@@ -27,6 +27,7 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
 import junit.framework.Assert.assertSame
@@ -94,6 +95,7 @@
                 mockContext,
                 Handler(testableLooper.looper),
                 testableLooper.looper,
+                mock(DumpManager::class.java),
                 mapOf(0 to mockUBRUser0, 1 to mockUBRUser1))
 
         // These should be valid filters
@@ -236,8 +238,9 @@
         context: Context,
         mainHandler: Handler,
         bgLooper: Looper,
+        dumpManager: DumpManager,
         var mockUBRMap: Map<Int, UserBroadcastDispatcher>
-    ) : BroadcastDispatcher(context, mainHandler, bgLooper) {
+    ) : BroadcastDispatcher(context, mainHandler, bgLooper, dumpManager) {
         override fun createUBRForUser(userId: Int): UserBroadcastDispatcher {
             return mockUBRMap.getOrDefault(userId, mock(UserBroadcastDispatcher::class.java))
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt
new file mode 100644
index 0000000..8d530ec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.dump
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.Dumpable
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.util.mockito.any
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.io.FileDescriptor
+import java.io.PrintWriter
+
+@SmallTest
+class DumpManagerTest : SysuiTestCase() {
+
+    private lateinit var dumpManager: DumpManager
+
+    @Mock
+    private lateinit var fd: FileDescriptor
+    @Mock
+    private lateinit var pw: PrintWriter
+
+    @Mock
+    private lateinit var dumpable1: Dumpable
+    @Mock
+    private lateinit var dumpable2: Dumpable
+    @Mock
+    private lateinit var dumpable3: Dumpable
+
+    @Mock
+    private lateinit var buffer1: LogBuffer
+    @Mock
+    private lateinit var buffer2: LogBuffer
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        dumpManager = DumpManager(mContext)
+    }
+
+    @Test
+    fun testDumpablesCanBeDumpedSelectively() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerDumpable("dumpable1", dumpable1)
+        dumpManager.registerDumpable("dumpable2", dumpable2)
+        dumpManager.registerDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+
+        // WHEN some of them are dumped explicitly
+        val args = arrayOf("dumpable1", "dumpable3", "buffer2")
+        dumpManager.dump(fd, pw, args)
+
+        // THEN only the requested ones have their dump() method called
+        verify(dumpable1).dump(fd, pw, args)
+        verify(dumpable2, never()).dump(
+                any(FileDescriptor::class.java),
+                any(PrintWriter::class.java),
+                any(Array<String>::class.java))
+        verify(dumpable3).dump(fd, pw, args)
+        verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt())
+        verify(buffer2).dump(pw, 0)
+    }
+
+    @Test
+    fun testDumpableMatchingIsBasedOnEndOfTag() {
+        // GIVEN a dumpable registered to the manager
+        dumpManager.registerDumpable("com.android.foo.bar.dumpable1", dumpable1)
+
+        // WHEN that module is dumped
+        val args = arrayOf("dumpable1")
+        dumpManager.dump(fd, pw, args)
+
+        // THEN its dump() method is called
+        verify(dumpable1).dump(fd, pw, args)
+    }
+
+    @Test
+    fun testCriticalDump() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerDumpable("dumpable1", dumpable1)
+        dumpManager.registerDumpable("dumpable2", dumpable2)
+        dumpManager.registerDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+
+        // WHEN a critical dump is requested
+        val args = arrayOf("--dump-priority", "CRITICAL")
+        dumpManager.dump(fd, pw, args)
+
+        // THEN all modules are dumped (but no buffers)
+        verify(dumpable1).dump(fd, pw, args)
+        verify(dumpable2).dump(fd, pw, args)
+        verify(dumpable3).dump(fd, pw, args)
+        verify(buffer1, never()).dump(any(PrintWriter::class.java), anyInt())
+        verify(buffer2, never()).dump(any(PrintWriter::class.java), anyInt())
+    }
+
+    @Test
+    fun testNormalDump() {
+        // GIVEN a variety of registered dumpables and buffers
+        dumpManager.registerDumpable("dumpable1", dumpable1)
+        dumpManager.registerDumpable("dumpable2", dumpable2)
+        dumpManager.registerDumpable("dumpable3", dumpable3)
+        dumpManager.registerBuffer("buffer1", buffer1)
+        dumpManager.registerBuffer("buffer2", buffer2)
+
+        // WHEN a critical dump is requested
+        val args = arrayOf("--dump-priority", "NORMAL")
+        dumpManager.dump(fd, pw, args)
+
+        // THEN all buffers are dumped (but no modules)
+        verify(dumpable1, never()).dump(
+                any(FileDescriptor::class.java),
+                any(PrintWriter::class.java),
+                any(Array<String>::class.java))
+        verify(dumpable2, never()).dump(
+                any(FileDescriptor::class.java),
+                any(PrintWriter::class.java),
+                any(Array<String>::class.java))
+        verify(dumpable3, never()).dump(
+                any(FileDescriptor::class.java),
+                any(PrintWriter::class.java),
+                any(Array<String>::class.java))
+        verify(buffer1).dump(pw, 0)
+        verify(buffer2).dump(pw, 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
new file mode 100644
index 0000000..3f095c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.util.mockito
+
+/**
+ * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects
+ * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not
+ * be null"). To fix this, we can use methods that modify the return type to be nullable. This
+ * causes Kotlin to skip the null checks.
+ */
+
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+
+/**
+ * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+/**
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
+ * when null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
+        ArgumentCaptor.forClass(T::class.java)