• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2019 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.row
18 
19 import android.app.Dialog
20 import android.app.INotificationManager
21 import android.app.NotificationChannel
22 import android.app.NotificationChannel.DEFAULT_CHANNEL_ID
23 import android.app.NotificationChannelGroup
24 import android.app.NotificationManager.IMPORTANCE_NONE
25 import android.content.Context
26 import android.graphics.Color
27 import android.graphics.PixelFormat
28 import android.graphics.drawable.Drawable
29 import android.graphics.drawable.ColorDrawable
30 import android.util.Log
31 import android.view.Gravity
32 import android.view.View
33 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
34 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
35 import android.view.Window
36 import android.view.WindowManager
37 import android.widget.TextView
38 import com.android.internal.annotations.VisibleForTesting
39 
40 import com.android.systemui.R
41 
42 import javax.inject.Inject
43 import javax.inject.Singleton
44 
45 const val TAG = "ChannelDialogController"
46 
47 /**
48  * ChannelEditorDialogController is the controller for the dialog half-shelf
49  * that allows users to quickly turn off channels. It is launched from the NotificationInfo
50  * guts view and displays controls for toggling app notifications as well as up to 4 channels
51  * from that app like so:
52  *
53  *   APP TOGGLE                                                 <on/off>
54  *   - Channel from which we launched                           <on/off>
55  *   -                                                          <on/off>
56  *   - the next 3 channels sorted alphabetically for that app   <on/off>
57  *   -                                                          <on/off>
58  */
59 @Singleton
60 class ChannelEditorDialogController @Inject constructor(
61     c: Context,
62     private val noMan: INotificationManager
63 ) {
64     val context: Context = c.applicationContext
65 
66     lateinit var dialog: Dialog
67 
68     private var appIcon: Drawable? = null
69     private var appUid: Int? = null
70     private var packageName: String? = null
71     private var appName: String? = null
72     private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null
73 
74     // Caller should set this if they care about when we dismiss
75     var onFinishListener: OnChannelEditorDialogFinishedListener? = null
76 
77     // Channels handed to us from NotificationInfo
78     @VisibleForTesting
79     internal val providedChannels = mutableListOf<NotificationChannel>()
80 
81     // Map from NotificationChannel to importance
82     private val edits = mutableMapOf<NotificationChannel, Int>()
83     var appNotificationsEnabled = true
84 
85     // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display
86     @VisibleForTesting
87     internal val groupNameLookup = hashMapOf<String, CharSequence>()
88     private val channelGroupList = mutableListOf<NotificationChannelGroup>()
89 
90     /**
91      * Give the controller all of the information it needs to present the dialog
92      * for a given app. Does a bunch of querying of NoMan, but won't present anything yet
93      */
94     fun prepareDialogForApp(
95         appName: String,
96         packageName: String,
97         uid: Int,
98         channels: Set<NotificationChannel>,
99         appIcon: Drawable,
100         onSettingsClickListener: NotificationInfo.OnSettingsClickListener?
101     ) {
102         this.appName = appName
103         this.packageName = packageName
104         this.appUid = uid
105         this.appIcon = appIcon
106         this.appNotificationsEnabled = checkAreAppNotificationsOn()
107         this.onSettingsClickListener = onSettingsClickListener
108 
109         channelGroupList.clear()
110         channelGroupList.addAll(fetchNotificationChannelGroups())
111         buildGroupNameLookup()
112         padToFourChannels(channels)
113     }
114 
115     private fun buildGroupNameLookup() {
116         channelGroupList.forEach { group ->
117             if (group.id != null) {
118                 groupNameLookup[group.id] = group.name
119             }
120         }
121     }
122 
123     private fun padToFourChannels(channels: Set<NotificationChannel>) {
124         providedChannels.clear()
125         // First, add all of the given channels
126         providedChannels.addAll(channels.asSequence().take(4))
127 
128         // Then pad to 4 if we haven't been given that many
129         providedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence())
130                 .filterNot { providedChannels.contains(it) }
131                 .distinct()
132                 .take(4 - providedChannels.size))
133 
134         // If we only got one channel and it has the default miscellaneous tag, then we actually
135         // are looking at an app with a targetSdk <= O, and it doesn't make much sense to show the
136         // channel
137         if (providedChannels.size == 1 && DEFAULT_CHANNEL_ID == providedChannels[0].id) {
138             providedChannels.clear()
139         }
140     }
141 
142     private fun getDisplayableChannels(
143         groupList: Sequence<NotificationChannelGroup>
144     ): Sequence<NotificationChannel> {
145 
146         val channels = groupList
147                 .flatMap { group ->
148                     group.channels.asSequence().filterNot { channel ->
149                         channel.isImportanceLockedByOEM
150                                 || channel.importance == IMPORTANCE_NONE
151                                 || channel.isImportanceLockedByCriticalDeviceFunction
152                     }
153                 }
154 
155         // TODO: sort these by avgSentWeekly, but for now let's just do alphabetical (why not)
156         return channels.sortedWith(compareBy { it.name?.toString() ?: it.id })
157     }
158 
159     fun show() {
160         initDialog()
161         dialog.show()
162     }
163 
164     /**
165      * Close the dialog without saving. For external callers
166      */
167     fun close() {
168         done()
169     }
170 
171     private fun done() {
172         resetState()
173         dialog.dismiss()
174         onFinishListener?.onChannelEditorDialogFinished()
175     }
176 
177     private fun resetState() {
178         appIcon = null
179         appUid = null
180         packageName = null
181         appName = null
182 
183         edits.clear()
184         providedChannels.clear()
185         groupNameLookup.clear()
186     }
187 
188     fun groupNameForId(groupId: String?): CharSequence {
189         return groupNameLookup[groupId] ?: ""
190     }
191 
192     fun proposeEditForChannel(channel: NotificationChannel, edit: Int) {
193         if (channel.importance == edit) {
194             edits.remove(channel)
195         } else {
196             edits[channel] = edit
197         }
198     }
199 
200     @Suppress("unchecked_cast")
201     private fun fetchNotificationChannelGroups(): List<NotificationChannelGroup> {
202         return try {
203             noMan.getNotificationChannelGroupsForPackage(packageName!!, appUid!!, false)
204                     .list as? List<NotificationChannelGroup> ?: listOf()
205         } catch (e: Exception) {
206             Log.e(TAG, "Error fetching channel groups", e)
207             listOf()
208         }
209     }
210 
211     private fun checkAreAppNotificationsOn(): Boolean {
212         return try {
213             noMan.areNotificationsEnabledForPackage(packageName!!, appUid!!)
214         } catch (e: Exception) {
215             Log.e(TAG, "Error calling NoMan", e)
216             false
217         }
218     }
219 
220     private fun applyAppNotificationsOn(b: Boolean) {
221         try {
222             noMan.setNotificationsEnabledForPackage(packageName!!, appUid!!, b)
223         } catch (e: Exception) {
224             Log.e(TAG, "Error calling NoMan", e)
225         }
226     }
227 
228     private fun setChannelImportance(channel: NotificationChannel, importance: Int) {
229         try {
230             channel.importance = importance
231             noMan.updateNotificationChannelForPackage(packageName!!, appUid!!, channel)
232         } catch (e: Exception) {
233             Log.e(TAG, "Unable to update notification importance", e)
234         }
235     }
236 
237     @VisibleForTesting
238     fun apply() {
239         for ((channel, importance) in edits) {
240             if (channel.importance != importance) {
241                 setChannelImportance(channel, importance)
242             }
243         }
244 
245         if (appNotificationsEnabled != checkAreAppNotificationsOn()) {
246             applyAppNotificationsOn(appNotificationsEnabled)
247         }
248     }
249 
250     @VisibleForTesting
251     fun launchSettings(sender: View) {
252         onSettingsClickListener?.onClick(sender, null, appUid!!)
253     }
254 
255     private fun initDialog() {
256         dialog = Dialog(context)
257 
258         dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
259         // Prevent a11y readers from reading the first element in the dialog twice
260         dialog.setTitle("\u00A0")
261         dialog.apply {
262             setContentView(R.layout.notif_half_shelf)
263             setCanceledOnTouchOutside(true)
264             findViewById<ChannelEditorListView>(R.id.half_shelf_container).apply {
265                 controller = this@ChannelEditorDialogController
266                 appIcon = this@ChannelEditorDialogController.appIcon
267                 appName = this@ChannelEditorDialogController.appName
268                 channels = providedChannels
269             }
270 
271             findViewById<TextView>(R.id.done_button)?.setOnClickListener {
272                 apply()
273                 done()
274             }
275 
276             findViewById<TextView>(R.id.see_more_button)?.setOnClickListener {
277                 launchSettings(it)
278                 done()
279             }
280 
281             window?.apply {
282                 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
283                 addFlags(wmFlags)
284                 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL)
285                 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
286 
287                 attributes = attributes.apply {
288                     format = PixelFormat.TRANSLUCENT
289                     title = ChannelEditorDialogController::class.java.simpleName
290                     gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
291                     width = MATCH_PARENT
292                     height = WRAP_CONTENT
293                 }
294             }
295         }
296     }
297 
298     private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
299             or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
300             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
301             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
302 }
303 
304 interface OnChannelEditorDialogFinishedListener {
onChannelEditorDialogFinishednull305     fun onChannelEditorDialogFinished()
306 }
307