summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Burns <pixel@google.com>2020-02-12 21:38:50 -0500
committerNed Burns <pixel@google.com>2020-02-20 22:11:19 -0500
commitc7cfa69ffde55126ed3e7648d1f6af651b88a06c (patch)
treeb32a31681999a756e58b6d41bfb88f335e4b5c29
parentb909557790eceab62e08d8f180fb28bb5efbe018 (diff)
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
-rw-r--r--packages/SystemUI/src/com/android/systemui/Dependency.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUI.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIApplication.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUISecondaryUserService.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIService.java74
-rw-r--r--packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIRootComponent.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt367
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/DependencyTest.java20
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/broadcast/BroadcastDispatcherTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt150
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt59
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)