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.log.echo
18 
19 import com.android.systemui.dagger.qualifiers.Application
20 import com.android.systemui.dagger.qualifiers.Background
21 import com.android.systemui.log.LogcatEchoTracker
22 import com.android.systemui.log.core.LogLevel
23 import com.android.systemui.log.echo.LogcatEchoTrackerCommand.Companion.ECHO_TRACKER_COMMAND_NAME
24 import com.android.systemui.statusbar.commandline.CommandRegistry
25 import com.android.systemui.util.settings.GlobalSettings
26 import javax.inject.Inject
27 import kotlinx.coroutines.CoroutineDispatcher
28 import kotlinx.coroutines.CoroutineScope
29 import kotlinx.coroutines.ExperimentalCoroutinesApi
30 import kotlinx.coroutines.launch
31 
32 /**
33  * A version of [LogcatEchoTracker] that supports fine-grained echoing of log messages to logcat,
34  * filtered by buffer, tag, and log level.
35  *
36  * Filters can be added and removed via a shell command (`adb shell cmd statusbar echo`). See
37  * [LogcatEchoTrackerCommand] for details.
38  *
39  * Note that some log messages may fail to be echoed while the systemui process is first starting
40  * up, before we load the echo settings.
41  */
42 @OptIn(ExperimentalCoroutinesApi::class)
43 class LogcatEchoTrackerDebug
44 @Inject
45 constructor(
46     @Application private val applicationScope: CoroutineScope,
47     @Background backgroundDispatcher: CoroutineDispatcher,
48     private val globalSettings: GlobalSettings,
49     private val commandRegistry: CommandRegistry,
50 ) : LogcatEchoTracker {
51 
52     // This class uses a single-writer, many-readers pattern that allows us to avoid the need for
53     // locking. In this case, this means that our shared state (the override maps) can be _read_ by
54     // any number of threads, but they're always written to by a single thread (dispatched by
55     // sequentialBgDispatcher). Such a pattern allows us to use the more performant @Volatile below
56     // instead of synchronization locks.
57     //
58     // Okay: some of what I just told you is a lie. sequentialBgDispatcher does not dispatch to a
59     // single thread. Instead, it guarantees that all work it schedules is _sequential_, meaning
60     // that Job B cannot start until Job A ends (this is actually a stronger guarantee than single-
61     // threaded execution due to the possibility of suspend functions). Because
62     // sequentialBgDispatcher is dispatching from the Dispatchers.IO thread pool, each individual
63     // "write" job might run on a different thread from that pool. However, because we are
64     // enforcing sequential execution, exactly which thread an individual write job runs on doesn't
65     // matter.
66     private val sequentialBgDispatcher = backgroundDispatcher.limitedParallelism(1)
67 
68     // Okay. So. Why are these @Volatile. We've eliminated the need for synchronization primitives,
69     // but now we must contend with thread memory caching. Without an explicit synchronization
70     // signal, other threads may see "stale" versions of our state when they try to read from these
71     // maps, even after they've been updated by the writer thread. @Volatile solves this problem:
72     // it eliminates the possibility of stale reads while still being much more performant than
73     // locking.
74     @Volatile private var bufferOverrides = mapOf<String, LogLevel>()
75     @Volatile private var tagOverrides = mapOf<String, LogLevel>()
76 
77     private val settingFormat = LogcatEchoSettingFormat()
78 
startnull79     fun start() {
80         loadEchoOverrides()
81 
82         commandRegistry.registerCommand(ECHO_TRACKER_COMMAND_NAME) {
83             LogcatEchoTrackerCommand(this)
84         }
85     }
86 
isBufferLoggablenull87     override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
88         return level >= (bufferOverrides[bufferName] ?: DEFAULT_LOG_LEVEL)
89     }
90 
isTagLoggablenull91     override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
92         return level >= (tagOverrides[tagName] ?: DEFAULT_LOG_LEVEL)
93     }
94 
listEchoOverridesnull95     fun listEchoOverrides(): List<LogcatEchoOverride> {
96         val list = mutableListOf<LogcatEchoOverride>()
97 
98         val frozenBufferOverrides = bufferOverrides
99         val frozenTagOverrides = tagOverrides
100 
101         for ((name, level) in frozenBufferOverrides) {
102             list.add(LogcatEchoOverride(EchoOverrideType.BUFFER, name, level))
103         }
104         for ((name, level) in frozenTagOverrides) {
105             list.add(LogcatEchoOverride(EchoOverrideType.TAG, name, level))
106         }
107         return list
108     }
109 
setEchoLevelnull110     fun setEchoLevel(type: EchoOverrideType, name: String, level: LogLevel?) {
111         applicationScope.launch(sequentialBgDispatcher) {
112             val newBufferOverrides = bufferOverrides.toMutableMap()
113             val newTagOverrides = tagOverrides.toMutableMap()
114 
115             val mutatedMap =
116                 when (type) {
117                     EchoOverrideType.BUFFER -> newBufferOverrides
118                     EchoOverrideType.TAG -> newTagOverrides
119                 }
120             if (level != null) {
121                 mutatedMap[name] = level
122             } else {
123                 mutatedMap.remove(name)
124             }
125 
126             bufferOverrides = newBufferOverrides
127             tagOverrides = newTagOverrides
128 
129             val list = listEchoOverrides()
130             globalSettings.putString(OVERRIDE_SETTING_PATH, settingFormat.stringifyOverrides(list))
131         }
132     }
133 
clearAllOverridesnull134     fun clearAllOverrides() {
135         applicationScope.launch(sequentialBgDispatcher) {
136             bufferOverrides = emptyMap()
137             tagOverrides = emptyMap()
138 
139             val list = listEchoOverrides()
140             globalSettings.putString(OVERRIDE_SETTING_PATH, settingFormat.stringifyOverrides(list))
141         }
142     }
143 
loadEchoOverridesnull144     private fun loadEchoOverrides() {
145         applicationScope.launch(sequentialBgDispatcher) {
146             val overrideSetting = globalSettings.getString(OVERRIDE_SETTING_PATH) ?: return@launch
147             val overrideList = settingFormat.parseOverrides(overrideSetting)
148 
149             val newBufferOverrides = mutableMapOf<String, LogLevel>()
150             val newTagOverrides = mutableMapOf<String, LogLevel>()
151 
152             for (override in overrideList) {
153                 val map =
154                     when (override.type) {
155                         EchoOverrideType.BUFFER -> newBufferOverrides
156                         EchoOverrideType.TAG -> newTagOverrides
157                     }
158                 map[override.name] = override.level
159             }
160 
161             bufferOverrides = newBufferOverrides
162             tagOverrides = newTagOverrides
163         }
164     }
165 }
166 
167 enum class EchoOverrideType {
168     BUFFER,
169     TAG,
170 }
171 
172 private const val TAG = "LogcatEchoTrackerDebug"
173 
174 private const val OVERRIDE_SETTING_PATH = "systemui/logbuffer_echo_overrides"
175 
176 private val DEFAULT_LOG_LEVEL = LogLevel.WARNING
177