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.content.DialogInterface
27 import android.graphics.Color
28 import android.graphics.PixelFormat
29 import android.graphics.drawable.Drawable
30 import android.graphics.drawable.ColorDrawable
31 import android.util.Log
32 import android.view.Gravity
33 import android.view.View
34 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
35 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
36 import android.view.Window
37 import android.view.WindowManager
38 import android.widget.TextView
39 import com.android.internal.annotations.VisibleForTesting
40 
41 import com.android.systemui.R
42 
43 import javax.inject.Inject
44 import javax.inject.Singleton
45 
46 const val TAG = "ChannelDialogController"
47 
48 /**
49  * ChannelEditorDialogController is the controller for the dialog half-shelf
50  * that allows users to quickly turn off channels. It is launched from the NotificationInfo
51  * guts view and displays controls for toggling app notifications as well as up to 4 channels
52  * from that app like so:
53  *
54  *   APP TOGGLE                                                 <on/off>
55  *   - Channel from which we launched                           <on/off>
56  *   -                                                          <on/off>
57  *   - the next 3 channels sorted alphabetically for that app   <on/off>
58  *   -                                                          <on/off>
59  */
60 @Singleton
61 class ChannelEditorDialogController @Inject constructor(
62     c: Context,
63     private val noMan: INotificationManager
64 ) {
65     val context: Context = c.applicationContext
66 
67     lateinit var dialog: Dialog
68 
69     private var appIcon: Drawable? = null
70     private var appUid: Int? = null
71     private var packageName: String? = null
72     private var appName: String? = null
73     private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null
74 
75     // Caller should set this if they care about when we dismiss
76     var onFinishListener: OnChannelEditorDialogFinishedListener? = null
77 
78     // Channels handed to us from NotificationInfo
79     @VisibleForTesting
80     internal val providedChannels = mutableListOf<NotificationChannel>()
81 
82     // Map from NotificationChannel to importance
83     private val edits = mutableMapOf<NotificationChannel, Int>()
84     var appNotificationsEnabled = true
85 
86     // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display
87     @VisibleForTesting
88     internal val groupNameLookup = hashMapOf<String, CharSequence>()
89     private val channelGroupList = mutableListOf<NotificationChannelGroup>()
90 
91     /**
92      * Give the controller all of the information it needs to present the dialog
93      * for a given app. Does a bunch of querying of NoMan, but won't present anything yet
94      */
95     fun prepareDialogForApp(
96         appName: String,
97         packageName: String,
98         uid: Int,
99         channels: Set<NotificationChannel>,
100         appIcon: Drawable,
101         onSettingsClickListener: NotificationInfo.OnSettingsClickListener?
102     ) {
103         this.appName = appName
104         this.packageName = packageName
105         this.appUid = uid
106         this.appIcon = appIcon
107         this.appNotificationsEnabled = checkAreAppNotificationsOn()
108         this.onSettingsClickListener = onSettingsClickListener
109 
110         channelGroupList.clear()
111         channelGroupList.addAll(fetchNotificationChannelGroups())
112         buildGroupNameLookup()
113         padToFourChannels(channels)
114     }
115 
116     private fun buildGroupNameLookup() {
117         channelGroupList.forEach { group ->
118             if (group.id != null) {
119                 groupNameLookup[group.id] = group.name
120             }
121         }
122     }
123 
124     private fun padToFourChannels(channels: Set<NotificationChannel>) {
125         providedChannels.clear()
126         // First, add all of the given channels
127         providedChannels.addAll(channels.asSequence().take(4))
128 
129         // Then pad to 4 if we haven't been given that many
130         providedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence())
131                 .filterNot { providedChannels.contains(it) }
132                 .distinct()
133                 .take(4 - providedChannels.size))
134 
135         // If we only got one channel and it has the default miscellaneous tag, then we actually
136         // are looking at an app with a targetSdk <= O, and it doesn't make much sense to show the
137         // channel
138         if (providedChannels.size == 1 && DEFAULT_CHANNEL_ID == providedChannels[0].id) {
139             providedChannels.clear()
140         }
141     }
142 
143     private fun getDisplayableChannels(
144         groupList: Sequence<NotificationChannelGroup>
145     ): Sequence<NotificationChannel> {
146 
147         val channels = groupList
148                 .flatMap { group ->
149                     group.channels.asSequence().filterNot { channel ->
150                         channel.isImportanceLockedByOEM
151                                 || channel.importance == IMPORTANCE_NONE
152                                 || channel.isImportanceLockedByCriticalDeviceFunction
153                     }
154                 }
155 
156         // TODO: sort these by avgSentWeekly, but for now let's just do alphabetical (why not)
157         return channels.sortedWith(compareBy { it.name?.toString() ?: it.id })
158     }
159 
160     fun show() {
161         initDialog()
162         dialog.show()
163     }
164 
165     /**
166      * Close the dialog without saving. For external callers
167      */
168     fun close() {
169         done()
170     }
171 
172     private fun done() {
173         resetState()
174         dialog.dismiss()
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             setOnDismissListener(object : DialogInterface.OnDismissListener {
265                 override fun onDismiss(dialog: DialogInterface?) {
266                     onFinishListener?.onChannelEditorDialogFinished()
267                 }
268             })
269             findViewById<ChannelEditorListView>(R.id.half_shelf_container).apply {
270                 controller = this@ChannelEditorDialogController
271                 appIcon = this@ChannelEditorDialogController.appIcon
272                 appName = this@ChannelEditorDialogController.appName
273                 channels = providedChannels
274             }
275 
276             findViewById<TextView>(R.id.done_button)?.setOnClickListener {
277                 apply()
278                 done()
279             }
280 
281             findViewById<TextView>(R.id.see_more_button)?.setOnClickListener {
282                 launchSettings(it)
283                 done()
284             }
285 
286             window?.apply {
287                 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
288                 addFlags(wmFlags)
289                 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL)
290                 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
291 
292                 attributes = attributes.apply {
293                     format = PixelFormat.TRANSLUCENT
294                     title = ChannelEditorDialogController::class.java.simpleName
295                     gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
296                     width = MATCH_PARENT
297                     height = WRAP_CONTENT
298                 }
299             }
300         }
301     }
302 
303     private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
304             or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
305             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
306             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
307 }
308 
309 interface OnChannelEditorDialogFinishedListener {
onChannelEditorDialogFinishednull310     fun onChannelEditorDialogFinished()
311 }
312