• 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
25 import android.content.Context
26 import android.graphics.Color
27 import android.graphics.PixelFormat
28 import android.graphics.drawable.ColorDrawable
29 import android.graphics.drawable.Drawable
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.WindowInsets.Type.statusBars
37 import android.view.WindowManager
38 import android.widget.TextView
39 import com.android.internal.annotations.VisibleForTesting
40 import com.android.systemui.res.R
41 import com.android.systemui.dagger.SysUISingleton
42 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
43 import javax.inject.Inject
44 
45 private 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 @SysUISingleton
60 class ChannelEditorDialogController @Inject constructor(
61     private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
62     private val noMan: INotificationManager,
63     private val dialogBuilder: ChannelEditorDialog.Builder,
64 ) {
65 
66     private var prepared = false
67     private lateinit var dialog: ChannelEditorDialog
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 channel: NotificationChannel? = null
74     private var onSettingsClickListener: NotificationInfo.OnSettingsClickListener? = null
75 
76     // Caller should set this if they care about when we dismiss
77     var onFinishListener: OnChannelEditorDialogFinishedListener? = null
78 
79     // Channels handed to us from NotificationInfo
80     @VisibleForTesting
81     internal val channelList = mutableListOf<NotificationChannel>()
82 
83     // Map from NotificationChannel to importance
84     private val edits = mutableMapOf<NotificationChannel, Int>()
85     private var appNotificationsEnabled = true
86     // System settings for app notifications
87     private var appNotificationsCurrentlyEnabled: Boolean? = null
88 
89     // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display
90     @VisibleForTesting
91     internal val groupNameLookup = hashMapOf<String, CharSequence>()
92     private val channelGroupList = mutableListOf<NotificationChannelGroup>()
93 
94     /**
95      * Give the controller all the information it needs to present the dialog
96      * for a given app. Does a bunch of querying of NoMan, but won't present anything yet
97      */
98     fun prepareDialogForApp(
99         appName: String,
100         packageName: String,
101         uid: Int,
102         channel: NotificationChannel,
103         appIcon: Drawable,
104         onSettingsClickListener: NotificationInfo.OnSettingsClickListener?
105     ) {
106         this.appName = appName
107         this.packageName = packageName
108         this.appUid = uid
109         this.appIcon = appIcon
110         this.appNotificationsEnabled = checkAreAppNotificationsOn()
111         this.onSettingsClickListener = onSettingsClickListener
112         this.channel = channel
113 
114         // These will always start out the same
115         appNotificationsCurrentlyEnabled = appNotificationsEnabled
116 
117         channelGroupList.clear()
118         channelGroupList.addAll(fetchNotificationChannelGroups())
119         buildGroupNameLookup()
120         populateChannelList()
121         initDialog()
122 
123         prepared = true
124     }
125 
126     private fun buildGroupNameLookup() {
127         channelGroupList.forEach { group ->
128             if (group.id != null) {
129                 groupNameLookup[group.id] = group.name
130             }
131         }
132     }
133 
134     private fun populateChannelList() {
135         channelList.clear()
136         if (DEFAULT_CHANNEL_ID != channel!!.id) {
137             channelList.add(0, channel!!)
138             channelList.addAll(getDisplayableChannels(channelGroupList.asSequence())
139                     .filterNot { it.id == channel!!.id }
140                     .distinct())
141         }
142     }
143 
144     private fun getDisplayableChannels(
145         groupList: Sequence<NotificationChannelGroup>
146     ): Sequence<NotificationChannel> {
147         val channels = groupList
148                 .flatMap { group ->
149                     group.channels.asSequence()
150                             .sortedWith(compareBy {group.name?.toString() ?: group.id})
151                             .filterNot { channel ->
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         if (!prepared) {
162             throw IllegalStateException("Must call prepareDialogForApp() before calling show()")
163         }
164         dialog.show()
165     }
166 
167     /**
168      * Close the dialog without saving. For external callers
169      */
170     fun close() {
171         done()
172     }
173 
174     private fun done() {
175         resetState()
176         dialog.dismiss()
177     }
178 
179     private fun resetState() {
180         appIcon = null
181         appUid = null
182         packageName = null
183         appName = null
184         appNotificationsCurrentlyEnabled = null
185 
186         edits.clear()
187         channelList.clear()
188         groupNameLookup.clear()
189     }
190 
191     fun groupNameForId(groupId: String?): CharSequence {
192         return groupNameLookup[groupId] ?: ""
193     }
194 
195     fun proposeEditForChannel(channel: NotificationChannel, @Importance edit: Int) {
196         if (channel.importance == edit) {
197             edits.remove(channel)
198         } else {
199             edits[channel] = edit
200         }
201 
202         dialog.updateDoneButtonText(hasChanges())
203     }
204 
205     fun proposeSetAppNotificationsEnabled(enabled: Boolean) {
206         appNotificationsEnabled = enabled
207         dialog.updateDoneButtonText(hasChanges())
208     }
209 
210     fun areAppNotificationsEnabled(): Boolean {
211         return appNotificationsEnabled
212     }
213 
214     private fun hasChanges(): Boolean {
215         return edits.isNotEmpty() || (appNotificationsEnabled != appNotificationsCurrentlyEnabled)
216     }
217 
218     @Suppress("unchecked_cast")
219     private fun fetchNotificationChannelGroups(): List<NotificationChannelGroup> {
220         return try {
221             noMan.getRecentBlockedNotificationChannelGroupsForPackage(packageName!!, appUid!!)
222                     .list as? List<NotificationChannelGroup> ?: listOf()
223         } catch (e: Exception) {
224             Log.e(TAG, "Error fetching channel groups", e)
225             listOf()
226         }
227     }
228 
229     private fun checkAreAppNotificationsOn(): Boolean {
230         return try {
231             noMan.areNotificationsEnabledForPackage(packageName!!, appUid!!)
232         } catch (e: Exception) {
233             Log.e(TAG, "Error calling NoMan", e)
234             false
235         }
236     }
237 
238     private fun applyAppNotificationsOn(b: Boolean) {
239         try {
240             noMan.setNotificationsEnabledForPackage(packageName!!, appUid!!, b)
241         } catch (e: Exception) {
242             Log.e(TAG, "Error calling NoMan", e)
243         }
244     }
245 
246     private fun setChannelImportance(channel: NotificationChannel, importance: Int) {
247         try {
248             channel.importance = importance
249             noMan.updateNotificationChannelForPackage(packageName!!, appUid!!, channel)
250         } catch (e: Exception) {
251             Log.e(TAG, "Unable to update notification importance", e)
252         }
253     }
254 
255     @VisibleForTesting
256     fun apply() {
257         for ((channel, importance) in edits) {
258             if (channel.importance != importance) {
259                 setChannelImportance(channel, importance)
260             }
261         }
262 
263         if (appNotificationsEnabled != appNotificationsCurrentlyEnabled) {
264             applyAppNotificationsOn(appNotificationsEnabled)
265         }
266     }
267 
268     @VisibleForTesting
269     fun launchSettings(sender: View) {
270         onSettingsClickListener?.onClick(sender, channel, appUid!!)
271     }
272 
273     private fun initDialog() {
274         dialogBuilder.setContext(shadeDialogContextInteractor.context)
275         dialog = dialogBuilder.build()
276 
277         dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
278         // Prevent a11y readers from reading the first element in the dialog twice
279         dialog.setTitle("\u00A0")
280         dialog.apply {
281             setContentView(R.layout.notif_half_shelf)
282             setCanceledOnTouchOutside(true)
283             setOnDismissListener { onFinishListener?.onChannelEditorDialogFinished() }
284 
285             val listView = findViewById<ChannelEditorListView>(R.id.half_shelf_container)
286             listView?.apply {
287                 controller = this@ChannelEditorDialogController
288                 appIcon = this@ChannelEditorDialogController.appIcon
289                 appName = this@ChannelEditorDialogController.appName
290                 channels = channelList
291             }
292 
293             setOnShowListener {
294                 // play a highlight animation for the given channel
295                 listView?.highlightChannel(channel!!)
296             }
297 
298             findViewById<TextView>(R.id.done_button)?.setOnClickListener {
299                 apply()
300                 done()
301             }
302 
303             findViewById<TextView>(R.id.see_more_button)?.setOnClickListener {
304                 launchSettings(it)
305                 done()
306             }
307 
308             window?.apply {
309                 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
310                 addFlags(wmFlags)
311                 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
312                 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
313 
314                 attributes = attributes.apply {
315                     format = PixelFormat.TRANSLUCENT
316                     title = ChannelEditorDialogController::class.java.simpleName
317                     gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
318                     fitInsetsTypes = attributes.fitInsetsTypes and statusBars().inv()
319                     width = MATCH_PARENT
320                     height = WRAP_CONTENT
321                 }
322             }
323         }
324     }
325 
326     private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
327             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
328             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
329 }
330 
331 class ChannelEditorDialog(context: Context, theme: Int) : Dialog(context, theme) {
updateDoneButtonTextnull332     fun updateDoneButtonText(hasChanges: Boolean) {
333         findViewById<TextView>(R.id.done_button)?.setText(
334                 if (hasChanges)
335                     R.string.inline_ok_button
336                 else
337                     R.string.inline_done_button)
338     }
339 
340     class Builder @Inject constructor() {
341         private lateinit var context: Context
setContextnull342         fun setContext(context: Context): Builder {
343             this.context = context
344             return this
345         }
346 
buildnull347         fun build(): ChannelEditorDialog {
348             return ChannelEditorDialog(context, R.style.Theme_SystemUI_Dialog)
349         }
350     }
351 }
352 
353 interface OnChannelEditorDialogFinishedListener {
onChannelEditorDialogFinishednull354     fun onChannelEditorDialogFinished()
355 }
356