1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar.notification
18 
19 import android.icu.text.SimpleDateFormat
20 import android.util.IndentingPrintWriter
21 import com.android.systemui.Dumpable
22 import com.android.systemui.Flags.notificationColorUpdateLogger
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dump.DumpManager
25 import com.android.systemui.flags.FeatureFlagsClassic
26 import com.android.systemui.util.Compile
27 import com.android.systemui.util.asIndenting
28 import com.android.systemui.util.printCollection
29 import com.android.systemui.util.withIncreasedIndent
30 import com.google.errorprone.annotations.CompileTimeConstant
31 import java.io.PrintWriter
32 import java.util.Locale
33 import java.util.SortedSet
34 import java.util.TreeSet
35 import javax.inject.Inject
36 
37 @SysUISingleton
38 class ColorUpdateLogger
39 @Inject
40 constructor(
41     val featureFlags: FeatureFlagsClassic,
42     dumpManager: DumpManager,
43 ) : Dumpable {
44 
45     inline val isEnabled
46         get() = Compile.IS_DEBUG && notificationColorUpdateLogger()
47     private val frames: MutableList<Frame> = mutableListOf()
48 
49     init {
50         dumpManager.registerDumpable(this)
51         if (isEnabled) {
52             instance = this
53         }
54     }
55 
56     @JvmOverloads
logTriggerEventnull57     fun logTriggerEvent(@CompileTimeConstant type: String, extra: String? = null) {
58         if (!isEnabled) return
59         val event = Event(type = type, extraValue = extra)
60         val didAppend = frames.lastOrNull()?.tryAddTrigger(event) == true
61         if (!didAppend) {
62             frames.add(Frame(event))
63             if (frames.size > maxFrames) frames.removeAt(0)
64         }
65     }
66 
67     @JvmOverloads
logEventnull68     fun logEvent(@CompileTimeConstant type: String, extra: String? = null) {
69         if (!isEnabled) return
70         val frame = frames.lastOrNull() ?: return
71         frame.events.add(Event(type = type, extraValue = extra))
72         frame.trim()
73     }
74 
75     @JvmOverloads
logNotificationEventnull76     fun logNotificationEvent(
77         @CompileTimeConstant type: String,
78         key: String,
79         extra: String? = null
80     ) {
81         if (!isEnabled) return
82         val frame = frames.lastOrNull() ?: return
83         frame.events.add(Event(type = type, extraValue = extra, notificationKey = key))
84         frame.trim()
85     }
86 
dumpnull87     override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
88         val pw = pwOrig.asIndenting()
89         pw.println("enabled: $isEnabled")
90         pw.printCollection("frames", frames) { it.dump(pw) }
91     }
92 
93     private class Frame(event: Event) {
94         val startTime: Long = event.time
95         val events: MutableList<Event> = mutableListOf(event)
<lambda>null96         val triggers: SortedSet<String> = TreeSet<String>().apply { add(event.type) }
97         var trimmedEvents: Int = 0
98 
tryAddTriggernull99         fun tryAddTrigger(newEvent: Event): Boolean {
100             if (newEvent.time < startTime) return false
101             if (newEvent.time - startTime > triggerStartsNewFrameAge) return false
102             if (newEvent.type in triggers) return false
103             triggers.add(newEvent.type)
104             events.add(newEvent)
105             trim()
106             return true
107         }
108 
trimnull109         fun trim() {
110             if (events.size > maxEventsPerFrame) {
111                 events.removeAt(0)
112                 trimmedEvents++
113             }
114         }
115 
dumpnull116         fun dump(pw: IndentingPrintWriter) {
117             pw.println("Frame")
118             pw.withIncreasedIndent {
119                 pw.println("startTime: ${timeString(startTime)}")
120                 pw.printCollection("triggers", triggers)
121                 pw.println("trimmedEvents: $trimmedEvents")
122                 pw.printCollection("events", events) { it.dump(pw) }
123             }
124         }
125     }
126 
127     private class Event(
128         @CompileTimeConstant val type: String,
129         val extraValue: String? = null,
130         val notificationKey: String? = null,
131     ) {
132         val time: Long = System.currentTimeMillis()
133 
dumpnull134         fun dump(pw: IndentingPrintWriter) {
135             pw.append(timeString(time)).append(": ").append(type)
136             extraValue?.let { pw.append(" ").append(it) }
137             notificationKey?.let { pw.append(" ---- ").append(logKey(it)) }
138             pw.println()
139         }
140     }
141 
142     private companion object {
143         @JvmStatic
144         var instance: ColorUpdateLogger? = null
145             private set
146         private const val maxFrames = 5
147         private const val maxEventsPerFrame = 250
148         private const val triggerStartsNewFrameAge = 5000
149 
150         private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
timeStringnull151         private fun timeString(time: Long): String = dateFormat.format(time)
152     }
153 }
154