2 * Copyright (C) 2019 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.statusbar.notification.row
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
41 import com.android.systemui.R
43 import javax.inject.Inject
44 import javax.inject.Singleton
46 const val TAG = "ChannelDialogController"
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:
55 * - Channel from which we launched <on/off>
57 * - the next 3 channels sorted alphabetically for that app <on/off>
61 class ChannelEditorDialogController @Inject constructor(
63 private val noMan: INotificationManager
65 val context: Context = c.applicationContext
67 lateinit var dialog: Dialog
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
75 // Caller should set this if they care about when we dismiss
76 var onFinishListener: OnChannelEditorDialogFinishedListener? = null
78 // Channels handed to us from NotificationInfo
80 internal val providedChannels = mutableListOf<NotificationChannel>()
82 // Map from NotificationChannel to importance
83 private val edits = mutableMapOf<NotificationChannel, Int>()
84 var appNotificationsEnabled = true
86 // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display
88 internal val groupNameLookup = hashMapOf<String, CharSequence>()
89 private val channelGroupList = mutableListOf<NotificationChannelGroup>()
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
95 fun prepareDialogForApp(
99 channels: Set<NotificationChannel>,
101 onSettingsClickListener: NotificationInfo.OnSettingsClickListener?
103 this.appName = appName
104 this.packageName = packageName
106 this.appIcon = appIcon
107 this.appNotificationsEnabled = checkAreAppNotificationsOn()
108 this.onSettingsClickListener = onSettingsClickListener
110 channelGroupList.clear()
111 channelGroupList.addAll(fetchNotificationChannelGroups())
112 buildGroupNameLookup()
113 padToFourChannels(channels)
116 private fun buildGroupNameLookup() {
117 channelGroupList.forEach { group ->
118 if (group.id != null) {
119 groupNameLookup[group.id] = group.name
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))
129 // Then pad to 4 if we haven't been given that many
130 providedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence())
131 .filterNot { providedChannels.contains(it) }
133 .take(4 - providedChannels.size))
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
138 if (providedChannels.size == 1 && DEFAULT_CHANNEL_ID == providedChannels[0].id) {
139 providedChannels.clear()
143 private fun getDisplayableChannels(
144 groupList: Sequence<NotificationChannelGroup>
145 ): Sequence<NotificationChannel> {
147 val channels = groupList
149 group.channels.asSequence().filterNot { channel ->
150 channel.isImportanceLockedByOEM
151 || channel.importance == IMPORTANCE_NONE
152 || channel.isImportanceLockedByCriticalDeviceFunction
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 })
166 * Close the dialog without saving. For external callers
177 private fun resetState() {
184 providedChannels.clear()
185 groupNameLookup.clear()
188 fun groupNameForId(groupId: String?): CharSequence {
189 return groupNameLookup[groupId] ?: ""
192 fun proposeEditForChannel(channel: NotificationChannel, edit: Int) {
193 if (channel.importance == edit) {
194 edits.remove(channel)
196 edits[channel] = edit
200 @Suppress("unchecked_cast")
201 private fun fetchNotificationChannelGroups(): List<NotificationChannelGroup> {
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)
211 private fun checkAreAppNotificationsOn(): Boolean {
213 noMan.areNotificationsEnabledForPackage(packageName!!, appUid!!)
214 } catch (e: Exception) {
215 Log.e(TAG, "Error calling NoMan", e)
220 private fun applyAppNotificationsOn(b: Boolean) {
222 noMan.setNotificationsEnabledForPackage(packageName!!, appUid!!, b)
223 } catch (e: Exception) {
224 Log.e(TAG, "Error calling NoMan", e)
228 private fun setChannelImportance(channel: NotificationChannel, importance: Int) {
230 channel.importance = importance
231 noMan.updateNotificationChannelForPackage(packageName!!, appUid!!, channel)
232 } catch (e: Exception) {
233 Log.e(TAG, "Unable to update notification importance", e)
239 for ((channel, importance) in edits) {
240 if (channel.importance != importance) {
241 setChannelImportance(channel, importance)
245 if (appNotificationsEnabled != checkAreAppNotificationsOn()) {
246 applyAppNotificationsOn(appNotificationsEnabled)
251 fun launchSettings(sender: View) {
252 onSettingsClickListener?.onClick(sender, null, appUid!!)
255 private fun initDialog() {
256 dialog = Dialog(context)
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")
262 setContentView(R.layout.notif_half_shelf)
263 setCanceledOnTouchOutside(true)
264 setOnDismissListener(object : DialogInterface.OnDismissListener {
265 override fun onDismiss(dialog: DialogInterface?) {
266 onFinishListener?.onChannelEditorDialogFinished()
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
276 findViewById<TextView>(R.id.done_button)?.setOnClickListener {
281 findViewById<TextView>(R.id.see_more_button)?.setOnClickListener {
287 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
289 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL)
290 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
292 attributes = attributes.apply {
293 format = PixelFormat.TRANSLUCENT
294 title = ChannelEditorDialogController::class.java.simpleName
295 gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
297 height = WRAP_CONTENT
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)
309 interface OnChannelEditorDialogFinishedListener {
310 fun onChannelEditorDialogFinished()