diff options
15 files changed, 683 insertions, 156 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 69bc2596d411..362a9e8306c3 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.colorextraction.SysuiColorExtractor; 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.DisplayController; 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 @@ public class Dependency { 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 @@ public class Dependency { 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 @@ public class Dependency { 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 @@ public class Dependency { 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 @@ public class Dependency { 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 f795faf30603..e880cc8fd10a 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.Context; 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 @@ public abstract class SystemUI { 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 13151527cd5f..cbdae4e6fe63 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -33,6 +33,7 @@ import android.util.TimingsTraceLog; 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 @@ public class SystemUIApplication extends Application implements } } + 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 @@ public class SystemUIApplication extends Application implements 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 2d2d91db4fe1..f4ec6f75b06b 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.app.Service; 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 class SystemUISecondaryUserService extends Service { 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 41d83148e093..e65fccd9f132 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 android.util.Slog; 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 @@ import javax.inject.Inject; 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 @@ public class SystemUIService extends Service { @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); - } - } - - 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); - } - } + // 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}; } - } - - 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 cedf7c354ccc..bd803fa76f13 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.internal.annotations.VisibleForTesting 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 @@ private const val DEBUG = true 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 @@ open class BroadcastDispatcher @Inject constructor ( */ @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 12b9be11817a..18c3eacbc693 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.Dependency; 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 @@ public interface SystemUIRootComponent { @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 000000000000..59a7a328e9ae --- /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 18c7baec1f74..7defef90380f 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 @@ import java.util.Locale * 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 @@ class LogBuffer( ) { 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 @@ class LogBuffer( } /** 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 @@ class LogBuffer( 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 4a7469c1bd2d..b1d972e6d97f 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.content.ContentResolver; 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 @@ public class LogModule { @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 @@ public class LogModule { @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 @@ public class LogModule { @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 bf2d4cd07165..475ddc1ea11a 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 @@ public class DependencyTest extends SysuiTestCase { } @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 b3071f957fdb..a7f4fa5768b4 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 @@ public class TestableDependency extends Dependency { 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 22b18373e81d..3357c5863d46 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.test.suitebuilder.annotation.SmallTest 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 @@ class BroadcastDispatcherTest : SysuiTestCase() { 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 @@ class BroadcastDispatcherTest : SysuiTestCase() { 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 000000000000..8d530ec0ef0a --- /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 000000000000..3f095c7900f3 --- /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) |