1 /*
2  * Copyright (C) 2021 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.collection.provider
18 
19 import android.os.Build
20 import android.util.Log
21 import com.android.systemui.Dumpable
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dump.DumpManager
24 import com.android.systemui.statusbar.commandline.Command
25 import com.android.systemui.statusbar.commandline.CommandRegistry
26 import com.android.systemui.statusbar.notification.collection.NotificationEntry
27 import com.android.systemui.util.Assert
28 import com.android.systemui.util.ListenerSet
29 import java.io.PrintWriter
30 import javax.inject.Inject
31 
32 /**
33  * A debug mode provider which is used by both the legacy and new notification pipelines to
34  * block unwanted notifications from appearing to the user, primarily for integration testing.
35  *
36  * The only configuration is a list of allowed packages.  When this list is empty, the feature is
37  * disabled.  When SystemUI starts up, this feature is disabled.
38  *
39  * To enabled filtering, provide the space-separated list of packages using the command:
40  *
41  * `$ adb shell cmd statusbar notif-filter allowed-pkgs <package> ...`
42  *
43  * To disable filtering, send the command without any packages, or explicitly reset:
44  *
45  * `$ adb shell cmd statusbar notif-filter reset`
46  *
47  * NOTE: this feature only works on debug builds, and when the broadcaster is root.
48  */
49 @SysUISingleton
50 class DebugModeFilterProvider @Inject constructor(
51     private val commandRegistry: CommandRegistry,
52     dumpManager: DumpManager
53 ) : Dumpable {
54     private var allowedPackages: List<String> = emptyList()
55     private val listeners = ListenerSet<Runnable>()
56 
57     init {
58         dumpManager.registerDumpable(this)
59     }
60 
61     /**
62      * Register a runnable to be invoked when the allowed packages changes, which would mean the
63      * result of [shouldFilterOut] may have changed for some entries.
64      */
65     fun registerInvalidationListener(listener: Runnable) {
66         Assert.isMainThread()
67         if (!Build.isDebuggable()) {
68             return
69         }
70         val needsInitialization = listeners.isEmpty()
71         listeners.addIfAbsent(listener)
72         if (needsInitialization) {
73             commandRegistry.registerCommand("notif-filter") { NotifFilterCommand() }
74             Log.d(TAG, "Registered notif-filter command")
75         }
76     }
77 
78     /**
79      * Determine if the given entry should be hidden from the user in debug mode.
80      * Will always return false in release.
81      */
82     fun shouldFilterOut(entry: NotificationEntry): Boolean {
83         if (allowedPackages.isEmpty()) {
84             return false
85         }
86         return entry.sbn.packageName !in allowedPackages
87     }
88 
89     override fun dump(pw: PrintWriter, args: Array<out String>) {
90         pw.println("initialized: ${listeners.isNotEmpty()}")
91         pw.println("allowedPackages: ${allowedPackages.size}")
92         allowedPackages.forEachIndexed { i, pkg ->
93             pw.println("  [$i]: $pkg")
94         }
95     }
96 
97     companion object {
98         private const val TAG = "DebugModeFilterProvider"
99     }
100 
101     inner class NotifFilterCommand : Command {
102         override fun execute(pw: PrintWriter, args: List<String>) {
103             when (args.firstOrNull()) {
104                 "reset" -> {
105                     if (args.size > 1) {
106                         return invalidCommand(pw, "Unexpected arguments for 'reset' command")
107                     }
108                     allowedPackages = emptyList()
109                 }
110                 "allowed-pkgs" -> {
111                     allowedPackages = args.drop(1)
112                 }
113                 null -> return invalidCommand(pw, "Missing command")
114                 else -> return invalidCommand(pw, "Unknown command: ${args.firstOrNull()}")
115             }
116             Log.d(TAG, "Updated allowedPackages: $allowedPackages")
117             if (allowedPackages.isEmpty()) {
118                 pw.print("Resetting allowedPackages ... ")
119             } else {
120                 pw.print("Updating allowedPackages: $allowedPackages ... ")
121             }
122             listeners.forEach(Runnable::run)
123             pw.println("DONE")
124         }
125 
126         private fun invalidCommand(pw: PrintWriter, reason: String) {
127             pw.println("Error: $reason")
128             pw.println()
129             help(pw)
130         }
131 
132         override fun help(pw: PrintWriter) {
133             pw.println("Usage: adb shell cmd statusbar notif-filter <command>")
134             pw.println("Available commands:")
135             pw.println("  reset")
136             pw.println("     Restore the default system behavior.")
137             pw.println("  allowed-pkgs <package> ...")
138             pw.println("     Hide all notification except from packages listed here.")
139             pw.println("     Providing no packages is treated as a reset.")
140         }
141     }
142 }
143