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.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
40 import com.android.systemui.R
42 import javax.inject.Inject
43 import javax.inject.Singleton
45 const val TAG = "ChannelDialogController"
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:
54 * - Channel from which we launched <on/off>
56 * - the next 3 channels sorted alphabetically for that app <on/off>
60 class ChannelEditorDialogController @Inject constructor(
62 private val noMan: INotificationManager
64 val context: Context = c.applicationContext
66 lateinit var dialog: Dialog
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
74 // Caller should set this if they care about when we dismiss
75 var onFinishListener: OnChannelEditorDialogFinishedListener? = null
77 // Channels handed to us from NotificationInfo
79 internal val providedChannels = mutableListOf<NotificationChannel>()
81 // Map from NotificationChannel to importance
82 private val edits = mutableMapOf<NotificationChannel, Int>()
83 var appNotificationsEnabled = true
85 // Keep a mapping of NotificationChannel.getGroup() to the actual group name for display
87 internal val groupNameLookup = hashMapOf<String, CharSequence>()
88 private val channelGroupList = mutableListOf<NotificationChannelGroup>()
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
94 fun prepareDialogForApp(
98 channels: Set<NotificationChannel>,
100 onSettingsClickListener: NotificationInfo.OnSettingsClickListener?
102 this.appName = appName
103 this.packageName = packageName
105 this.appIcon = appIcon
106 this.appNotificationsEnabled = checkAreAppNotificationsOn()
107 this.onSettingsClickListener = onSettingsClickListener
109 channelGroupList.clear()
110 channelGroupList.addAll(fetchNotificationChannelGroups())
111 buildGroupNameLookup()
112 padToFourChannels(channels)
115 private fun buildGroupNameLookup() {
116 channelGroupList.forEach { group ->
117 if (group.id != null) {
118 groupNameLookup[group.id] = group.name
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))
128 // Then pad to 4 if we haven't been given that many
129 providedChannels.addAll(getDisplayableChannels(channelGroupList.asSequence())
130 .filterNot { providedChannels.contains(it) }
132 .take(4 - providedChannels.size))
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
137 if (providedChannels.size == 1 && DEFAULT_CHANNEL_ID == providedChannels[0].id) {
138 providedChannels.clear()
142 private fun getDisplayableChannels(
143 groupList: Sequence<NotificationChannelGroup>
144 ): Sequence<NotificationChannel> {
146 val channels = groupList
148 group.channels.asSequence().filterNot { channel ->
149 channel.isImportanceLockedByOEM
150 || channel.importance == IMPORTANCE_NONE
151 || channel.isImportanceLockedByCriticalDeviceFunction
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 })
165 * Close the dialog without saving. For external callers
174 onFinishListener?.onChannelEditorDialogFinished()
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 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
271 findViewById<TextView>(R.id.done_button)?.setOnClickListener {
276 findViewById<TextView>(R.id.see_more_button)?.setOnClickListener {
282 setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
284 setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL)
285 setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
287 attributes = attributes.apply {
288 format = PixelFormat.TRANSLUCENT
289 title = ChannelEditorDialogController::class.java.simpleName
290 gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
292 height = WRAP_CONTENT
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)
304 interface OnChannelEditorDialogFinishedListener {
305 fun onChannelEditorDialogFinished()