summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIService.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt311
-rw-r--r--packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt277
-rw-r--r--packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt150
-rw-r--r--packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/io/Files.java58
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt)23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt169
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/io/FakeBasicFileAttributes.java126
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java13
15 files changed, 935 insertions, 270 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index d0080886ae7a..f1cb66784263 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -28,7 +28,7 @@ import android.util.Slog;
import com.android.internal.os.BinderInternal;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.SystemUIAuxiliaryDumpService;
import java.io.FileDescriptor;
@@ -39,15 +39,15 @@ import javax.inject.Inject;
public class SystemUIService extends Service {
private final Handler mMainHandler;
- private final DumpManager mDumpManager;
+ private final DumpHandler mDumpHandler;
@Inject
public SystemUIService(
@Main Handler mainHandler,
- DumpManager dumpManager) {
+ DumpHandler dumpHandler) {
super();
mMainHandler = mainHandler;
- mDumpManager = dumpManager;
+ mDumpHandler = dumpHandler;
}
@Override
@@ -94,10 +94,10 @@ public class SystemUIService extends Service {
String[] massagedArgs = args;
if (args.length == 0) {
massagedArgs = new String[] {
- DumpManager.PRIORITY_ARG,
- DumpManager.PRIORITY_ARG_CRITICAL};
+ DumpHandler.PRIORITY_ARG,
+ DumpHandler.PRIORITY_ARG_CRITICAL};
}
- mDumpManager.dump(fd, pw, massagedArgs);
+ mDumpHandler.dump(fd, pw, massagedArgs);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
new file mode 100644
index 000000000000..fa951fa09ef6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
@@ -0,0 +1,311 @@
+/*
+ * 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 com.android.systemui.R
+import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
+import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
+import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
+import com.android.systemui.log.LogBuffer
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Oversees SystemUI's output during bug reports (and dumpsys in general)
+ *
+ * Dump output is split into two sections, CRITICAL and NORMAL. In general, the CRITICAL section
+ * contains all dumpables that were registered to the [DumpManager], while the NORMAL sections
+ * contains all [LogBuffer]s (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
+ *
+ * # And if you need to be reminded of this list of commands:
+ * $ <invocation> -h
+ * $ <invocation> --help
+ * ```
+ */
+class DumpHandler @Inject constructor(
+ private val context: Context,
+ private val dumpManager: DumpManager,
+ private val logBufferEulogizer: LogBufferEulogizer
+) {
+ /**
+ * Dump the diagnostics! Behavior can be controlled via [args].
+ */
+ 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 -> dumpCritical(fd, pw, parsedArgs)
+ PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs)
+ else -> dumpParameterized(fd, pw, parsedArgs)
+ }
+
+ pw.println()
+ pw.println("Dump took ${SystemClock.uptimeMillis() - start}ms")
+ Trace.endSection()
+ }
+
+ private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+ when (args.command) {
+ "bugreport-critical" -> dumpCritical(fd, pw, args)
+ "bugreport-normal" -> dumpNormal(pw, args)
+ "dumpables" -> dumpDumpables(fd, pw, args)
+ "buffers" -> dumpBuffers(pw, args)
+ "config" -> dumpConfig(pw)
+ "help" -> dumpHelp(pw)
+ else -> dumpTargets(args.nonFlagArgs, fd, pw, args)
+ }
+ }
+
+ private fun dumpCritical(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+ dumpManager.dumpDumpables(fd, pw, args.rawArgs)
+ dumpConfig(pw)
+ }
+
+ private fun dumpNormal(pw: PrintWriter, args: ParsedArgs) {
+ dumpManager.dumpBuffers(pw, args.tailLength)
+ logBufferEulogizer.readEulogyIfPresent(pw)
+ }
+
+ private fun dumpDumpables(fw: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+ if (args.listOnly) {
+ dumpManager.listDumpables(pw)
+ } else {
+ dumpManager.dumpDumpables(fw, pw, args.rawArgs)
+ }
+ }
+
+ private fun dumpBuffers(pw: PrintWriter, args: ParsedArgs) {
+ if (args.listOnly) {
+ dumpManager.listBuffers(pw)
+ } else {
+ dumpManager.dumpBuffers(pw, args.tailLength)
+ }
+ }
+
+ private fun dumpTargets(
+ targets: List<String>,
+ fd: FileDescriptor,
+ pw: PrintWriter,
+ args: ParsedArgs
+ ) {
+ if (targets.isNotEmpty()) {
+ for (target in targets) {
+ dumpManager.dumpTarget(target, fd, pw, args.rawArgs, args.tailLength)
+ }
+ } else {
+ if (args.listOnly) {
+ pw.println("Dumpables:")
+ dumpManager.listDumpables(pw)
+ pw.println()
+
+ pw.println("Buffers:")
+ dumpManager.listBuffers(pw)
+ } else {
+ pw.println("Nothing to dump :(")
+ }
+ }
+ }
+
+ 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 dumpHelp(pw: PrintWriter) {
+ pw.println("Let <invocation> be:")
+ pw.println("$ adb shell dumpsys activity service com.android.systemui/.SystemUIService")
+ pw.println()
+
+ pw.println("Most common usage:")
+ pw.println("$ <invocation> <targets>")
+ pw.println("$ <invocation> NotifLog")
+ pw.println("$ <invocation> StatusBar FalsingManager BootCompleteCacheImpl")
+ pw.println("etc.")
+ pw.println()
+
+ pw.println("Special commands:")
+ pw.println("$ <invocation> dumpables")
+ pw.println("$ <invocation> buffers")
+ pw.println("$ <invocation> bugreport-critical")
+ pw.println("$ <invocation> bugreport-normal")
+ pw.println()
+
+ pw.println("Targets can be listed:")
+ pw.println("$ <invocation> --list")
+ pw.println("$ <invocation> dumpables --list")
+ pw.println("$ <invocation> buffers --list")
+ pw.println()
+
+ pw.println("Show only the most recent N lines of buffers")
+ pw.println("$ <invocation> NotifLog --tail 30")
+ }
+
+ 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, arg) {
+ it.toInt()
+ }
+ }
+ "-l", "--list" -> {
+ pArgs.listOnly = true
+ }
+ "-h", "--help" -> {
+ pArgs.command = "help"
+ }
+ else -> {
+ throw ArgParseException("Unknown flag: $arg")
+ }
+ }
+ }
+ }
+
+ if (pArgs.command == null && 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")
+ }
+ }
+
+ 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 class ParsedArgs(
+ val rawArgs: Array<String>,
+ val nonFlagArgs: List<String>
+) {
+ var dumpPriority: String? = null
+ var tailLength: Int = 0
+ var command: String? = null
+ var listOnly = false
+}
+
+class ArgParseException(message: String) : Exception(message)
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index 59a7a328e9ae..a4141b1b7cf0 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -16,15 +16,8 @@
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
@@ -32,58 +25,16 @@ import javax.inject.Inject
import javax.inject.Singleton
/**
- * Oversees SystemUI's output during bug reports (and dumpsys in general)
+ * Maintains a registry of things that should be dumped when a bug report is taken
*
* 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
- * ```
+ * See [DumpHandler] for more information on how and when this information is dumped.
*/
@Singleton
-class DumpManager @Inject constructor(
- private val context: Context
-) {
+class DumpManager @Inject constructor() {
private val dumpables: MutableMap<String, RegisteredDumpable<Dumpable>> = ArrayMap()
private val buffers: MutableMap<String, RegisteredDumpable<LogBuffer>> = ArrayMap()
@@ -97,10 +48,6 @@ class DumpManager @Inject constructor(
*/
@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")
}
@@ -128,76 +75,16 @@ class DumpManager @Inject constructor(
}
/**
- * Dump the diagnostics! Behavior can be controlled via [args].
+ * Dumps the first dumpable or buffer whose registered name ends with [target]
*/
@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(
+ fun dumpTarget(
target: String,
fd: FileDescriptor,
pw: PrintWriter,
- args: ParsedArgs
+ args: Array<String>,
+ tailLength: Int
) {
- if (target == "config") {
- dumpConfig(pw)
- return
- }
-
for (dumpable in dumpables.values) {
if (dumpable.name.endsWith(target)) {
dumpDumpable(dumpable, fd, pw, args)
@@ -207,21 +94,49 @@ class DumpManager @Inject constructor(
for (buffer in buffers.values) {
if (buffer.name.endsWith(target)) {
- dumpBuffer(buffer, pw, args)
+ dumpBuffer(buffer, pw, tailLength)
return
}
}
}
- private fun dumpDumpablesLocked(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
+ /**
+ * Dumps all registered dumpables to [pw]
+ */
+ @Synchronized
+ fun dumpDumpables(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
for (module in dumpables.values) {
dumpDumpable(module, fd, pw, args)
}
}
- private fun dumpBuffersLocked(pw: PrintWriter, args: ParsedArgs) {
+ /**
+ * Dumps the names of all registered dumpables (one per line)
+ */
+ @Synchronized
+ fun listDumpables(pw: PrintWriter) {
+ for (module in dumpables.values) {
+ pw.println(module.name)
+ }
+ }
+
+ /**
+ * Dumps all registered [LogBuffer]s to [pw]
+ */
+ @Synchronized
+ fun dumpBuffers(pw: PrintWriter, tailLength: Int) {
for (buffer in buffers.values) {
- dumpBuffer(buffer, pw, args)
+ dumpBuffer(buffer, pw, tailLength)
+ }
+ }
+
+ /**
+ * Dumps the names of all registered buffers (one per line)
+ */
+ @Synchronized
+ fun listBuffers(pw: PrintWriter) {
+ for (buffer in buffers.values) {
+ pw.println(buffer.name)
}
}
@@ -229,139 +144,33 @@ class DumpManager @Inject constructor(
dumpable: RegisteredDumpable<Dumpable>,
fd: FileDescriptor,
pw: PrintWriter,
- args: ParsedArgs
+ args: Array<String>
) {
pw.println()
pw.println("${dumpable.name}:")
pw.println("----------------------------------------------------------------------------")
- dumpable.dumpable.dump(fd, pw, args.rawArgs)
+ dumpable.dumpable.dump(fd, pw, args)
}
private fun dumpBuffer(
buffer: RegisteredDumpable<LogBuffer>,
pw: PrintWriter,
- args: ParsedArgs
+ tailLength: Int
) {
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")
- }
+ buffer.dumpable.dump(pw, tailLength)
}
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/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
new file mode 100644
index 000000000000..603cb672175d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.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 android.content.Context
+import android.util.Log
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.util.io.Files
+import com.android.systemui.util.time.SystemClock
+import java.io.IOException
+import java.io.PrintWriter
+import java.io.UncheckedIOException
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.StandardOpenOption.CREATE
+import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING
+import java.nio.file.attribute.BasicFileAttributes
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Dumps all [LogBuffer]s to a file
+ *
+ * Intended for emergencies, i.e. we're about to crash. This file can then be read at a later date
+ * (usually in a bug report).
+ */
+@Singleton
+class LogBufferEulogizer(
+ private val dumpManager: DumpManager,
+ private val systemClock: SystemClock,
+ private val files: Files,
+ private val logPath: Path,
+ private val minWriteGap: Long,
+ private val maxLogAgeToDump: Long
+) {
+ @Inject constructor(
+ context: Context,
+ dumpManager: DumpManager,
+ systemClock: SystemClock,
+ files: Files
+ ) : this(
+ dumpManager,
+ systemClock,
+ files,
+ Paths.get(context.filesDir.toPath().toString(), "log_buffers.txt"),
+ MIN_WRITE_GAP,
+ MAX_AGE_TO_DUMP
+ )
+
+ /**
+ * Dumps all active log buffers to a file
+ *
+ * The file will be prefaced by the [reason], which will then be returned (presumably so it can
+ * be thrown).
+ */
+ fun <T : Exception> record(reason: T): T {
+ val start = systemClock.uptimeMillis()
+ var duration = 0L
+
+ Log.i(TAG, "Performing emergency dump of log buffers")
+
+ val millisSinceLastWrite = getMillisSinceLastWrite(logPath)
+ if (millisSinceLastWrite < minWriteGap) {
+ Log.w(TAG, "Cannot dump logs, last write was only $millisSinceLastWrite ms ago")
+ return reason
+ }
+
+ try {
+ val writer = files.newBufferedWriter(logPath, CREATE, TRUNCATE_EXISTING)
+ writer.use { out ->
+ val pw = PrintWriter(out)
+
+ pw.println(DATE_FORMAT.format(systemClock.currentTimeMillis()))
+ pw.println()
+ pw.println("Dump triggered by exception:")
+ reason.printStackTrace(pw)
+ dumpManager.dumpBuffers(pw, 0)
+ duration = systemClock.uptimeMillis() - start
+ pw.println()
+ pw.println("Buffer eulogy took ${duration}ms")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception while attempting to dump buffers, bailing", e)
+ }
+
+ Log.i(TAG, "Buffer eulogy took ${duration}ms")
+
+ return reason
+ }
+
+ /**
+ * If a eulogy file is present, writes its contents to [pw].
+ */
+ fun readEulogyIfPresent(pw: PrintWriter) {
+ try {
+ val millisSinceLastWrite = getMillisSinceLastWrite(logPath)
+ if (millisSinceLastWrite > maxLogAgeToDump) {
+ Log.i(TAG, "Not eulogizing buffers; they are " +
+ TimeUnit.HOURS.convert(millisSinceLastWrite, TimeUnit.MILLISECONDS) +
+ " hours old")
+ return
+ }
+
+ files.lines(logPath).use { s ->
+ pw.println()
+ pw.println()
+ pw.println("=============== BUFFERS FROM MOST RECENT CRASH ===============")
+ s.forEach { line ->
+ pw.println(line)
+ }
+ }
+ } catch (e: IOException) {
+ // File doesn't exist, okay
+ } catch (e: UncheckedIOException) {
+ Log.e(TAG, "UncheckedIOException while dumping the core", e)
+ }
+ }
+
+ private fun getMillisSinceLastWrite(path: Path): Long {
+ val stats = try {
+ files.readAttributes(path, BasicFileAttributes::class.java)
+ } catch (e: IOException) {
+ // File doesn't exist
+ null
+ }
+ return systemClock.currentTimeMillis() - (stats?.lastModifiedTime()?.toMillis() ?: 0)
+ }
+}
+
+private const val TAG = "BufferEulogizer"
+private val MIN_WRITE_GAP = TimeUnit.MINUTES.toMillis(5)
+private val MAX_AGE_TO_DUMP = TimeUnit.HOURS.toMillis(48)
+private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
index 431cd6360b0b..da983ab03a1d 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
+++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
@@ -35,11 +35,11 @@ import javax.inject.Inject;
* all other services.
*/
public class SystemUIAuxiliaryDumpService extends Service {
- private final DumpManager mDumpManager;
+ private final DumpHandler mDumpHandler;
@Inject
- public SystemUIAuxiliaryDumpService(DumpManager dumpManager) {
- mDumpManager = dumpManager;
+ public SystemUIAuxiliaryDumpService(DumpHandler dumpHandler) {
+ mDumpHandler = dumpHandler;
}
@Override
@@ -50,9 +50,9 @@ public class SystemUIAuxiliaryDumpService extends Service {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
// Simulate the NORMAL priority arg being passed to us
- mDumpManager.dump(
+ mDumpHandler.dump(
fd,
pw,
- new String[] { DumpManager.PRIORITY_ARG, DumpManager.PRIORITY_ARG_NORMAL });
+ new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL });
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
index 7defef90380f..342db346e14b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
@@ -209,4 +209,4 @@ class LogBuffer(
}
private const val TAG = "LogBuffer"
-private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.S", Locale.US)
+private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 057683329512..d7365e6db935 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -61,6 +61,7 @@ import androidx.annotation.NonNull;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dumpable;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.LogBufferEulogizer;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
@@ -126,6 +127,7 @@ public class NotifCollection implements Dumpable {
private final IStatusBarService mStatusBarService;
private final FeatureFlags mFeatureFlags;
private final NotifCollectionLogger mLogger;
+ private final LogBufferEulogizer mEulogizer;
private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
private final Collection<NotificationEntry> mReadOnlyNotificationSet =
@@ -146,10 +148,12 @@ public class NotifCollection implements Dumpable {
IStatusBarService statusBarService,
DumpManager dumpManager,
FeatureFlags featureFlags,
- NotifCollectionLogger logger) {
+ NotifCollectionLogger logger,
+ LogBufferEulogizer logBufferEulogizer) {
Assert.isMainThread();
mStatusBarService = statusBarService;
mLogger = logger;
+ mEulogizer = logBufferEulogizer;
dumpManager.registerDumpable(TAG, this);
mFeatureFlags = featureFlags;
}
@@ -223,7 +227,8 @@ public class NotifCollection implements Dumpable {
requireNonNull(stats);
if (entry != mNotificationSet.get(entry.getKey())) {
- throw new IllegalStateException("Invalid entry: " + entry.getKey());
+ throw mEulogizer.record(
+ new IllegalStateException("Invalid entry: " + entry.getKey()));
}
if (entry.getDismissState() == DISMISSED) {
@@ -367,8 +372,11 @@ public class NotifCollection implements Dumpable {
final NotificationEntry entry = mNotificationSet.get(sbn.getKey());
if (entry == null) {
- throw new IllegalStateException("No notification to remove with key " + sbn.getKey());
+ throw mEulogizer.record(
+ new IllegalStateException("No notification to remove with key "
+ + sbn.getKey()));
}
+
entry.mCancellationReason = reason;
tryRemoveNotification(entry);
applyRanking(rankingMap);
@@ -426,12 +434,15 @@ public class NotifCollection implements Dumpable {
*/
private boolean tryRemoveNotification(NotificationEntry entry) {
if (mNotificationSet.get(entry.getKey()) != entry) {
- throw new IllegalStateException("No notification to remove with key " + entry.getKey());
+ throw mEulogizer.record(
+ new IllegalStateException("No notification to remove with key "
+ + entry.getKey()));
}
if (!isCanceled(entry)) {
- throw new IllegalStateException("Cannot remove notification " + entry.getKey()
- + ": has not been marked for removal");
+ throw mEulogizer.record(
+ new IllegalStateException("Cannot remove notification " + entry.getKey()
+ + ": has not been marked for removal"));
}
if (isDismissedByUser(entry)) {
@@ -501,11 +512,11 @@ public class NotifCollection implements Dumpable {
checkForReentrantCall();
if (!entry.mLifetimeExtenders.remove(extender)) {
- throw new IllegalStateException(
+ throw mEulogizer.record(new IllegalStateException(
String.format(
"Cannot end lifetime extension for extender \"%s\" (%s)",
extender.getName(),
- extender));
+ extender)));
}
mLogger.logLifetimeExtensionEnded(
@@ -581,11 +592,11 @@ public class NotifCollection implements Dumpable {
checkForReentrantCall();
if (!entry.mDismissInterceptors.remove(interceptor)) {
- throw new IllegalStateException(
+ throw mEulogizer.record(new IllegalStateException(
String.format(
"Cannot end dismiss interceptor for interceptor \"%s\" (%s)",
interceptor.getName(),
- interceptor));
+ interceptor)));
}
if (!isDismissIntercepted(entry)) {
@@ -608,7 +619,7 @@ public class NotifCollection implements Dumpable {
private void checkForReentrantCall() {
if (mAmDispatchingToOtherCode) {
- throw new IllegalStateException("Reentrant call detected");
+ throw mEulogizer.record(new IllegalStateException("Reentrant call detected"));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/io/Files.java b/packages/SystemUI/src/com/android/systemui/util/io/Files.java
new file mode 100644
index 000000000000..7d633a769600
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/io/Files.java
@@ -0,0 +1,58 @@
+/*
+ * 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.io;
+
+import androidx.annotation.NonNull;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Wrapper around {@link java.nio.file.Files} that can be mocked in tests.
+ */
+@Singleton
+public class Files {
+ @Inject
+ public Files() { }
+
+ /** See {@link java.nio.file.Files#newBufferedWriter} */
+ public BufferedWriter newBufferedWriter(Path path, OpenOption... options) throws IOException {
+ return java.nio.file.Files.newBufferedWriter(path, StandardCharsets.UTF_8, options);
+ }
+
+ /** See {@link java.nio.file.Files#lines} */
+ public Stream<String> lines(Path path) throws IOException {
+ return java.nio.file.Files.lines(path);
+ }
+
+ /** See {@link java.nio.file.Files#readAttributes} */
+ public <A extends BasicFileAttributes> A readAttributes(
+ @NonNull Path path,
+ @NonNull Class<A> type,
+ @NonNull LinkOption... options) throws IOException {
+ return java.nio.file.Files.readAttributes(path, type, options);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java b/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java
index 6fef59f6b995..6f32cc1a156d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java
+++ b/packages/SystemUI/src/com/android/systemui/util/time/SystemClock.java
@@ -37,4 +37,7 @@ public interface SystemClock {
/** @see android.os.SystemClock#currentThreadTimeMillis() */
long currentThreadTimeMillis();
+
+ /** @see System#currentTimeMillis() */
+ long currentTimeMillis();
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java b/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java
index f0c701490f13..4e508cfaed19 100644
--- a/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/time/SystemClockImpl.java
@@ -42,4 +42,9 @@ public class SystemClockImpl implements SystemClock {
public long currentThreadTimeMillis() {
return android.os.SystemClock.currentThreadTimeMillis();
}
+
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
index 8d530ec0ef0a..9e67eda57607 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -32,9 +32,12 @@ import java.io.FileDescriptor
import java.io.PrintWriter
@SmallTest
-class DumpManagerTest : SysuiTestCase() {
+class DumpHandlerTest : SysuiTestCase() {
- private lateinit var dumpManager: DumpManager
+ private lateinit var dumpHandler: DumpHandler
+
+ @Mock
+ private lateinit var logBufferEulogizer: LogBufferEulogizer
@Mock
private lateinit var fd: FileDescriptor
@@ -53,11 +56,13 @@ class DumpManagerTest : SysuiTestCase() {
@Mock
private lateinit var buffer2: LogBuffer
+ private val dumpManager = DumpManager()
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- dumpManager = DumpManager(mContext)
+ dumpHandler = DumpHandler(mContext, dumpManager, logBufferEulogizer)
}
@Test
@@ -71,7 +76,7 @@ class DumpManagerTest : SysuiTestCase() {
// WHEN some of them are dumped explicitly
val args = arrayOf("dumpable1", "dumpable3", "buffer2")
- dumpManager.dump(fd, pw, args)
+ dumpHandler.dump(fd, pw, args)
// THEN only the requested ones have their dump() method called
verify(dumpable1).dump(fd, pw, args)
@@ -91,7 +96,7 @@ class DumpManagerTest : SysuiTestCase() {
// WHEN that module is dumped
val args = arrayOf("dumpable1")
- dumpManager.dump(fd, pw, args)
+ dumpHandler.dump(fd, pw, args)
// THEN its dump() method is called
verify(dumpable1).dump(fd, pw, args)
@@ -108,7 +113,7 @@ class DumpManagerTest : SysuiTestCase() {
// WHEN a critical dump is requested
val args = arrayOf("--dump-priority", "CRITICAL")
- dumpManager.dump(fd, pw, args)
+ dumpHandler.dump(fd, pw, args)
// THEN all modules are dumped (but no buffers)
verify(dumpable1).dump(fd, pw, args)
@@ -127,9 +132,9 @@ class DumpManagerTest : SysuiTestCase() {
dumpManager.registerBuffer("buffer1", buffer1)
dumpManager.registerBuffer("buffer2", buffer2)
- // WHEN a critical dump is requested
+ // WHEN a normal dump is requested
val args = arrayOf("--dump-priority", "NORMAL")
- dumpManager.dump(fd, pw, args)
+ dumpHandler.dump(fd, pw, args)
// THEN all buffers are dumped (but no modules)
verify(dumpable1, never()).dump(
@@ -147,4 +152,4 @@ class DumpManagerTest : SysuiTestCase() {
verify(buffer1).dump(pw, 0)
verify(buffer2).dump(pw, 0)
}
-}
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
new file mode 100644
index 000000000000..cb38846a0514
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.SysuiTestCase
+import com.android.systemui.util.io.FakeBasicFileAttributes
+import com.android.systemui.util.io.Files
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.io.BufferedWriter
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.io.PrintWriter
+import java.nio.file.LinkOption
+import java.nio.file.OpenOption
+import java.nio.file.Paths
+import java.nio.file.attribute.BasicFileAttributes
+import java.util.Arrays
+
+@SmallTest
+class LogEulogizerTest : SysuiTestCase() {
+
+ lateinit var eulogizer: LogBufferEulogizer
+
+ @Mock
+ lateinit var dumpManager: DumpManager
+
+ @Mock
+ lateinit var files: Files
+
+ private val clock = FakeSystemClock()
+
+ private val path = Paths.get("/foo/bar/baz.txt")
+ private val fileAttrs = FakeBasicFileAttributes()
+ private val fileStream = ByteArrayOutputStream()
+ private val fileWriter = BufferedWriter(OutputStreamWriter(fileStream))
+
+ private val dumpStream = ByteArrayOutputStream()
+ private val dumpWriter = PrintWriter(OutputStreamWriter(dumpStream))
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ eulogizer =
+ LogBufferEulogizer(dumpManager, clock, files, path, MIN_WRITE_GAP, MAX_READ_AGE)
+
+ Mockito.`when`(files.newBufferedWriter(eq(path), any(OpenOption::class.java)))
+ .thenReturn(fileWriter)
+
+ Mockito.`when`(
+ files.readAttributes(eq(path),
+ eq(BasicFileAttributes::class.java),
+ any(LinkOption::class.java))
+ ).thenReturn(fileAttrs)
+
+ Mockito.`when`(files.lines(eq(path))).thenReturn(Arrays.stream(FAKE_LINES))
+ }
+
+ @Test
+ fun testFileIsCreated() {
+ // GIVEN that the log file doesn't already exist
+ Mockito.`when`(
+ files.readAttributes(eq(path),
+ eq(BasicFileAttributes::class.java),
+ any(LinkOption::class.java))
+ ).thenThrow(IOException("File not found"))
+
+ // WHEN .record() is called
+ val exception = RuntimeException("Something bad happened")
+ assertEquals(exception, eulogizer.record(exception))
+
+ // THEN the buffers are dumped to the file
+ verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
+ assertTrue(fileStream.toString().isNotEmpty())
+ }
+
+ @Test
+ fun testExistingFileIsOverwritten() {
+ // GIVEN that the log file already exists but hasn't been modified in a while
+ fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MIN_WRITE_GAP - 20)
+
+ // WHEN .record() is called
+ val exception = RuntimeException("Something bad happened")
+ assertEquals(exception, eulogizer.record(exception))
+
+ // THEN the buffers are dumped to the file
+ verify(dumpManager).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
+ assertTrue(fileStream.toString().isNotEmpty())
+ }
+
+ @Test
+ fun testYoungFileIsNotOverwritten() {
+ // GIVEN that the log file has been modified recently
+ fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MIN_WRITE_GAP + 7)
+
+ // WHEN .record() is called
+ val exception = RuntimeException("Something bad happened")
+ assertEquals(exception, eulogizer.record(exception))
+
+ // THEN the file isn't written to
+ verify(dumpManager, never()).dumpBuffers(any(PrintWriter::class.java), Mockito.anyInt())
+ assertTrue(fileStream.toString().isEmpty())
+ }
+
+ @Test
+ fun testRecentFileIsDumped() {
+ // GIVEN that the log file was written to "recently"
+ fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MAX_READ_AGE + 7)
+
+ // WHEN we're asked to eulogize the log
+ eulogizer.readEulogyIfPresent(dumpWriter)
+ dumpWriter.close()
+
+ // THEN the log file is written to the output stream
+ verify(files).lines(eq(path))
+ assertTrue(dumpStream.toString().isNotBlank())
+ }
+
+ @Test
+ fun testOldFileIsNotDumped() {
+ // GIVEN that the log file was written to a long time ago
+ fileAttrs.setLastModifiedTime(clock.currentTimeMillis() - MAX_READ_AGE - 7)
+
+ // WHEN we're asked to eulogize the log
+ eulogizer.readEulogyIfPresent(dumpWriter)
+ dumpWriter.close()
+
+ // THEN the log file is NOT written to the output stream
+ verify(files, never()).lines(eq(path))
+ assertTrue(dumpStream.toString().isEmpty())
+ }
+}
+
+private const val MIN_WRITE_GAP = 10L
+private const val MAX_READ_AGE = 100L
+
+private val FAKE_LINES =
+ arrayOf(
+ "First line",
+ "Second line",
+ "Third line"
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 82de4a3b490c..ca9cc299b36d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -34,6 +34,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -65,6 +66,7 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.LogBufferEulogizer;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
@@ -100,11 +102,13 @@ import java.util.Map;
public class NotifCollectionTest extends SysuiTestCase {
@Mock private IStatusBarService mStatusBarService;
+ @Mock private FeatureFlags mFeatureFlags;
@Mock private NotifCollectionLogger mLogger;
+ @Mock private LogBufferEulogizer mEulogizer;
+
@Mock private GroupCoalescer mGroupCoalescer;
@Spy private RecordingCollectionListener mCollectionListener;
@Mock private CollectionReadyForBuildListener mBuildListener;
- @Mock private FeatureFlags mFeatureFlags;
@Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1");
@Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2");
@@ -136,13 +140,16 @@ public class NotifCollectionTest extends SysuiTestCase {
when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(true);
when(mFeatureFlags.isNewNotifPipelineEnabled()).thenReturn(true);
+ when(mEulogizer.record(any(Exception.class))).thenAnswer(i -> i.getArguments()[0]);
+
mListenerInOrder = inOrder(mCollectionListener);
mCollection = new NotifCollection(
mStatusBarService,
mock(DumpManager.class),
mFeatureFlags,
- mLogger);
+ mLogger,
+ mEulogizer);
mCollection.attach(mGroupCoalescer);
mCollection.addCollectionListener(mCollectionListener);
mCollection.setBuildListener(mBuildListener);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/io/FakeBasicFileAttributes.java b/packages/SystemUI/tests/src/com/android/systemui/util/io/FakeBasicFileAttributes.java
new file mode 100644
index 000000000000..f7a04dcdbbd8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/io/FakeBasicFileAttributes.java
@@ -0,0 +1,126 @@
+/*
+ * 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.io;
+
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fake implementation of {@link BasicFileAttributes} (for use in tests)
+ */
+public class FakeBasicFileAttributes implements BasicFileAttributes {
+ private FileTime mLastModifiedTime = FileTime.from(0, TimeUnit.MILLISECONDS);
+ private FileTime mLastAccessTime = FileTime.from(0, TimeUnit.MILLISECONDS);
+ private FileTime mCreationTime = FileTime.from(0, TimeUnit.MILLISECONDS);
+ private boolean mIsRegularFile = true;
+ private boolean mIsDirectory = false;
+ private boolean mIsSymbolicLink = false;
+ private boolean mIsOther = false;
+ private long mSize = 0;
+ private Object mFileKey = null;
+
+ @Override
+ public FileTime lastModifiedTime() {
+ return mLastModifiedTime;
+ }
+
+ @Override
+ public FileTime lastAccessTime() {
+ return mLastAccessTime;
+ }
+
+ @Override
+ public FileTime creationTime() {
+ return mCreationTime;
+ }
+
+ @Override
+ public boolean isRegularFile() {
+ return mIsRegularFile;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return mIsDirectory;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return mIsSymbolicLink;
+ }
+
+ @Override
+ public boolean isOther() {
+ return mIsOther;
+ }
+
+ @Override
+ public long size() {
+ return mSize;
+ }
+
+ @Override
+ public Object fileKey() {
+ return mFileKey;
+ }
+
+ public FakeBasicFileAttributes setLastModifiedTime(long millis) {
+ mLastModifiedTime = FileTime.from(millis, TimeUnit.MILLISECONDS);
+ return this;
+ }
+
+ public FakeBasicFileAttributes setLastAccessTime(long millis) {
+ mLastAccessTime = FileTime.from(millis, TimeUnit.MILLISECONDS);
+ return this;
+ }
+
+ public FakeBasicFileAttributes setCreationTime(long millis) {
+ mCreationTime = FileTime.from(millis, TimeUnit.MILLISECONDS);
+ return this;
+ }
+
+ public FakeBasicFileAttributes setRegularFile(boolean regularFile) {
+ mIsRegularFile = regularFile;
+ return this;
+ }
+
+ public FakeBasicFileAttributes setDirectory(boolean directory) {
+ mIsDirectory = directory;
+ return this;
+ }
+
+ public FakeBasicFileAttributes setSymbolicLink(boolean symbolicLink) {
+ mIsSymbolicLink = symbolicLink;
+ return this;
+ }
+
+ public FakeBasicFileAttributes setOther(boolean other) {
+ mIsOther = other;
+ return this;
+ }
+
+ public FakeBasicFileAttributes setSize(long size) {
+ mSize = size;
+ return this;
+ }
+
+ public FakeBasicFileAttributes setFileKey(Object fileKey) {
+ mFileKey = fileKey;
+ return this;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java
index 601f88e0a9c6..ecfb357aa068 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java
@@ -36,8 +36,9 @@ public class FakeSystemClock implements SystemClock {
private long mElapsedRealtime = 10000;
private long mCurrentThreadTimeMillis = 10000;
- private final List<ClockTickListener> mListeners = new ArrayList<>();
+ private long mCurrentTimeMillis = 1555555500000L;
+ private final List<ClockTickListener> mListeners = new ArrayList<>();
@Override
public long uptimeMillis() {
return mUptimeMillis;
@@ -58,10 +59,19 @@ public class FakeSystemClock implements SystemClock {
return mCurrentThreadTimeMillis;
}
+ @Override
+ public long currentTimeMillis() {
+ return mCurrentTimeMillis;
+ }
+
public void setUptimeMillis(long uptime) {
advanceTime(uptime - mUptimeMillis);
}
+ public void setCurrentTimeMillis(long millis) {
+ mCurrentTimeMillis = millis;
+ }
+
public void advanceTime(long uptime) {
advanceTime(uptime, 0);
}
@@ -74,6 +84,7 @@ public class FakeSystemClock implements SystemClock {
if (uptime > 0 || sleepTime > 0) {
mUptimeMillis += uptime;
mElapsedRealtime += uptime + sleepTime;
+ mCurrentTimeMillis += uptime + sleepTime;
mCurrentThreadTimeMillis += Math.ceil(uptime * 0.5);