diff options
| author | 2020-05-20 17:30:17 +0000 | |
|---|---|---|
| committer | 2020-05-20 17:30:17 +0000 | |
| commit | 52c8bac7d731463bb9d146c29cf559fde298631d (patch) | |
| tree | 2d147e75df26379b349bc380824faadaec6a978f | |
| parent | ec95b137caaf3fe8381ed0ce5a733a6238c071f9 (diff) | |
| parent | 51aa7435f3a7feb814e371efd109b8786cd45030 (diff) | |
Merge changes I22ff91e8,I6dcef977 into rvc-dev
* changes:
Dump LogBuffers to a file before crashing
Split DumpManager into two
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); |